JS逆向实战:破解前端加密参数payload与sig的完整流程
1. 项目概述:从抓包到逆向,破解前端JS加密参数
最近在分析一个数据接口时,遇到了典型的JS加密参数问题。目标网站的数据加载方式很有意思,它不是传统的分页,而是通过鼠标下滑动态加载。用Selenium这类自动化工具去模拟滚动,效率低不说,还容易被反爬机制识别。更关键的是,直接请求其API,返回的数据是加密的,而请求必须携带两个由前端JavaScript动态生成的加密参数:payload和sig。去掉或者传错这两个参数,服务器直接返回错误。这显然是一个学习JS逆向和Python模拟加密的绝佳案例。通过逆向分析,我们不仅能拿到数据,这套思路同样适用于分析那些对登录密码进行前端加密的站点,进行安全审计或授权测试。今天,我就把完整的分析思路、逆向过程和Python实现代码拆解清楚,让你不仅能复现,更能理解背后的“为什么”。
2. 核心思路与逆向分析前的准备
2.1 目标分析与工具选型
首先明确目标:我们需要绕过前端JS加密,直接使用Python构造合法的HTTP请求来获取数据。核心在于逆向出payload和sig这两个参数的生成算法。
为什么不直接用Selenium?原文提到了,网站是动态加载,没有分页。这意味着你需要用Selenium控制浏览器不断模拟滚动,等待新内容加载,再提取。这个过程极其耗时,对网络稳定性要求高,并且大量、频繁的自动化操作很容易触发网站的反爬策略(如IP限制、验证码)。对于需要稳定、高效获取数据的场景,直接逆向接口才是更优解。
工欲善其事,必先利其器。逆向分析离不开浏览器开发者工具:
- Chrome/Edge DevTools:核心工具。主要用到Network(网络)面板抓包,和Sources(源代码)面板进行JavaScript调试。
- Overrides(本地覆盖):Chrome DevTools的一个神级功能。允许你将在线JS文件映射到本地修改后的版本,实现断点持久化、代码修改即时生效,无需每次刷新都重新寻找断点。
- Python环境:需要
requests库发起网络请求,以及hashlib等标准库用于实现加密算法。
注意:所有分析与操作均应在法律允许和网站授权范围内进行,仅用于学习交流与技术研究,切勿用于非法爬取、侵犯他人隐私或破坏网站正常运行。
2.2 抓包与参数定位
打开目标网页,清空Network记录,然后进行下滑操作,触发数据加载。很快就能在Network面板中找到一个XHR或Fetch请求,其响应内容是一串看似乱码的加密数据。
点击这个请求,查看Headers部分,重点是Payload或Query String Parameters。在这个案例中,我们发现在请求的负载(Payload)里,有两个关键的参数:
payload: 一长串Base64编码样式的字符串。sig: 一个32位的十六进制大写字符串,很像MD5。
尝试在Python中用requests库模拟这个请求,如果不带这两个参数,或者任意修改其中一个字符,服务器返回的都是错误信息(如{"code": 400, "msg": "invalid signature"})。这证实了这两个参数是服务端进行请求合法性校验的关键。
初步观察,payload参数很可能包含了我们真正的查询条件(如页码、每页条数),并被加密了。而sig看起来像是一个签名,很可能由payload加上某个密钥(_P)后,再经过MD5之类哈希算法生成,用于防止参数被篡改。
3. JS逆向实战:深入加密函数腹地
逆向的核心是在庞大的JS代码中找到生成这两个参数的函数。这需要耐心和技巧。
3.1 定位加密入口:搜索与断点
- 全局搜索:在Sources面板,按
Ctrl+Shift+F进行全局搜索。可以尝试搜索关键词如payload、sig、encrypt、md5、_P等。在这个案例中,搜索sig可能直接定位到类似sig: md5(e + _p).toUpperCase()的代码行,这就是突破口。 - XHR/Fetch断点:如果搜索无果,可以在Network面板找到那个请求的
Initiator(发起者),点击跳转到发起请求的JS代码行。更通用的方法是,在Sources面板的XHR/Fetch Breakpoints里,添加一个包含部分请求URL的断点。当浏览器发起匹配的请求时,代码执行会自动暂停。 - 事件监听断点:在
Event Listener Breakpoints中勾选Script->Script First Statement,然后触发页面动作(如下滑),代码会在第一时间暂停,然后你可以一步步(F10)执行,观察网络请求何时被发出。
通过上述方法,我们成功在代码中找到了疑似生成sig的地方,并在此处打上断点。
3.2 逆向payload加密流程
刷新页面或再次触发数据加载,代码会在断点处暂停。我们顺着调用栈(Call Stack)向上追溯,寻找payload被加密的地方。
从原文描述和调试过程,我们梳理出payload的加密路径:
- 明文
payload对象:最初是一个简单的JavaScript对象,例如{sort: 1, start: 40, limit: 20}。其中start是起始位置,limit是每页条数,通过改变start来实现“翻页”。 - 进入函数
e2(e):参数e是明文对象。内部调用了_u_e(e),这个函数看起来只是将对象JSON序列化成字符串'{"sort":1,"start":40,"limit":20}',此时值未变。 e2(e)的后续操作:在_u_e返回后,e2函数内部有一个for循环,对字符串进行了处理。经过这一步,字符串变成了一个包含不可见字符和乱码的中间状态:",x177WB:d\ym{1L$'=x10nx02x04x15p8[ '&olwx022"`。这是一个关键转变,说明加密的核心可能发生在这里,可能是某种自定义的混淆或编码。- 进入函数
e1(e):上一步的结果作为参数e传入e1。这个函数执行后,返回了最终的payload加密值:LBc3V0I6ZGB5bXsxTCQnPRBuBwYJfnZeJCM7OXR/AH8q。
逆向心得:payload的加密并非标准算法(如AES),而是一个网站自定义的、由e2和e1两个函数组成的流程。其中e2可能负责混淆,e1看起来像是进行了Base64编码(因为输出字符集符合Base64特征)。但注意,直接对中间状态的乱码做Base64编码,是得不到这个结果的。这说明e1内部可能还包含了字符替换或二次加密。我们需要把e2和e1这两个函数的完整JS代码抠出来。
3.3 逆向sig签名生成流程
sig的生成相对清晰。在断点处继续执行,发现sig的值是通过md5(e + _p).toUpperCase()计算得出的。
e:就是上一步得到的加密后的payload字符串(LBc3V0I6ZGB5bXsxTCQnPRBuBwYJfnZeJCM7OXR/AH8q)。_p:一个常量字符串,在JS代码中可以找到,例如可能是"W5D80NFZHAYB8EUI2T649RT2MNRMVE2O"。- 将两者拼接,然后进行MD5哈希,最后将结果转为大写。
通过跟踪md5函数(通常是一个名为md5的函数或CryptoJS.MD5),确认其内部实现是标准的MD5算法,没有额外的魔改。这意味着在Python中,我们可以直接用hashlib库的标准MD5来实现,无需调用JS环境。
踩坑记录:在抠
md5函数时,注意检查它是否依赖了其他全局变量或函数。有些站点会修改MD5的初始常量(IV)来定制算法。务必在调试器中,用相同的输入(payload+_p)运行JS的md5函数,与Pythonhashlib.md5().hexdigest().upper()的结果进行比对,确保完全一致。
4. Python实现:还原加密与数据获取
分析清楚后,我们就可以用Python来模拟整个流程了。这里分为两个部分:实现加密函数,以及发起请求并解密响应。
4.1 环境准备与JS代码移植
首先,我们需要将关键的JS函数移植到Python中。对于sig的MD5,直接用Python标准库。
import hashlib def generate_sig(encrypted_payload: str, p_constant: str) -> str: """ 生成 sig 参数 :param encrypted_payload: 加密后的 payload 字符串 :param p_constant: 从JS中提取的常量 _P :return: 大写格式的MD5 hexdigest """ data = encrypted_payload + p_constant return hashlib.md5(data.encode('utf-8')).hexdigest().upper()对于payload的加密,我们需要将e2和e1函数用Python重写。这需要仔细分析原JS代码。
假设我们通过“Overrides”功能将JS文件保存到本地,并仔细分析后,发现e2函数是一个简单的字符替换/位移混淆,e1是一个变种的Base64编码(可能更换了码表)。
import json import base64 # 假设我们分析出的 e2 函数是一个简单的异或混淆 def e2_js_logic(plain_dict: dict) -> str: """模拟JS中的 e2 函数""" json_str = json.dumps(plain_dict, separators=(',', ':')) # 模拟 _u_e,紧凑格式 # 假设JS中的for循环是每个字符码点与一个固定值进行异或 key = 0x17 # 这个key需要从JS代码中分析得出 confused_chars = [] for char in json_str: confused_chars.append(chr(ord(char) ^ key)) confused_str = ''.join(confused_chars) return confused_str # 假设我们分析出的 e1 函数是更换了码表的Base64编码 # 标准码表: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ # 自定义码表: 从JS代码中提取,例如可能是倒序或替换的 CUSTOM_B64_TABLE = "W5D80NFZHAYB8EUI2T649RT2MNRMVE2OabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/" # 示例,需替换真实码表 STANDARD_B64_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" def e1_js_logic(confused_str: str) -> str: """模拟JS中的 e1 函数:自定义Base64编码""" # 先进行标准base64编码 standard_b64 = base64.b64encode(confused_str.encode('utf-8')).decode('ascii') # 然后根据自定义码表进行字符替换 translation_table = str.maketrans(STANDARD_B64_TABLE, CUSTOM_B64_TABLE) custom_b64 = standard_b64.translate(translation_table) return custom_b64 def generate_payload(plain_dict: dict) -> str: """生成加密的 payload 参数""" confused = e2_js_logic(plain_dict) encrypted = e1_js_logic(confused) return encrypted重要提示:上面的e2_js_logic和e1_js_logic是示例逻辑,绝非真实算法。你必须根据自己逆向分析出的真实JS代码来编写对应的Python函数。可能需要使用execjs库直接执行抠出来的JS代码片段,这对于复杂混淆是最稳妥的办法。
import execjs # 将抠出来的完整e2和e1函数代码,放在一个字符串里 js_code = """ function _u_e(obj) { return JSON.stringify(obj); } function e2(e) { // ... 完整的e2函数JS代码 ... } function e1(e) { // ... 完整的e1函数JS代码 ... } function generatePayload(obj) { return e1(e2(obj)); } """ ctx = execjs.compile(js_code) encrypted_payload = ctx.call("generatePayload", {"sort": 1, "start": 40, "limit": 20}) print(encrypted_payload) # 输出应与浏览器生成的一致4.2 构建请求与解密响应
有了生成参数的函数,就可以组装请求了。
import requests def get_data(page: int, page_size: int = 20): """ 获取某一页数据 :param page: 页码(从0开始) :param page_size: 每页条数 """ # 1. 构造明文参数 plain_params = { "sort": 1, "start": page * page_size, # 计算start "limit": page_size } # 2. 生成加密payload (使用execjs或Python还原函数) encrypted_payload = generate_payload(plain_params) # 使用上文定义的方法 # 3. 生成sig (假设已知常量_P) _P = "W5D80NFZHAYB8EUI2T649RT2MNRMVE2O" # 从JS中提取的真实常量 sig = generate_sig(encrypted_payload, _P) # 4. 构造请求 url = "https://目标网站/api/data" # 替换为真实API地址 headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Content-Type": "application/x-www-form-urlencoded", # 根据抓包确定 # 可能还需要其他Headers,如Referer, Authorization等 } data = { "payload": encrypted_payload, "sig": sig } # 5. 发送请求 resp = requests.post(url, data=data, headers=headers) resp.raise_for_status() # 检查HTTP错误 # 6. 处理加密响应 encrypted_response = resp.json().get('d') # 假设响应JSON中数据在'd'字段 if not encrypted_response: print("响应中未找到加密数据字段") return None # 响应数据也是加密的,需要找到对应的JS解密函数并移植 # 假设解密函数叫 decryptData decrypted_data = decrypt_response_data(encrypted_response) # 需要实现此函数 return decrypted_data # 使用示例 data_page_2 = get_data(page=2) # 获取第三页数据(start=40) if data_page_2: print(json.dumps(data_page_2, indent=2, ensure_ascii=False))4.3 响应数据解密
原文提到返回的d字段也是加密的。我们需要用同样的逆向思路,找到解密d的JS函数。通常,解密函数会在同一个JS文件里,可能在加密函数附近。找到后,用同样的方式(分析算法用Python重写,或直接用execjs调用)实现解密。
def decrypt_response_data(encrypted_data: str) -> dict: """ 解密响应中的d字段 :param encrypted_data: 加密的字符串 :return: 解密后的Python字典 """ # 方法A:如果解密算法简单,用Python重写 # ... 分析JS解密函数并实现 ... # 方法B:使用execjs调用抠出来的JS解密函数(推荐,更可靠) js_decrypt_code = """ function decryptData(encryptedStr) { // ... 抠出来的完整JS解密函数 ... } """ ctx = execjs.compile(js_decrypt_code) decrypted_str = ctx.call("decryptData", encrypted_data) return json.loads(decrypted_str)5. 常见问题、调试技巧与进阶思考
5.1 逆向与调试中的常见问题
断点被反调试:有些网站会检测开发者工具,并在调试时触发无限debugger或跳转。解决方法:
- 在Sources面板,找到干扰的代码行,右键选择
Never pause here。 - 使用
Overrides功能,将包含反调试代码的JS文件替换为清理后的版本。 - 使用条件断点(右键行号 ->
Add conditional breakpoint),设置一个永远为假的条件。
- 在Sources面板,找到干扰的代码行,右键选择
代码被混淆压缩:变量名变成a,b,c,难以阅读。解决方法:
- 利用浏览器的Pretty Print功能(
{}图标)格式化代码。 - 关注字符串常量、API接口URL,它们通常不会被混淆,是重要的定位锚点。
- 逐步执行,观察变量值的变化来推断函数功能。
- 利用浏览器的Pretty Print功能(
加密函数依赖浏览器环境:某些函数可能依赖
window、document或其他浏览器特有对象。在Node.js或execjs环境中执行时会报错。解决方法:- 使用
jsdom、pyppeteer等库模拟一个简易浏览器环境。 - 更简单的方法是,分析其依赖,在JS代码执行前,手动在全局注入这些缺失的对象或函数(
window = {}; document = {};),但只注入必要的空对象或模拟函数。
- 使用
Python生成的签名与JS不一致:这是最常遇到的问题。
- 编码问题:确保字符串拼接和编码完全一致。JS和Python的字符串编码要统一,通常使用UTF-8。在MD5前,确认拼接后的字符串完全一致,包括不可见字符。
- 常量错误:双重检查从JS中提取的常量
_P是否正确,是否有隐藏的换行符或空格。 - 算法差异:确认JS中的MD5是否是标准实现。可以用一个已知的字符串(如
"test")分别在JS控制台和Python中计算MD5,进行比对。
5.2 效率优化与工程化建议
- 缓存与复用:对于固定的
_P常量和不变化的加密逻辑部分,只需初始化一次(如execjs编译环境),避免每次请求都重新编译JS,极大提升效率。 - 错误重试与日志:网络请求不稳定,应添加重试机制和详细的日志记录,记录每次请求的参数和响应,便于排查问题。
- 参数化与配置:将URL、请求头、常量
_P、加解密函数代码等提取到配置文件或单独模块中,使代码更易维护。 - 应对算法更新:网站可能会更新加密算法。最好能监控请求是否突然开始失败,并准备快速响应,重新进行逆向分析。
5.3 进阶:更复杂的加密与RPC调用
本例的加密相对直接。你可能会遇到更复杂的情况:
- WebAssembly加密:核心算法用Wasm实现,逆向难度大。可以考虑直接调用Wasm模块,或者用工具将Wasm反编译为C/Go再分析。
- Obfuscator等高级混淆:代码被严重混淆,控制流扁平化。需要耐心和强大的静态分析工具辅助。
- 动态密钥:
_P这类常量可能不是固定的,而是每次页面加载时从服务器动态获取。这就需要先请求一个初始化接口,获取本次会话的密钥。
这套JS逆向的思路,其价值远不止于爬虫。在Web安全测试中,常用于分析登录接口的密码加密方式,进行安全的密码爆破测试(在授权范围内)。在前端开发中,理解加密流程有助于设计更安全的API交互方案。整个过程锻炼的是对网络协议、浏览器运行机制和代码调试的深入理解能力,这才是最重要的收获。当你再遇到加密参数时,不会再感到无从下手,而是会系统地抓包、搜索、下断点、跟栈、抠代码,最终解决问题。
