当前位置: 首页 > news >正文

AST解混淆与JS签名算法Python复现实战指南

1. 这不是“又一个爬虫教程”,而是一份2026年仍在生效的反混淆作战手记

你肯定见过这样的场景:凌晨三点,刚写好的爬虫突然返回一串乱码,控制台里满屏undefined is not a function;F12切到Sources面板,看到的不是JS源码,而是一整页密密麻麻的_0x4a7b[0](_0x4a7b[1], _0x4a7b[2]);用格式化工具一按,代码缩进整齐了,但变量名还是_0x123a_0x5f7c,像一本用十六进制密码写成的日记。更糟的是,你尝试替换字符串、模拟调用,结果接口直接返回{"code":403,"msg":"invalid signature"}——连请求都发不出去。这不是玄学,是2026年主流站点仍在大规模部署的AST级动态混淆+运行时算法校验双保险机制。它不依赖简单的User-Agent或Cookie,而是把核心签名逻辑编译成抽象语法树后打散、重命名、插入死代码、再注入运行时环境检测,让静态分析失效,让动态调试卡在无限debugger断点里。本文标题里的“AST解混淆+算法还原+Python复现”不是三个并列动作,而是一条不可跳过的链式流程:先从AST层面逆向出原始控制流与数据流,再从中剥离出真正参与签名计算的纯函数逻辑,最后用Python实现等价数学模型,绕过JS引擎依赖。适合两类人:一是已能熟练写Selenium和Requests但总在关键接口卡壳的中级爬虫工程师;二是想系统理解现代前端保护机制、不再靠“找加密入口+扣JS”的安全/逆向初学者。全文不讲概念,只拆真实对抗过程——从Chrome DevTools里第一行报错开始,到本地Python脚本稳定输出合法sign,每一步都带原理、带陷阱、带我亲手踩过的坑。

2. 为什么必须从AST入手?——传统“扣JS”在2026年已彻底失效

2.1 传统方案的三大死穴:调试器失效、字符串不可信、环境检测无解

过去我们习惯的“扣JS”流程是:F12 → 找到发起请求的JS文件 → Ctrl+F搜索signtokensignature→ 定位生成函数 → 复制整个函数到Python里用exec或PyExecJS执行。这套方法在2026年面对主流电商、金融、政企类站点时,失败率接近100%。原因有三,且层层递进:

第一,调试器被主动反制。现在90%以上的混淆JS会在入口处插入if (window.navigator.webdriver || window.outerHeight < 100) { debugger; },但更致命的是debugger语句本身已被混淆为eval('\x64\x65\x62\x75\x67\x67\x65\x72'),甚至嵌套在setTimeout里延迟触发。你以为关掉断点就安全了?实际代码会检测console.log.toString()是否被重写、performance.now()调用间隔是否异常、甚至检查DevTools窗口是否处于激活状态(通过document.hasFocus()配合visibilitychange事件)。我实测某券商APP的登录签名JS,只要DevTools打开超过8秒,就会触发window.location.reload()强制刷新页面——你根本来不及下断点。

第二,字符串常量被动态拼接且含校验逻辑。比如一个看似简单的sign = md5(timestamp + secret + data),在混淆后可能变成:

var _0x1a2b = ['md5', 'timestamp', 'secret', 'data', 'split', 'join']; function _0x3c4d(_0x5e6f) { var _0x7g8h = _0x5e6f[_0x1a2b[4]](''); return _0x7g8h[_0x1a2b[5]](_0x1a2b[0]); } var sign = _0x3c4d(_0x1a2b[1] + _0x1a2b[2] + _0x1a2b[3]);

表面看只是字符串数组索引,但_0x1a2b数组本身可能在运行时被atob()解码、reverse()翻转,甚至根据当前时间戳动态生成。更狠的是,_0x3c4d函数内部会校验传入字符串是否包含'timestamp'字面量,若直接传'1712345678'则返回空字符串——你复制的JS在Python里跑通了,但结果永远是错的。

第三,环境检测与算法强耦合。2026年的签名算法不再是独立函数,而是与浏览器环境深度绑定。例如某政务平台的getSign()函数,其核心逻辑是:

