JS逆向实战:定位与Python复现AES加密爬虫参数

📅 2026/7/6 8:57:49
JS逆向实战:定位与Python复现AES加密爬虫参数
1. 项目概述与核心思路拆解最近在爬虫圈子里一个老生常谈但又永远绕不开的话题就是JS逆向。尤其是当目标网站的数据请求参数被AES这类对称加密算法保护时很多朋友的第一反应就是“头大”。今天我就以一个近期实际处理过的案例为蓝本和大家深入聊聊如何从浏览器纷繁复杂的JS代码中定位到AES加密的关键逻辑并用Python完整复现这一过程。整个过程与其说是技术对抗不如说是一场耐心的“寻宝游戏”当你成功解密出第一段明文数据时那种成就感懂的都懂。这个项目的核心目标很明确我们需要爬取一个网站上的列表数据但它的请求参数比如data或encryptData是一串看似无意义的密文。浏览器能正常发送请求并获取数据说明加解密逻辑必然存在于前端加载的JavaScript代码中。我们的任务就是找到它、理解它、并最终在我们的Python脚本里重现它。这不仅仅是写几行requests代码那么简单它要求我们具备一定的前端调试能力、密码学基础知识和逆向思维。别担心我会把每一步都掰开揉碎了讲即便你是刚接触逆向的新手跟着走一遍也能摸清门道。2. 逆向环境准备与核心工具链工欲善其事必先利其器。在开始逆向之前搭建一个顺手的调试环境至关重要。这里没有绝对最好的工具只有最适合当前场景的组合。2.1 浏览器开发者工具你的主战场Chrome或Edge的开发者工具F12打开是逆向分析的基石。我们主要用到以下几个面板Network网络这是起点。刷新页面找到那个携带加密参数的XHR或Fetch请求。重点关注请求的Headers特别是Request Payload和Initiator发起者标签后者能帮你快速定位到发起这个请求的JS文件。Sources源代码核心战场。在这里你可以查看、搜索、调试所有加载的JS文件。学会使用CtrlShiftF进行全局搜索关键词可以是加密参数名如encrypt、常见的加密库函数名如CryptoJS、encrypt、mode、padding或常量如AES、CBC、PKCS7。Console控制台试验场。你可以在这里执行找到的JS函数传入参数并立即看到输出这是验证你的理解是否正确的最快方式。同时一些关键对象如window下的加密函数也可以在这里直接调用和检查。2.2 辅助调试与格式化工具现代网站为了性能JS代码通常是被压缩minify和混淆obfuscate的变量名都变成了a, b, c可读性极差。代码美化Pretty Print在Sources面板里找到压缩的JS文件点击底部行号附近的{}图标Chrome会自动将代码格式化恢复一定的可读性。这是逆向分析的第一步也是必不可少的一步。断点调试Breakpoint在疑似加密函数的位置打上断点然后重新触发请求比如点击翻页。当代码执行到断点时程序会暂停此时你可以查看当前作用域内所有变量的值单步执行F10步入函数F11一步步跟踪加密数据的生成过程。这是理解逻辑最直接的方法。2.3 Python端复现环境当我们把JS逻辑搞清楚后就需要在Python中复现。这里的关键是选择与前端匹配的加解密库。pycryptodome/cryptography这是Python社区最主流、功能最强大的密码学库。pycryptodome是久经考验的pycrypto的继任者API友好文档齐全对于AES、DES、RSA等算法支持完善。通常用这个就够了。execjs一个非常有趣的库。它的思路是“打不过就加入”——既然JS逻辑复杂难以完全用Python重写那就直接在Python中调用一个JavaScript运行时如Node.js来执行那段JS代码。这在逆向初期快速验证、或遇到极其复杂的混淆代码时是一个高效的“捷径”。但缺点是部署环境需要安装Node.js且性能不如纯Python实现。注意工具的选择取决于目标网站的防御强度和你对代码的理解深度。对于简单的、未混淆的CryptoJS加密用pycryptodome重写是优雅且高效的选择。对于重度混淆、逻辑缠绕的代码前期用execjs快速打通流程再逐步尝试替换为纯Python实现是一个稳妥的策略。3. 实战逆向定位与解析AES加密逻辑理论说再多不如一次实战。假设我们目标网站的某个搜索接口其POST请求的Payload里有一个名为encData的字段值是一长串Base64编码的字符串。这就是我们的突破口。3.1 网络请求追踪与入口定位首先打开开发者工具的Network面板勾选Preserve log保留日志然后进行触发加密请求的操作如点击搜索。在请求列表中找到目标请求查看其Request Payload确认encData存在。接下来点击这个请求查看Initiator标签。这里会显示调用栈Call Stack告诉你这个请求是由哪个JS文件中的哪一行代码发起的。顺着调用栈往上点击我们通常会进入一个经过压缩的JS文件。此时立即点击{}进行格式化。3.2 关键代码搜索与逻辑分析格式化后代码可读性增强。我们开始搜索关键词。首先搜索encData看看它在哪里被赋值。可能会找到类似这样的代码var params { page: pageNum, keyword: keyword }; var encData encrypt(JSON.stringify(params)); // 疑似加密函数 xhr.send(encData encodeURIComponent(encData));太好了我们找到了一个名为encrypt的函数。接下来在文件中搜索function encrypt或者encrypt:如果是对象方法定位到这个函数的定义。假设我们找到了如下代码这是美化并简化后的示例function encrypt(data) { var key CryptoJS.enc.Utf8.parse(1234567890123456); // 密钥 var iv CryptoJS.enc.Utf8.parse(abcdefghijklmnop); // 偏移量IV var encrypted CryptoJS.AES.encrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); // 默认返回Base64格式的密文 }这段代码非常清晰它使用了CryptoJS这个前端常用的加密库。我们获得了几个关键信息算法AES。模式CBC密码分组链接模式。填充Pkcs7。密钥Key1234567890123456并用UTF-8编码解析成了CryptoJS的内部WordArray格式。初始向量IVabcdefghijklmnop同样用UTF-8解析。输入函数接收的data是字符串在调用前被JSON.stringify了。输出encrypted.toString()返回的是Base64字符串。3.3 控制台验证与参数捕获在逆向中眼见不一定为实。我们需要验证这个函数是否就是生成encData的那个。在Console面板中我们可以直接操作// 复制整个encrypt函数定义到控制台或者如果它是全局函数直接调用 // 模拟加密过程 var testParams {page:1, keyword:测试}; var testData JSON.stringify(testParams); var result encrypt(testData); console.log(result);将输出的结果与Network面板中真实请求的encData值进行对比。如果一致恭喜你核心加密逻辑已经找到。如果不一致说明可能找错了函数或者加密过程有额外的步骤比如对密钥进行了二次处理或者加密前对数据做了变换。此外还需要注意密钥和IV是否是硬编码的。有时它们可能来自其他接口、由其他函数动态生成、或者是对某个固定字符串进行MD5等哈希后的结果。这就需要你沿着代码调用链继续向上追溯。4. Python复现从JS逻辑到可执行脚本一旦我们在JS端确认了所有加密参数和流程就可以着手用Python复现了。这里我们分别介绍纯Python实现和execjs桥接两种方法。4.1 方法一使用PyCryptodome纯Python实现这是最推荐的方式不依赖外部JS环境性能好部署简单。首先安装库pip install pycryptodome。根据前面分析的结果我们知道是AES-128-CBC模式因为密钥是16字节PKCS7填充。Python实现如下import json from base64 import b64encode from Crypto.Cipher import AES from Crypto.Util.Padding import pad def encrypt_data(params): 模拟前端JS的AES加密函数 # 1. 准备参数与JS端一致 key 1234567890123456.encode(utf-8) # 16字节密钥 iv abcdefghijklmnop.encode(utf-8) # 16字节IV # 2. 将参数转换为JSON字符串与JS的JSON.stringify对应 data_str json.dumps(params, ensure_asciiFalse, separators(,, :)) # 注意json.dumps默认会输出空格而JS的JSON.stringify通常不会。 # 使用separators(,, :)来移除空格确保与前端生成的字符串完全一致。 # 3. 进行PKCS7填充AES块大小是16字节 data_bytes data_str.encode(utf-8) padded_data pad(data_bytes, AES.block_size, stylepkcs7) # 4. 创建AES-CBC加密器并加密 cipher AES.new(key, AES.MODE_CBC, iv) ciphertext cipher.encrypt(padded_data) # 5. 返回Base64编码的密文 encrypted_b64 b64encode(ciphertext).decode(utf-8) return encrypted_b64 # 测试加密 if __name__ __main__: test_params {page: 1, keyword: 测试} enc_result encrypt_data(test_params) print(f加密结果: {enc_result}) # 可以将此结果与浏览器Network中捕获的真实encData进行比对关键细节与避坑指南JSON序列化一致性这是最容易出错的地方。Python的json.dumps和JavaScript的JSON.stringify在默认情况下输出的字符串可能有细微差别如空格、中文编码。必须使用ensure_asciiFalse来正确输出中文并使用separators(,, :)来移除所有不必要的空白字符确保两端生成的字符串完全一致。一个字节的差异都会导致加密结果天差地别。填充模式PyCryptodome的pad函数明确指定stylepkcs7。AES是块加密需要将数据填充至16字节的整数倍。PKCS7是标准填充方式。密钥和IV编码确保它们以字节串bytes形式传入AES.new。字符串需要通过.encode(utf-8)转换。Base64输出b64encode返回的是字节串通常需要.decode(utf-8)转为字符串以便在HTTP请求中传输。4.2 方法二使用ExecJS桥接执行如果JS代码混淆严重逻辑复杂例如密钥经过多次非线性变换短时间内难以用Python重写可以使用execjs作为过渡方案。 首先安装pip install PyExecJS。同时确保系统已安装Node.js运行时环境。import execjs import json # 1. 读取包含加密函数的JS文件或直接定义JS代码字符串 # 假设我们把找到的encrypt函数及其依赖的CryptoJS库代码都保存到了 encrypt.js 中 with open(encrypt.js, r, encodingutf-8) as f: js_code f.read() # 2. 创建JS执行上下文 ctx execjs.compile(js_code) # 3. 调用JS函数 def encrypt_with_js(params): data_str json.dumps(params, ensure_asciiFalse, separators(,, :)) # 注意这里调用的是JS环境中的函数名 encrypt enc_result ctx.call(encrypt, data_str) return enc_result # 测试 if __name__ __main__: test_params {page: 1, keyword: 测试} result encrypt_with_js(test_params) print(result)encrypt.js文件内容示例需要包含完整的CryptoJS库源码或引用以及encrypt函数// 这里需要引入CryptoJS库可以从 https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js 获取 // 或者将整个库的代码复制粘贴进来体积较大 // 然后是我们的加密函数 function encrypt(data) { var key CryptoJS.enc.Utf8.parse(1234567890123456); var iv CryptoJS.enc.Utf8.parse(abcdefghijklmnop); var encrypted CryptoJS.AES.encrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); }注意execjs的性能开销较大且依赖外部环境。它适合用于逆向分析阶段的快速验证和原型搭建。对于需要长期稳定运行、高并发的爬虫项目最终目标还是应该将其转化为纯Python实现。5. 整合入爬虫与请求发送加密函数复现成功后将其整合到爬虫流程中就水到渠成了。我们使用requests库来发送请求。import requests import time import hashlib # 假设后续可能用到其他哈希 def get_encrypted_payload(page, keyword): 构造请求参数并加密 params { page: page, keyword: keyword, timestamp: int(time.time() * 1000), # 常见防爬措施时间戳 # 可能还有其他固定参数或签名参数 } # 使用我们之前写好的加密函数这里以纯Python版为例 enc_data encrypt_data(params) return enc_data def fetch_data(keyword, max_page5): 爬取数据主函数 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/x-www-form-urlencoded; charsetUTF-8, # 注意Content-Type根据实际情况调整 # Referer: https://目标网站.com, # Cookie: 你的cookie, # 如果需要登录态 } session requests.Session() for page in range(1, max_page 1): # 1. 生成加密的请求体 encrypted_payload get_encrypted_payload(page, keyword) # 2. 构造表单数据根据实际请求格式 form_data { encData: encrypted_payload, # 可能还有其他未加密的字段 # token: xxx, } # 3. 发送POST请求 try: resp session.post(https://目标网站/api/search, dataform_data, headersheaders, timeout10) resp.raise_for_status() # 检查HTTP错误 result resp.json() # 假设返回的是JSON # 4. 处理解密后的数据如果响应也是加密的可能需要解密 data_list result.get(data, []) print(f第{page}页获取到{len(data_list)}条数据) # ... 你的数据解析和存储逻辑 ... # 5. 礼貌性延迟避免请求过快 time.sleep(1) except requests.exceptions.RequestException as e: print(f请求第{page}页失败: {e}) break except json.JSONDecodeError: print(f第{page}页响应不是有效的JSON: {resp.text[:200]}) break if __name__ __main__: fetch_data(Python, max_page3)6. 常见问题排查与进阶技巧即使按照步骤操作你也可能会遇到各种问题。下面是一些常见坑点及解决方案。6.1 加密结果不一致问题排查表问题现象可能原因排查步骤与解决方案Python加密结果与JS结果完全不同1. 密钥/IV错误。2. 加密模式/填充模式错误。3. 待加密的原始字符串不一致。1.核对密钥和IV在JS控制台打印key和iv的原始字符串和Hex值与Python代码中的进行逐字节比对。2.确认算法参数仔细检查JS代码中的mode和padding值确保Python端完全一致CBC, PKCS7。3.比对输入明文在JS控制台和Python中分别打印出即将被加密的字符串JSON.stringify后的结果。使用在线Diff工具或仔细目视检查确保完全一致包括所有空格、引号、中文编码是否都为UTF-8。加密结果部分相似但末尾不同PKCS7填充不一致。确认JS和Python使用的都是标准的PKCS7填充。在Python中pycryptodome的pad函数默认就是PKCS7。可以手动计算填充字节验证。能加密但不能解密1. 解密时密钥/IV不一致。2. 解密时代码逻辑错误。3. 网站返回的密文可能经过额外编码或组合。1. 确保加解密使用同一套密钥和IV。2. 解密时先对Base64密文解码再解密最后去除填充。3. 检查网络返回的密文是否是一个JSON对象中的某个字段可能需要先提取。有时返回的可能是Hex字符串而非Base64。6.2 进阶场景与应对策略动态密钥/IV密钥和IV不是硬编码而是由服务器动态下发例如在一个初始化接口的响应中或者由前端通过某个算法如用时间戳哈希实时生成。这时你需要先模拟获取密钥/IV的请求或者逆向生成它们的JS函数。加密函数被混淆和嵌套核心的encrypt函数可能被隐藏在多层闭包、立即执行函数表达式(IIFE)中变量名被替换成无意义的字符。这时断点调试是你的最佳武器。在Network请求发起处打上XHR断点然后一步步跟进去。也可以搜索特征字符串如encrypt、AES、mode、CBC等即使变量名变了这些字符串常量也可能被保留。CryptoJS的不同引入方式网站可能使用Webpack等打包工具CryptoJS不是全局变量CryptoJS而是某个模块的导出对象。你需要找到它被赋值给了哪个局部变量。在Console中可以在加密函数执行时通过console.log(typeof CryptoJS)或查看作用域变量来定位。响应数据也是加密的有些网站“送佛送到西”返回的数据也是加密的。处理方式同理在Network中找到返回的密文在JS代码中搜索解密函数通常叫decrypt然后用Python复现解密过程。6.3 个人实操心得耐心是第一生产力逆向分析往往伴随着大量的搜索、断点、单步调试。一个复杂的加密可能隐藏在上万行混淆代码中。保持耐心像侦探一样梳理线索。控制台是你的沙盒大胆地在Console里执行代码片段、修改变量、测试函数。这是验证猜想最快的方式比反复修改Python脚本效率高得多。从结果反推如果全局搜索常见关键词无果可以尝试在加密请求发起的地方xhr.send或fetch打上断点然后观察调用栈逆向追踪参数是如何一步步生成的。备份与记录每找到一个关键函数或变量最好将其代码片段保存下来并做好注释。复杂的逆向过程可能会持续数天清晰的记录能帮你快速恢复上下文。理解重于复制尽量去理解加密的每一个步骤密钥来源、模式、填充、输入输出格式而不是单纯地复制粘贴JS代码到execjs。理解之后你才能从容应对网站的小幅更新也才能写出更健壮、高效的Python代码。逆向的世界没有银弹每个网站都可能有自己的“小九九”。但万变不离其宗掌握基本的调试方法、理解对称加密的原理、保持清晰的排查思路绝大多数AES逆向的关卡都能被攻克。这个过程锻炼的不仅是爬虫技术更是解决问题的能力。当你成功拿下第一个站点后你会发现后面遇到的很多问题都是类似的套路处理起来也会越来越得心应手。