JS逆向实战:定位与Python复现AES加密爬虫参数
2026/7/6 8:57:39 网站建设 项目流程

1. 项目概述与核心思路拆解

最近在爬虫圈子里,一个老生常谈但又永远绕不开的话题就是JS逆向。尤其是当目标网站的数据请求参数被AES这类对称加密算法保护时,很多朋友的第一反应就是“头大”。今天,我就以一个近期实际处理过的案例为蓝本,和大家深入聊聊如何从浏览器纷繁复杂的JS代码中,定位到AES加密的关键逻辑,并用Python完整复现这一过程。整个过程,与其说是技术对抗,不如说是一场耐心的“寻宝游戏”,当你成功解密出第一段明文数据时,那种成就感,懂的都懂。

这个项目的核心目标很明确:我们需要爬取一个网站上的列表数据,但它的请求参数(比如dataencryptData)是一串看似无意义的密文。浏览器能正常发送请求并获取数据,说明加解密逻辑必然存在于前端加载的JavaScript代码中。我们的任务就是找到它、理解它、并最终在我们的Python脚本里重现它。这不仅仅是写几行requests代码那么简单,它要求我们具备一定的前端调试能力、密码学基础知识和逆向思维。别担心,我会把每一步都掰开揉碎了讲,即便你是刚接触逆向的新手,跟着走一遍也能摸清门道。

2. 逆向环境准备与核心工具链

工欲善其事,必先利其器。在开始逆向之前,搭建一个顺手的调试环境至关重要。这里没有绝对最好的工具,只有最适合当前场景的组合。

2.1 浏览器开发者工具:你的主战场

Chrome或Edge的开发者工具(F12打开)是逆向分析的基石。我们主要用到以下几个面板:

  • Network(网络):这是起点。刷新页面,找到那个携带加密参数的XHR或Fetch请求。重点关注请求的Headers(特别是Request Payload)和Initiator(发起者)标签,后者能帮你快速定位到发起这个请求的JS文件。
  • Sources(源代码):核心战场。在这里你可以查看、搜索、调试所有加载的JS文件。学会使用Ctrl+Shift+F进行全局搜索,关键词可以是加密参数名(如encrypt)、常见的加密库函数名(如CryptoJSencryptmodepadding)或常量(如AESCBCPKCS7)。
  • 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这个前端常用的加密库。我们获得了几个关键信息:

  1. 算法:AES。
  2. 模式:CBC(密码分组链接模式)。
  3. 填充:Pkcs7。
  4. 密钥(Key)'1234567890123456',并用UTF-8编码解析成了CryptoJS的内部WordArray格式。
  5. 初始向量(IV)'abcdefghijklmnop',同样用UTF-8解析。
  6. 输入:函数接收的data是字符串,在调用前被JSON.stringify了。
  7. 输出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_ascii=False, 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, style='pkcs7') # 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_ascii=False来正确输出中文,并使用separators=(',', ':')来移除所有不必要的空白字符,确保两端生成的字符串完全一致。一个字节的差异都会导致加密结果天差地别。
  • 填充模式PyCryptodomepad函数明确指定style='pkcs7'。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', encoding='utf-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_ascii=False, 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_page=5): """爬取数据主函数""" headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-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', data=form_data, headers=headers, timeout=10) 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_page=3)

6. 常见问题排查与进阶技巧

即使按照步骤操作,你也可能会遇到各种问题。下面是一些常见坑点及解决方案。

6.1 加密结果不一致问题排查表

问题现象可能原因排查步骤与解决方案
Python加密结果与JS结果完全不同1. 密钥/IV错误。
2. 加密模式/填充模式错误。
3. 待加密的原始字符串不一致。
1.核对密钥和IV:在JS控制台打印keyiv的原始字符串和Hex值,与Python代码中的进行逐字节比对。
2.确认算法参数:仔细检查JS代码中的modepadding值,确保Python端完全一致(CBC, PKCS7)。
3.比对输入明文:在JS控制台和Python中分别打印出即将被加密的字符串(JSON.stringify后的结果)。使用在线Diff工具或仔细目视检查,确保完全一致,包括所有空格、引号、中文编码(是否都为UTF-8)。
加密结果部分相似,但末尾不同PKCS7填充不一致。确认JS和Python使用的都是标准的PKCS7填充。在Python中,pycryptodomepad函数默认就是PKCS7。可以手动计算填充字节验证。
能加密但不能解密1. 解密时密钥/IV不一致。
2. 解密时代码逻辑错误。
3. 网站返回的密文可能经过额外编码或组合。
1. 确保加解密使用同一套密钥和IV。
2. 解密时,先对Base64密文解码,再解密,最后去除填充。
3. 检查网络返回的密文是否是一个JSON对象中的某个字段,可能需要先提取。有时返回的可能是Hex字符串而非Base64。

6.2 进阶场景与应对策略

  • 动态密钥/IV:密钥和IV不是硬编码,而是由服务器动态下发(例如,在一个初始化接口的响应中),或者由前端通过某个算法(如用时间戳哈希)实时生成。这时,你需要先模拟获取密钥/IV的请求,或者逆向生成它们的JS函数。
  • 加密函数被混淆和嵌套:核心的encrypt函数可能被隐藏在多层闭包、立即执行函数表达式(IIFE)中,变量名被替换成无意义的字符。这时,断点调试是你的最佳武器。在Network请求发起处打上XHR断点,然后一步步跟进去。也可以搜索特征字符串,如encryptAESmodeCBC等,即使变量名变了,这些字符串常量也可能被保留。
  • CryptoJS的不同引入方式:网站可能使用Webpack等打包工具,CryptoJS不是全局变量CryptoJS,而是某个模块的导出对象。你需要找到它被赋值给了哪个局部变量。在Console中,可以在加密函数执行时,通过console.log(typeof CryptoJS)或查看作用域变量来定位。
  • 响应数据也是加密的:有些网站“送佛送到西”,返回的数据也是加密的。处理方式同理:在Network中找到返回的密文,在JS代码中搜索解密函数(通常叫decrypt),然后用Python复现解密过程。

6.3 个人实操心得

  1. 耐心是第一生产力:逆向分析往往伴随着大量的搜索、断点、单步调试。一个复杂的加密可能隐藏在上万行混淆代码中。保持耐心,像侦探一样梳理线索。
  2. 控制台是你的沙盒:大胆地在Console里执行代码片段、修改变量、测试函数。这是验证猜想最快的方式,比反复修改Python脚本效率高得多。
  3. 从结果反推:如果全局搜索常见关键词无果,可以尝试在加密请求发起的地方(xhr.sendfetch)打上断点,然后观察调用栈,逆向追踪参数是如何一步步生成的。
  4. 备份与记录:每找到一个关键函数或变量,最好将其代码片段保存下来,并做好注释。复杂的逆向过程可能会持续数天,清晰的记录能帮你快速恢复上下文。
  5. 理解重于复制:尽量去理解加密的每一个步骤(密钥来源、模式、填充、输入输出格式),而不是单纯地复制粘贴JS代码到execjs。理解之后,你才能从容应对网站的小幅更新,也才能写出更健壮、高效的Python代码。

逆向的世界没有银弹,每个网站都可能有自己的“小九九”。但万变不离其宗,掌握基本的调试方法、理解对称加密的原理、保持清晰的排查思路,绝大多数AES逆向的关卡都能被攻克。这个过程锻炼的不仅是爬虫技术,更是解决问题的能力。当你成功拿下第一个站点后,你会发现后面遇到的很多问题都是类似的套路,处理起来也会越来越得心应手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询