关于JavaScript反调试与混淆识别举例详解
作者:shenyan~
一、常见反调试手段识别
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
场景
到此这篇关于关于JavaScript反调试与混淆识别的文章就介绍到这了,更多相关js反调试与混淆识别内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!