引言
在现代 Web 应用中,JavaScript 几乎承载了所有的前端逻辑,从用户交互到数据加密,从 API 调用到业务流程。当我们需要分析网站的工作原理、调试第三方脚本、或者还原被混淆的代码时,JavaScript 逆向工程就成为了一项不可或缺的技能。
本文将从最基础的 Chrome DevTools 开始,带你一步步走进 JS 逆向的世界,了解常见的混淆技术,并掌握实用的混淆还原方法。无论你是前端开发者、安全研究员还是对逆向感兴趣的技术爱好者,都能从这篇文章中获得有价值的知识。
一、Chrome DevTools:逆向工程师的瑞士军刀
Chrome 开发者工具 (DevTools) 是每个前端开发者的必备工具,同时也是 JS 逆向工程最基础、最强大的武器。很多人只使用它的基本功能,但它隐藏着大量专为逆向分析设计的高级特性。
1.1 Sources 面板:代码调试的核心
Sources 面板是 JS 逆向的主战场,它提供了完整的代码浏览、断点调试和执行控制功能。
- 文件树导航:左侧的文件树展示了网站加载的所有资源,包括 JavaScript 文件、CSS 样式表和图片。你可以在这里找到需要分析的目标脚本。
- 代码编辑器:中间区域显示选中文件的源代码,支持语法高亮、代码折叠和行号显示。
- 调试控制面板:右侧提供了断点管理、调用栈、作用域变量、监视表达式等调试功能。
1.2 断点类型与使用技巧
断点是逆向分析中最常用的技术,它允许我们在代码执行的特定位置暂停,观察程序的状态和数据流。
- 行断点:点击代码行号即可设置,当程序执行到该行时暂停。
- 条件断点:右键点击行号选择 "Add conditional breakpoint",只有当条件表达式为 true 时才会触发暂停。这在分析循环或频繁调用的函数时特别有用。
- DOM 断点:在 Elements 面板中右键点击元素,选择 "Break on",可以在元素被修改、属性变化或子节点被移除时触发断点。
- XHR/fetch 断点:在 Sources 面板的 XHR/fetch Breakpoints 部分添加 URL 过滤规则,当网站发送匹配的 AJAX 请求时暂停。这是分析 API 接口和数据加密的常用方法。
- 事件监听器断点:在 Event Listener Breakpoints 部分展开事件类别,勾选需要监听的事件,当该事件被触发时暂停。
1.3 控制台 (Console) 的高级用法
Console 面板不仅是输出日志的地方,更是逆向分析中执行任意代码、修改变量和调用函数的强大工具。
- 实时修改变量:在断点暂停时,可以在控制台中直接修改变量的值,改变程序的执行流程。
- 访问私有变量:即使是闭包中的私有变量,在断点暂停时也可以通过作用域链访问和修改。
- 复制对象 :使用
copy(object)函数可以将 JavaScript 对象转换为 JSON 字符串并复制到剪贴板。 - 监控函数调用 :使用
monitor(function)可以在函数被调用时打印调用信息,包括参数和返回值。 - 查看函数源码 :使用
console.log(function.toString())可以查看函数的源代码,即使它被混淆过。
1.4 性能 (Performance) 与网络 (Network) 面板
- Network 面板:记录所有网络请求,包括请求头、响应头、请求体和响应体。你可以在这里分析 API 接口的参数和返回值,找到加密的请求数据。
- Performance 面板:记录页面的性能数据,包括 JavaScript 执行时间、渲染时间和网络时间。通过分析性能火焰图,你可以找到代码中计算密集的部分,这往往是加密或混淆算法所在的位置。
二、JS 逆向的基本流程
掌握了 Chrome DevTools 的基本功能后,我们来了解 JS 逆向的一般流程。
2.1 确定逆向目标
在开始逆向之前,首先要明确你的目标是什么。常见的逆向目标包括:
- 分析 API 接口的签名算法
- 还原前端数据加密 / 解密逻辑
- 破解反爬虫机制
- 分析第三方 SDK 的工作原理
- 修复被混淆的 buggy 代码
2.2 定位关键代码
这是逆向过程中最关键也是最耗时的一步。你需要找到实现目标功能的 JavaScript 代码。
常用的定位方法:
- 搜索关键词:在 Sources 面板中使用 Ctrl+Shift+F 全局搜索相关关键词,如 "sign"、"encrypt"、"token"、"password" 等。
- XHR/fetch 断点:当目标功能涉及 AJAX 请求时,设置 XHR 断点,然后查看调用栈找到发起请求的代码。
- 事件监听器断点:当目标功能由用户交互触发时,设置相应的事件断点,如点击事件、提交事件等。
- DOM 断点:当目标功能修改页面 DOM 时,设置 DOM 断点。
- 函数搜索:在 Sources 面板中使用 Ctrl+P 搜索函数名。
2.3 动态调试分析
找到关键代码后,设置断点并触发目标功能,让程序在断点处暂停。然后:
- 查看调用栈,了解代码的执行流程
- 检查作用域变量,观察数据的变化
- 单步执行代码,跟踪数据的流向
- 修改变量值,测试不同的执行路径
2.4 提取并验证算法
在理解了代码的逻辑后,提取出关键的算法部分,然后在本地环境中运行验证,确保它能产生与原网站相同的结果。
三、常见的 JS 混淆技术
为了保护代码不被轻易分析和篡改,很多网站会对 JavaScript 代码进行混淆处理。混淆后的代码变得难以阅读和理解,但它的功能保持不变。
3.1 变量名和函数名混淆
这是最基础也是最常见的混淆技术。混淆器会将有意义的变量名和函数名替换为无意义的字符,如 a、b、c 或_0x1234、_0x5678 等。
混淆前:
javascript
运行
function calculateMD5(data) {
const salt = "abc123";
return md5(data + salt);
}
混淆后:
javascript
运行
function _0x1a2b(_0x3c4d) {
const _0x5e6f = "abc123";
return _0x7g8h(_0x3c4d + _0x5e6f);
}
3.2 字符串加密
混淆器会将代码中的字符串常量进行加密处理,然后在运行时动态解密。这使得直接搜索关键词变得困难。
混淆后示例:
javascript
运行
const _0x1234 = atob("YWJjMTIz"); // 解密后为"abc123"
更复杂的混淆会使用自定义的加密算法和解密函数:
javascript
运行
function _0x4567(_0x7890) {
let _0xabcd = "";
for (let i = 0; i < _0x7890.length; i++) {
_0xabcd += String.fromCharCode(_0x7890.charCodeAt(i) ^ 0x12);
}
return _0xabcd;
}
const _0xefgh = _0x4567("\x13\x24\x35\x46\x57\x68");
3.3 控制流平坦化
控制流平坦化是一种非常有效的混淆技术,它将原本清晰的控制流 (if-else、for、while 等) 转换为基于 switch-case 的状态机结构。这使得代码的执行流程变得极其复杂和难以跟踪。
混淆前:
javascript
运行
function calculate(a, b, op) {
if (op === "+") {
return a + b;
} else if (op === "-") {
return a - b;
} else if (op === "*") {
return a * b;
} else {
return a / b;
}
}
混淆后:
javascript
运行
function calculate(a, b, op) {
let _0x1234 = 0;
while (true) {
switch (_0x1234) {
case 0:
if (op === "+") {
_0x1234 = 1;
} else {
_0x1234 = 2;
}
break;
case 1:
return a + b;
case 2:
if (op === "-") {
_0x1234 = 3;
} else {
_0x1234 = 4;
}
break;
case 3:
return a - b;
case 4:
if (op === "*") {
_0x1234 = 5;
} else {
_0x1234 = 6;
}
break;
case 5:
return a * b;
case 6:
return a / b;
}
}
}
3.4 代码压缩与合并
混淆器通常会将多个 JavaScript 文件合并为一个,并删除所有的空格、换行和注释,使代码变成一行难以阅读的长字符串。
3.5 反调试技术
为了防止被逆向分析,很多混淆后的代码会加入反调试技术,如:
- 检测 DevTools 是否打开,如果打开则停止执行或进入死循环
- 检测是否在非浏览器环境中运行
- 检测是否被断点调试
- 定时清除控制台输出
四、JS 混淆还原的实用方法
面对混淆后的代码,我们有多种方法可以进行还原和分析。
4.1 手动还原
对于简单的混淆,手动还原是最直接有效的方法。
- 重命名变量和函数:在理解了代码的功能后,将无意义的变量名和函数名替换为有意义的名称。Chrome DevTools 支持在代码编辑器中右键点击标识符,选择 "Rename symbol" 进行批量重命名。
- 提取字符串:找到字符串解密函数,然后在控制台中调用它,解密所有加密的字符串。
- 简化控制流:逐步分析 switch-case 结构,还原出原本的 if-else 和循环结构。
4.2 使用在线还原工具
有很多在线工具可以帮助我们自动还原混淆后的 JavaScript 代码。
- JS Beautifier:最常用的代码格式化工具,可以将压缩后的代码恢复为易读的格式。
- Unminify:功能与 JS Beautifier 类似,支持多种编程语言。
- JSNice:基于机器学习的代码美化工具,可以自动推断变量名和函数名。
- Deobfuscate.io:专门用于还原混淆后的 JavaScript 代码,支持多种混淆器。
4.3 使用 AST (抽象语法树) 工具
AST 工具可以将 JavaScript 代码解析为抽象语法树,然后我们可以对语法树进行修改和优化,最后再转换回 JavaScript 代码。这是处理复杂混淆的最强大方法。
常用的 AST 工具:
- Babel:最流行的 JavaScript 编译器,提供了完整的 AST 操作 API。
- Acorn:一个轻量级的 JavaScript 解析器。
- Esprima:另一个常用的 JavaScript 解析器。
- Recast:可以在修改 AST 的同时保留原始代码的格式。
使用 Babel 还原字符串加密的示例:
javascript
运行
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;
const types = require("@babel/types");
// 混淆后的代码
const code = `
function _0x4567(_0x7890) {
let _0xabcd = "";
for (let i = 0; i < _0x7890.length; i++) {
_0xabcd += String.fromCharCode(_0x7890.charCodeAt(i) ^ 0x12);
}
return _0xabcd;
}
const _0xefgh = _0x4567("\\x13\\x24\\x35\\x46\\x57\\x68");
console.log(_0xefgh);
`;
// 解析为AST
const ast = parser.parse(code);
// 遍历AST
traverse(ast, {
CallExpression(path) {
// 找到调用_0x4567函数的地方
if (types.isIdentifier(path.node.callee, { name: "_0x4567" })) {
// 获取加密的字符串参数
const encrypted = path.node.arguments[0].value;
// 解密字符串
let decrypted = "";
for (let i = 0; i < encrypted.length; i++) {
decrypted += String.fromCharCode(encrypted.charCodeAt(i) ^ 0x12);
}
// 将函数调用替换为解密后的字符串
path.replaceWith(types.stringLiteral(decrypted));
}
}
});
// 生成还原后的代码
const result = generate(ast, {}, code);
console.log(result.code);
4.4 处理反调试技术
对于反调试技术,我们可以通过以下方法进行绕过:
- 重写反调试函数:在控制台中重新定义反调试函数,覆盖原有的实现。
- 使用断点绕过:在反调试代码执行前设置断点,然后修改相关变量的值。
- 使用浏览器扩展:有一些专门的浏览器扩展可以自动绕过常见的反调试技术,如 "Disable JavaScript Debugger"。
五、实战案例:还原一个简单的签名算法
让我们通过一个简单的实战案例来巩固所学的知识。假设我们有一个网站,它在发送 API 请求时会在请求头中添加一个 "X-Sign" 签名,我们需要还原这个签名算法。
步骤 1:定位签名生成代码
首先,我们在 Network 面板中找到目标 API 请求,然后在 Headers 选项卡中找到 "X-Sign" 请求头。
接下来,我们在 Sources 面板中设置 XHR/fetch 断点,输入 API 的 URL 路径。然后触发请求,程序会在发送请求前暂停。
在调用栈中,我们向上追溯,找到生成签名的函数。假设我们找到了以下代码:
javascript
运行
function _0x1a2b(_0x3c4d) {
const _0x5e6f = _0x7g8h("abc123");
const _0x9i0j = _0x3c4d + _0x5e6f + Date.now().toString();
return _0x7g8h(_0x9i0j);
}
步骤 2:分析字符串解密函数
我们发现代码中调用了一个名为_0x7g8h的函数,它看起来是一个字符串解密函数。我们在控制台中输入_0x7g8h.toString(),查看它的源代码:
javascript
运行
function _0x7g8h(_0x1234) {
return btoa(_0x1234);
}
原来它只是一个简单的 Base64 编码函数。
步骤 3:还原签名算法
现在我们可以还原出完整的签名算法:
- 将字符串 "abc123" 进行 Base64 编码,得到盐值
- 将请求数据、盐值和当前时间戳拼接成一个字符串
- 将拼接后的字符串进行 Base64 编码,得到最终的签名
步骤 4:本地验证
我们在本地编写代码验证这个算法:
javascript
运行
function generateSign(data) {
const salt = btoa("abc123");
const timestamp = Date.now().toString();
const combined = data + salt + timestamp;
return btoa(combined);
}
// 测试
const data = "test";
const sign = generateSign(data);
console.log(sign);
运行代码,我们得到的签名与网站生成的签名一致,说明我们成功还原了签名算法。
六、法律与道德边界
在进行 JavaScript 逆向工程时,我们必须遵守法律法规和道德准则。
- 仅用于合法目的:逆向工程应该用于学习、研究、调试自己的代码或获得授权的第三方代码。
- 遵守网站服务条款:很多网站的服务条款明确禁止逆向工程、破解或篡改其代码。
- 不用于恶意用途:不要将逆向技术用于攻击网站、窃取数据、破解付费内容等恶意行为。
- 尊重知识产权:代码是开发者的劳动成果,我们应该尊重他人的知识产权。
七、进阶学习建议
如果你想深入学习 JavaScript 逆向工程,可以从以下几个方面入手:
- 深入学习 JavaScript 语言:掌握闭包、原型链、异步编程等高级特性。
- 学习浏览器工作原理:了解 V8 引擎的执行机制、DOM 渲染流程和网络请求过程。
- 学习加密算法:掌握常见的对称加密、非对称加密和哈希算法。
- 学习 AST 操作:深入学习 Babel 等 AST 工具的使用,能够编写复杂的代码转换插件。
- 参与开源项目:有很多优秀的开源逆向工程工具和项目,参与其中可以快速提升自己的技能。
- 关注安全社区:关注安全社区的最新动态和技术分享,了解最新的混淆和反混淆技术。
总结
JavaScript 逆向工程是一项充满挑战但也非常有趣的技能。从 Chrome DevTools 的基本使用,到常见混淆技术的识别,再到混淆还原的实用方法,我们已经走过了 JS 逆向入门的完整路径。
记住,逆向工程的核心是理解代码的逻辑和思想,而不仅仅是还原代码的形式。随着 Web 技术的不断发展,新的混淆技术和反逆向手段也会不断出现,我们需要保持学习的热情和好奇心,不断提升自己的技能。
希望这篇文章能够为你打开 JS 逆向世界的大门,祝你在逆向的道路上越走越远!