ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

JS逆向攻防实战:反调试、代码混淆与AST加密的对抗技术

JS逆向攻防实战:反调试、代码混淆与AST加密的对抗技术

1. 项目概述:当JS成为攻防主战场

在Web安全领域,前端JavaScript代码早已不是简单的交互点缀,而是承载着核心业务逻辑、加密算法、用户凭证和敏感接口的关键载体。因此,针对JS应用的攻防对抗,其激烈程度丝毫不亚于后端。标题中的“Day64”暗示了一个持续性的学习或实战系列,而“WEB攻防-JS应用&反调试分析&代码混淆&AST加密还原&本地覆盖&断点条件”则精准地勾勒出了一幅从攻击者(或安全研究者)视角出发,对前端JS防线进行层层剖析的路线图。这不仅仅是简单的代码阅读,更是一场在浏览器这个“沙箱”环境中,与开发者布下的重重迷雾进行的智力博弈。

核心目标非常明确:理解、穿透并最终控制目标JS应用的执行逻辑。无论是为了安全审计、漏洞挖掘,还是逆向分析某个加密参数的生成本质,我们都需要一套系统的方法来应对现代前端开发中常见的防护手段——反调试、代码混淆、AST(抽象语法树)层级的加密变换。同时,作为攻击方,我们也要掌握“本地覆盖”和“条件断点”这类动态调试技巧,以便在运行时精准拦截和修改程序状态。简单来说,这就是一场“矛”与“盾”的较量,我们既要学会打造坚固的“盾”(理解防御原理),更要精通如何锻造锋利的“矛”(掌握攻击与逆向技术)。

2. 核心防御技术拆解:开发者如何保护JS代码

在深入攻击技术之前,我们必须先理解防御者构筑的工事。现代前端,尤其是涉及敏感操作(如登录、支付、数据加密)的页面,通常会采用多种技术组合来增加代码分析的难度。

2.1 代码混淆:让代码“面目全非”

代码混淆是最基础、最广泛使用的防护手段。它的目的不是防止执行,而是极大增加人工阅读和理解的难度。混淆器会对代码进行各种语义等价但结构复杂的变换。

常见混淆类型:

  1. 标识符混淆:将有意义的变量名、函数名替换为短而无意义的字符,如a,b,_0x1a2b3c
    // 混淆前 function calculateTotalPrice(price, quantity) { return price * quantity; } // 混淆后 function a(b, c) { return b * c; }
  2. 字符串混淆:将字符串常量进行加密或拆分,在运行时还原。
    // 混淆前 var apiEndpoint = "https://api.example.com/login"; // 混淆后 var apiEndpoint = atob("aHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20vbG9naW4="); // Base64解码 // 或更复杂的拆分、Unicode转义 var _0x5c38=['\x68\x74\x74\x70\x73\x3a\x2f\x2f','\x61\x70\x69','\x2e\x65\x78']; // 十六进制转义数组拼接
  3. 控制流平坦化:这是“大杀器”之一。它打破代码原有的线性或分支结构,将所有代码块放到一个大的switchif-else调度器中,通过一个“分发器”变量来决定下一个执行哪块代码。这使得即便去除了变量混淆,代码的执行流程依然像一团乱麻,难以追踪。
  4. 死代码注入与不透明谓词:插入大量永远不会被执行到的代码片段,以及条件永远为真或为假的判断语句,干扰分析者的视线。

注意:混淆会显著增加代码体积和执行开销,因此需要在安全性和性能之间权衡。高强度的混淆可能影响页面加载速度。

2.2 反调试:阻止开发者工具窥探

如果说混淆是让代码“难看懂”,那么反调试就是让代码“难被调试”。它的核心是检测浏览器开发者工具(DevTools)的存在或状态,并采取对抗措施。

