关于 js:8. 反调试与混淆识别
一、常见反调试手段识别
1. debugger
死循环(阻塞调试器)
样例代码:
while (true) {debugger;
}
原理:
-
每次执行到
debugger
语句,如果 DevTools 打开,将自动触发断点。 -
如果在死循环中,调试器会被频繁打断,卡死页面或无法操作。
特征识别:
-
出现在自执行函数中
(function(){...})()
,或者 setInterval、setTimeout 里。 -
常与加密代码混合,隐藏在核心逻辑附近。
应对方法:
-
打开 Chrome DevTools → 设置 → 关闭 "暂停于异常" 和自动断点。
-
手动删除
debugger
,或用 Babel/AST 脚本批量清除:
traverse(ast, {DebuggerStatement(path) {path.remove();}
});
2. 时间延迟检测(检测调试耗时)
样例代码:
const start = Date.now();
for (let i = 0; i < 1e8; i++) {}
const end = Date.now();
if (end - start > 500) {alert("你调试我了!");
}
原理:
-
调试器中手动打断点,会暂停脚本执行。
-
利用
Date.now()
或performance.now()
计算耗时,如果超过阈值,说明你在调试。
特征识别:
-
Date.now()
、performance.now()
出现多次用于比对时间差。 -
常用于函数入口、加密函数前后。
应对方法:
-
重写时间函数,返回恒定时间:
Date.now = () => 123456789;
performance.now = () => 100;
- 或删除时间差判断代码段(使用 AST 或脚本)
3. 函数串扰检测(检测函数是否被 hook)
样例代码:
const realAlert = alert;
alert = function () {console.log("你调试了吗?");return realAlert.apply(this, arguments);
};
原理:
-
检测是否篡改了系统函数,例如 alert、console、eval 等。
-
调试器经常 hook 函数,攻击者利用此逻辑检测调试行为。
特征识别:
-
检测
alert
,eval
,console.log
等函数是否被改写。 -
Function.prototype.toString()
经常联合使用,判断函数体内容是否[native code]
应对方法:
-
恢复原始函数:
alert = window.__proto__.alert;
console.log = window.__proto__.console.log;
- 用 Proxy 包装函数,同时伪装
toString()
输出。
4. Function.prototype.toString
检测(伪装函数原型)
样例代码:
if (alert.toString().indexOf('[native code]') === -1) {throw new Error('你修改了 alert 函数!');
}
原理:
-
JS 中原生函数
toString()
会返回[native code]
。 -
一旦你对
alert
之类做了 hook 或包装,这一特征就消失。
反制手段:
-
伪装 toString 输出:
alert = new Proxy(alert, {apply: function(target, thisArg, argumentsList) {return target.apply(thisArg, argumentsList);}
});
alert.toString = function() {return "function alert() { [native code] }";
};
5. window.console
检测
样例代码:
if (!window.console || typeof console.log !== "function") {throw new Error("console 被篡改!");
}
原理:
-
某些调试工具会临时修改 console 行为。
-
检测 console 存在性和完整性也是一种反调试手段。
解决方式:
-
恢复原始 console 对象。
-
或伪造 console 对象结构(手动定义全套方法)。
6. 异常捕获干扰调试
样例代码:
try {throw new Error("调试干扰");
} catch (e) {debugger;
}
原理:
-
在 try-catch 中故意嵌入 debugger、死循环或跳转逻辑,扰乱调试节奏。
-
有的框架会在异常栈中判断调试器是否存在。
应对方案:
-
修改 try-catch 中间代码,绕开 debugger。
-
用 AST 替换整个异常处理段。
7. 堆栈跟踪干扰(检测调试器存在)
样例代码:
function checkStack() {try {throw new Error();} catch (e) {if (e.stack.indexOf("Debugger") !== -1) {alert("发现调试器");}}
}
原理:
-
抛异常时,
Error.stack
会包含调用堆栈信息。 -
如果你在 Chrome DevTools 设置了断点,可能会在 stack 中体现。
应对:
-
重写 Error 构造函数或其 stack 属性,返回空堆栈。
8. requestAnimationFrame
反调试
样例代码:
let lastTime = performance.now();
function checkDebugger() {let now = performance.now();if (now - lastTime > 100) {alert("调试器卡住了浏览器");}lastTime = now;requestAnimationFrame(checkDebugger);
}
requestAnimationFrame(checkDebugger);
原理:
-
requestAnimationFrame
频率非常高(约每16ms执行一次),调试器会明显卡顿,时间差变大。
应对方法:
-
重写
requestAnimationFrame
,屏蔽检测逻辑。 -
或 hook
performance.now()
伪造时间。
9. 全局函数自毁 / 还原干扰
样例代码:
(function(){Function = null;
})();
原理:
-
故意销毁 JS 全局函数,阻止我们运行 eval / Function 等调试代码。
应对方法:
-
在中断点处提前
window.Function = Function
备份 -
或直接改写函数覆盖逻辑,让其失效
总结
技术名 | 检测方式 | 应对手段 |
---|---|---|
debugger 死循环 | debugger 频繁触发 | AST 移除 / DevTools 设置 |
时间延迟检测 | Date.now() / performance.now() | Hook 时间函数 / 删除判断 |
函数串扰 | 检查 alert、console、eval 是否被 hook | 还原函数 / Proxy + toString 伪装 |
toString 检测 | 判断函数是否为 [native code] | 改写 toString 方法 |
console 检测 | 是否存在 console 方法 | 伪造完整 console 对象结构 |
异常捕获干扰 | try-catch 嵌入 debugger | 替换整个代码块 |
堆栈跟踪调试检测 | 分析 Error.stack 内容 | 重写 Error / 清空 stack |
requestAnimationFrame 检测 | 检测浏览器执行频率 | 重写 rAF / 时间伪造 |
二、JavaScript 混淆手法
混淆的目标是让代码变得:
-
难读(降低可读性)
-
难调试(隐藏执行流程)
-
难还原(阻碍逆向)
1. 字符串拆分拼接
原始代码:
var key = "secretKey";
混淆后代码示例:
var _0x1a2b = "sec" + "ret" + "Key";
甚至更复杂:
var a = "s";
var b = "e";
var c = b + "c";
var d = a + c + "ret" + "Key"; // 最终是 "secretKey"
混淆目的:
-
隐藏字符串字面量,防止关键词命中(如 w、sign、token 等)。
识别与还原方法:
-
手动打断点查看变量值(或用 console.log 打印)
-
或者用 AST 脚本静态还原拼接结果(字符串折叠优化):
traverse(ast, {BinaryExpression(path) {if (path.node.operator === "+" &&t.isStringLiteral(path.node.left) &&t.isStringLiteral(path.node.right)) {path.replaceWith(t.stringLiteral(path.node.left.value + path.node.right.value));}}
});
2. base64 编码隐藏字符串
混淆代码示例:
var data = atob("c2VjcmV0S2V5"); // "secretKey"
原理:
-
用 Base64 编码字符串后解码执行,达到隐藏真实内容的目的。
-
通常与
eval
,Function
,new Image()
等组合使用。
混淆目的:
-
防止明文暴露关键字(如 UA、token、cookie、sign 等)
识别特征:
-
atob()
、btoa()
、Buffer.from(...).toString(...)
出现 -
明文字符串为 4 的倍数长度,末尾常有
=
填充符
还原方式:
-
浏览器或脚本执行:
console.log(atob("c2VjcmV0S2V5"));
3. eval
/ Function
动态执行
示例 1(eval):
eval("console." + "log('hello')");
示例 2(Function):
var code = "return 5 + 5;";
var fn = new Function(code);
console.log(fn()); // 输出 10
原理:
-
动态执行字符串拼接后的代码,防止静态分析。
-
通常与字符串拼接、base64 一起使用。
特征识别:
-
出现
eval()
,new Function()
,setTimeout(code, 0)
等动态执行语句。 -
动态生成的代码中常含混淆函数调用、加密入口、hook 代码。
还原与处理:
1)打补丁替换 eval
为 console.log
:
eval = console.log; // 打印出真实代码
2)拦截 Function:
window.Function = function(code) {console.log("[HOOKED FUNCTION]:", code);return () => {};
};
3)使用 AST 替换 eval:
traverse(ast, {CallExpression(path) {if (path.node.callee.name === 'eval') {path.node.callee.name = 'console.log';}}
});
4. 数组 + 索引跳转(Control Flow Flattening)
混淆代码示例:
var _0xabc = ["log", // 索引 0"Hello", // 索引 1"console", // 索引 2
];(function(arr) {var a = arr[2]; // "console"var b = arr[0]; // "log"var c = arr[1]; // "Hello"window[a][b](c); // => console.log("Hello")
})(_0xabc);
或者极端一点:
var arr = ["\x63\x6f\x6e\x73\x6f\x6c\x65", "\x6c\x6f\x67"];
window[arr[0]][arr[1]]("hi");
原理:
-
字符串存入数组,索引读取,打乱顺序。
-
控制流 flatten:真实执行路径隐藏在数组索引组合里。
特征识别:
-
有大数组存放字符串
-
数组通过索引访问,变量命名毫无意义(如
_0xabc[1]
) -
出现 “mapping 函数”:例如
_0xabc = function(i){ return arr[i]; }
还原方法:
-
手动记录数组内容 → 替换索引值
-
使用 Babel AST 扫描,把数组取值还原成字符串
-
工具推荐:
-
de4js
:https://lelinhtinh.github.io/de4js/ -
自写还原脚本处理全局映射数组
-
总结
混淆类型 | 特征 | 应对手段 |
---|---|---|
字符串拼接 | 多个字符串拼成关键字 | AST 静态还原 / 打断点查看 |
Base64 编码 | 出现 atob() , 字符串有 = | 解码查看 / Python 辅助 |
动态执行 | eval , Function , setTimeout | Hook 动态函数 / AST 替换打印 |
数组+索引跳转 | 大数组 + 随机索引访问 | 还原数组映射 / 替换所有访问语句 |
三、AST 抽象语法树分析
AST(抽象语法树) 是程序源代码的结构化、树状表示。
在 JavaScript 中,一段代码:
var a = "hello";
会被转换为一个 AST 树结构,描述这段代码的结构,比如:VariableDeclaration -> VariableDeclarator -> Identifier + Literal
它并不是运行代码,而是「代码结构本身」的抽象。
在 JS 混淆还原、定位加密函数、批量清理垃圾逻辑时,AST 是最强的静态分析工具:
任务 | AST 作用 |
---|---|
还原混淆(字符串拼接、数组索引) | 静态提取还原拼接结果 |
删除垃圾代码(无用判断等) | 删除某些结构的语句(如 if (false) ) |
替换函数调用 | 将 eval() 改为 console.log() 等 |
查找加密入口、核心参数生成 | 定位函数名和依赖链,追踪代码调用路径 |
Babel 是 JS 编译领域的核心工具,它能:
-
解析 JS 源码为 AST(
@babel/parser
) -
遍历和修改 AST(
@babel/traverse
) -
将 AST 重新生成代码(
@babel/generator
)
1. Babel AST 操作的基本流程
安装依赖(Node 环境)
npm install @babel/parser @babel/traverse @babel/generator
1)将代码解析成 AST
const parser = require('@babel/parser');
const code = 'var a = "he" + "llo";';const ast = parser.parse(code, {sourceType: 'module'
});
2)遍历 AST 并修改
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');traverse(ast, {BinaryExpression(path) {if (path.node.operator === '+' &&t.isStringLiteral(path.node.left) &&t.isStringLiteral(path.node.right)) {// 替换拼接表达式为结果字符串path.replaceWith(t.stringLiteral(path.node.left.value + path.node.right.value));}}
});
3)生成新代码
const generate = require('@babel/generator').default;
const output = generate(ast);
console.log(output.code); // var a = "hello";
2. 实战:还原混淆数组+索引跳转代码
示例混淆代码:
var _0xabc = ["se", "cret", "Key"];
var key = _0xabc[0] + _0xabc[1] + _0xabc[2];
还原目标:把 _0xabc[0]
直接替换成 "se"
等,变成:
var key = "secretKey";
解法思路:
-
找出数组声明内容,建立索引映射
-
遍历代码中所有的
MemberExpression
(属性访问) -
如果是
_0xabc[0]
,直接替换成"se"
的字符串字面量
Babel 脚本:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');const code = `
var _0xabc = ["se", "cret", "Key"];
var key = _0xabc[0] + _0xabc[1] + _0xabc[2];
`;const ast = parser.parse(code);let mapping = {};traverse(ast, {VariableDeclarator(path) {if (t.isIdentifier(path.node.id) &&t.isArrayExpression(path.node.init)) {const arrName = path.node.id.name;const elements = path.node.init.elements;mapping[arrName] = elements.map(e => e.value);}},MemberExpression(path) {const obj = path.node.object;const prop = path.node.property;if (t.isIdentifier(obj) &&mapping[obj.name] &&t.isNumericLiteral(prop)) {const value = mapping[obj.name][prop.value];path.replaceWith(t.stringLiteral(value));}}
});const output = generate(ast);
console.log(output.code);
3. 辅助工具推荐
工具 | 说明 |
---|---|
AST Explorer | 可视化查看 AST 结构,非常适合新手理解 |
Babel + Node 脚本 | 实际静态还原代码 |
Chrome DevTools | 配合调试、打断点验证逻辑是否还原成功 |
总结
AST 是在面对 JS 混淆与参数还原时,最强的“静态分析武器”,一旦掌握,就能自动还原大量加密、反调试逻辑,让 JS 逆向效率质变!
四、定位核心参数生成函数
在爬虫、逆向场景中,服务端通常要求提交一些“加密参数”:
-
常见名称有:
w
、sign
、token
、auth
、xyz
、m
、h
等 -
这些参数通过 JS 中隐藏/混淆的函数生成,是反爬的关键一环
我们的任务是定位并还原这些函数!
1. 典型例子
例如某请求:
POST /api/check
headers:w: "e72fa5b18d320...(加密值)"
需要搞清楚:
-
谁生成了
w
? -
w
用了哪些参数(时间戳、cookie、UA、行为数据)? -
生成函数是否被混淆?
-
是否用了动态执行(eval、Function)?
-
是否跑在 WebWorker 或 iframe 中?
2. 定位思路总览
方法 | 原理 |
---|---|
1. 关键词搜索 | 搜索 w= , sign= , headers , FormData , .w , .sign |
2. hook XMLHttpRequest / fetch | 拦截请求参数,看 w 的生成前有哪些代码执行 |
3. 打断点(XHR/fetch/send) | 手动调试,寻找传输逻辑、函数调用栈 |
4. 控制台 hook 全局函数 | 重定义 CryptoJS.MD5 、btoa() ,打印入参与结果 |
5. 格式化 + 搜索函数调用 | 格式化 JS 源码,搜索可疑函数调用 |
6. DOM 元素关联 | 有些加密数据来源于点击、坐标、行为序列 |
7. AST 分析 | 静态查找函数依赖链、追踪返回值 |
8. Blob / Worker 调试 | 查看是否把加密逻辑放在独立线程或动态 blob JS 里 |
3. 最常用方式详解
【方法 1】关键字搜索法(适用于未严重混淆)
搜索:
"w="
"sign="
"form.append"
"headers"
"return {"
例子:
var t = get_w(UA, timestamp);
formData.append("w", t);
通过定位 get_w()
,再深入分析。
【方法 2】hook fetch / XMLHttpRequest 拦截入参
// hook fetch
window.fetch = new Proxy(window.fetch, {apply(target, thisArg, args) {console.log("[fetch]", args);return Reflect.apply(target, thisArg, args);}
});// hook XHR
const open = XMLHttpRequest.prototype.open;
const send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function () {this._url = arguments[1];return open.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {console.log("[XHR]", this._url, body);return send.apply(this, arguments);
};
作用:
-
拦截请求,打印出
w
,sign
等参数值 -
分析是在哪段逻辑设置的这些参数
-
可结合堆栈
console.trace()
查看是谁生成的
【方法 3】打断点(最直接有效)
位置推荐:
-
fetch
、XMLHttpRequest.prototype.send
上打断点 -
document.cookie
被读取时(可用Monitor Events
) -
CryptoJS.MD5()
、btoa()
、encodeURIComponent()
等加密函数调用处 -
eval()
、Function()
调用处,观察执行前的参数
技巧:
-
打断点后,切换到 Call Stack,顺藤摸瓜,追函数栈
-
使用 Chrome DevTools「黑盒」方式隐藏无用框架代码
【方法 4】hook 加密函数打印入参
常见加密函数有:
CryptoJS.MD5(xxx)
CryptoJS.AES.encrypt
btoa()
encodeURIComponent()
可以这样 hook:
CryptoJS.MD5 = function (arg) {console.log("[MD5]", arg);return originalMD5(arg); // 原函数
}
或者 hook 所有函数:
Function.prototype.call = new Proxy(Function.prototype.call, {apply(target, thisArg, args) {console.log("[CALL]", thisArg, args);return Reflect.apply(target, thisArg, args);}
});
【方法 5】格式化搜索函数调用
使用 Pretty Print 格式化混淆代码,再查找形如:
var w = a.b(c, d); // 参数生成函数
-
重点关注:
a.b
这种链式调用,常是封装后的加密函数 -
把
a.b
替换成打印函数,输出参数和返回值
【方法 6】AST 静态追踪核心函数
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const fs = require("fs");const code = fs.readFileSync("./encrypt.js").toString();
const ast = parser.parse(code);traverse(ast, {CallExpression(path) {const { callee } = path.node;if (callee.type === "Identifier" &&callee.name === "get_w" // 可替换成你猜测的函数名) {console.log("Found w generator:", path.toString());}}
});
也可静态追踪返回的字符串是否带有 w=...
4. 实战案例简化版
例子:
function gen_w(ts, cookie, ua) {var str = ts + "|" + cookie + "|" + ua;return btoa(str);
}let w = gen_w(Date.now(), document.cookie, navigator.userAgent);
分析流程:
-
搜索
w=
,发现gen_w()
-
跟进
gen_w()
,看到参数组成 -
分析加密逻辑
btoa(str)
-
结论:
w
是 base64(ts|cookie|ua)
就可以写脚本还原它。
5. W 参数常见特征
特征 | 解读 |
---|---|
固定长度 | 多为 32、64、128 位(MD5、SHA1、AES 编码) |
每次不同 | 含时间戳、行为 ID、cookie 等 |
与滑动验证/行为交互相关 | w 中可能包含点击坐标、移动轨迹、session_id、lot_number 等 |
通常通过层层封装 | 多层函数嵌套,常混淆关键函数名 |
6. 辅助工具推荐
工具名 | 用途 |
---|---|
Charles/Fiddler | 抓包查看真实参数 |
DevTools Source Map | 调试压缩源码前的真实结构 |
Babel Parser + Traverse | 静态定位函数/AST 跟踪 |
mitmproxy + JS hook | 手机端逆向生成参数 |
Obfuscator-IO-Deobfuscator | 一键还原混淆代码 |
总结
定位加密函数 = 抓到“w 参数生成”的函数,并拆解其中逻辑(参数输入、算法过程、输出)
通常会结合这些手段:
-
抓包 + 调试断点 + 函数 hook + AST 分析
-
同时注意
Worker
/iframe
/动态 eval
场景