简介:这份资源聚焦JavaScript逆向工程中hexin-v参数的生成逻辑,面向有一定JS基础、正在研究网络请求参数加密与混淆还原的开发者与安全爱好者。资源包内共1个文件,为单个js脚本,压缩包约16KB,体量轻巧,便于直接阅读与调试。内容围绕hexin-v.js展开,涉及数据获取、加密算法调用、代码混淆还原以及参数拼接构造等关键环节,可帮助读者理解变量赋值、函数定义与加密操作之间的调用关系,进而追踪参数生成路径。对于需要处理同X顺类参数顺序或字符串匹配问题的场景,该脚本可作为逆向分析的切入点,配合浏览器开发者工具逐步还原混淆代码,定位核心加密函数。目前已有4058人学习下载,适合希望掌握JS逆向参数还原思路、提升加密逻辑分析能力的中级学习者参考。
1. 拆开 hexin-v.js:一个让请求签名对不上的参数到底怎么生成
抓包抓到某个行情接口,请求头里挂着一个hexin-v,值是一长串看起来像 Base64 又像十六进制的字符串。你把同样的 URL、同样的 Cookie、同样的 body 原样重放,服务端返回的却是校验失败或者干脆空数据。问题几乎都出在这个hexin-v上——它不是固定值,而是每次请求前由前端 JS 现算出来的。hexin-v.js这个文件就是干这件事的:把时间戳、随机数、页面上下文等输入,经过混淆过的加密逻辑,拼成服务端认得的签名。做 javascript 逆向 的人拿到它,等于拿到了复现请求的钥匙。这篇笔记面向需要稳定复现该签名的后端、爬虫和数据工程师,从文件结构讲到补环境、定位入口、参数构造,再到几个我实际翻过车的地方。
2. 先看清 hexin-v.js 的结构:混淆层、加密层与入口函数
拿到hexin-v.js的第一件事不是急着跑,而是先判断它属于哪一类混淆。不同混淆方式决定了你后面用静态分析还是动态调试为主。我一般会先看文件头部有没有明显的字符串数组、有没有大段的自执行函数、变量名是不是清一色的单字母或_0x前缀。这些特征直接指向混淆器类型,也决定了还原成本。
2.1 三类常见混淆特征与对应处理策略
第一类是字符串数组混淆,典型表现是文件开头有一个巨大的数组,后面所有字符串都通过下标访问,比如_0x12ab[3]。这种用 AST 工具批量还原最省事,把数组和引用替换回字面量即可。第二类是控制流平坦化,函数体被拆成一个个 case,用一个状态变量驱动,读起来像开关语句套娃。这类硬还原成本高,通常配合动态调试,在关键节点打日志观察实际走向。第三类是变量名与属性名混淆,a.b.c变成_0x1['x2']['y3'],需要结合运行时把属性名映射回来。
判断方法很直接:在 Node 里require这个文件,看它导出什么;如果报错,说明它依赖浏览器环境(window、document、navigator),那就得先补环境再谈分析。我一般会先跑一遍看报错栈,报错位置往往就是入口附近。
2.2 定位生成 hexin-v 的入口函数
不要一上来就通读全文。先搜关键字:hexin-v、hexinV、setRequestHeader、XMLHttpRequest、fetch。因为签名最终要挂到请求头上,所以拦截请求发送的位置,往回追调用栈,就能找到计算函数。常见做法是重写XMLHttpRequest.prototype.setRequestHeader,在它被调用时打印堆栈:
// 在页面加载前注入,拦截请求头写入,回溯 hexin-v 的调用来源 const rawSet = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function (key, value) { if (key.toLowerCase() === 'hexin-v') { console.log('hexin-v =', value); console.trace('调用栈'); // 打印堆栈,定位生成函数 } return rawSet.apply(this, arguments); };这段代码的作用是把每次写入hexin-v的时机和调用链暴露出来。console.trace会输出完整的调用栈,栈顶往下数几层,通常就能看到那个负责拼签名的函数名。参数说明:key是请求头名,做小写比较是为了兼容大小写差异;value就是最终签名值,先记下来,后面要拿它和本地计算结果比对。
2.3 补环境:让 hexin-v.js 在 Node 里跑起来
浏览器里能跑不代表 Node 里能跑。hexin-v.js大概率会访问window、document、navigator.userAgent、location这些对象。补环境的核心思路是:缺什么补什么,但补的值要尽量贴近真实浏览器,否则算出来的签名服务端不认。常见做法是用jsdom起一个最小 DOM,再把navigator、screen等属性手动挂上去。
// 用 jsdom 构造最小浏览器环境,供 hexin-v.js 加载 const { JSDOM } = require('jsdom'); const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', { url: 'https://example.com', // location 相关逻辑依赖它 referrer: 'https://example.com', pretendToBeVisual: true }); global.window = dom.window; global.document = dom.window.document; global.navigator = dom.window.navigator; global.location = dom.window.location; // 补 navigator.userAgent,很多签名会把它作为输入之一 Object.defineProperty(global.navigator, 'userAgent', { value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', configurable: true }); require('./hexin-v.js'); // 加载目标文件,观察是否还报错逻辑说明:JSDOM的url参数决定location.href的值,如果签名里混入了当前页面地址,这个必须和真实请求页一致。userAgent用defineProperty覆盖是因为部分环境下它是只读的。加载后如果还报xxx is not defined,就按报错继续补,直到文件能正常执行并导出计算函数。这一步是整个逆向里最磨人的环节,补多了算出来不对,补少了直接报错,边界要靠比对真实签名来收敛。
3. 还原加密逻辑:从输入拼接到最终签名的完整链路
环境跑通之后,重点转向搞清楚签名到底由哪些输入、经过什么运算得到。这一步的目标不是把混淆代码全部读懂,而是找到「输入 → 输出」的映射关系,能在本地稳定复现即可。
3.1 用插桩确认输入项:时间戳、随机数与页面上下文
签名函数通常接收一个对象或几个参数。我在入口函数第一行插桩,把入参打印出来:
// 在定位到的签名函数入口插桩,观察真实入参 function sign(payload) { console.log('入参:', JSON.stringify(payload)); console.log('时间戳:', Date.now()); // ...原有逻辑 }跑几次真实请求,对比每次的入参差异。如果发现某个字段每次都变,大概率是时间戳或随机数;如果某字段和页面 URL、Cookie 里的某项一致,那就是上下文输入。常见组合是:时间戳 + 随机串 + 业务参数排序后的字符串,再整体做一次哈希或对称加密。把变化项和固定项分开记录,是后面本地复现的基础。
3.2 识别加密算法:哈希、AES 还是自定义变换
看运算特征比看函数名靠谱。如果代码里出现0x67452301、0xefcdab89这类常量,基本是 MD5 或 SHA 系列;出现 S 盒、轮常量、subBytes之类,是 AES;如果是一堆位移、异或、查表混在一起,多半是自定义变换。我一般先按标准算法试:把已知输入喂给 Node 的crypto模块算一遍,和真实签名比对。
// 用标准算法验证猜测:先试 MD5,再试 SHA256 const crypto = require('crypto'); const input = 'timestamp=1700000000&rand=abc123&biz=xxx'; const md5 = crypto.createHash('md5').update(input).digest('hex'); const sha256 = crypto.createHash('sha256').update(input).digest('hex'); console.log('md5:', md5); console.log('sha256:', sha256); // 把输出和抓到的 hexin-v 对比,命中则说明算法和拼接顺序正确参数说明:input的拼接顺序极其关键,字段顺序错一位结果就完全不同。如果标准算法都对不上,再回到混淆代码里找自定义部分,重点看有没有额外的盐值(salt)或密钥被拼进输入。这一步的坑在于,很多实现会先对参数做一次排序再拼接,排序规则可能是字典序也可能是自定义顺序,需要从代码里确认。
3.3 本地复现签名并与真实请求比对
确认算法和输入后,把整条链路在本地串起来,输出签名,和抓包结果逐字符比对。我习惯写一个小脚本,固定时间戳和随机数,让输出可复现:
// 固定输入,验证本地签名与真实签名是否一致 const fixedTs = 1700000000000; const fixedRand = 'abc123'; const biz = 'symbol=000001&type=quote'; // 按还原出的顺序拼接 const raw = `${fixedTs}${fixedRand}${biz}`; const sign = crypto.createHash('md5').update(raw).digest('hex'); console.log('本地签名:', sign); // 与抓包中同一组输入下的 hexin-v 对比如果对不上,优先排查三处:拼接顺序、是否漏了某个隐藏输入(比如 Cookie 里的 token)、编码方式(UTF-8 还是 GBK)。我遇到过签名里混入了navigator.platform的情况,补环境时随手填的值和真实浏览器不一致,导致怎么算都差几位。把这几处逐一锁定,签名就能稳定复现。
4. 避坑与排查:hexin-v 复现里最容易翻车的五个点
这一章是我踩过的坑合集,每条按现象、原因、解决来写,照着排查能省不少时间。
现象一:本地算出的签名长度和真实值不一样。原因通常是编码方式不同,真实实现可能先做了 Base64 再截取,或者对哈希结果做了十六进制以外的编码。解决:打印真实签名的字符集,判断是 hex、Base64 还是自定义字母表,再对应调整输出编码。
现象二:签名能对上,但请求仍被拒。原因多半是签名之外的请求头或 Cookie 缺失,服务端校验的是组合条件。解决:把完整请求头逐项对比,重点看Referer、Origin、User-Agent是否和签名时的环境一致,签名和环境是绑定的。
现象三:补环境后代码能跑,但结果每次都不一样。原因是签名里混入了随机数或时间戳,而你没固定它们。解决:在插桩阶段就把变化项找出来,本地复现时用固定值,线上再换成实时值。
现象四:换了页面或换了账号,签名逻辑失效。原因是签名依赖页面上下文,比如某个全局变量在登录后才被赋值。解决:确认签名函数的输入是否包含页面级状态,必要时在补环境时手动注入对应变量。
现象五:混淆代码里明明有加密函数,调用却没走到。原因是存在多套分支,不同条件下走不同实现。解决:在多个分支入口都插桩,观察实际执行路径,别只盯着一处看。
提示:每次改动补环境或拼接逻辑后,都用同一组固定输入回归一次,避免改 A 坏 B。
5. 进阶技巧:把 hexin-v 生成封装成可复用模块
签名跑通只是第一步,真正要用起来,得把它封装成稳定、可测试的模块。我一般会把补环境、加载目标文件、导出签名函数这三步拆开,做成一个工厂函数,输入业务参数,输出签名。
// 封装 hexin-v 生成器,隔离环境与业务调用 function createHexinV(envOptions) { const { JSDOM } = require('jsdom'); const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', { url: envOptions.url, pretendToBeVisual: true }); global.window = dom.window; global.document = dom.window.document; global.navigator = dom.window.navigator; global.location = dom.window.location; const mod = require('./hexin-v.js'); // 目标文件导出签名函数 return function sign(bizParams) { const ts = Date.now(); const rand = Math.random().toString(36).slice(2, 10); return mod.generate(ts, rand, bizParams); // 按还原出的入参顺序调用 }; } const sign = createHexinV({ url: 'https://example.com/quote' }); console.log(sign('symbol=000001&type=quote'));逻辑说明:工厂函数把环境初始化收拢在一处,业务侧只关心bizParams。ts和rand每次调用现取,保证签名新鲜。参数说明:envOptions.url必须和真实请求页一致,否则依赖location的签名会错;mod.generate的签名以你实际还原出的导出名为准,不同版本可能叫getV、sign或别的名字。
封装好之后,建议加一层校验:拿真实抓包的一组输入和输出做成测试用例,每次改动都跑一遍。我吃过亏——有次升级了jsdom版本,navigator的默认属性变了,签名静默算错,直到线上请求大面积失败才发现。从那以后我每次动环境依赖,都强制走一遍固定用例回归,确认签名逐字符一致才敢上线。希望帮到你。
本文还有配套的精品资源,点击获取