ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

破解高强度混淆JS代码:逆向工程实战与Webpack模块分析

破解高强度混淆JS代码:逆向工程实战与Webpack模块分析 1. 项目概述当JS代码变得“变态”我们该如何应对最近在逆向一个名为“诺禾”的网站时我遇到了从业十多年来堪称最“变态”的JavaScript代码混淆和打包方案。这里的“变态”并非贬义而是指其代码保护手段之复杂、结构之诡异远超常规的压缩、变量名混淆甚至比常见的Webpack打包分析还要棘手数倍。它完美地融合了多种前端工程化技术并进行了深度定制形成了一个坚固的“黑盒”。对于前端开发者、安全研究员或是对爬虫与数据采集感兴趣的朋友来说破解这类代码不仅是技术的挑战更是一次对JavaScript运行机制和现代前端构建流程的深度理解之旅。简单来说这个项目就是一次针对高强度混淆JS代码的“外科手术式”逆向工程实战。我们将从最外层的网络请求和文件加载入手层层剥开其打包外壳如Webpack定位核心业务逻辑并最终理解其数据流转和加密逻辑。无论你是想学习JS逆向的思路、应对前端反爬策略还是单纯好奇现代Web应用如何保护其客户端代码这篇详尽的复盘都能为你提供一套可复现的方法论和大量踩坑经验。2. 核心难点与逆向思路拆解面对一坨被高度混淆和打包的JS代码最忌讳的就是一头扎进那数万行难以阅读的压缩代码中。正确的做法是像侦探一样先建立全局视角再寻找突破口。2.1 “变态”在何处—— 特征分析首先我们需要定义“诺禾”网站JS代码的“变态”之处这决定了我们的逆向策略多重混合打包它并非单纯使用Webpack或Rollup而是可能将多个模块打包器Bundle的输出再次组合或者使用了深度定制的Webpack配置。在开发者工具中你可能会看到多个webpackJsonp数组或类似的异步加载结构并存这增加了定位入口的难度。极度激进的混淆变量名不仅被替换为a,b,c函数名和参数也面目全非。更棘手的是它可能使用了“控制流扁平化”和“不透明谓词”等高级混淆技术使得代码的执行路径像迷宫一样静态分析几乎无法进行。运行时动态加载与解密核心的业务逻辑或关键函数可能并非直接存在于初始加载的JS文件中而是以加密的字符串形式存在在浏览器执行到特定阶段时通过一个内置的解密函数动态eval执行。这要求我们必须让代码“跑起来”在运行时进行调试。反调试与环境检测代码中可能埋设了针对开发者工具DevTools的检测。例如通过检查console.log是否被重写、debugger语句的触发时间差或者检测某些全局变量如window.__REACT_DEVTOOLS_GLOBAL_HOOK__来判断是否处于调试环境一旦发现可能触发无限debugger、跳入死循环或直接抛出错误。2.2 逆向工程的核心思路基于以上特征我制定了“由外而内、动静结合”的逆向策略网络抓包与静态分析外使用浏览器开发者工具的Network面板记录页面加载过程中的所有JS文件。重点关注文件大小异常很大或很小、名称有特征如chunk-vendors、app、runtime或加载时机较晚的文件。同时使用Sources面板全局搜索关键API端点、域名或已知的参数名这是定位代码块的捷径。格式化与初步探索静对疑似核心的JS文件点击开发者工具Sources面板左下角的{}Pretty-print按钮进行格式化。虽然混淆依旧但结构会清晰很多。此时可以搜索webpackJsonp、__webpack_require__、module.exports等Webpack关键标识符找到模块系统的边界。动态调试与行为追踪动这是破解高级混淆的关键。通过下断点、监听事件XHR/Fetch断点、DOM断点、Hook关键函数如JSON.parse、Object.defineProperty、Function.prototype.constructor等方式在代码执行过程中观察数据流。我们的目标是找到从用户交互如点击搜索到网络请求发出之间参数是如何被构造和加密的。环境模拟与代码提取合在理清核心逻辑后可能需要将关键的加解密函数或参数生成逻辑从浏览器环境中“剥离”出来在Node.js或纯JS环境中复现。这涉及到补全浏览器特有的对象如window、document或函数以及处理那些依赖Webpack模块系统的代码。3. 实战工具链与关键步骤解析工欲善其事必先利其器。下面是我在这次逆向中用到并认为至关重要的工具和技巧。3.1 必备工具清单浏览器与开发者工具Chrome/Edge DevTools 是主战场。Firefox的开发者工具在某些方面如样式编辑有独特优势可作辅助。抓包与调试代理Fiddler Everywhere或Charles。它们不仅能记录所有HTTP/HTTPS请求更重要的是可以设置断点修改请求和响应用于测试参数是否有效或者拦截并分析未解密的响应数据。Fiddler的AutoResponder功能还能用本地文件替换线上JS方便我们测试修改后的代码。注意在Fiddler中设置过滤器时初期建议不要过滤任何请求以观察全貌。后期可以针对性地过滤例如只显示包含特定域名或路径的请求避免信息过载。Node.js环境用于在本地运行剥离出来的JS代码片段验证其功能是否独立。vm2模块可以提供一个相对安全的沙箱环境来执行不受信任的代码。代码编辑与搜索工具VSCode配合Search in Files功能对于分析下载到本地的巨型JS文件非常高效。Prettier插件可以辅助格式化代码。浏览器插件ReRes或Resource Override这类插件可以更方便地在浏览器层面实现JS文件的重定向将线上文件指向本地修改后的版本无需配置复杂的代理。3.2 关键步骤实操详解步骤一网络记录与入口定位打开目标网站清空Network记录然后刷新页面或进行关键操作如登录、搜索。在Network面板中筛选JS类型文件。按大小Size降序排列最大的几个文件通常是核心的打包文件。同时关注Initiator列看是谁发起了这个JS的加载。找到一个疑似主包的JS文件如app.xxxxxx.js在Sources面板中打开并格式化它。在格式化后的代码中搜索webpackJsonp。Webpack打包的模块通常被包裹在这个函数调用中。找到类似webpackJsonp([[0], { ... }])的结构。这个对象{ ... }里面就是所有的模块键是模块ID值是一个函数模块本身。步骤二模块映射与函数搜索将整个JS文件保存到本地用编辑器打开。搜索你感兴趣的关键词比如API的URL路径如/api/search、已知的请求参数名如token、sign、或者明显的加密函数名如encrypt、md5、SHA。由于混淆直接搜索可能无果可以尝试搜索URL的一部分或参数值的特征如固定字符串。如果找到了相关代码所在的模块函数记下它的模块ID。例如你发现加密逻辑在function(e, t, n) { ... }这个函数里而这个函数是模块123的内容。步骤三动态调试与“跟栈”这是最核心、最考验耐心的环节。XHR/Fetch断点在Sources面板的XHR/Fetch Breakpoints区域添加一个包含API关键字的断点如*search*。当网站发起相关请求时执行流会自动暂停。调用栈分析断住后查看右侧的Call Stack调用栈。调用栈显示了当前暂停的函数是由谁一层层调用的。从栈顶最上面往下看就是代码的执行路径。逐行跟蹤在调用栈中点击栈帧中的函数名可以跳转到对应的源代码位置即使代码被混淆。通过单步执行F10、步入F11、步出ShiftF11结合观察Scope面板中局部变量的变化一步步追踪参数是如何被计算出来的。Console实时求值在调试过程中可以在Console面板中直接引用当前作用域内的变量执行一些表达式来测试你的理解。例如看到变量c可能是一个对象可以在Console里输入c查看其内容或者输入JSON.stringify(c)将其序列化。实操心得面对控制流扁平化混淆单步调试会非常痛苦因为代码会在多个看似无关的代码块间跳转。此时一个有效的策略是不关心“怎么来的”只关心“结果是什么”。在调用栈中找到距离网络请求发送如fetch或XMLHttpRequest.send最近的那个函数重点分析这个函数接收到的参数是什么。然后向上回溯一层看这个参数是从哪里传进来的以此类推像剥洋葱一样忽略中间复杂的混淆逻辑只关注数据的输入和输出。步骤四Hook关键函数当直接跟踪困难时Hook钩子是获取信息的强大手段。// 示例Hook JSON.stringify常用于观察什么数据被序列化后发送 var originalStringify JSON.stringify; JSON.stringify function(...args) { console.trace(JSON.stringify called with:, args); // 打印调用栈和参数 // 可以在这里添加debugger语句 // debugger; return originalStringify.apply(this, args); }; // 示例Hook fetch API拦截所有网络请求 var originalFetch window.fetch; window.fetch function(...args) { console.log(Fetch intercepted:, args); // 如果需要修改请求可以在这里操作args return originalFetch.apply(this, args).then(response { console.log(Fetch response for:, args[0], response); // 如果需要修改响应可以克隆response并处理 return response; }); };将上述代码粘贴到开发者工具的Console中执行即可生效。这样当目标代码调用这些函数时相关信息就会在Console中打印出来为我们指明方向。4. 针对特定混淆技术的破解技巧4.1 处理“无限Debugger”有些网站会通过setInterval或循环中插入debugger;语句来干扰调试。解决方法有几种在Sources面板找到对应的行右键点击行号选择“Never pause here”。在触发debugger前通过条件断点或Overrides功能用空函数替换setInterval或直接注释掉debugger语句。更彻底的方法是在开发者工具设置F1打开中勾选“Disable JavaScript”然后刷新页面再取消勾选并立即下好需要的断点。但这可能影响页面功能。4.2 还原Webpack模块系统我们的目标是将一个依赖Webpack环境的模块变成独立的、可运行的函数。假设我们找到了目标模块函数它在Webpack中的结构是这样的// 在 webpackJsonp 的模块对象里 123: function(e, t, n) { var r n(456); // 依赖模块456 var o r.someFunction; // ... 业务逻辑 t.exports { myFunc: function() { ... } }; }n是Webpack的__webpack_require__函数。要独立运行这个函数我们需要找到它依赖的所有模块如456。创建一个模拟的n函数当传入模块ID时返回对应的导出对象。准备一个空的t对象{ exports: {} }作为第二个参数。执行这个模块函数然后从t.exports中获取我们需要的函数。这通常需要手动分析依赖图或者编写脚本自动提取。一个取巧的办法是在浏览器调试时直接在该模块函数的作用域内将t.exports赋值给一个全局变量比如window.myModule t.exports然后就可以在Console里直接调用myModule.myFunc()了。4.3 解密动态Eval的代码如果核心逻辑是经过加密后动态执行的你可能会在代码中看到大量的字符串拼接和Function构造器或eval调用。在eval或new Function调用处下断点。当断点触发时在Console中查看即将被执行的字符串内容。Chrome DevTools可以直接将鼠标悬停在变量上或者将其复制出来。复制出来的字符串很可能是一段可读的JS代码这就是被解密后的核心逻辑。你可以将其保存下来单独分析。5. 案例复盘定位一个加密参数sign以虚构的“诺禾”网站搜索请求为例假设其请求体为{“keyword”: “test”, “page”: 1, “sign”: “a1b2c3d4e5f”}其中sign是加密参数。下断点在Network面板找到搜索请求右键选择“Copy - Copy as fetch”。在Console中粘贴并修改添加debugger语句在fetch调用前然后执行。或者直接使用XHR/Fetch断点。跟栈断住后在Call Stack中从fetch或send往上找找到构造请求体的函数。假设我们找到了一个函数buildParams(keyword, page)。分析进入buildParams函数单步执行。发现它调用了另一个函数generateSign(params)。步入这个函数。破解generateSign内部非常混淆。但我们观察到它最后调用了CryptoJS.MD5(someString).toString()。我们关注someString是如何生成的。通过单步和Console求值发现someString是keyword、page和一个隐藏的timestamp按特定格式拼接再和一个从window._globalConfig对象里取出的secretKey拼接后的结果。验证在Console中我们手动模拟这个过程var keyword “test”; var page 1; var timestamp Date.now(); // 假设是当前时间戳 var secretKey window._globalConfig.secretKey; // 从内存中获取 var strToSign ${keyword}|${page}|${timestamp} secretKey; var calculatedSign CryptoJS.MD5(strToSign).toString(); console.log(calculatedSign); // 与请求中的sign对比剥离将generateSign函数及其依赖的CryptoJS或找到的MD5实现提取到本地Node.js脚本中。注意补全window._globalConfig这个环境变量它可能在另一个JS文件中初始化我们需要找到并提取这个secretKey的赋值逻辑。6. 常见问题与排查技巧实录在逆向过程中你几乎一定会遇到下面这些问题。这里是我的排查清单问题现象可能原因排查思路与解决方案搜索关键词在代码中完全找不到1. 关键词被混淆或编码。2. 代码被分割到多个Chunk当前文件不包含。3. 逻辑由Web Worker或Service Worker执行。1. 尝试搜索编码后的形式如Base64、Unicode转义。2. 在Network面板查看所有加载的JS文件逐个搜索。3. 在Application面板检查Workers。调用栈非常深且混乱无法跟踪代码经过了严重的控制流扁平化混淆。1.放弃逐行跟踪采用“输入-输出”黑盒分析法。2. 在疑似入口和出口函数下断点对比传入和传出的数据。3. 使用console.trace()Hook关键函数只看调用关系。复现的加密函数结果与浏览器不一致1. 环境依赖缺失如浏览器全局对象、特定API。2. 依赖了其他未提取的模块内部状态。3. 有随机数或时间戳参与运算。1. 在Node中用global模拟window补全document,navigator等。2. 仔细检查函数作用域内所有引用的外部变量确保都已正确实现。3. 将浏览器中运行时的随机数或时间戳固定下来再进行比较。网站更新后原有逻辑失效加密算法或密钥发生变更。1. 重新抓包对比新旧请求的差异。2. 检查核心JS文件的哈希值是否改变。3. 重新运行一遍逆向流程通常新的逻辑与旧的在结构上相似。触发无限Debugger或页面卡死网站启用了反调试。1. 使用“Never pause here”禁用特定debugger语句。2. 尝试在无痕模式或禁用所有扩展的情况下调试。3. 使用setInterval function(){};等代码在Console中提前覆盖反调试函数。最后的建议JS逆向是一个极其需要耐心和细致观察力的工作。它没有一成不变的银弹。最重要的不是记住所有工具和技巧而是培养一种系统性的分析思维从网络层到应用层从静态结构到动态执行大胆假设小心求证。每一次成功的逆向都是对JavaScript这座冰山更深一层的潜入。当你终于捋顺那看似“变态”的逻辑时所获得的不仅是目标数据更是一种解决问题的通透感。这个过程本身就是最好的学习。
返回列表