function getSign(data) { const t = Date.now(); const r = Math.random().toString(36).substr(2, 9); // 关键:这里调用了一个隐藏的WebAssembly模块 const w = wasmModule.calc(t, r, navigator.userAgent); return md5(data + w + t); }

你扣JS时能看到wasmModule.calc,但wasmModule是通过fetch('xxx.wasm')动态加载的,且WASM二进制文件本身经过LLVM-OBFUSCATOR加壳,内存中解密后才初始化。这意味着:不启动完整浏览器环境,你连wasmModule对象都拿不到;而启动浏览器,又触发前述的环境检测。死循环。

提示:当你发现JS里出现WebAssembly.instantiateStreamingnew Worker('xxx.js')import('./chunk-xxx.js')等动态加载模式时,立刻放弃“扣JS”思路。这标志着你面对的是AST级混淆,必须回归语法树层面。

2.2 AST解混淆:为什么它是唯一破局点?

AST(Abstract Syntax Tree,抽象语法树)是JS引擎将源码解析后的中间表示。它剥离了所有语法糖、空格、注释,只保留程序的结构本质:哪些是变量声明、哪些是函数调用、哪些是条件分支、哪些是二元运算。混淆器对JS的操作,本质上都是对AST节点的变换:

  • 字符串数组提取:将'abc'变成_0x123['0'],对应AST中MemberExpression节点指向ArrayExpression的索引;
  • 控制流扁平化:把if (a) { b() } else { c() }变成switch(_0x456) { case 1: b(); break; case 2: c(); break; },对应AST中SwitchStatement替代IfStatement
  • 死代码插入:添加if (false) { console.log('dead'); },对应AST中IfStatementtest属性为Literal(false)
  • 标识符重命名:将function calcSign()变成function _0x789(),对应AST中Identifier节点的name属性变更。

关键在于:这些变换都在AST层面完成,而AST是确定性的、可逆的。只要你能拿到混淆前的原始AST(或足够接近的形态),就能通过遍历节点、识别变换模式、应用逆向规则,逐步还原出原始逻辑。这不像动态调试受制于环境,也不像字符串替换受制于上下文,它是纯粹的程序结构分析。

我用一个真实案例说明:某招聘网站的简历投递接口,其签名函数混淆后有237行,含12层嵌套try/catch和7个eval调用。用传统方法,我在Chrome里调试了6小时,最终发现eval里执行的字符串是atob('aHR0cHM6Ly9hcGkueHh4LmNvbS9zaWduYXR1cmU='),解码后是https://api.xxx.com/signature——这根本不是算法,而是又一个网络请求!而用AST解析器(如acorn)加载该JS,遍历所有CallExpression节点,过滤出callee.name === 'atob'的调用,直接提取其arguments[0].value,30秒内就拿到了原始URL。这就是AST的力量:它不关心代码怎么运行,只关心代码长什么样。

2.3 2026年可用的AST工具链:轻量、可靠、免环境

2026年,我们不需要复杂的IDE或在线服务。一套本地化、命令行友好的工具链足以应对绝大多数场景。核心组件只有三个,全部用npm安装,无任何浏览器依赖:

  1. acorn(v8.10+):业界最轻量、最标准的JS解析器。它将JS源码解析为符合ESTree规范的AST对象,体积仅120KB,解析速度比Babel快3倍。关键优势是零副作用——它不执行代码,不加载模块,不访问网络,只做纯文本到树的转换。安装命令:npm install acorn

  2. escodegen(v2.4+)acorn的反向工具,将AST对象重新生成可读JS代码。它支持自定义生成规则,比如强制展开所有MemberExpression为字面量、删除所有DebuggerStatement节点。安装命令:npm install escodegen

  3. estraverse(v5.3+):AST遍历器。提供traverse方法,让你以深度优先顺序访问每个节点,并在enter/leave钩子中修改节点属性。它是实现“逆向变换”的核心。安装命令:npm install estraverse

这三者组合,构成一个完整的AST处理流水线:

原始混淆JS → acorn.parse() → AST对象 → estraverse.traverse() → 修改节点 → escodegen.generate() → 还原JS

为什么不用Babel?因为Babel的@babel/parser虽强大,但默认启用大量插件(如JSX、TypeScript),解析速度慢,且错误提示不友好。而acorn专精JS,对ES2023语法支持完善,报错时直接指出字符位置(如Unexpected token '}' at 123:45),排查效率极高。我对比过10个主流站点的混淆JS,acorn平均解析耗时86ms,@babel/parser为210ms,且后者在遇到export default未声明时会抛出模糊的SyntaxError: Unexpected token,而acorn明确提示'export' is not allowed here

注意:不要试图用正则表达式“匹配混淆字符串”。我曾见有人写/_[0-9a-f]{4}\['\d+'\]/g来替换,结果因混淆器插入的/* comment */导致正则跨行失效,修复后又因_0x123[0+1]这种动态索引崩溃。AST是结构化数据,正则是字符串模式,二者不在同一维度。坚持用AST,这是2026年爬虫工程师的基本素养。

3. AST解混淆实战:从237行乱码到12行可读函数

3.1 第一步:获取原始混淆JS并确认AST可解析性

对抗始于第一步:确保你能拿到干净、完整的混淆JS。很多人卡在这一步,以为F12里看到的就是全部,其实不然。现代站点常用以下三种方式加载混淆逻辑:

  • 内联Script标签:HTML中<script>var _0x123=['a','b'];...</script>。这是最简单的情况,右键“查看网页源代码”,Ctrl+F搜索<script>,复制内容即可。
  • 外部JS文件<script src="/static/js/chunk-456.js"></script>。此时需在Network面板中筛选JS类型,找到对应文件,右键“Open in Sources panel”,再右键“Copy content”。
  • 动态Importimport('./chunk-789.js').then(m => m.signFunc())。这种情况最棘手,因为chunk-789.js的URL可能是动态生成的(如/static/js/chunk-${Math.floor(Math.random()*1000)}.js)。解决方案是:在Application → Service Workers中禁用所有Service Worker,然后清空缓存并硬性刷新(Ctrl+F5),再抓包。你会发现,首次加载时,HTML里会明文写出<script src="/static/js/chunk-789.js?v=20260401">——版本号就是破解钥匙。

拿到JS后,先用acorn验证是否可解析:

# 将JS保存为 obfuscated.js node -e " const acorn = require('acorn'); const fs = require('fs'); try { const code = fs.readFileSync('obfuscated.js', 'utf8'); const ast = acorn.parse(code, { ecmaVersion: 2023, sourceType: 'module' }); console.log('✅ AST解析成功,共', ast.body.length, '个顶层节点'); } catch (e) { console.error('❌ AST解析失败:', e.message); }"

如果报错Unexpected token,大概率是混淆器插入了非法字符(如零宽空格\u200b)。用VS Code打开文件,开启“显示所有字符”(Ctrl+Shift+P → “Toggle Render Whitespace”),删除所有非打印字符。2026年,约15%的混淆JS会故意插入零宽字符破坏解析,这是初级反制手段。

3.2 第二步:识别并还原字符串数组(String Array Reconstruction)

这是AST解混淆的第一道关卡。几乎所有混淆器(如javascript-obfuscator、Obfuscator.io)都会将字符串常量提取到一个全局数组中,再用索引访问。原始代码:

function getSign(data) { return md5(data + 'secret_key' + Date.now()); }

混淆后:

var _0x123 = ['md5', 'secret_key', 'getTime', 'now', 'Date', 'data', '+']; function _0x456(_0x789) { return _0x123[0](_0x789 + _0x123[1] + _0x123[4][_0x123[2]]()[_0x123[3]]()); }

目标:将_0x123[1]还原为'secret_key'_0x123[4][_0x123[2]]()[_0x123[3]]()还原为Date.now()

estraverse遍历AST,定位VariableDeclarator节点(变量声明),检查其init属性是否为ArrayExpression

const acorn = require('acorn'); const estraverse = require('estraverse'); const fs = require('fs'); const code = fs.readFileSync('obfuscated.js', 'utf8'); const ast = acorn.parse(code, { ecmaVersion: 2023 }); // 步骤1:找到字符串数组声明,如 `var _0x123 = ['a','b'];` let stringArrayName = null; let stringArrayValues = []; estraverse.traverse(ast, { enter(node) { if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier' && node.init?.type === 'ArrayExpression') { stringArrayName = node.id.name; stringArrayValues = node.init.elements.map(el => el?.type === 'Literal' ? el.value : null ); // 找到即退出,避免重复赋值 this.break(); } } }); console.log('🔍 识别到字符串数组:', stringArrayName, '共', stringArrayValues.length, '项'); // 输出:🔍 识别到字符串数组: _0x123 共 7 项

接着,遍历所有MemberExpression节点(即obj[prop]形式),当object.name等于stringArrayNameproperty.typeLiteral(字面量索引)时,将其替换为对应的字符串字面量:

estraverse.traverse(ast, { enter(node, parent) { // 匹配 _0x123[0] 形式 if (node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === stringArrayName && node.property.type === 'Literal') { const index = node.property.value; const replacement = stringArrayValues[index]; if (replacement !== undefined && replacement !== null) { // 创建新的Literal节点替换原MemberExpression const newLiteral = { type: 'Literal', value: replacement, raw: `'${replacement}'` }; // 替换父节点中的该子节点 if (parent.type === 'BinaryExpression' && parent.left === node) { parent.left = newLiteral; } else if (parent.type === 'BinaryExpression' && parent.right === node) { parent.right = newLiteral; } else if (parent.type === 'CallExpression' && parent.callee === node) { parent.callee = newLiteral; } // 强制跳过后续遍历,避免重复处理 this.skip(); } } } });

这段代码的核心是精准定位并原地替换。注意this.skip()的使用——它防止遍历器继续深入已替换的节点,避免因节点结构变化导致崩溃。实测某电商JS,此步骤可将237行代码压缩至189行,消除所有_0x123[0]类引用。

3.3 第三步:剥离死代码与无用分支(Dead Code Elimination)

混淆器常插入大量if (false) {...}while (0) {...}try { throw 0; } catch(e) {}等死代码,目的是增加静态分析难度。AST层面,它们对应IfStatement(test为Literal(false))、WhileStatement(test为Literal(0))、TryStatement(body中仅有ThrowStatement)。

我们的策略是:遍历所有IfStatement,检查其test属性。若test.type === 'Literal'test.value === false,则直接移除整个IfStatement节点:

estraverse.traverse(ast, { enter(node, parent) { // 移除 if (false) { ... } if (node.type === 'IfStatement' && node.test.type === 'Literal' && node.test.value === false) { // 在父节点中移除该IfStatement if (parent.type === 'BlockStatement') { const index = parent.body.indexOf(node); if (index > -1) { parent.body.splice(index, 1); } this.skip(); } } // 移除 while (0) { ... } if (node.type === 'WhileStatement' && node.test.type === 'Literal' && node.test.value === 0) { if (parent.type === 'BlockStatement') { const index = parent.body.indexOf(node); if (index > -1) { parent.body.splice(index, 1); } this.skip(); } } } });

更复杂的是try/catch。有些混淆器会将真实逻辑放在catch块中,而try块里是throw new Error('fake')。这时不能简单删除,需判断catch块是否被外部引用。安全做法是:只删除try块为空、且catch块中无returnthrow的节点。我封装了一个函数:

function isSafeToRemoveTryCatch(node) { // try块为空 const isEmptyTry = node.block.body.length === 0; // catch块中无return/throw const hasReturnOrThrow = node.handler?.body?.some(stmt => stmt.type === 'ReturnStatement' || stmt.type === 'ThrowStatement' ) === true; return isEmptyTry && !hasReturnOrThrow; } // 在遍历中调用 if (node.type === 'TryStatement' && isSafeToRemoveTryCatch(node)) { if (parent.type === 'BlockStatement') { const index = parent.body.indexOf(node); if (index > -1) { parent.body.splice(index, 1); } this.skip(); } }

此步骤后,代码行数通常减少30%-40%。某招聘网站JS经此处理,从189行降至112行,所有debugger语句和console.log调用均被清除。

3.4 第四步:还原控制流扁平化(Control Flow Deobfuscation)

这是最考验功底的一步。控制流扁平化将线性代码打散为switch+while(true)结构,例如:

// 原始 function calc(a, b) { if (a > b) return a * 2; else return b * 3; } // 混淆后 function calc(a, b) { var _0x1 = 0; while (true) { switch (_0x1) { case 0: if (a > b) _0x1 = 1; else _0x1 = 2; break; case 1: return a * 2; case 2: return b * 3; } } }

目标:将switch结构还原为原始if/else

核心思路是构建控制流图(CFG):遍历所有SwitchCase,记录每个case_0x1值(即consequent中的AssignmentExpression右侧值)和跳转目标。然后,将case按顺序重组为if/else if/else链。

由于篇幅限制,此处给出关键逻辑(完整代码见GitHub仓库):

// 找到 while(true) { switch(...) { ... } } 结构 estraverse.traverse(ast, { enter(node, parent) { if (node.type === 'WhileStatement' && node.test.type === 'Literal' && node.test.value === true && node.body.type === 'BlockStatement' && node.body.body.length === 1 && node.body.body[0].type === 'SwitchStatement') { const switchNode = node.body.body[0]; const cases = switchNode.cases; // 构建跳转映射:caseValue -> nextCaseValue const jumpMap = new Map(); for (let i = 0; i < cases.length; i++) { const caseNode = cases[i]; const caseValue = caseNode.test?.value; if (caseValue === undefined) continue; // 查找case块中唯一的AssignmentExpression,如 `_0x1 = 1;` const assign = caseNode.consequent.find(stmt => stmt.type === 'ExpressionStatement' && stmt.expression.type === 'AssignmentExpression' && stmt.expression.left.type === 'Identifier' && stmt.expression.right.type === 'Literal' ); if (assign) { const nextValue = assign.expression.right.value; jumpMap.set(caseValue, nextValue); } } // 根据jumpMap,将cases重排为if/else链 // (具体实现略,涉及AST节点重构) this.skip(); } } });

此步骤需深度理解AST节点类型(IfStatementConditionalExpressionBlockStatement),并手动构造新节点。我建议初学者先用escodegen.generate()打印中间AST,对照原始代码理解节点关系。实测表明,正确还原控制流后,函数逻辑清晰度提升80%,为下一步算法还原奠定基础。

4. 算法还原:从JS函数到Python数学模型的精确映射

4.1 为什么不能直接用PyExecJS?——环境差异导致的精度灾难

很多开发者认为:“JS能跑,Python用PyExecJS执行一样JS,结果应该一致”。这是2026年最大的认知误区。问题出在浮点数精度、位运算行为、日期处理三大领域:

  • 浮点数精度:JS的Number是IEEE 754双精度,但PyExecJS底层V8引擎在不同系统上可能启用--harmony-numeric-seed等实验性标志,导致0.1 + 0.2在某些机器上返回0.30000000000000004,另一些机器返回0.3。而签名算法常对浮点结果取整(Math.floor(x * 1000)),微小差异直接导致sign不匹配。

  • 位运算行为:JS中>>>(无符号右移)对负数会先转为无符号32位整数,而Python的>>是算术右移。例如-1 >>> 0在JS中是4294967295,Python中-1 >> 0仍是-1。某金融平台的签名核心是((a ^ b) >>> 0).toString(16),直接移植会导致哈希值完全错误。

  • 日期处理:JS的Date.now()返回毫秒时间戳,但PyExecJS执行时,JS上下文的Date对象可能被混淆器重写(如Date = function(){return 1234567890;}),而Python的time.time()无法模拟这种篡改。

我做过严格测试:同一段JS签名代码,在Chrome DevTools、Node.js、PyExecJS(Ubuntu)、PyExecJS(Windows)四个环境中运行,结果一致率仅为68%。其中32%的差异源于上述底层行为,而非算法逻辑。

提示:当你发现PyExecJS输出的sign偶尔正确、偶尔错误,且错误时console.log显示的中间值与Chrome中不一致,请立即放弃该方案。这不是你的代码问题,是环境不可控。

4.2 算法还原三原则:纯函数、无副作用、可验证

真正的算法还原,不是复制JS代码,而是提取其数学本质。遵循三个铁律:

第一,纯函数原则:还原出的Python函数,输入参数必须完全由调用方提供,不依赖任何全局变量、window对象、document属性。例如JS中navigator.userAgent,必须作为参数传入Python函数,而非在函数内调用platform.uname()——因为混淆JS可能篡改navigator,而Python无法模拟篡改。

第二,无副作用原则:函数内部不能有printlogging、网络请求、文件IO等任何外部交互。它必须是一个纯粹的数学映射:(data, timestamp, user_agent) → sign。这保证了可测试性——你可以用Chrome中捕获的真实参数,喂给Python函数,验证输出是否100%一致。

第三,可验证原则:每一步中间计算,都必须能在Chrome DevTools中console.log出来,并与Python中print结果逐行比对。例如,JS中var x = a + b; var y = x * 2;,Python中必须有x = a + b; print('x=', x); y = x * 2; print('y=', y)。不验证的还原,等于没还原。

以某政务平台的真实签名算法为例,其JS核心逻辑是:

function getSign(data, ts) { var key = 'abc123'; var s = data + key + ts; var h = CryptoJS.SHA256(s).toString(CryptoJS.enc.Hex); var r = h.substr(0, 16) + h.substr(-16); return r.toUpperCase(); }

还原为Python时,不能直接调用pycryptodomeSHA256,因为CryptoJS.SHA256的输入编码是UTF-16LE(JS默认),而pycryptodome默认UTF-8。必须显式指定:

import hashlib def get_sign(data: str, ts: str, key: str = 'abc123') -> str: # JS中CryptoJS.SHA256输入是UTF-16LE编码 s = (data + key + ts).encode('utf-16le') h = hashlib.sha256(s).hexdigest() r = h[:16] + h[-16:] return r.upper() # 验证:在Chrome中 console.log(getSign('test', '1712345678')) # Python中 print(get_sign('test', '1712345678')) # 两者必须完全相等

4.3 关键算法组件的手动实现:MD5、Base64、AES的Python等价体

2026年,混淆JS中高频出现的算法组件,往往需要手动实现Python版本,而非调用现成库。原因有二:一是混淆器可能修改算法细节(如MD5的初始向量、AES的填充方式),二是第三方库行为与JS库存在隐式差异。

MD5手动实现要点

  • JS的CryptoJS.MD5默认对字符串进行UTF-8编码,但若字符串含中文,CryptoJS.enc.Utf8.parse(str)会先转UTF-8再处理。Python中必须用str.encode('utf-8')
  • 初始向量(IV)和轮函数(round function)必须与RFC 1321完全一致。我推荐直接使用hashlib.md5(),但需确保输入字节流与JS完全相同。验证方法:用CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse('hello'))得到aGVsbG8=,Python中base64.b64encode('hello'.encode('utf-8')).decode()必须输出相同结果。

Base64编解码陷阱: 混淆JS常用btoa()/atob(),但btoa只接受Latin-1字符。若字符串含中文,需先encodeURIComponentbtoa。Python中对应:

import base64 from urllib.parse import quote, unquote def js_btoa(s: str) -> str: # 模拟JS btoa对Unicode的处理 return base64.b64encode( quote(s, safe='').encode('utf-8') ).decode('utf-8') def js_atob(s: str) -> str: decoded = base64.b64decode(s) return unquote(decoded.decode('utf-8'))

AES-CBC手动实现: 某物流平台用CryptoJS.AES.encrypt(data, key, {mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7})。Python中需用pycryptodome,但关键参数必须匹配:

  • Key长度:CryptoJSkey字符串会自动用MD5哈希为128位,Python中需hashlib.md5(key.encode()).digest()
  • IV:CryptoJS默认用随机IV,但签名算法中IV常固定为'0000000000000000'(16字节)。
  • Padding:Pkcs7填充,Python中用PKCS7(128).pad(data.encode(), 16)

完整示例:

from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from Crypto.Hash import MD5 import base64 def aes_encrypt_js_style(data: str, key: str) -> str: # JS中CryptoJS用MD5(key)生成128位key key_bytes = MD5.new(key.encode()).digest() iv = b'0000000000000000' # 固定IV cipher = AES.new(key_bytes, AES.MODE_CBC, iv) # PKCS7填充,block_size=16 padded_data = pad(data.encode('utf-8'), 16) encrypted = cipher.encrypt(padded_data) return base64.b64encode(encrypted).decode('utf-8')

4.4 实战案例:某电商“秒杀签名校验”的全流程还原

我们以某头部电商的“限时秒杀”接口为例,完整走一遍从AST解混淆到Python复现的流程。接口URL为POST https://api.xxx.com/seckill/sign,请求体含{ "itemId": "123456", "userId": "789012", "ts": 1712345678 },响应要求sign字段。

Step 1:AST解混淆

  • 获取JS:Network中找到seckill-sign.js,大小1.2MB,含3个eval调用。
  • acorn解析成功,识别出主字符串数组_0xabcdef,共142项。
  • 还原字符串后,代码降至893行;剥离死代码后,剩621行;控制流还原后,剩417行。
  • 最终定位到核心函数_0x123456(data, ts),23行。

Step 2:算法分析函数核心逻辑:

function _0x123456(data, ts) { var _0x789 = CryptoJS.enc.Utf8.parse(data); var _0xabc = CryptoJS.enc.Utf8.parse('salt_2026'); var _0xdef = CryptoJS.SHA256(_0x789.concat(_0xabc)).toString(); var _0xghi = CryptoJS.enc.Base64.parse(_0xdef); var _0xjkl = CryptoJS.enc.Base64.stringify(_0xghi); return _0xjkl.substring(0, 16) + _0xjkl.substring(16, 32); }

关键发现:CryptoJS.enc.Base64.parsestringify是互逆操作,_0xjkl其实就是_0xdef的Base64编码。而_0xdef是SHA256哈希值的十六进制字符串,长度为64。因此,_0xjkl是64字节字符串的Base64编码(长度≈86),substring(0,16)取前16字符,substring(16,32)

http://www.jsqmd.com/news/881305/

相关文章:

  • 基于特征解耦VAE的公平机器学习:消除工效学评估中的算法偏见
  • Unity物体世界坐标实时保存到TXT的稳健方案
  • 多光谱LiDAR点云树种分类:3D深度学习、2D深度学习与机器学习的实战对比
  • Selenium运行原理深度解析:从WebDriver协议到浏览器引擎四层架构
  • 别再只会用cp了!用dd命令给硬盘做‘全身体检’和‘克隆手术’(附实战命令)
  • 不止于播放:用VideoPlayer脚本控制实现一个简易的Unity视频播放器UI
  • Windows彻底关机再进Ubuntu就不报ACPI错了?聊聊双系统引导那些“玄学”问题
  • 处理器芯片自动化设计:QiMeng系统与AI驱动EDA技术
  • 告别跨平台烦恼:详解Mac磁盘工具里那个神秘的‘APFS容器’,以及彻底删除它的正确姿势
  • 分子动力学与机器学习融合:高效设计高性能可回收塑料
  • 量子机器学习在时间序列预测中的性能基准研究与实践复盘
  • Fay数字人框架服务器安全基线实战指南
  • Java NIO.2 异步字节通道:AsynchronousByteChannel 接口契约与并发安全深度剖析
  • MFCC与随机森林量化分析汉语母语者英语发音的声学特征
  • Unity军事场景模块化搭建:战壕、地堡与掩体的工业化管线
  • 机器学习赋能银河系考古:CatBoost模型高精度预测恒星年龄
  • Armv9 SME架构FMOP4A指令:混合精度矩阵运算优化
  • Unity视频控制器架构:延迟播放、事件总线与多视频管理
  • 初识递归算法
  • 亚太赫兹ISAC技术:机器联觉与多模态融合的6G通信
  • 基于神经网络的短码长ISAC双功能信号联合优化设计
  • 华硕天选一代无线网卡断网
  • Windows Server 2019真实渗透实战:从WebShell到域控的完整红队链路
  • 机器学习预测暗物质晕形成时间:随机森林与CNN在天体物理中的应用
  • Go-File安全加固手册:防止未授权访问的8个关键配置
  • UE5 GAS实战:用一张曲线表格(Curve Table)搞定RPG游戏中的等级成长与回复效果
  • 小型本地LLM框架在教育领域的应用与实现
  • Java NIO 1.0 架构基石:SelectorProvider 源码深度剖析与 SPI 工厂模式
  • 开源社区贡献者画像分析:核心与外围贡献者的行为差异与影响
  • Elastic stack 技术栈学习(七)—— kibana中索引的基本操作(创建、删除、更新、查看)以及文档的基本操作