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抓包发现,有效请求必须包含三个关键参数:
t:13位时间戳sign:32位MD5值token:Base64编码字符串
其中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); };