常见反调试技巧:

  1. 检测DevTools打开状态
    • debugger语句循环:在关键代码前插入debugger;语句,并配合setInterval循环触发,导致调试器不断暂停,无法正常执行。
      setInterval(function(){ debugger; }, 100);
    • 检测控制台API:尝试重写console.log或检测console对象与默认对象的差异。
    • 检测窗口大小:DevTools打开时,浏览器窗口的可用内宽/内高(window.innerWidth/Height)或外宽/外高(window.outerWidth/Height)会有特定变化,代码可以监听resize事件进行检测。
  2. 干扰断点
    • 通过Function.prototype.constructor动态生成函数,使断点难以设置在正确的行号上。
    • 使用evalnew Function执行核心代码,这些动态执行的代码在Sources面板中不易定位。
  3. 时间差检测:在代码关键路径前后记录时间戳。在调试模式下,单步执行会导致代码运行时间远超正常值,从而触发反调试逻辑(如跳转到错误页面、清空数据)。
    const start = performance.now(); // ... 一些复杂的计算或加密操作 ... const end = performance.now(); if (end - start > 1000) { // 假设正常执行应小于1秒 console.error('检测到调试行为!'); window.location.href = '/anti-debug.html'; }

2.3 AST加密与代码变换:更高级的“改头换面”

这是近年来更高级的防护方式,其核心在于操作JavaScript代码的抽象语法树(AST)。AST是代码的树状结构表示,包含了所有的语法信息。通过对AST进行自定义的遍历、修改和重组,可以实现比传统字符串替换式混淆更复杂、更难以还原的变换。

AST加密的常见操作:

  1. 常量折叠与展开:将简单的计算(如1+2)在AST层面替换为结果3,或者反向将常量展开为复杂的表达式。
  2. 控制流变换:在AST层面实现更彻底的控制流平坦化,插入不透明谓词。
  3. 自定义编码:将字符串、数字等字面量,在AST生成阶段就转换为自定义的编码函数调用,在运行时解码。
  4. 代码结构打乱:随机调整函数声明、变量声明的顺序,但保持其作用域和依赖关系。

这类防护通常需要使用专门的工具(如Babel插件、UglifyJS插件等)在构建阶段完成。还原的难度极大,因为变换规则是自定义的,且可能多层嵌套。

3. 攻击与逆向技术:穿透防御的利刃

面对重重防御,安全研究人员或攻击者需要一套组合拳。我们的目标是在浏览器环境中,动态地观察、干预并最终理解代码逻辑。

3.1 动态调试基础:控制执行流

这是所有逆向分析的起点,核心工具就是浏览器自带的开发者工具(Chrome DevTools / Firefox Developer Tools)。

关键面板与操作:

  1. Sources面板:核心战场。在这里可以查看加载的JS文件,设置断点。
  2. Console面板:执行任意JS代码,查看变量,与页面上下文交互。这是执行“本地覆盖”和探测环境的主要窗口。
  3. Network面板:监控所有网络请求,查看请求参数、响应数据,是分析加密接口的入口。
  4. 断点类型
    • 行断点:最常用,点击行号即可设置。
    • 条件断点:右键行断点,选择“Edit breakpoint”,可以输入一个JS表达式。只有当表达式为真时,断点才会暂停。这是过滤无关执行、精准定位关键代码的神器。例如,在遍历数组的函数中,可以设置条件item.id === 'target'
    • 事件监听器断点:在Sources面板右侧的“Event Listener Breakpoints”中,可以勾选特定事件(如click, XHR),当这些事件被触发时自动暂停。
    • DOM断点:在Elements面板,右键DOM节点,可以设置当其子树修改、属性修改或节点移除时暂停。
    • 函数断点:在Sources面板右侧的“Watch”下方,点击“+”,输入函数名(如encryptData)即可对该函数的任何调用设置断点。

3.2 本地覆盖:实时修改代码逻辑

这是动态分析中极具威力的一招。它允许你在不刷新页面的情况下,用你自己修改后的JS文件替换掉服务器加载的原始文件。这样,你可以插入日志、绕过检测、或者修复错误以继续执行流程。

操作步骤(以Chrome为例):

  1. 在Sources面板,找到目标JS文件。
  2. 在文件内容区域右键,选择“Save for overrides”。
  3. 浏览器会提示你选择一个本地文件夹作为“工作区”。
  4. 之后,该文件会出现在左侧的“Overrides”选项卡下。你可以直接在这个面板里编辑代码,编辑后按Ctrl+S保存。
  5. 刷新页面,浏览器将加载你本地修改后的版本,而不是网络版本。

