ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

WebpackJsonp逆向分析:破解前端打包加密的核心技术

WebpackJsonp逆向分析:破解前端打包加密的核心技术 1. 从一次失败的抓包说起为什么webpackJsonp是JS逆向的“拦路虎”最近在分析一个数据接口时遇到了一个典型又让人头疼的场景。用浏览器开发者工具抓包能看到一个关键的data请求返回的是一串加密的密文。这很正常逆向的日常工作就是跟这些密文打交道。但当我把目光投向发起这个请求的JavaScript代码时问题来了——在“Network”面板里点击这个请求再跳转到“Initiator”标签看到的调用堆栈Call Stack最顶层往往是一个名字非常奇怪、毫无意义的函数比如__webpack_require__.f.j或者一个由数字和字母组成的匿名函数。点击这个函数试图查看其源码逻辑时浏览器会直接定位到一个巨大的、被压缩过的JS文件。这个文件通常有几MB甚至十几MB里面充斥着类似(0, o.default)(e, t)、n n.__esModule ? n : { default: n }这样的“天书”。更令人沮丧的是你在这个庞然大物里搜索任何与业务逻辑相关的关键词比如“encrypt”、“sign”、“params”都一无所获。所有的业务代码仿佛被一个黑洞吞噬了你看到的只是一个由webpack打包工具生成的、高度抽象的模块加载和执行框架。这就是window.webpackJsonp及其背后Webpack打包技术给JS逆向工程师设下的第一道关卡。它不像传统的、将所有函数和变量都暴露在全局作用域下的代码你可以轻易地在控制台通过window.xxx来调试和追踪。Webpack将源代码切分成数十上百个模块并用一个运行时runtime来动态加载和管理它们。window.webpackJsonp就是这个运行时机制在异步加载模块时与浏览器环境交互的一个关键“钩子”或“协议”。对于不熟悉其机制的分析者来说面对一个被Webpack打包的站点就像面对一个上了锁的黑箱你知道数据在里面被处理却找不到处理数据的那把“钥匙”——具体的业务函数——在哪里。因此理解window.webpackJsonp不仅仅是认识一个全局变量更是掌握一套逆向分析现代前端打包代码的“地图”和“开锁工具”。它直接关系到你能否在数以万计的压缩代码行中定位到真正负责加密、签名、参数构造的核心函数从而完成逆向分析中最关键的一步理清数据流转链路。2. Webpack模块化与webpackJsonp的诞生从源码到运行时要拆解window.webpackJsonp我们必须先回到问题的源头Webpack是如何工作的。现代前端开发早已告别了在HTML里写几十个script标签的时代。开发者使用ES6的import/export语法将代码组织成清晰、独立的模块。例如一个加密函数可能单独放在utils/encrypt.js文件中然后在主业务文件app.js中引入。Webpack的核心任务就是将这些分散的模块根据依赖关系打包成一个或多个“捆绑包”bundle。在这个过程中Webpack会做几件重要的事依赖分析从入口文件如index.js开始分析所有import语句构建出一张完整的模块依赖图。代码转换与打包将每个模块的代码包裹在一个函数中这个函数通常被称为模块函数然后将所有这些模块函数组合到一个大的JS文件里。同时Webpack会生成一段自己的“运行时”代码负责模块的加载、缓存和执行。作用域隔离每个模块的函数都有自己的局部作用域模块内定义的变量和函数在默认情况下不会污染全局作用域即window对象。模块之间通过Webpack运行时提供的一套机制主要是__webpack_require__函数来互相引用。那么window.webpackJsonp是在什么环节出现的呢这涉及到Webpack一个重要的优化策略代码分割Code Splitting。为了提升首屏加载速度开发者不希望把所有代码都打到一个巨大的bundle里。他们会使用动态导入import()语法或者配置SplitChunks插件将某些模块如第三方库、路由组件单独打包成一个个“代码块”chunk。这些chunk文件通常是按需加载的。webpackJsonp就是Webpack用于在浏览器端动态加载并整合这些异步chunk的“粘合剂”。它的本质是一个全局数组或者更准确地说是一个被挂载到window对象上的数组。Webpack的运行时代码会定义这个数组并给它挂载一个push方法。这个push方法被“魔改”过它不仅仅是向数组添加元素更重要的是它会执行传入的模块数据将它们注册到Webpack的内部模块管理系统里。一个典型的异步chunk文件内容结构如下(window.webpackJsonp window.webpackJsonp || []).push([ [chunkId], // 代码块的ID { // 模块映射表 ./src/utils/encrypt.js: (function(module, exports, __webpack_require__) { // 这就是我们心心念念的加密模块的源码 function md5Encrypt(data) { // ... 具体的加密实现 return encryptedData; } exports.default md5Encrypt; }), ./src/api/request.js: (function(module, exports, __webpack_require__) { // 发起网络请求的模块 const encryptor __webpack_require__(./src/utils/encrypt.js); function sendData(data) { const encrypted encryptor.default(data); // ... 发送请求 } exports.sendData sendData; }) } ]);当浏览器通过script标签加载这个chunk文件时文件会立即执行调用window.webpackJsonp.push()。这个“智能”的push方法接收到模块数据后会将其中的模块函数如./src/utils/encrypt.js对应的函数注册到Webpack内部的模块仓库中。之后当主bundle或其他模块通过__webpack_require__(./src/utils/encrypt.js)请求这个模块时Webpack运行时就能从仓库中找到并执行它返回导出的内容。所以window.webpackJsonp是连接主bundle和异步chunk的桥梁是Webpack实现动态加载的“协议”或“事件总线”。在逆向中我们看到的那些奇怪的函数名往往就是某个模块函数内部的一行代码它通过__webpack_require__调用了另一个模块而这个调用链的源头可能就始于某次webpackJsonp.push的调用。3. 逆向实战定位并“拿下”webpack打包后的核心模块理解了原理我们进入实战环节。面对一个使用了Webpack打包的网站我们的目标是从window.webpackJsonp这个线索出发找到具体的业务模块并从中提取出我们需要的函数比如加密函数。以下是经过多次实战总结出的标准化操作流程。3.1 环境准备与初步侦察首先用Chrome浏览器打开目标网站并开启无痕模式避免插件干扰。打开开发者工具F12直接切换到“Sources”面板。这里是我们作战的主战场。在左侧的文件导航栏中通常能看到以域名命名的文件夹下面有大量的.js文件。其中文件名不带哈希值、体积最大通常有几MB的那个往往就是主bundle文件例如app.xxxx.js或main.xxxx.js。除此之外你还会看到许多以数字或短哈希命名的文件如1.xxxx.js,2.xxxx.js,chunk-vendors.xxxx.js这些就是异步加载的chunk。第一步直接在全站范围内搜索webpackJsonp。在“Sources”面板下按CtrlShiftF(Windows) 或CmdOptF(Mac) 打开全局搜索。输入webpackJsonp。搜索结果通常会显示两处关键位置在主bundle文件的头部或尾部有一段定义webpackJsonp数组并重写其push方法的代码。这是Webpack的运行时。在各个异步chunk文件中有对window.webpackJsonp.push的调用。找到主bundle中的定义后我们可以直接在控制台Console输入window.webpackJsonp并回车。如果网站使用了这种模式你会看到一个数组里面可能已经包含了一些元素每个元素对应一个已加载的chunk。这个数组本身的结构就是[[chunkId], modules]。3.2 关键技巧HookwebpackJsonp.push方法这是逆向中最常用、最有效的一招。既然所有异步模块都要通过调用webpackJsonp.push来注册那我们就在它被调用时“截胡”拿到原始的模块数据。在控制台或通过油猴脚本等方式注入以下代码// 保存原始的push方法 var oldPush window.webpackJsonp.push; // 重写push方法 window.webpackJsonp.push function(data) { // data 就是传入的 [[chunkId], modules] 数组 console.log([WebpackChunk] Chunk ID:, data[0]); console.log([WebpackChunk] Modules:, data[1]); // 遍历模块对象打印出所有模块路径方便我们查找 var modules data[1]; for (var modulePath in modules) { console.log( -, modulePath); // 如果你想深入研究某个模块可以在这里把它的函数体也打印出来 // console.log(modules[modulePath].toString()); } // 最后还是调用原始方法确保网站功能正常 return oldPush.apply(this, arguments); };执行这段代码后刷新页面或触发某个操作比如点击按钮触发新的异步加载控制台就会源源不断地输出新加载的chunk信息。你会看到类似这样的输出[WebpackChunk] Chunk ID: [123] [WebpackChunk] Modules: {…} - ./src/modules/user/login.js - ./src/utils/crypto/aes.js - ./src/api/account.js注意模块路径如./src/utils/crypto/aes.js是Webpack打包时内部的模块标识符它通常与源码的目录结构对应是寻找目标函数最直接的线索如果你在逆向一个加密参数看到crypto、encrypt、sign这类路径就要像猎人发现猎物踪迹一样兴奋。3.3 模块提取与函数重建通过Hook我们知道了包含目标函数的模块路径。接下来我们需要把这个模块函数“捞出来”并在我们自己的环境中执行它以便分析和调用。方法一直接从Hook中捕获。在上面的Hook代码里当你看到目标模块路径时可以立即将整个模块函数对象保存到一个全局变量中。window.webpackJsonp.push function(data) { var modules data[1]; if (modules[./src/utils/crypto/aes.js]) { // 把整个模块定义函数存起来 window.myTargetModule modules[./src/utils/crypto/aes.js]; console.log(目标模块已捕获); } return oldPush.apply(this, arguments); };然后你需要模拟Webpack的环境来执行这个模块函数。模块函数通常接受module,exports,__webpack_require__三个参数。我们可以自己构造一个简易的__webpack_require__函数或者直接使用网站原有的如果它能被访问到的话来执行它。// 假设我们已经把模块函数保存为 window.myTargetModule var module { exports: {} }; var require function(moduleId) { // 一个非常简单的require只能处理已经捕获的模块实际可能需要更复杂的实现 console.log(Require called for:, moduleId); // 这里需要根据实际情况返回依赖模块的exports // 如果依赖简单可以手动模拟 return {}; }; window.myTargetModule(module, module.exports, require); // 执行后加密函数很可能就挂在 module.exports.default 或 module.exports 上 console.log(模块导出内容:, module.exports); var encryptFunc module.exports.default || module.exports;方法二更稳健的方法是直接使用网站已经加载好的模块系统。Webpack通常会把__webpack_require__函数暴露在某个闭包内但我们可以通过遍历所有已加载的模块来寻找。一个更取巧的办法是在控制台直接尝试调用你怀疑是入口的函数并利用Chrome的调试功能定位其模块来源。例如在“Network”里找到加密请求在“Initiator”里点击那个最顶层的匿名函数然后在“Sources”面板里在这个函数内部某行打上断点触发请求。当断点命中时观察右侧的“Scope”面板在“Closure”或“Local”作用域里你很可能能看到清晰的、未被压缩的变量名和函数名从这里反向查找模块定义会更方便。实操心得很多站点的Webpack配置会将mode设置为production这会对模块函数进行名称压缩和混淆但模块路径字符串通常不会被混淆。因此通过HookwebpackJsonp.push打印出的模块路径是逆向过程中最稳定、最可靠的“路标”。即使函数内部的变量名都变成了a, b, c只要你能通过路径找到这个函数就成功了一大半。4. 进阶挑战应对Webpack的变种与混淆策略随着逆向攻防的升级网站开发者也会采用一些策略来增加分析的难度。window.webpackJsonp本身也可能被“伪装”或“改造”。情况一webpackJsonp被重命名或封装。有些站点不会直接使用window.webpackJsonp这个标志性的名字。你全局搜索可能找不到。这时我们需要寻找其本质一个数组并且它的push方法被重写。可以在控制台尝试以下命令来寻找可疑对象// 查找所有全局数组并检查其push方法是否被重写 Object.keys(window).forEach(key { var prop window[key]; if (Array.isArray(prop) prop.push.toString().includes(webpackJsonp)) { console.log(发现疑似webpackJsonp对象:, key, prop); } }); // 或者更宽泛地查找任何包含“webpack”字符串的全局变量 Object.keys(window).filter(k k.toLowerCase().includes(webpack));情况二模块ID被数字哈希替代。这是更常见的混淆。你Hook到的模块路径不再是./src/utils/encrypt.js而是变成了一个数字ID比如123。模块映射表变成了{123: function(...){...}}。这增加了我们通过路径关键词搜索的难度。 应对策略不要放弃Hook即使ID是数字Hook时依然能捕获到模块函数本身。你需要从函数体内部寻找线索。将模块函数toString()后搜索函数体内是否包含一些独特的字符串常量比如加密算法中常见的初始化向量iv、魔数0x67452301MD5、或特定的API URL片段。这些字符串是定位功能的关键。关联分析触发不同的网络请求观察每次新加载的chunk中包含哪些模块ID。结合请求的功能如登录、提交数据可以推测某些ID对应的模块功能。利用Source Map如果存在在开发者工具的“Sources”面板有时会看到一个webpack://的虚拟目录。这需要网站部署时未删除.map源映射文件。如果存在你可以直接在这里看到还原的源代码包括清晰的模块路径和变量名这是最理想的情况但生产环境较少见。情况三Webpack 5 的变化。从Webpack 5开始webpackJsonp机制逐渐被新的webpackChunk全局变量和push方法所取代但其核心思想不变——依然是一个全局的数组或对象用于chunk加载。你可能会遇到window.webpackChunk。处理方法同上Hook其push方法即可。踩坑记录有一次分析某电商网站其加密逻辑分散在多个异步chunk中。我成功Hook并提取了第一个chunk里的AES加密函数但调用时总是失败。排查了很久才发现这个加密函数内部依赖了另一个chunk里的一个用于生成密钥的辅助函数。我只提取了前者没有提供后者依赖的环境导致执行错误。教训是在模拟执行模块函数时如果遇到__webpack_require__调用失败需要检查该模块依赖的其他模块是否也已就位。有时需要将相关联的一整个chunk的所有模块都捕获并搭建一个简单的运行环境才能让目标函数正常工作。5. 构建可持续的逆向分析环境手动在控制台Hook和提取代码适合一次性或初步分析。但对于需要长期跟踪、或算法可能更新的情况我们需要更自动化和持久化的方案。方案一使用本地代理与脚本注入。使用像mitmproxy、AnyProxy或Fiddler这样的代理工具设置规则在目标网站的页面HTML或特定的JS文件返回时自动注入我们的Hook脚本。这样每次访问页面Hook代码都会自动执行无需手动操作控制台。方案二编写浏览器插件。开发一个简单的Chrome插件在content_script中注入Hook逻辑。这样可以获得更稳定的执行环境和更早的注入时机在页面JS执行之前确保能捕获到最早的webpackJsonp定义。方案三使用自动化调试工具。对于复杂的、动态加载严重的站点可以结合Puppeteer或Playwright这类无头浏览器库。在脚本中启动浏览器加载页面直接通过CDPChrome DevTools Protocol在页面上下文中执行Hook代码并持续监听webpackJsonp.push的调用将捕获到的模块函数和元数据自动保存到本地文件供后续分析。一个简单的Puppeteer示例如下const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch({ headless: false }); const page await browser.newPage(); // 在页面加载前注入我们的Hook脚本 await page.evaluateOnNewDocument(() { // 这里写和之前控制台一样的Hook代码 var oldPush (window.webpackJsonp window.webpackJsonp || []).push; (window.webpackJsonp window.webpackJsonp || []).push function(data) { console.log(Chunk loaded:, data[0]); // 可以在这里将数据发送到外部服务器或保存到变量 window._capturedChunks window._capturedChunks || []; window._capturedChunks.push(data); return oldPush.apply(this, arguments); }; }); await page.goto(https://target-website.com); // 等待并触发一些操作... await page.click(#some-button); // 稍后可以从页面上下文中提取捕获的数据 const chunks await page.evaluate(() window._capturedChunks); console.log(总共捕获了, chunks.length, 个chunk); await browser.close(); })();我个人在实际操作中的体会是对于window.webpackJsonp的分析其难点不在于理解这个变量本身而在于如何在动态、混淆、模块化的代码海洋中建立起一套有效的“追踪-定位-提取-还原”的方法论。它要求逆向工程师不仅要有扎实的JavaScript基础更要有耐心和系统性的思维。每一次成功的定位都像是完成一次精密的拆弹你需要顺着webpackJsonp这根“导线”小心翼翼地拆开Webpack打包机制的外壳最终触达核心的业务逻辑代码。掌握了这套方法市面上绝大多数基于Webpack的前端应用其JS逆向的大门就已经向你敞开了。
返回列表