ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

webpack打包代码逆向实战:从sign签名定位到Python爬虫实现

webpack打包代码逆向实战:从sign签名定位到Python爬虫实现 做爬虫这几年最怕碰到的就是 webpack 打包的前端代码。一坨压缩到极致的大文件动辄几万行挤在一起变量名全被压成 e、t、n、o你想找一个签名参数是从哪儿生成的真像在一堆乱麻里找线头。但掌握了 webpack 打包代码的调试思路之后回头看这事儿其实没那么玄——它约等于拆集装箱所有模块都明明白白摆在那里你缺的只是入口和调用顺序。这篇文章我就用一条完整链路带你走一遍发现某个后台站点的请求里带了 sign 签名参数用 Chrome DevTools 定位到 webpack 模块把签名算法逆向出来再到 Node.js 里补环境跑通最后让 Python 爬虫直接复用这套签名逻辑发请求。整个过程适用于大多数 webpack 打包的页面适合刚开始接触 JS 逆向、或者对 webpack 源码一头雾水的同学。1. webpack 打包的项目到底长什么样先说一句实话很多人在逆向时卡住不是因为后面的算法有多难而是因为压根没看懂眼前这堆 JS 是什么结构。webpack 打包出来的代码外表看起来全是压缩后的乱码内部却有一套非常规律的模块机制。把这套机制看明白了后面所有步骤都顺理成章。1.1 先看懂 webpack 的模块机制webpack 的核心思想是把项目里几十上百个 JS 模块统一打包成一个或多个 bundle 文件。打包之后每个模块都有一个数字 ID模块之间通过一个叫__webpack_require__的函数按 ID 互相加载。你可以把它理解成一个微型 CommonJS 运行时__webpack_require__(12)就相当于require(./xxx)只是这里传入的是数字 ID。下面这段代码是 webpack 4 打包后最典型的运行时骨架我特意整理成了易读版本(function(modules) { var installedModules {}; function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module installedModules[moduleId] { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.l true; return module.exports; } __webpack_require__.m modules; __webpack_require__.c installedModules; __webpack_require__.d function(exports, name, getter) { if (!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter }); } }; __webpack_require__.o function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }; return __webpack_require__((__webpack_require__.s 0)); })([ /* module 0 */ function(module, exports, __webpack_require__) { // 入口模块 }, /* module 1 */ function(module, exports, __webpack_require__) { // 某个业务模块可能就有签名逻辑 } ]);webpack 5 的运行时变量名和加载细节会有些差异但总体思路完全一样。实际逆向时你会看到压缩版本!function(e) { var t {}; function n(r) { if (t[r]) return t[r].exports; var o t[r] { i: r, l: !1, exports: {} }; return e[r].call(o.exports, o, o.exports, n), o.l !0, o.exports; } // ... return n(n.s 0); }([/* ... */]);不管变量名是什么你只需要记住三件事最外层自执行函数接收一个模块数组n(r)按 ID 加载模块n.s 0是入口模块 ID。这三件事搞懂了webpack 包在你眼里就不是乱码而是一个个可以单独拎出来的模块仓库。1.2 快速识别目标和入口模块拿到一个站点怎么快速判断它是 webpack 打包的三个特征在 Sources 面板里打开主 JS 文件开头是一个!function(...)([...])的自执行表达式后面跟着巨大的数组数组元素是模块函数。全局搜索webpackJsonp字样如果搜到了说明站点还可能用了动态分包加载后面会多一个异步模块注入的环节。搜索__webpack_require__或者压缩后的特征代码\.s也能确认。确认是 webpack 之后建议先定位入口模块。入口模块通常包含应用启动逻辑会调用很多其他模块。定位方法很简单看包文件最后一行的n(n.s 0)或者return __webpack_require__(__webpack_require__.s 0)那个数字就是入口 ID。但说实话我们在逆向签名参数时并不需要从入口开始读代码一般直接从请求参数和调用栈切入会更快。提示如果你在 F12 的 Sources 面板里直接打开 JS 文件可以先点左下角的“{}”按钮把代码格式化。格式化之后虽然变量名还是压缩状态但起码缩进和换行正常了搜索定位会轻松很多。2. 调试工具与断点思路工具不贪多Chrome DevTools 一个就够。Fiddler 或 Charles 这类抓包工具只有在需要拦截响应、替换 JS 文件时才用到后面会提。这里我重点讲怎么用 DevTools 把断点下在“请求发出之前”这是还原签名参数最关键的入口。2.1 工具选型和前期准备我日常逆向的前期准备就三步用 Chrome 打开目标页面按 F12 进入 DevTools。切到 Network 面板勾选 Preserve log避免页面跳转后日志清空。在页面里触发一个需要签名的请求找到对应的 XHR 请求先看它的 URL、请求头、请求体确认需要还原的参数名。准备工作做完核心矛盾就清晰了sign到底是在哪个 JS 函数里生成的生成前发生了哪些拼接和计算接下来就是用断点和 Hook 把现场“冻结”住一步一步看它是怎么算出来的。2.2 两种断点切入的经典姿势姿势一XHR 断点。在 Sources 面板右侧找到 XHR/fetch Breakpoints点加号输入接口 URL 里的某个关键词比如api/list。再触发请求时代码会停在发起请求的那一行附近。这个方式对大多数 webpack 应用都很有效因为用户点击按钮后总会走到某个地方调用XMLHttpRequest.send或者fetch而签名参数一定在发送之前生成。姿势二全局搜索断点。如果你知道签名参数名不一定要等断点。在 Sources 面板按CtrlShiftF全局搜索sign搜索结果里会列出所有包含 sign 字符串的代码位置。很多站点会把 sign 作为对象属性名比如t.sign ...、{sign: ...}这样能直接跳到赋值位置。跳到那里打下断点再触发一次请求就能看到 sign 的生成现场。这两个姿势不冲突往往是搭配使用。我自己的习惯是先用 XHR 断点拿到调用栈再回头看调用栈中哪个函数做了 sign 的赋值然后去那个函数里补一个普通断点单步追踪。2.3 用 Hook 把程序“定住”有些站点比较刁钻请求不是你自己点出来的而是页面内部定时器自动触发的手动断点比较费劲。这时候用 Hook 更省事。Hook 的本质是替换掉浏览器里的原生函数在它执行前后插入自己的日志或断点。// 记录所有 XHR 请求的 URL 和发送体 var origOpen XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open function(method, url, async, user, pass) { this._url url; return origOpen.call(this, method, url, async, user, pass); }; var origSend XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send function(body) { console.trace([xhr], this._url, body); return origSend.call(this, body); };把这段代码粘贴到 Console 里执行之后所有后续的 XHR 发送都会在 Console 里打印调用栈。调用栈里的一层层函数就是从事件入口到发送请求的完整路径顺着路径找 sign 的生成位置比盲目搜索准确得多。Hook 的时机要讲究。如果在页面加载完成后再粘贴代码只能 Hook 到之后触发的请求如果页面刚加载就自动发请求你需要把 Hook 脚本放到页面代码之前执行。Chrome 的 Overrides 功能可以做到把修改后的 JS 文件保存到本地并覆盖线上文件或者用油猴脚本在页面开始阶段注入代码。注意Hook 只是辅助定位不要依赖它直接改签名结果。签名结果通常会被网站后台校验你不能也不应该篡改它我们的目标只是“还原它的生成逻辑”让 Python 能生成一模一样的合法签名。3. 从函数调用栈追到签名算法XHR 断点或者 Hook 把请求调用栈打出来之后真正的逆向才开始。这里我拿一个典型的内部数据后台来做演示。这个后台的请求长这样POST /api/customer/list { page: 1, pageSize: 20, ts: 1735000000000, nonce: 8f3a2bc9d4e5f617, sign: 9b7a1c3d4e5f6a7b8c9d0e1f2a3b4c5d }ts是毫秒时间戳nonce像是随机字符串sign是一串 32 位的十六进制字符串熟悉的人一看就知道八成是 MD5。3.1 入口函数的定位先按CtrlShiftF搜索sign。搜索结果里一般会有几十条记录我们筛选出像t.sign、sign:,setSign(这类和参数赋值相关的行。点进去之后把断点下在这一行然后刷新页面或者重新触发请求。断点命中后代码停住了。我通常先看 Scope 面板里的局部变量尤其是t、e、n这些压缩变量。如果在某个对象里已经能看到sign属性说明断点位置就在签名生成之后如果还没看到 sign就往上翻调用栈找更靠前的函数继续看。实际操作中我习惯把断点下在t.sign这一行然后在 Console 里手动执行t里的其他字段确认时间戳和 nonce 是否已经生成。这样能看清签名生成前哪些参数已经确定了哪些参数是签名内部自己算的。3.2 逐层逆向签名函数我第一次在这个后台里断到 sign 赋值时看到的代码大概是这样的var t { page: e.page, pageSize: e.pageSize, ts: Date.now(), nonce: Object(u.a)(16) }; t.sign Object(n.f)(Object.keys(t).sort().map(function(e) { return e t[e]; }).join());这段代码在格式化之后非常直白把page、pageSize、ts、nonce放进对象取所有 key 排序拼成keyvaluekey2value2形式的字符串再调用n.f生成签名。压缩版本里Object(n.f)表示从模块n中取f方法这里的n是__webpack_require__的另一个模块引用。我当时顺着Object(n.f)找到了签名算法模块。这个模块内部是这样的var r function(e) { var t e key7f8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e; return Object(a.md5)(t).toString(); };到这里谜底就揭晓了拼接原始字符串加上固定密钥key再算 MD5。整个签名过程没有任何复杂的 RSA、AES就是最普通的拼接哈希。但如果没有断点和调用栈一层层追下来这个n.f背后的逻辑很难凭空猜出来。3.3 把算法模块从 webpack 大包里剥离签名逻辑确认后下一步是把相关模块从 webpack 大包里“扣”出来。在 Console 里执行// 假设签名模块 ID 是 56 copy(__webpack_require__.m[56].toString());copy方法会把模块源码以字符串形式复制到剪贴板。粘贴出来之后你就得到了一个函数function(module, exports, __webpack_require__) { // 签名算法实现 }这个函数还依赖其他模块依赖关系要一并处理。简单的办法在 Console 里执行__webpack_require__(56)看看它的导出对象长什么样。如果导出对象里正好有生成 sign 的方法那就完美了——我们把整个模块和它的直接依赖模块一起提取出来组成一个独立的“迷你 webpack 包”。实操心得不要试图一次性把整个 webpack 包 4 万行代码全部看懂。谁也没有那个精力。我们的目标是“最小可用集合”目标模块 它依赖的模块 必要的基础库。范围越小后面补环境越省事。4. 用 Node.js 把签名逻辑跑起来模块扣出来了接下来要让它脱离浏览器环境运行。webpack 模块内部经常引用window、document、navigator、localStorage这类浏览器对象而 Node.js 里没有这些东西直接跑肯定报window is not defined。解决思路很朴素先装一个假的浏览器环境缺什么补什么直到代码能跑通。4.1 补环境的核心思路补环境不能瞎补我的策略是“报错驱动”先把扣出来的模块放进 Node.js 里跑它报什么错我就补什么对象。比如报window is not defined就在环境对象里加window: {}报navigator is not defined就加navigator: { userAgent: ... }。补完之后再跑遇到新的报错再补如此循环。但有些模块不会直接报引用错误而是运行到某个方法时才报错比如localStorage.getItem is not a function。这时候要仔细看报错堆栈定位到具体是哪一行用了这个方法再决定补什么。常见需要补的对象有window、self、globalThisdocument及其子对象navigatorlocationlocalStorage、sessionStorageXMLHttpRequest、fetch、FormDataatob、btoa、TextEncoder、TextDecoderCanvasRenderingContext2D、HTMLCanvasElement等图形相关对象站点做了 canvas 指纹时用补环境时有个小技巧window和globalThis要指到同一个对象很多 JS 会判断typeof window ! undefined如果缺了一个分支逻辑就会走偏。4.2 在 Node 里加载 webpack 模块的两种方式方式一整体加载 webpack 包 vm 沙箱。把整个 webpack 包保存成target.bundle.js用 Node 的vm模块创建一个沙箱在沙箱里预置浏览器环境对象然后运行 bundle。为了能从外部调用内部模块需要想办法拿到__webpack_require__引用。标准的 webpack 包没有暴露它我一般用字符串替换的方式在包的开头之后、自执行函数内部插入一行暴露代码// 把 return __webpack_require__((__webpack_require__.s 0)); // 替换为 window.__webpack_require__ __webpack_require__; return __webpack_require__((__webpack_require__.s 0));然后在 Node 脚本里这样加载const fs require(fs); const vm require(vm); const bundle fs.readFileSync(./target.bundle.js, utf-8) .replace( /return __webpack_require__\(\(__webpack_require__\.s (\d)\)\);/, window.__webpack_require__ __webpack_require__; return __webpack_require__((__webpack_require__.s $1)); ); const sandbox { console: console, setTimeout: setTimeout, setInterval: setInterval, clearTimeout: clearTimeout, clearInterval: clearInterval, Date: Date, Math: Math, JSON: JSON, Object: Object, Array: Array, String: String, Number: Number, parseInt: parseInt, parseFloat: parseFloat, encodeURIComponent: encodeURIComponent, decodeURIComponent: decodeURIComponent, location: { href: https://target.com/, search: , hash: }, navigator: { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36 }, localStorage: { getItem: () null, setItem: () {}, removeItem: () {} }, sessionStorage: { getItem: () null, setItem: () {}, removeItem: () {} }, document: {}, XMLHttpRequest: function() {}, FormData: function() {}, atob: (s) Buffer.from(s, base64).toString(binary), btoa: (s) Buffer.from(String(s), binary).toString(base64) }; sandbox.window sandbox; sandbox.self sandbox; sandbox.globalThis sandbox; vm.createContext(sandbox); vm.runInContext(bundle, sandbox, { filename: bundle.js }); // 现在可以通过 sandbox.__webpack_require__(56) 调用签名模块 const signModule sandbox.__webpack_require__(56); console.log(signModule);方式二提取模块 自己写一个迷你 webpack 运行时。这种方式更可控也适合分享给团队其他人使用。把前面copy出来的目标模块和依赖模块整理成一个对象key 是模块 IDvalue 是模块函数。然后写一个极简加载器// mini-runtime.js const modules { 12: function(module, exports, __webpack_require__) { // 从 bundle 中提取的模块 12 }, 56: function(module, exports, __webpack_require__) { // 从 bundle 中提取的模块 56签名模块 } }; function __webpack_require__(id) { if (__webpack_require__.cache[id]) { return __webpack_require__.cache[id].exports; } const module { i: id, l: false, exports: {} }; __webpack_require__.cache[id] module; modules[id].call(module.exports, module, module.exports, __webpack_require__); module.l true; return module.exports; } __webpack_require__.cache {}; module.exports { __webpack_require__, getSignModule() { return __webpack_require__(56); } };两种方式各有利弊。整体加载最省事不用精确分析依赖关系但沙箱里要补的环境对象可能非常多提取模块最干净但要搞清楚模块依赖一旦提取不全就会报错。我的建议是初学阶段先用“整体加载 报错驱动补环境”把签名跑通了再考虑提取模块做精简。4.3 验证签名的正确性签名函数在 Node 里跑起来之后一定要做一步验证用浏览器里的同一组参数在 Node 里生成一次签名对比两边结果是否一致。具体操作是在浏览器里触发一次请求把page、pageSize、ts、nonce记录下来。在 Node 里调用签名函数传入完全相同的参数。对比 Node 生成的 sign 和浏览器实际请求里的 sign。如果一致说明算法和参数顺序都正确如果不一致大概率是拼接顺序不同、多了隐藏字段或者有些参数在签名前被做了类型转换比如数字转字符串、对象被 JSON.stringify。这一步做好了后面 Python 对接才有意义。5. Python 对接与签名参数还原签名逻辑在 Node 里跑通之后Python 爬虫怎么用这里有好几条路选哪条取决于你的场景和签名算法复杂度。5.1 方案选型纯 Python 重写还是调用 JS我列一个简单对比表供大家按需选择方案优点缺点适合场景纯 Python 重写算法无额外依赖、性能好、部署方便算法复杂时还原成本高签名只是 MD5、SHA 等标准哈希或简单拼接execjs 调用 Node开发快、保留原始 JS依赖本机 Node 环境、性能一般算法中等复杂、不要求极高并发子进程调用 Node 脚本可控性强、数据通过 JSON 传递需要维护进程通信大量并发请求、需要稳定输出py-mini-racer 嵌入式 V8轻量嵌入式运行包维护一般、ES6 支持有限简单 JS 模块、不想依赖外部进程回到我那个后台的例子它的签名本质是md5(sortedParams key)这类标准算法直接在 Python 里重写是最干净的没有必要为了一个 MD5 去启动 Node 进程。但在实战中很多 webpack 站点的签名会有 Obfuscator 混淆、多轮编码、依赖某个固定环境指纹纯 Python 重写成本太高这时候就需要让 Python 直接调用之前跑通的 JS 模块。5.2 用 execjs 让 Python 直接调用 JS 签名函数PyExecJS是 PyPI 上的老牌库它的作用就是在 Python 里执行 JavaScript 代码。基本用法很简单pip install PyExecJS然后写一个get_sign.js文件把 Node 里验证过的签名逻辑放进去function getSign(page, pageSize, ts, nonce) { const params { page: page, pageSize: pageSize, ts: ts, nonce: nonce }; const raw Object.keys(params).sort() .map(key key params[key]) .join() key7f8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e; // 这里假设 md5 通过一个外部依赖获取实际要替换成你提取到的模块方法 const crypto require(crypto); return crypto.createHash(md5).update(raw).digest(hex); } module.exports { getSign: getSign };Python 端调用import execjs with open(get_sign.js, r, encodingutf-8) as f: js_code f.read() ctx execjs.compile(js_code) page, page_size, ts, nonce 1, 20, 1735000000000, 8f3a2bc9d4e5f617 sign ctx.call(getSign, page, page_size, ts, nonce) print(sign)如果签名逻辑很复杂你的get_sign.js可能不是一个独立文件而是包含迷你 webpack 运行时和目标模块的文件。这时候只要把第 4 章里验证过的脚本封装成一个函数导出供 execjs 调用思路完全一致。5.3 用子进程调用 Node 脚本性能更好execjs 每次调用都会启动一个 Node 进程如果爬虫频率高性能开销会比较大。更高效的做法是让 Python 直接通过标准输入输出和 Node 进程通信import subprocess import json def get_sign_with_node(params): result subprocess.run( [node, get_sign_worker.js], inputjson.dumps(params), capture_outputTrue, textTrue, encodingutf-8 ) if result.returncode ! 0: raise RuntimeError(fNode 执行失败: {result.stderr}) return result.stdout.strip()对应的get_sign_worker.jsconst { getSign } require(./get_sign.js); let input ; process.stdin.on(data, chunk input chunk); process.stdin.on(end, () { const params JSON.parse(input); const sign getSign(params.page, params.pageSize, params.ts, params.nonce); process.stdout.write(sign); });这种方式的优点是稳定、不依赖第三方库而且 Node 进程可以按需复用。缺点是进程管理要自己注意不能让子进程堆积。注意Python 传给 JS 的参数不管是走 execjs 还是子进程统一推荐 JSON 序列化。Python 里的字典 key 排序、数字类型、中文字符串在 JSON 序列化后不会出幺蛾子。如果遇到中文被转义的问题在 Python 侧用json.dumps(params, ensure_asciiFalse)JS 侧JSON.parse就能还原。5.4 完整测试从造参到请求成功最后一步是走通完整的 Python 爬虫流程。我给一个极简的例子把签名、请求、校验串在一起import hashlib import json import time import secrets import requests def generate_sign(page, page_size, ts, nonce): params {page: page, pageSize: page_size, ts: ts, nonce: nonce} raw .join(f{k}{params[k]} for k in sorted(params)) key7f8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e return hashlib.md5(raw.encode(utf-8)).hexdigest() def fetch_customer_list(page, page_size): ts int(time.time() * 1000) nonce secrets.token_hex(8) sign generate_sign(page, page_size, ts, nonce) payload { page: page, pageSize: page_size, ts: ts, nonce: nonce, sign: sign } resp requests.post(https://target.example.com/api/customer/list, jsonpayload) resp.raise_for_status() return resp.json() if __name__ __main__: data fetch_customer_list(1, 20) print(json.dumps(data, ensure_asciiFalse, indent2))这个示例用的是纯 Python 重写方式因为例子里的签名就是 MD5。如果你遇到复杂签名只需要把generate_sign换成 execjs 或子进程调用 Node 的那套逻辑其他部分不用动。实测下来只要签名正确服务端返回的数据和浏览器里完全一致。6. 常见问题与排查技巧实录做 JS 逆向踩坑是常态。我把实际操作中最高频的问题整理成一个速查表再分享几条只有折腾过才会懂的老鸟经验。6.1 高频报错速查表报错信息原因解决办法window is not definedNode 环境没有 window 对象在 sandbox 里定义window: {}并让window sandboxnavigator is not defined缺 navigator 对象补一个带userAgent的 navigator内容最好和浏览器一致document is not defined缺 document 对象补{ createElement: () ({}) }等最小可用对象Cannot read property xxx of undefined某个对象没有初始化看报错堆栈定位到具体对象逐步补全属性xxx is not a function模块依赖没有正确加载检查模块 ID 和依赖模块是否全部提取可能是 ES Module 的.default没取TextEncoder is not defined浏览器 API 缺失在 Node 里const { TextEncoder, TextDecoder } require(util)后注入 sandboxregeneratorRuntime is not defined代码里用了 async/await 等语法补regenerator-runtime依赖排查时有一个总原则报错信息只告诉你“缺少什么”不告诉你“为什么缺少”。把报错堆栈完整贴到编辑器里看是哪一行代码触发再判断是“环境没补”还是“模块依赖没加载”两种处理路径完全不同。我见过太多人一看到报错就盲猜结果补了一堆没用的对象。6.2 老鸟才懂的几个常规套路第一用 Chrome Overrides 把 JS 文件替换成本地文件可以在关键函数里插入console.log再刷新页面看输出。这是定位 webpack 模块内部执行顺序的利器。操作方法是Sources → Overrides → 选择本地文件夹 → 在 Sources 里右键要修改的 JS → Save for overrides之后就能直接改本地文件并生效。第二反调试不是绕不过去的坎。有些站点会在代码里插入debugger语句或者检测 Console 是否被打开导致你一开 F12 就陷入无限断点循环。这时候可以在 DevTools 里右键断点所在行选择 “Never pause here”或者直接用CtrlF8关闭所有断点再配合 Overrides 把debugger关键字替换成空字符。第三跟字符串数组混淆打交道时别硬读代码。常见混淆方案会把所有字符串拆成字符数组运行时再拼接还原。你可以先找到数组定义的代码在 Console 里执行一次数组展开把还原后的字符串打印出来再回去读业务逻辑就清楚多了。第四webpack 5 的包结构和 webpack 4 有差异尤其是异步加载模块时webpackJsonpCallback的注入方式变了。如果你发现目标模块不在主包数组里而是通过import()动态加载的别慌在 Network 面板里找到额外加载的 JS chunk单独分析即可。6.3 一定要记得整理“签名还原笔记”这个建议可能看着不像技术但长期来看价值极大。每逆向完一个站点我会把以下内容整理成一个 Markdown 文档请求 URL、需要还原的参数名、签名算法描述、涉及的模块 ID、依赖模块 ID、补环境时的最小环境对象清单、Python 调用代码的最终版本。为什么这个习惯很重要因为同一个站点或者同一套前端框架改版后签名逻辑大概率只是小改动。有了笔记下次改版只需要重新定位模块、对比差异通常半小时就能重新跑通没有笔记一切从零开始可能又要花一天。对于做采集、做数据分析和做自动化测试的人来说这个“时间复利”远比想象中可观。最后分享一点个人体会webpack 逆向这件事本质上不是魔法而是把打包后的模块化代码“翻译”回可读逻辑。刚开始面对几万行的压缩文件我也觉得无从下手但兜兜转转几轮之后发现只要沉住气顺着调用栈走完一次完整链路后面再遇到类似的站点思路基本是复用的找入口、跟参数、扣模块、补环境、跑通 Python。另外想提醒一句这些技术手段只应该用在你有权访问和研究的系统上。无论是分析自己负责的前端项目、给旧系统做数据迁移还是做接口兼容测试前提都是合法合规。带着这个前提去折腾心态也会稳很多。最后再分享一个小技巧调试过程中所有保存下来的 JS 片段、补环境脚本、测试参数都按“站点名日期”建目录保存。等你做多了十几个站点之后这些目录就是你最宝贵的“签名还原字典”很多常见的算法和混淆套路都能在里面直接找到参考实现。
返回列表