实战应用场景:

  • 绕过反调试:找到检测DevTools的代码段,直接将其注释掉或修改为false
  • 注入日志:在关键的加密函数入口和出口,添加console.log,打印出输入参数、中间变量和最终结果。
  • 劫持函数:重写某个关键函数,例如XMLHttpRequest.prototype.sendfetch,以拦截和查看所有网络请求的详细数据。
  • 修复错误继续执行:有时代码因环境差异报错,导致流程中断。你可以本地修改代码,跳过错误部分,让流程继续走下去,以便观察后续逻辑。

实操心得:使用Overrides时,务必注意浏览器缓存。有时修改未生效,可能是缓存所致。可以打开DevTools的“Network”面板,勾选“Disable cache”。另外,Overrides是基于文件URL的,如果文件带哈希(如app.a1b2c3.js),那么哈希变化后覆盖会失效。

3.3 断点条件与单步调试技巧

设置断点后,如何高效地跟踪代码执行是门学问。

  1. 单步执行控制

    • F10 (Step Over):执行当前行,如果该行有函数调用,不会进入函数内部,直接得到结果。
    • F11 (Step Into):执行当前行,如果该行有函数调用,会进入该函数内部。
    • Shift+F11 (Step Out):快速执行完当前函数内剩余的所有代码,并跳出到调用该函数的位置。
    • F8 (Continue):继续执行,直到遇到下一个断点。
  2. 作用域与调用栈观察

    • 在右侧的“Scope”窗格,可以查看当前断点处的局部作用域闭包作用域全局作用域中的所有变量值。
    • 在“Call Stack”窗格,可以看到函数调用链,点击可以跳转到对应的上下文中,这对于理解代码执行路径至关重要。
  3. 条件断点的高级用法

    • 基于调用栈的条件:例如,你只想在某个函数被特定父函数调用时才暂停。可以在条件中使用console.trace()先查看调用栈,然后提炼条件。
    • 基于数据特征的条件:分析加密请求时,发现参数data是一个长字符串。可以设置条件断点:data.length > 100,这样只有在处理大型加密数据时才会暂停,过滤掉大量无关的初始化调用。

3.4 对抗反调试策略

遇到反调试时,不能硬碰硬,需要一些技巧来绕过。

  1. 禁用无限Debugger循环

    • 方法一(推荐):在遇到debugger语句暂停时,在Sources面板右侧,找到该行对应的行号,右键选择“Never pause here”。这样,这个特定的debugger语句就会被忽略。
    • 方法二:在Console中执行以下代码,重写debugger关键字(注意,这可能会影响页面正常功能):
      Function.prototype.constructor = function() {}; // 或者更彻底地,禁用所有debugger语句的执行 var _debugger = debugger; debugger = function(){};
    • 方法三:在DevTools设置中,找到“Debugger”或“Sources”相关选项,勾选“Deactivate breakpoints”(停用断点)按钮,然后再触发页面逻辑。但这样你自己的断点也会失效。
  2. 绕过时间差检测

    • 找到检测时间的代码,通过本地覆盖直接注释或修改判断条件。
    • 更隐蔽的方法是,在Console中重写performance.nowDate.now,使其返回固定的或受控的时间值。
      const originalNow = performance.now; let timeOffset = 0; performance.now = function() { return originalNow.call(performance) - timeOffset; }; // 当检测到可能进入时间检测代码段时,在Console中增大timeOffset来“压缩”感知到的时间差 // timeOffset += 2000; // 让时间快进2秒
  3. 隐身模式与无头浏览器

    • 有些反调试仅针对普通窗口。使用Chrome的“无头模式”或通过Puppeteer、Playwright等自动化工具启动浏览器,有时可以绕过简单的检测。
    • 在DevTools中,按Ctrl+Shift+P打开命令面板,输入disable javascript并执行,可以临时禁用所有JS执行,快速进入页面,然后再启用,有时能打乱检测节奏。

4. 代码还原实战:从混淆到可读

动态调试让我们能看到运行时的数据,但要彻底理解算法,往往需要静态分析一份相对清晰的代码。这就涉及到对混淆代码的还原。

4.1 自动化反混淆工具

对于常见的、已知的混淆器(如obfuscator.io, javascript-obfuscator等),可以尝试使用现成的反混淆工具或插件。

  1. 浏览器插件:如“Deobfuscator”、“JavaScript Deobfuscator”等,可以一键尝试还原某些格式的混淆代码。
  2. 在线网站:一些网站提供粘贴混淆代码并还原的服务,但需注意代码安全性,敏感代码勿上传。
  3. Node.js工具:如deobfuscatorjs-beautify(主要美化格式)等。js-beautify是基础,可以格式化压缩后的代码,使其有缩进和换行,极大提升可读性。
    npm install -g js-beautify js-beautify ugly.js -o pretty.js

