1. 项目概述从“黑盒”到“白盒”的实战之旅“JS逆向”这四个字对于很多刚接触网络数据采集的朋友来说既神秘又令人头疼。它不像写个请求、解析个HTML那么简单更像是在和网站的后台工程师玩一场“猫鼠游戏”。对方用JavaScript简称JS把关键数据层层加密、混淆让你拿到的响应是一堆看不懂的乱码而你的任务就是像侦探一样抽丝剥茧找到数据加密的原始逻辑并用自己的代码复现它最终拿到明文数据。这个过程就是JS逆向。我之所以写这个“案例实战2”是因为我发现很多教程要么停留在理论讲一堆AST、混淆原理看得人头大却不知从何下手要么就是给一个已经过时的、极其简单的案例读者照做一遍后遇到真实网站依然束手无策。我的目标很直接通过一个高度模拟真实商业网站防护水平的实战案例带你走完从打开浏览器开发者工具到最终用Python成功拿到数据的完整闭环。你会经历定位加密入口、分析调用栈、扣取关键代码、补环境、调试直至成功复现的全过程。这不仅是一个技术操作指南更是一套面对未知加密网站时的通用分析心法和排错思路。无论你是爬虫工程师、安全研究员还是对Web技术原理有浓厚兴趣的开发者这套实战经验都能让你在面对JS加密时从“无从下手”变得“心中有谱”。2. 目标网站分析与加密定位策略2.1 目标选择与初步侦察本次实战我们选择一个具有典型反爬特征的网站作为目标为避嫌我们称其为A站。A站的数据接口返回的JSON中核心数据字段如列表内容、价格等是一串无规律的密文而页面却能正常显示。这明确告诉我们数据在传输过程中被加密解密逻辑必然由前端JS完成。第一步永远是“侦察”。打开Chrome开发者工具F12切换到Network网络面板刷新页面或触发数据加载动作。很快我们找到一个关键的XHR/Fetch请求其响应Response类似于{ code: 200, data: U2FsdGVkX1/...很长一串Base64样子的字符串..., message: success }这里的data字段就是加密后的数据。我们的核心目标就是找到将这段data解密成明文对象的JS代码。注意不要一上来就搜索“decrypt”、“decode”等关键词在高度混淆的代码中这些函数名很可能被改得面目全非。更可靠的方法是追踪数据流。2.2 基于“堆栈”的加密入口定位法最有效的方法是利用开发者工具的“堆栈”追踪功能。在Network面板中找到那个返回加密data的请求右键点击它选择“Replay XHR”有时不可靠更好的方法是在请求的Headers标签页找到Request URL。回到Sources源代码面板按CtrlShiftFWindows或CmdOptFMac进行全局搜索。搜索该URL的一部分通常是接口路径如/api/data/list。这样能直接找到发起这个网络请求的JS代码位置。找到代码位置后在其附近打上断点。重新触发请求代码执行会在断点处暂停。此时关键操作来了不要直接步进Step Into而是看向开发者工具右侧的Call Stack调用堆栈面板。调用堆栈显示了当前断点位置是由哪些函数一层层调用过来的。这是一个“自顶向下”的调用链。我们的目标——解密函数——很可能就在这个调用链中在发起请求的代码之后被执行因为解密发生在拿到响应之后。我们需要在堆栈中寻找处理响应response的地方。通常处理响应的代码会在Promise的then方法、async/await函数或者XMLHttpRequest的onreadystatechange事件回调里。在堆栈中点击这些可能的函数查看其源代码。如果看到有代码对response.data或类似变量进行了操作比如调用了一个函数或者进行了一系列赋值这里就可能是解密入口。我个人的心得是重点关注堆栈中靠近顶部的、非库文件如jquery.min.js的匿名函数或项目自身JS文件。在这里我找到了一个名为_0xabc123的函数它接收了response.data作为参数。通过单步调试F11进入这个函数确认其输出正是解密后的明文对象。至此加密入口锁定成功。3. 核心加密逻辑分析与代码扣取3.1 逆向分析与逻辑梳理锁定入口函数_0xabc123后我们进入最核心的逆向分析阶段。这个函数本身可能不复杂但它内部会调用其他函数形成一条调用链。我们需要使用开发者工具的调试功能Step Over, Step Into, Step Out来理清逻辑。首先在_0xabc123函数内部打上断点观察传入的参数即加密的data字符串和最终返回的结果。然后一步步执行注意观察变量转换data字符串是否先被atobBase64解码或进行了一些字符串分割操作关键函数调用data被传递给了哪个函数这个函数的名字可能被混淆如_0xdef456。常量与密钥在解密过程中是否用到了某些固定的字符串或数值这些可能就是密钥Key或初始化向量IV。在调试器的Scope作用域面板或直接将鼠标悬停在变量上可以查看其值。以本次案例为例我跟踪发现流程如下加密数据 (data) - Base64解码 (atob) - 转换为Uint8Array - 调用函数 _0xdef456(解码后数据, key, iv) - 返回解密后的Uint8Array - 通过 TextDecoder 解码为明文字符串 - JSON.parse 为对象其中key和iv是两个关键的参数。通过调试发现key并非硬编码在代码里而是由另一个函数_0xghi789()动态生成的这个函数又依赖于从网页某个全局变量window._GLOBAL_CONFIG中获取的一个seed种子。3.2 “扣代码”实战提取与重构“扣代码”的目标是把浏览器中运行良好的JS解密逻辑独立出来能在Node.js或Python的JS环境如PyExecJS、js2py中运行。这不是简单的复制粘贴因为浏览器提供了庞大的环境如window、document、navigator而独立JS环境是纯净的。基础扣取从入口函数_0xabc123开始将其依赖的所有函数如_0xdef456_0xghi789以及它们之间依赖的变量全部复制到一个新的JS文件中。你可以使用开发者工具中Sources面板的代码格式化功能左下角{}图标让混淆的代码稍微易读一些。补环境这是扣代码最大的坑。浏览器中window._GLOBAL_CONFIG是存在的。但在Node.js中没有window对象。因此我们需要在代码执行前“补”上这个环境。// 在扣出的JS文件开头补上缺失的环境 if (typeof window undefined) { // 模拟一个window对象 global.window { _GLOBAL_CONFIG: { seed: 这里是通过调试获取到的实际seed值 // 注意这个seed可能是固定的也可能是变化的 } }; }你需要仔细检查扣出的代码用到了哪些浏览器特有的对象document,location,navigator.userAgent等并一一模拟。一个常见的技巧是在浏览器控制台直接输入console.log(navigator.userAgent)把结果字符串直接硬编码到你的模拟对象里。关键依赖如果解密用到了现代加密算法如AES、RSA代码中可能会引用CryptoJS这个库。你需要判断如果网站是直接引入的CryptoJS库文件你需要把整个CryptoJS的源代码也扣下来或者更简单地在Node.js中使用npm install crypto-js安装然后在你的JS文件中用require引入。如果网站使用的是Web Crypto APIwindow.crypto.subtle那么在Node.js中模拟起来就非常复杂通常考虑用Python的密码学库如cryptography来替代实现这要求你完全理解算法细节。在本案例中我们发现它使用的是CryptoJS.AES.decrypt且网站自身加载了CryptoJS。我们选择将CryptoJS源码合并到扣出的JS文件中确保所有依赖在单一文件内。4. 本地化复现与Python集成4.1 构建独立的JS解密模块将扣取并补好环境的完整JS代码保存为一个文件例如decrypt_a.js。其核心结构如下// decrypt_a.js // 1. 补环境 if (typeof window undefined) { global.window { _GLOBAL_CONFIG: { seed: your_fixed_seed_here } }; // 可能还需要补其他如navigator global.navigator { userAgent: Mozilla/5.0... }; } // 2. 插入CryptoJS库源码很长此处省略 var CryptoJS (function(){...})(); // 3. 扣取的核心函数 function _0xghi789() { // 根据seed生成key的逻辑 // ... return generatedKey; } function _0xdef456(encryptedData, key, iv) { // 调用CryptoJS进行AES解密的逻辑 // ... return decryptedBytes; } function _0xabc123(encryptedBase64Str) { // 主入口函数Base64解码 - 调用_0xdef456 - 解码文本 - JSON解析 // ... return decryptedObj; } // 4. 导出函数供外部调用CommonJS格式 module.exports { decryptData: _0xabc123 };在Node.js中测试这个模块node -e const m require(./decrypt_a.js); console.log(m.decryptData(加密的data字符串))。确保能正确输出解密后的对象。4.2 Python调用JS引擎的几种方式在Python中执行JS有几种常用方案各有优劣方案优点缺点适用场景PyExecJS安装简单支持多种后端Node.js, PhantomJS等性能较差进程间通信开销大调试复杂快速验证、逻辑简单的解密js2py纯Python实现无需安装Node.js对ES6语法支持有限执行复杂JS易出错无Node环境、代码语法简单Node.js子进程直接调用Node.js性能好兼容性最佳需要管理子进程错误处理稍复杂生产环境首选性能要求高PyV8 / dukpy性能好安装配置复杂生态不活跃特定场景不推荐新手对于本次实战我们选择Node.js子进程方案因为它稳定、高效最贴近真实浏览器环境。4.3 完整的Python集成代码创建一个Python脚本使用subprocess模块调用Node.js执行我们的解密模块。import json import subprocess import requests class ASiteDecryptor: def __init__(self, decrypt_js_pathdecrypt_a.js): self.decrypt_js_path decrypt_js_path # 这里可以初始化session添加headers等 self.session requests.Session() self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., }) def get_encrypted_data(self, page1): 模拟请求获取加密的接口数据 url https://www.a-site.com/api/data/list params {page: page} try: resp self.session.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.json() # 假设返回的是包含加密data字段的JSON except requests.RequestException as e: print(f请求失败: {e}) return None def decrypt_via_node(self, encrypted_data_str): 调用Node.js子进程执行解密 # 构建Node.js命令 node_script f const decryptor require({self.decrypt_js_path}); const result decryptor.decryptData({encrypted_data_str}); console.log(JSON.stringify(result)); try: # 启动子进程 process subprocess.Popen( [node, -e, node_script], stdoutsubprocess.PIPE, stderrsubprocess.PIPE, shellFalse, textTrue ) stdout, stderr process.communicate(timeout5) # 设置超时 if process.returncode ! 0: print(fNode.js解密失败错误信息:\n{stderr}) return None # 解析Node.js输出的JSON decrypted_result json.loads(stdout.strip()) return decrypted_result except subprocess.TimeoutExpired: print(解密过程超时) process.kill() return None except json.JSONDecodeError as e: print(f解密结果JSON解析失败: {e}, 原始输出:\n{stdout}) return None except Exception as e: print(f调用解密过程发生未知错误: {e}) return None def run(self): 主流程 encrypted_json self.get_encrypted_data(page1) if not encrypted_json or data not in encrypted_json: print(未获取到加密数据) return encrypted_data_str encrypted_json[data] print(f获取到加密数据长度: {len(encrypted_data_str)}) decrypted_obj self.decrypt_via_node(encrypted_data_str) if decrypted_obj: print(解密成功) # 这里可以处理解密后的数据如保存、分析等 print(json.dumps(decrypted_obj, indent2, ensure_asciiFalse)) else: print(解密失败。) if __name__ __main__: decryptor ASiteDecryptor() decryptor.run()这段代码定义了一个类封装了请求和解密流程。decrypt_via_node方法通过subprocess启动一个Node.js进程执行一行内联的JS代码该代码加载我们扣取的模块并调用解密函数。这种方式隔离性好效率也远高于PyExecJS。5. 动态参数与反爬虫对抗的深度处理5.1 处理动态密钥与签名在更复杂的场景中key或iv可能不是固定的甚至整个请求都需要一个动态的sign签名参数。这些参数通常由前端JS根据当前时间、请求体、一个固定盐值salt等计算生成。应对策略追踪生成逻辑在发起请求的代码处即我们最初找加密入口的附近打上XHR断点在开发者工具Sources面板的XHR/fetch Breakpoints里添加请求URL包含的字符串。当请求发起时代码会暂停。此时在调用堆栈中寻找计算sign或动态key的函数。完整扣取将这个生成动态参数的函数及其所有依赖一并扣取到我们的JS文件中。这意味着我们的decrypt_a.js可能需要增加一个generateSign(params)或generateDynamicKey()的函数。Python集成在Python发起请求前先调用这个JS函数计算出必要的动态参数然后将其填入请求的headers或params中。# 在ASiteDecryptor类中新增方法 def get_dynamic_params(self, request_params): 调用JS计算动态签名等参数 node_script f const decryptor require({self.decrypt_js_path}); const params {json.dumps(request_params)}; const sign decryptor.generateSign(params); console.log(JSON.stringify({{sign: sign}})); # ... 同样的subprocess调用逻辑获取sign ... # 将sign添加到请求参数中5.2 应对代码混淆与反调试网站可能会使用更强的混淆工具如obfuscator.io或者设置反调试。无限Debugger代码中会有debugger;语句或通过Function构造函数动态生成调试语句导致调试器不断暂停。应对方法是在开发者工具中找到包含debugger的代码行右键选择“Never pause here”或者通过条件断点绕过。时间差检测在代码开始和结束用console.time/console.timeEnd或Date.now()计算执行时间如果时间过长说明可能打了断点就进入死循环或抛出错误。对付这个需要找到检测代码并修改其逻辑或者使用“禁止断点”模式快速通过该代码段。代码流扁平化混淆将代码逻辑打乱用大量的switch-case或if-else控制流程极难阅读。这没有捷径需要耐心。可以尝试使用反混淆工具如de4js在线工具进行初步还原但完全自动化还原几乎不可能最终还是要结合动态调试来理解核心逻辑。我的经验是对于高度混淆的代码动态调试远胜于静态阅读。始终跟着数据流走关注函数的输入和输出暂时忽略中间复杂的控制流。将核心函数扣出来后其内部混淆的代码只要不影响执行结果可以原封不动。6. 调试技巧与常见问题排查实录即使按照上述流程你也一定会遇到各种报错。下面是我总结的常见问题及排查清单问题现象可能原因排查步骤与解决方案Node.js执行报错ReferenceError: window is not defined环境缺失扣取的代码中直接使用了window或document。1. 检查报错堆栈定位到具体哪一行代码。2. 在扣取的JS文件开头补上对应的全局对象如global.window {};global.document {};。Node.js执行报错TypeError: Cannot read property xxx of undefined模拟的环境对象结构不完整缺少某个属性。1. 在浏览器控制台查看完整的对象结构。例如console.log(navigator)。2. 在补环境的代码中完整地模拟这个对象。例如global.navigator { appName: Netscape, userAgent: ..., platform: ... };。解密结果为空或乱码1. 密钥(Key/IV)错误。2. 加密模式/填充方式不匹配。3. 传入的密文格式不对比如该Base64解码的没解码。1.核对密钥在浏览器调试中将解密函数执行时的key、iv值完整打印出来与Node.js中使用的进行比对。2.核对算法确认CryptoJS使用的模式如mode.CBC和填充如pad.Pkcs7。在Python中使用cryptography库时需完全一致。3.核对输入单步调试对比浏览器和Node.js中传入解密函数的每一步的中间数据如Base64解码后的字符串、转换后的ArrayBuffer是否完全一致。Python调用Node.js超时或无响应1. Node.js脚本中有死循环或未捕获的异常。2. 子进程路径或权限问题。1.本地测试Node脚本先用node your_script.js单独运行扣取的JS文件确保它能独立运行并输出结果。2.增加调试输出在扣取的JS文件关键位置加入console.log查看执行到哪一步卡住。3.检查子进程命令确保node命令在系统PATH中或者使用绝对路径。网站更新后解密失效1. 加密算法或密钥生成逻辑改变。2. 请求接口增加新的验证参数。1.重新调试按照第2步的流程重新定位加密入口和密钥生成逻辑。2.版本化管理对扣取的JS代码做好版本备份方便对比变化。3.监控机制在生产环境中对解密失败要有报警和重试机制。最重要的调试心法“对比调试法”。在浏览器中让代码运行到解密成功的那一刻。然后在Node.js中用完全相同的输入密文、密钥执行扣出的代码。在两个环境中分别打印出每一步的中间变量值进行逐字节对比。只要有一个环节对不上结果就是错的。这个方法是定位问题最直接、最有效的手段。7. 进阶思考与工程化建议当你能成功逆向一个站点后可以考虑如何让这套流程更稳健、更高效。环境模拟的自动化手动补环境繁琐且易错。可以尝试使用jsdom库在Node.js中模拟一个更完整的浏览器环境但这会引入新的复杂度。对于生产环境更推荐精细化的手工补环境只补用到的部分这样更轻量、可控。纯Python实现替代JS如果加密算法是标准的如AES、RSA在完全弄清其参数密钥、IV、模式、填充后可以放弃调用JS转而用Python的cryptography或pycryptodome库重写解密逻辑。这能彻底摆脱对Node.js的依赖性能也更高。但这要求逆向分析必须百分之百准确。将解密服务化如果爬虫系统是分布式的可以考虑将Node.js解密脚本封装成一个简单的HTTP服务使用Express.js或Koa框架。Python爬虫程序只需将加密数据POST到这个服务即可获取解密结果。这样便于维护、升级解密逻辑也实现了解密能力的复用。关注法律与道德边界JS逆向技术是一把双刃剑。务必确保你的数据采集行为遵守目标网站的robots.txt协议尊重其服务条款不进行对对方服务器造成过大压力的暴力请求且采集的数据用于合法、正当的目的。技术的学习和挑战应在法律与道德的框架内进行。回过头看JS逆向的本质是一场理解与复现的较量。它考验的不仅是你的JavaScript功底和调试技巧更是耐心、逻辑思维和系统化解决问题的能力。每一次成功的逆向都是对Web应用前后端交互机制的一次深刻理解。希望这个从实战中总结的流程能为你打开这扇门并提供一条清晰、可循的路径。记住当你在调试中感到困惑时回到“数据流”这个本源一步一步跟下去光总会出现在隧道尽头。
JS逆向实战:从加密定位到Python集成的完整数据解密方案
发布时间:2026/7/4 10:58:15
1. 项目概述从“黑盒”到“白盒”的实战之旅“JS逆向”这四个字对于很多刚接触网络数据采集的朋友来说既神秘又令人头疼。它不像写个请求、解析个HTML那么简单更像是在和网站的后台工程师玩一场“猫鼠游戏”。对方用JavaScript简称JS把关键数据层层加密、混淆让你拿到的响应是一堆看不懂的乱码而你的任务就是像侦探一样抽丝剥茧找到数据加密的原始逻辑并用自己的代码复现它最终拿到明文数据。这个过程就是JS逆向。我之所以写这个“案例实战2”是因为我发现很多教程要么停留在理论讲一堆AST、混淆原理看得人头大却不知从何下手要么就是给一个已经过时的、极其简单的案例读者照做一遍后遇到真实网站依然束手无策。我的目标很直接通过一个高度模拟真实商业网站防护水平的实战案例带你走完从打开浏览器开发者工具到最终用Python成功拿到数据的完整闭环。你会经历定位加密入口、分析调用栈、扣取关键代码、补环境、调试直至成功复现的全过程。这不仅是一个技术操作指南更是一套面对未知加密网站时的通用分析心法和排错思路。无论你是爬虫工程师、安全研究员还是对Web技术原理有浓厚兴趣的开发者这套实战经验都能让你在面对JS加密时从“无从下手”变得“心中有谱”。2. 目标网站分析与加密定位策略2.1 目标选择与初步侦察本次实战我们选择一个具有典型反爬特征的网站作为目标为避嫌我们称其为A站。A站的数据接口返回的JSON中核心数据字段如列表内容、价格等是一串无规律的密文而页面却能正常显示。这明确告诉我们数据在传输过程中被加密解密逻辑必然由前端JS完成。第一步永远是“侦察”。打开Chrome开发者工具F12切换到Network网络面板刷新页面或触发数据加载动作。很快我们找到一个关键的XHR/Fetch请求其响应Response类似于{ code: 200, data: U2FsdGVkX1/...很长一串Base64样子的字符串..., message: success }这里的data字段就是加密后的数据。我们的核心目标就是找到将这段data解密成明文对象的JS代码。注意不要一上来就搜索“decrypt”、“decode”等关键词在高度混淆的代码中这些函数名很可能被改得面目全非。更可靠的方法是追踪数据流。2.2 基于“堆栈”的加密入口定位法最有效的方法是利用开发者工具的“堆栈”追踪功能。在Network面板中找到那个返回加密data的请求右键点击它选择“Replay XHR”有时不可靠更好的方法是在请求的Headers标签页找到Request URL。回到Sources源代码面板按CtrlShiftFWindows或CmdOptFMac进行全局搜索。搜索该URL的一部分通常是接口路径如/api/data/list。这样能直接找到发起这个网络请求的JS代码位置。找到代码位置后在其附近打上断点。重新触发请求代码执行会在断点处暂停。此时关键操作来了不要直接步进Step Into而是看向开发者工具右侧的Call Stack调用堆栈面板。调用堆栈显示了当前断点位置是由哪些函数一层层调用过来的。这是一个“自顶向下”的调用链。我们的目标——解密函数——很可能就在这个调用链中在发起请求的代码之后被执行因为解密发生在拿到响应之后。我们需要在堆栈中寻找处理响应response的地方。通常处理响应的代码会在Promise的then方法、async/await函数或者XMLHttpRequest的onreadystatechange事件回调里。在堆栈中点击这些可能的函数查看其源代码。如果看到有代码对response.data或类似变量进行了操作比如调用了一个函数或者进行了一系列赋值这里就可能是解密入口。我个人的心得是重点关注堆栈中靠近顶部的、非库文件如jquery.min.js的匿名函数或项目自身JS文件。在这里我找到了一个名为_0xabc123的函数它接收了response.data作为参数。通过单步调试F11进入这个函数确认其输出正是解密后的明文对象。至此加密入口锁定成功。3. 核心加密逻辑分析与代码扣取3.1 逆向分析与逻辑梳理锁定入口函数_0xabc123后我们进入最核心的逆向分析阶段。这个函数本身可能不复杂但它内部会调用其他函数形成一条调用链。我们需要使用开发者工具的调试功能Step Over, Step Into, Step Out来理清逻辑。首先在_0xabc123函数内部打上断点观察传入的参数即加密的data字符串和最终返回的结果。然后一步步执行注意观察变量转换data字符串是否先被atobBase64解码或进行了一些字符串分割操作关键函数调用data被传递给了哪个函数这个函数的名字可能被混淆如_0xdef456。常量与密钥在解密过程中是否用到了某些固定的字符串或数值这些可能就是密钥Key或初始化向量IV。在调试器的Scope作用域面板或直接将鼠标悬停在变量上可以查看其值。以本次案例为例我跟踪发现流程如下加密数据 (data) - Base64解码 (atob) - 转换为Uint8Array - 调用函数 _0xdef456(解码后数据, key, iv) - 返回解密后的Uint8Array - 通过 TextDecoder 解码为明文字符串 - JSON.parse 为对象其中key和iv是两个关键的参数。通过调试发现key并非硬编码在代码里而是由另一个函数_0xghi789()动态生成的这个函数又依赖于从网页某个全局变量window._GLOBAL_CONFIG中获取的一个seed种子。3.2 “扣代码”实战提取与重构“扣代码”的目标是把浏览器中运行良好的JS解密逻辑独立出来能在Node.js或Python的JS环境如PyExecJS、js2py中运行。这不是简单的复制粘贴因为浏览器提供了庞大的环境如window、document、navigator而独立JS环境是纯净的。基础扣取从入口函数_0xabc123开始将其依赖的所有函数如_0xdef456_0xghi789以及它们之间依赖的变量全部复制到一个新的JS文件中。你可以使用开发者工具中Sources面板的代码格式化功能左下角{}图标让混淆的代码稍微易读一些。补环境这是扣代码最大的坑。浏览器中window._GLOBAL_CONFIG是存在的。但在Node.js中没有window对象。因此我们需要在代码执行前“补”上这个环境。// 在扣出的JS文件开头补上缺失的环境 if (typeof window undefined) { // 模拟一个window对象 global.window { _GLOBAL_CONFIG: { seed: 这里是通过调试获取到的实际seed值 // 注意这个seed可能是固定的也可能是变化的 } }; }你需要仔细检查扣出的代码用到了哪些浏览器特有的对象document,location,navigator.userAgent等并一一模拟。一个常见的技巧是在浏览器控制台直接输入console.log(navigator.userAgent)把结果字符串直接硬编码到你的模拟对象里。关键依赖如果解密用到了现代加密算法如AES、RSA代码中可能会引用CryptoJS这个库。你需要判断如果网站是直接引入的CryptoJS库文件你需要把整个CryptoJS的源代码也扣下来或者更简单地在Node.js中使用npm install crypto-js安装然后在你的JS文件中用require引入。如果网站使用的是Web Crypto APIwindow.crypto.subtle那么在Node.js中模拟起来就非常复杂通常考虑用Python的密码学库如cryptography来替代实现这要求你完全理解算法细节。在本案例中我们发现它使用的是CryptoJS.AES.decrypt且网站自身加载了CryptoJS。我们选择将CryptoJS源码合并到扣出的JS文件中确保所有依赖在单一文件内。4. 本地化复现与Python集成4.1 构建独立的JS解密模块将扣取并补好环境的完整JS代码保存为一个文件例如decrypt_a.js。其核心结构如下// decrypt_a.js // 1. 补环境 if (typeof window undefined) { global.window { _GLOBAL_CONFIG: { seed: your_fixed_seed_here } }; // 可能还需要补其他如navigator global.navigator { userAgent: Mozilla/5.0... }; } // 2. 插入CryptoJS库源码很长此处省略 var CryptoJS (function(){...})(); // 3. 扣取的核心函数 function _0xghi789() { // 根据seed生成key的逻辑 // ... return generatedKey; } function _0xdef456(encryptedData, key, iv) { // 调用CryptoJS进行AES解密的逻辑 // ... return decryptedBytes; } function _0xabc123(encryptedBase64Str) { // 主入口函数Base64解码 - 调用_0xdef456 - 解码文本 - JSON解析 // ... return decryptedObj; } // 4. 导出函数供外部调用CommonJS格式 module.exports { decryptData: _0xabc123 };在Node.js中测试这个模块node -e const m require(./decrypt_a.js); console.log(m.decryptData(加密的data字符串))。确保能正确输出解密后的对象。4.2 Python调用JS引擎的几种方式在Python中执行JS有几种常用方案各有优劣方案优点缺点适用场景PyExecJS安装简单支持多种后端Node.js, PhantomJS等性能较差进程间通信开销大调试复杂快速验证、逻辑简单的解密js2py纯Python实现无需安装Node.js对ES6语法支持有限执行复杂JS易出错无Node环境、代码语法简单Node.js子进程直接调用Node.js性能好兼容性最佳需要管理子进程错误处理稍复杂生产环境首选性能要求高PyV8 / dukpy性能好安装配置复杂生态不活跃特定场景不推荐新手对于本次实战我们选择Node.js子进程方案因为它稳定、高效最贴近真实浏览器环境。4.3 完整的Python集成代码创建一个Python脚本使用subprocess模块调用Node.js执行我们的解密模块。import json import subprocess import requests class ASiteDecryptor: def __init__(self, decrypt_js_pathdecrypt_a.js): self.decrypt_js_path decrypt_js_path # 这里可以初始化session添加headers等 self.session requests.Session() self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., }) def get_encrypted_data(self, page1): 模拟请求获取加密的接口数据 url https://www.a-site.com/api/data/list params {page: page} try: resp self.session.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.json() # 假设返回的是包含加密data字段的JSON except requests.RequestException as e: print(f请求失败: {e}) return None def decrypt_via_node(self, encrypted_data_str): 调用Node.js子进程执行解密 # 构建Node.js命令 node_script f const decryptor require({self.decrypt_js_path}); const result decryptor.decryptData({encrypted_data_str}); console.log(JSON.stringify(result)); try: # 启动子进程 process subprocess.Popen( [node, -e, node_script], stdoutsubprocess.PIPE, stderrsubprocess.PIPE, shellFalse, textTrue ) stdout, stderr process.communicate(timeout5) # 设置超时 if process.returncode ! 0: print(fNode.js解密失败错误信息:\n{stderr}) return None # 解析Node.js输出的JSON decrypted_result json.loads(stdout.strip()) return decrypted_result except subprocess.TimeoutExpired: print(解密过程超时) process.kill() return None except json.JSONDecodeError as e: print(f解密结果JSON解析失败: {e}, 原始输出:\n{stdout}) return None except Exception as e: print(f调用解密过程发生未知错误: {e}) return None def run(self): 主流程 encrypted_json self.get_encrypted_data(page1) if not encrypted_json or data not in encrypted_json: print(未获取到加密数据) return encrypted_data_str encrypted_json[data] print(f获取到加密数据长度: {len(encrypted_data_str)}) decrypted_obj self.decrypt_via_node(encrypted_data_str) if decrypted_obj: print(解密成功) # 这里可以处理解密后的数据如保存、分析等 print(json.dumps(decrypted_obj, indent2, ensure_asciiFalse)) else: print(解密失败。) if __name__ __main__: decryptor ASiteDecryptor() decryptor.run()这段代码定义了一个类封装了请求和解密流程。decrypt_via_node方法通过subprocess启动一个Node.js进程执行一行内联的JS代码该代码加载我们扣取的模块并调用解密函数。这种方式隔离性好效率也远高于PyExecJS。5. 动态参数与反爬虫对抗的深度处理5.1 处理动态密钥与签名在更复杂的场景中key或iv可能不是固定的甚至整个请求都需要一个动态的sign签名参数。这些参数通常由前端JS根据当前时间、请求体、一个固定盐值salt等计算生成。应对策略追踪生成逻辑在发起请求的代码处即我们最初找加密入口的附近打上XHR断点在开发者工具Sources面板的XHR/fetch Breakpoints里添加请求URL包含的字符串。当请求发起时代码会暂停。此时在调用堆栈中寻找计算sign或动态key的函数。完整扣取将这个生成动态参数的函数及其所有依赖一并扣取到我们的JS文件中。这意味着我们的decrypt_a.js可能需要增加一个generateSign(params)或generateDynamicKey()的函数。Python集成在Python发起请求前先调用这个JS函数计算出必要的动态参数然后将其填入请求的headers或params中。# 在ASiteDecryptor类中新增方法 def get_dynamic_params(self, request_params): 调用JS计算动态签名等参数 node_script f const decryptor require({self.decrypt_js_path}); const params {json.dumps(request_params)}; const sign decryptor.generateSign(params); console.log(JSON.stringify({{sign: sign}})); # ... 同样的subprocess调用逻辑获取sign ... # 将sign添加到请求参数中5.2 应对代码混淆与反调试网站可能会使用更强的混淆工具如obfuscator.io或者设置反调试。无限Debugger代码中会有debugger;语句或通过Function构造函数动态生成调试语句导致调试器不断暂停。应对方法是在开发者工具中找到包含debugger的代码行右键选择“Never pause here”或者通过条件断点绕过。时间差检测在代码开始和结束用console.time/console.timeEnd或Date.now()计算执行时间如果时间过长说明可能打了断点就进入死循环或抛出错误。对付这个需要找到检测代码并修改其逻辑或者使用“禁止断点”模式快速通过该代码段。代码流扁平化混淆将代码逻辑打乱用大量的switch-case或if-else控制流程极难阅读。这没有捷径需要耐心。可以尝试使用反混淆工具如de4js在线工具进行初步还原但完全自动化还原几乎不可能最终还是要结合动态调试来理解核心逻辑。我的经验是对于高度混淆的代码动态调试远胜于静态阅读。始终跟着数据流走关注函数的输入和输出暂时忽略中间复杂的控制流。将核心函数扣出来后其内部混淆的代码只要不影响执行结果可以原封不动。6. 调试技巧与常见问题排查实录即使按照上述流程你也一定会遇到各种报错。下面是我总结的常见问题及排查清单问题现象可能原因排查步骤与解决方案Node.js执行报错ReferenceError: window is not defined环境缺失扣取的代码中直接使用了window或document。1. 检查报错堆栈定位到具体哪一行代码。2. 在扣取的JS文件开头补上对应的全局对象如global.window {};global.document {};。Node.js执行报错TypeError: Cannot read property xxx of undefined模拟的环境对象结构不完整缺少某个属性。1. 在浏览器控制台查看完整的对象结构。例如console.log(navigator)。2. 在补环境的代码中完整地模拟这个对象。例如global.navigator { appName: Netscape, userAgent: ..., platform: ... };。解密结果为空或乱码1. 密钥(Key/IV)错误。2. 加密模式/填充方式不匹配。3. 传入的密文格式不对比如该Base64解码的没解码。1.核对密钥在浏览器调试中将解密函数执行时的key、iv值完整打印出来与Node.js中使用的进行比对。2.核对算法确认CryptoJS使用的模式如mode.CBC和填充如pad.Pkcs7。在Python中使用cryptography库时需完全一致。3.核对输入单步调试对比浏览器和Node.js中传入解密函数的每一步的中间数据如Base64解码后的字符串、转换后的ArrayBuffer是否完全一致。Python调用Node.js超时或无响应1. Node.js脚本中有死循环或未捕获的异常。2. 子进程路径或权限问题。1.本地测试Node脚本先用node your_script.js单独运行扣取的JS文件确保它能独立运行并输出结果。2.增加调试输出在扣取的JS文件关键位置加入console.log查看执行到哪一步卡住。3.检查子进程命令确保node命令在系统PATH中或者使用绝对路径。网站更新后解密失效1. 加密算法或密钥生成逻辑改变。2. 请求接口增加新的验证参数。1.重新调试按照第2步的流程重新定位加密入口和密钥生成逻辑。2.版本化管理对扣取的JS代码做好版本备份方便对比变化。3.监控机制在生产环境中对解密失败要有报警和重试机制。最重要的调试心法“对比调试法”。在浏览器中让代码运行到解密成功的那一刻。然后在Node.js中用完全相同的输入密文、密钥执行扣出的代码。在两个环境中分别打印出每一步的中间变量值进行逐字节对比。只要有一个环节对不上结果就是错的。这个方法是定位问题最直接、最有效的手段。7. 进阶思考与工程化建议当你能成功逆向一个站点后可以考虑如何让这套流程更稳健、更高效。环境模拟的自动化手动补环境繁琐且易错。可以尝试使用jsdom库在Node.js中模拟一个更完整的浏览器环境但这会引入新的复杂度。对于生产环境更推荐精细化的手工补环境只补用到的部分这样更轻量、可控。纯Python实现替代JS如果加密算法是标准的如AES、RSA在完全弄清其参数密钥、IV、模式、填充后可以放弃调用JS转而用Python的cryptography或pycryptodome库重写解密逻辑。这能彻底摆脱对Node.js的依赖性能也更高。但这要求逆向分析必须百分之百准确。将解密服务化如果爬虫系统是分布式的可以考虑将Node.js解密脚本封装成一个简单的HTTP服务使用Express.js或Koa框架。Python爬虫程序只需将加密数据POST到这个服务即可获取解密结果。这样便于维护、升级解密逻辑也实现了解密能力的复用。关注法律与道德边界JS逆向技术是一把双刃剑。务必确保你的数据采集行为遵守目标网站的robots.txt协议尊重其服务条款不进行对对方服务器造成过大压力的暴力请求且采集的数据用于合法、正当的目的。技术的学习和挑战应在法律与道德的框架内进行。回过头看JS逆向的本质是一场理解与复现的较量。它考验的不仅是你的JavaScript功底和调试技巧更是耐心、逻辑思维和系统化解决问题的能力。每一次成功的逆向都是对Web应用前后端交互机制的一次深刻理解。希望这个从实战中总结的流程能为你打开这扇门并提供一条清晰、可循的路径。记住当你在调试中感到困惑时回到“数据流”这个本源一步一步跟下去光总会出现在隧道尽头。