
1. 猿人学第六题逆向分析实战第一次看到猿人学第六题时我注意到页面加载后会有明显的延迟控制台不断输出加密日志。这种特征通常意味着存在前端反调试机制需要先解决基础防护才能进行后续分析。1.1 环境准备与工具链工欲善其事必先利其器我准备了以下工具组合Chrome DevTools带条件断点功能Fiddler Everywhere流量分析WebStorm代码格式化AST反混淆工具Babel插件版注意不要直接使用浏览器的美化功能某些混淆代码会被错误解析。建议先将JS文件保存本地后用专业IDE处理。1.2 初步反调试对抗打开题目页面后立即按下F12进入调试模式。这时观察到三种反调试手段定时器检测每300ms检查console是否打开debugger语句在关键函数入口处插入函数重定义重写toString/caller等属性应对方案// 在控制台先执行这些代码阻断反调试 Object.defineProperty(window, console, { get: () window._console, set: () {} }); Function.prototype.constructor function() {};2. 核心加密逻辑解析2.1 请求参数特征分析通过Fiddler抓包发现有效请求必须包含三个关键参数t13位时间戳sign32位MD5值tokenBase64编码字符串其中sign的生成最复杂涉及多个动态变量function getSign() { let n Date.now(); let r Math.floor(Math.random() * 9e5) 1e5; let i md5(window._secretKey n r); return { t: n, sign: i, token: btoa(r.toString()) }; }2.2 动态密钥追踪关键点在于window._secretKey的生成逻辑。通过调用栈回溯发现这个值是在页面初始化时通过WebSocket实时获取的let ws new WebSocket(wss://target.com/ws-key); ws.onmessage (e) { let data JSON.parse(e.data); window._secretKey data.key _ Date.now().toString(36); setTimeout(() delete window._secretKey, 3e4); };这里有个巧妙的设计密钥有效期只有30秒且每次连接都会变化。破解方案是Hook WebSocket构造函数let originalWS WebSocket; WebSocket function(url) { if(url.includes(ws-key)) { this.addEventListener(message, (e) { console.log(截获密钥:, e.data); }); } return new originalWS(url); };3. AST反混淆实战3.1 代码混淆特征核心加密函数被混淆成这种结构function _0x12ab5(a,b) { return _0x45cd[_0x56ee(0x1a3)](a,b); }使用Babel插件进行反混淆的步骤安装babel-parser和babel-generator编写识别十六进制常量的visitor处理字符串数组的移位操作关键转换代码const parser require(babel/parser); const generator require(babel/generator); function deobfuscate(code) { let ast parser.parse(code); traverse(ast, { StringLiteral(path) { // 处理字符串解密逻辑 }, MemberExpression(path) { // 解析数组引用 } }); return generator.default(ast).code; }3.2 动态执行追踪某些参数会在运行时动态生成需要结合AST分析和执行追踪在加密函数入口打条件断点记录所有变量赋值操作提取关键运算逻辑到本地环境// 示例提取MD5算法核心 let _md5 {}; function md5(str) { let state [0x67452301, 0xEFCDAB89]; // ...完整运算过程 _md5.state state; return hex(state); }4. 完整解决方案实现4.1 自动化请求流程最终实现的请求生成逻辑async function getData(page) { let {t, sign, token} await getDynamicKey(); let params new URLSearchParams(); params.append(page, page); params.append(t, t); params.append(sign, sign); params.append(token, token); let res await fetch(/api/data, { method: POST, body: params }); return res.json(); }4.2 密钥维护机制设计一个自动更新密钥的守护进程class KeyManager { constructor() { this.key null; this.expire 0; this.ws null; } connect() { this.ws new WebSocket(wss://target.com/ws-key); this.ws.onmessage (e) { let data JSON.parse(e.data); this.key data.key; this.expire Date.now() 25000; // 提前5秒更新 }; } getKey() { if(!this.key || Date.now() this.expire) { this.connect(); return new Promise(resolve { let timer setInterval(() { if(this.key) { clearInterval(timer); resolve(this.key); } }, 500); }); } return Promise.resolve(this.key); } }5. 常见问题与调试技巧5.1 典型错误排查表现象可能原因解决方案返回403密钥过期检查KeyManager更新时间差sign校验失败时间戳不同步使用服务器时间而非本地时间连接中断WebSocket被检测修改WS握手头中的Origin字段5.2 性能优化建议缓存AST解析结果对不变的混淆代码只解析一次Worker多线程加解密操作放在Web Worker中请求合并相同参数签名可重复使用3-5秒// 签名缓存示例 let signCache new Map(); function getSign(params) { let key JSON.stringify(params); if(signCache.has(key)) { let {sign, expire} signCache.get(key); if(Date.now() expire) return sign; } // ...计算新签名 signCache.set(key, { sign: newSign, expire: Date.now() 3000 }); return newSign; }5.3 高级对抗技巧当遇到更复杂的防护时可以尝试内存漫游通过Memory dump查找关键变量WASM Hook对WebAssembly函数进行拦截浏览器插件开发定制插件实现自动化// WASM Hook示例 let oldInstantiate WebAssembly.instantiate; WebAssembly.instantiate function(buffer, imports) { if(buffer.byteLength 10000) { let module new WebAssembly.Module(buffer); let exports WebAssembly.Module.exports(module); exports.forEach(exp { if(exp.name encrypt) { // 替换导出函数 } }); } return oldInstantiate(buffer, imports); };