十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JS逆向实战:hexin-v.js签名生成与补环境复现指南

JS逆向实战:hexin-v.js签名生成与补环境复现指南 简介这份资源聚焦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 htmlhtmlbody/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 timestamp1700000000randabc123bizxxx; 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 symbol000001typequote; // 按还原出的顺序拼接 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 htmlhtmlbody/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(symbol000001typequote));逻辑说明工厂函数把环境初始化收拢在一处业务侧只关心bizParams。ts和rand每次调用现取保证签名新鲜。参数说明envOptions.url必须和真实请求页一致否则依赖location的签名会错mod.generate的签名以你实际还原出的导出名为准不同版本可能叫getV、sign或别的名字。封装好之后建议加一层校验拿真实抓包的一组输入和输出做成测试用例每次改动都跑一遍。我吃过亏——有次升级了jsdom版本navigator的默认属性变了签名静默算错直到线上请求大面积失败才发现。从那以后我每次动环境依赖都强制走一遍固定用例回归确认签名逐字符一致才敢上线。希望帮到你。本文还有配套的精品资源点击获取
返回列表