4.2 手动分析与还原策略

当自动化工具失效时(尤其是面对自定义的AST加密),就需要手动分析。

  1. 格式化代码:使用js-beautify或DevTools的“Pretty print”功能(Sources面板底部{}图标)格式化代码,这是第一步。
  2. 识别模式
    • 寻找字符串解密函数:搜索atobfromCharCodejoin('')replace等关键字,通常能找到一个集中处理字符串数组的函数。
    • 定位入口点:从网络请求的发起处(如fetchXMLHttpRequest.send)或事件监听器(如button.addEventListener('click', ...))开始反向追踪。
    • 还原控制流平坦化:这是体力活。找到那个巨大的switchif-else分发器,以及控制它的“状态变量”。通过动态调试,记录下不同逻辑块执行时该状态变量的值,逐步将分散的代码块重新排列回原本的逻辑顺序。有时可以编写简单的脚本辅助分析。
  3. 使用AST解析库进行半自动还原:对于AST级别的加密,最根本的对抗方法是在AST层面进行还原。可以使用@babel/parser@babel/traverse@babel/generator等库。
    • 思路:将混淆代码解析成AST,然后编写自己的“反变换”插件,遍历AST,识别出特定的混淆模式(如特定的函数调用模式、节点结构),并将其替换回简化形式。
    • 示例(还原简单的字符串数组拼接)
      const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const generate = require('@babel/generator').default; const types = require('@babel/types'); const code = `var _0x5c38=['\x68\x65\x6c\x6c\x6f']; console.log(_0x5c38[0]);`; const ast = parser.parse(code); traverse(ast, { VariableDeclarator(path) { // 查找类似 _0x5c38=['...'] 的声明 if (types.isIdentifier(path.node.id) && path.node.id.name.startsWith('_0x') && types.isArrayExpression(path.node.init)) { const arrayName = path.node.id.name; const elements = path.node.init.elements; // 存储映射关系,这里简化处理,实际需要处理各种编码 const stringMap = elements.map(el => el.value); // 遍历作用域内所有对该数组的成员引用 path.scope.path.traverse({ MemberExpression(memberPath) { if (types.isIdentifier(memberPath.node.object, {name: arrayName}) && types.isNumericLiteral(memberPath.node.property)) { const index = memberPath.node.property.value; // 用实际的字符串字面量替换成员表达式 memberPath.replaceWith(types.stringLiteral(stringMap[index])); } } }); // 可选:移除原变量声明 // path.remove(); } } }); const output = generate(ast).code; console.log(output); // 输出:console.log("hello");
    • 这个过程需要对Babel AST和混淆器的变换规则有深入理解,是最高阶的逆向手段。

5. 实战案例:分析一个简单的加密登录过程

假设我们遇到一个登录页面,密码在提交前被加密,表单数据如下:

username: admin password: (加密后的字符串,如 "a7f3e8d1c5b2")

我们的目标:找出加密算法,并能够用脚本模拟加密过程。

分析步骤:

  1. 网络抓包:打开Network面板,勾选“Preserve log”,进行登录操作。找到提交登录请求的XHR或Fetch请求,查看其请求负载(Payload)。确认password字段已被加密。

  2. 全局搜索:在Sources面板按Ctrl+Shift+F,在整个项目代码中搜索关键词,如passwordencryptencode、提交请求的URL端点等。

  3. 设置断点

    • 在提交请求的代码处(如fetchXMLHttpRequest.send调用前)设置行断点。
    • 在可能存在的加密函数名上设置函数断点。
    • 在表单提交事件上设置事件监听器断点。
  4. 动态追踪:触发登录操作,代码会在断点处暂停。

    • 在Console中查看password变量此时的值,应该是明文。
    • 使用F11逐步步入,跟踪明文password如何传递、被哪个函数处理、最终变成密文。
    • 密切关注右侧“Scope”中变量的变化,特别是经过加密函数后的返回值。
  5. 定位加密函数:通过单步调试,最终会进入一个函数,其输入是明文密码,输出是加密后的字符串。这个函数就是我们的核心目标。在函数内部,可能需要继续步进来理解其具体实现(是简单的Base64?MD5?还是自定义的AES?)。

  6. 提取与模拟

    • 如果加密是标准算法(如CryptoJS的AES),那么需要找到密钥(key)和初始化向量(iv)。这些可能在代码中硬编码,或从服务器响应获取。
    • 如果是自定义算法,就需要仔细分析函数内的每一步操作(可能是异或、位移、查表等)。
    • 在Console中,可以尝试直接调用这个加密函数,传入测试密码,看输出是否与网络请求一致。
    • 最终,将这段加密逻辑用Python或Node.js重写,完成自动化脚本。
  7. 处理反调试:如果在第4步发现不断触发debugger暂停,则使用“Never pause here”功能禁用那个特定的debugger行。如果遇到时间检测导致流程中断,尝试使用本地覆盖功能,注释掉检测代码。

常见问题与排查:

  • Q:找不到加密函数,搜索关键词也无果。

    • A:加密可能被混淆成极其简短的函数名(如c()),或内联在提交逻辑中。尝试在发送请求的代码行设置断点,然后仔细查看调用栈(Call Stack),栈中下方的函数可能就是加密逻辑的入口。另外,关注对password变量值进行修改的所有操作。
  • Q:加密函数依赖了浏览器环境特有的对象(如window.btoaCrypto.subtle),如何在Node.js中复现?

    • A:需要寻找替代库。btoa对应Node.js的Buffer.from(str).toString('base64')。Web Crypto API (crypto.subtle) 在Node.js中对应crypto模块,但API略有不同,需要查阅文档进行适配。
  • Q:代码经过高度控制流平坦化,完全无法阅读执行顺序。

    • A:优先使用动态调试。通过多次执行和条件断点,记录下“分发器”变量的变化序列,从而推断出真实代码块的执行顺序。这是一个缓慢但有效的过程。也可以考虑寻找并尝试已有的去平坦化工具或脚本。

6. 工具链与资源推荐

工欲善其事,必先利其器。以下工具能极大提升JS逆向分析的效率。

  1. 浏览器与插件

    • Chrome DevTools / Firefox Developer Tools:核心,无需多言。
    • EditThisCookie:方便地查看和编辑Cookie。
    • Requestly:拦截和修改网络请求与响应,用于测试。
    • SwitchyOmega:管理代理,配合抓包工具使用。
  2. 抓包与调试代理

    • Fiddler / Charles:老牌抓包工具,可以查看和修改HTTP/HTTPS流量,对于分析移动端或桌面应用发出的请求尤其有用。
    • mitmproxy:命令行抓包工具,支持Python脚本扩展,自动化能力强。
  3. Node.js逆向相关库

    • js-beautify:代码格式化。
    • @babel/parser,@babel/traverse,@babel/generator:进行AST分析和转换的利器。
    • vm2:一个安全的沙箱环境,可以在Node.js中安全地执行不受信任的JS代码,用于测试还原后的代码片段。
    • crypto-js:如果目标代码使用了CryptoJS,这个库可以帮助你在Node.js中模拟。
  4. 自动化浏览器

    • Puppeteer / Playwright:通过代码控制无头或有头Chrome/Firefox。可以自动化执行页面操作、绕过简单的反调试(因为开发者工具默认未打开),并提取运行时的数据。对于需要模拟完整用户流程的逆向任务非常有用。
  5. 在线资源与社区

    • AST Explorer(https://astexplorer.net/):在线可视化JS代码的AST结构,是学习AST和编写Babel插件的神器。
    • Stack Overflow / 相关安全论坛:遇到具体问题时的最佳求助场所。

JS逆向与攻防是一个需要极大耐心和细致观察力的领域。它没有一成不变的银弹,每一个目标都可能带来新的挑战。核心思路永远是:结合静态分析与动态调试,大胆假设,小心求证,利用浏览器的强大能力作为我们的主战场。从简单的格式化代码和设置断点开始,逐步深入到条件断点、本地覆盖,最终挑战AST还原,每一步的突破都会带来巨大的成就感。记住,理解防御是为了更好地进攻,而所有的进攻最终都是为了构建更坚固的防御。在这个猫鼠游戏中,保持学习和探索的心态,是最大的利器。

返回列表