JavaScript逆向实战:从请求参数签名到响应解密的爬虫全链路解析
1. 项目概述:一次从“黑盒”到“白盒”的逆向实战
最近在带新人做爬虫实战练习时,选用了“图灵练习平台”的“码上爬”系列题目作为靶场。这个系列非常经典,它模拟了真实网站中常见的各种反爬机制,尤其是第1到第19题,几乎每一题都围绕一个核心的“加密点”展开。对于爬虫工程师来说,这类题目不再是简单的请求-解析,而是一场从浏览器到服务器,再从服务器返回浏览器的全链路攻防演练。你需要像一个侦探,追踪数据在每一个环节的形态变化,找到那个关键的“加密/解密”函数,并最终在你的代码中复现这一逻辑。
“码上爬”的题目设计得很巧妙,它把爬虫对抗中最核心、也最让人头疼的“参数加密”和“响应解密”问题,拆解成了一个个独立的关卡。有的题目是请求参数被JavaScript动态计算,有的则是响应内容被加密,需要本地解密才能看到真实数据。这不仅仅是写几行 requests 代码那么简单,它要求你深入理解HTTP请求/响应的生命周期,熟练使用开发者工具进行动态调试,并具备一定的JavaScript逆向分析能力。本次解析,我将带你走一遍从发起请求到获得明文响应的完整链路,分享我在破解这19个加密点时总结的通用思路、核心工具和那些容易踩坑的细节。无论你是正在刷题的新手,还是想系统提升逆向能力的同行,相信这篇实战记录都能给你带来直接的帮助。
2. 核心思路与工具链:构建你的“解密工作台”
面对一个未知的加密点,盲目尝试是最低效的。建立一个系统性的分析思路和顺手的工具链,能让你事半功倍。我的核心思路可以概括为“由外而内,动静结合”。
2.1 通用分析流程:四步定位加密核心
我的分析通常遵循以下四个步骤,这是一个层层递进的过程:
-
网络抓包,定位关键请求 :这是所有工作的起点。打开Chrome DevTools的Network面板,清空日志,然后执行题目页面的触发操作(如点击查询、登录等)。在纷繁复杂的请求中,快速定位到那个携带了疑似加密参数(通常是一长串无规律的字符串,或参数名如
sign、token、data等)或返回加密响应体的XHR/Fetch请求。重点关注其Headers(尤其是Query String Parameters和Form Data)以及Response。 -
静态搜索,寻找线索 :在Sources面板中,对页面加载的所有JavaScript文件进行全局搜索(
Ctrl+Shift+F)。搜索关键词包括:加密参数名(如encryptData)、加密后的特征值片段、已知的加密算法常量(如AES、RSA、CryptoJS)、或用于加密的常见函数名(如encrypt、sign、md5、hmac)。这一步能快速缩小目标函数所在文件的范围。 -
动态调试,验证逻辑 :这是最关键的一步。在疑似包含加密逻辑的JS文件中设置断点,通常是搜索到的函数入口,或者是网络请求发起(如
fetch、XMLHttpRequest.send)的调用栈附近。重新触发请求,代码会在断点处暂停。此时,你可以利用Call Stack查看调用链,利用Scope面板观察所有变量的实时值,特别是传入加密函数的原始参数和加密后的输出结果。通过单步执行(F10)、步入(F11),一步步跟踪数据的加工过程。 -
代码还原与本地复现 :在理清加密逻辑后,将关键的JavaScript函数代码提取出来。这可能涉及将混淆的代码进行局部格式化、重命名变量以增强可读性。然后,在Python环境中使用对应的库(如
hashlib,hmac,Crypto,execjs)复现相同的算法。复现后,必须用已知的输入输出进行单元测试,确保与浏览器行为完全一致。
2.2 必备工具链
工欲善其事,必先利其器。以下是我日常高频使用的工具组合:
- 浏览器开发者工具(Chrome DevTools) :核心中的核心。Network面板用于抓包,Sources面板用于断点调试,Console面板用于执行临时代码测试函数。
- 编辑与格式化工具 :对于混淆严重的JS,可以使用在线工具或VS Code插件进行初步的格式化,让代码结构稍微清晰一些。
- Python环境及相关库 :
requests: 发送HTTP请求。execjs: 执行提取出的复杂JavaScript加密函数。当加密逻辑涉及大量浏览器环境特有的对象或复杂的原型链操作时,直接使用execjs调用JS代码是最稳妥的方式。hashlib,hmac: 处理MD5、SHA系列、HMAC等哈希算法。pycryptodome/cryptography: 处理AES、DES、RSA等对称/非对称加密算法。json,re,time: 基础的数据处理和时间模块。
- 辅助搜索与学习 :善于利用搜索引擎。当你看到一串加密字符串具有特定特征(如以
eyJ开头,很可能就是JWT;末尾常有==,可能是Base64编码),或者代码中出现CryptoJS.AES.encrypt这样的字样时,直接搜索相关关键词,能快速帮你确定算法方向。
注意 :在整个分析过程中,请务必遵守法律法规和网站的使用条款。
图灵练习平台是一个合法的、用于技术学习与测试的平台。我们的所有操作仅限于该平台提供的题目范围内,旨在学习技术原理,严禁用于对任何非授权网站进行爬取或攻击。
3. 典型加密场景深度拆解与实战
“码上爬”1-19题的加密点虽然各异,但大体可以归纳为几种经典场景。下面我选取几个最具代表性的题型,拆解其全链路过程。
3.1 场景一:请求参数哈希签名(Sign)
这是最常见的一种反爬手段,多见于查询、列表接口。服务器通过验证签名来确保请求参数在传输过程中未被篡改,并且请求是“合法”生成的。
- 题目特征 :Network中看到的请求参数里,除了正常的查询条件(如
page=1&keyword=test),会多出一个类似sign=4f9d8a7e0b1c3a2f5e6d8c7b0a9f4e3d的参数。这个sign值通常很长,且每次请求即使其他参数不变,它也可能变化(因为可能加入了时间戳)。 - 实战解析(以某题为例) :
- 抓包观察 :发现请求携带
keyword,page,timestamp,sign等参数。sign值是一串32位的十六进制字符串,疑似MD5。 - 搜索与断点 :在JS文件中搜索
sign,找到一处赋值语句:params['sign'] = md5(encrypt_string)。在此处md5函数调用前打上断点。 - 动态跟踪 :触发请求后,程序暂停。在Console中查看
encrypt_string的值,发现其格式为keyword=xxx&page=xxx×tamp=xxx&key=SECRET_KEY。原来,客户端将所有的业务参数按字母顺序排序后,拼接成key=value&形式的字符串,然后在末尾拼接了一个固定的密钥(SECRET_KEY),最后对这个拼接后的整体字符串进行MD5哈希,得到sign。 - 本地复现 :
import hashlib import time def generate_sign(keyword, page): timestamp = int(time.time() * 1000) # 获取毫秒级时间戳 secret_key = "turing@123" # 通过调试发现的密钥 # 按参数名排序并拼接 params = { "keyword": keyword, "page": page, "timestamp": timestamp } param_string = '&'.join([f'{k}={params[k]}' for k in sorted(params.keys())]) sign_string = param_string + '&key=' + secret_key # 计算MD5 m = hashlib.md5() m.update(sign_string.encode('utf-8')) return m.hexdigest(), timestamp # 使用 sign, ts = generate_sign("python", 1) print(f"timestamp: {ts}, sign: {sign}")
- 抓包观察 :发现请求携带
- 避坑指南 :
- 参数顺序 :拼接顺序至关重要,必须是按特定规则(如字母升序)排序后的结果,务必与JS逻辑完全一致。
- 密钥隐藏 :密钥(
SECRET_KEY)可能硬编码在JS里,也可能通过另一个接口动态获取,需要仔细查找。 - 编码问题 :确保拼接字符串时的编码与JavaScript一致,通常都是UTF-8。
3.2 场景二:响应内容加密(AES/Base64)
这种场景下,请求可以正常发送和接收,但服务器返回的响应体( Response )是一串看似乱码的加密文本,无法直接解析为JSON或HTML。
- 题目特征 :Network中查看接口的
Preview或Response标签,看到的不是清晰的JSON结构,而是一大串像U2FsdGVkX1+...这样的字符,很可能是一个AES加密后再用Base64编码的字符串。 - 实战解析 :
- 抓包确认 :请求看似正常,但响应体无法直接阅读。观察响应头,有时会包含加密提示,如
Content-Encoding: custom-aes,但更多时候没有。 - 搜索解密线索 :在JS中搜索
decrypt、AES、CryptoJS等关键词。很可能在接收到数据后,页面会调用一个解密函数来解析数据,再渲染到DOM中。 - 定位解密函数 :找到解密函数后,重点分析其使用的 密钥(Key) 、 初始向量(IV) 、 加密模式(如CBC) 和 填充方式(如PKCS7) 。这些是复现解密算法的关键四要素。
- 本地复现解密 :
from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import base64 def decrypt_response(encrypted_b64_text): # 从JS调试中获取的Key和IV,注意可能是字符串,需要转为bytes key = b'turing_key_16bytes' # 16, 24 or 32 bytes iv = b'turing_iv_16bytes_' # 16 bytes for CBC # 1. Base64解码 encrypted_bytes = base64.b64decode(encrypted_b64_text) # 2. 创建AES解密器,使用CBC模式和获取到的IV cipher = AES.new(key, AES.MODE_CBC, iv) # 3. 解密并去除填充 decrypted_padded = cipher.decrypt(encrypted_bytes) decrypted_bytes = unpad(decrypted_padded, AES.block_size) # 4. 解码为字符串(假设原文是UTF-8 JSON) return decrypted_bytes.decode('utf-8') # 假设 resp_text 是从网络获取的加密响应文本 # decrypted_json_str = decrypt_response(resp_text) # data = json.loads(decrypted_json_str)
- 抓包确认 :请求看似正常,但响应体无法直接阅读。观察响应头,有时会包含加密提示,如
- 避坑指南 :
- 参数对齐 :Python的
pycryptodome库和JS的CryptoJS在默认配置上可能有细微差别(如默认的AES加密模式、默认的Key/IV处理方式)。必须确保所有参数(Key/IV/模式/填充)完全匹配。 - Key/IV来源 :Key和IV可能不是硬编码,而是由请求参数或某个固定值通过哈希衍生而来,需要跟踪其生成逻辑。
- 编码与解码 :注意Base64编码的变种(如标准Base64、URL安全的Base64)。解密后的字节流需要根据原文编码正确解码。
- 参数对齐 :Python的
3.3 场景三:请求载荷整体加密(RSA + AES混合)
这是安全性较高的一种方式,常见于登录、支付等关键接口。它结合了非对称加密(RSA)和对称加密(AES)的优点。
- 题目特征 :
Form Data或Request Payload中看不到明文参数,只有一个或两个加密字段,如encryptedKey和encryptedData。 - 全链路逻辑解析 :
- 客户端随机生成一个 AES密钥(sessionKey) 。
- 客户端用这个
sessionKey,采用AES算法加密原始的请求参数(JSON字符串),得到encryptedData。 - 客户端使用服务器提供的 RSA公钥 ,加密刚才生成的
sessionKey,得到encryptedKey。 - 客户端将
encryptedKey和encryptedData一起发送给服务器。 - 服务器用自己的RSA私钥解密
encryptedKey,得到sessionKey。 - 服务器用
sessionKey解密encryptedData,得到原始请求参数。
- 实战要点 :
- 寻找公钥 :RSA公钥通常内嵌在页面HTML或某个初始化的JS变量中。在Sources中搜索
PUBLIC_KEY、encrypt或-----BEGIN PUBLIC KEY-----这类字符串。 - 调试生成逻辑 :重点跟踪
sessionKey的生成(通常是随机字符串)、AES加密参数、以及RSA加密sessionKey这三步。 - Python复现难点 :需要
rsa或Crypto.PublicKey.RSA库。注意JS库(如JSEncrypt)使用的RSA填充方案通常是PKCS1_v1_5,在Python中需要对应指定。import json from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA from Crypto.Random import get_random_bytes import base64 def encrypt_request_data(data_dict, rsa_public_key_pem): # 1. 生成随机AES密钥和IV session_key = get_random_bytes(16) # AES-128 iv = get_random_bytes(16) # 2. AES加密数据 cipher_aes = AES.new(session_key, AES.MODE_CBC, iv) data_str = json.dumps(data_dict, separators=(',', ':')) # 紧凑JSON padded_data = data_str.encode() + b'\x00' * (AES.block_size - len(data_str) % AES.block_size) # 注意填充方式 encrypted_data = cipher_aes.encrypt(padded_data) # 3. RSA加密AES密钥 key_rsa = RSA.import_key(rsa_public_key_pem) cipher_rsa = PKCS1_v1_5.new(key_rsa) encrypted_key = cipher_rsa.encrypt(session_key) # 4. Base64编码后发送 payload = { 'encryptedKey': base64.b64encode(encrypted_key).decode('utf-8'), 'encryptedData': base64.b64encode(iv + encrypted_data).decode('utf-8') # 有时IV会拼在数据前 } return payload
- 寻找公钥 :RSA公钥通常内嵌在页面HTML或某个初始化的JS变量中。在Sources中搜索
4. 全链路调试技巧与问题排查实录
理论清楚了,但实战中总会遇到各种意想不到的问题。下面是我在打通“请求-响应”全链路时,积累的一些高阶调试技巧和常见问题解决方案。
4.1 如何高效追踪加密函数调用栈?
当你在一个庞大的、经过混淆的JS文件中找到加密函数时,往往不知道它被谁、在何时调用。这时, Call Stack (调用堆栈)面板是你的救命稻草。
- 在加密函数入口处打上断点。
- 触发请求,程序暂停后,不要急着看变量,先看右侧的
Call Stack面板。 - 这里会显示从当前函数一直回溯到最顶层调用者的完整链条。点击堆栈中的上一级函数,可以直接跳转到对应的代码位置,并查看当时的上下文变量。
- 顺着调用栈一层层往上分析,你就能理清整个加密参数的准备、计算、赋值的完整流程。这对于理解那些把加密逻辑分散在多个模块或闭包中的代码尤其有效。
4.2 如何处理“无限Debugger”反调试?
有些题目会设置反调试机制,比如在代码中插入 debugger; 语句,或检测开发者工具是否打开,导致脚本不断暂停,无法正常操作。
- 应对方法一(禁用断点) :在Sources面板,找到包含
debugger;语句的那一行,右键点击行号,选择“Never pause here”。这样调试器就会忽略这个断点。 - 应对方法二(重写函数) :如果反调试是通过函数实现的(如重写
setInterval或检测debugger关键字),可以在Console中执行代码将其“干掉”。例如:// 示例:禁止所有debugger语句生效 Function.prototype.constructor = function() { if (arguments[0] && arguments[0].includes('debugger')) { return function(){}; } return Function.apply(this, arguments); }; // 注意:这种方法可能不总是有效,且需要根据具体代码调整。 - 最推荐的方法 :使用条件断点。在关键的加密函数入口设置断点时,不要用普通断点,而是右键选择“Add conditional breakpoint”,输入一个永远为假的条件,如
false。这样断点永远不会触发,但你可以随时在Console中手动调用相关函数进行测试,绕过了基于行号的检测。
4.3 本地复现时,结果为何与浏览器不一致?
这是最常遇到的问题,通常由以下几个原因导致:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 哈希值(MD5/SHA)不同 | 1. 字符串拼接顺序或格式不同。 2. 编码不一致(JS默认可能是UTF-16?实际多为UTF-8)。 3. 密钥或盐值(Salt)找错或遗漏。 |
1. 将浏览器中调试时拼接的原始字符串完整打印到Console,复制出来。 2. 在Python中,用完全相同的字符串(注意转义字符)进行哈希计算,对比结果。 3. 使用 JSON.stringify(params) 和 python json.dumps(params, separators=(‘,’,‘:’)) 确保JSON字符串化结果一致。 |
| AES解密失败或乱码 | 1. Key、IV、Mode、Padding 四要素有任何一项不匹配。 2. 加密数据在传输前可能经过了其他处理(如字符编码转换)。 3. JS中可能对Key/IV进行了哈希或编码衍生,而非直接使用原文。 |
1. 最有效的方法 :在JS解密函数成功执行后,立刻在Console中打印出 CryptoJS.AES.decrypt() 调用时传入的所有参数(ciphertext, key, iv, mode, padding),确保Python端完全复现这些参数值。 2. 检查Python解密后的字节,尝试不同的编码(utf-8, gbk, latin1)解码。 |
| RSA加密/解密失败 | 1. 填充方案不匹配(如JS用PKCS1_v1_5,Python用了OAEP)。 2. 公钥格式问题(需要去除头尾标记和换行,或反之)。 3. 加密的数据长度超过RSA密钥长度所能处理的最大值。 |
1. 明确JS库使用的RSA填充方案( JSEncrypt 默认是PKCS1_v1_5)。 2. 确保公钥字符串的格式一致,可以用 RSA.import_key() 先测试导入是否成功。 3. RSA通常只用于加密密钥(如AES的key),而不是加密大量数据。 |
4.4 如何应对代码混淆与动态加载?
“码上爬”后期题目可能会遇到代码压缩混淆(变量名变成a,b,c)或动态加载JS。
- 对于混淆 :不要试图还原整个文件。聚焦于你找到的加密函数片段,利用浏览器的“Pretty-print”功能({}按钮)格式化代码,使其可读性稍好。然后,通过断点和Console,动态地观察关键变量的值,理解其逻辑,而不是阅读所有代码。
- 对于动态加载 :有些加密函数可能在页面初始化后通过XHR或Fetch动态请求一个JS文件并执行。在Network面板中过滤
JS类型请求,关注在核心接口请求之前加载的、名称可疑的新JS文件。可以将其内容复制到本地或直接在该文件的代码里设置断点。
5. 从练习到实战:思维延伸与经验总结
完成“码上爬”这19道题,绝不仅仅是为了获取答案。更重要的是,通过这个过程,建立起一套应对未知加密场景的方法论。在实际工作中,你遇到的加密方式可能更复杂、更隐蔽,但核心的排查思路是相通的。
5.1 建立你的“加密特征”速查表
平时可以积累一些常见加密算法的输出特征,这能帮助你在抓包的第一时间做出初步判断:
- MD5/SHA1/SHA256 :固定长度的十六进制字符串(32位、40位、64位)。
- Base64 :字符串由
A-Za-z0-9+/=组成,末尾常带=填充,长度通常是4的倍数。 - AES加密后Base64 :类似
U2FsdGVkX1...(这是OpenSSL格式的Salt头,Salted__的Base64),或者无头的密文。 - JWT :由三部分组成,用点
.分隔,形如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c,第一部分头部解码后通常能看到alg声明。
5.2 实战中的取舍:复现 vs 调用
当加密逻辑极其复杂,涉及大量浏览器特有对象或复杂的第三方库时,完全用Python复现可能成本极高。此时,可以考虑使用 execjs 或 PyExecJS 库,直接将调试好的JavaScript函数代码在Python环境中执行。
import execjs
# 1. 将从浏览器中提取的JS函数代码,保存为一个字符串或文件。
# 假设我们提取了一个名为 `calculateSign` 的函数。
js_code = """
function calculateSign(params, timestamp) {
// ... 复杂的加密逻辑 ...
return sign;
}
"""
# 2. 创建JS上下文并编译
ctx = execjs.compile(js_code)
# 3. 调用函数
sign = ctx.call("calculateSign", {"page": 1, "keyword": "test"}, 1648886400000)
print(sign)
这种方法的好处是“精准还原”,但缺点是依赖Node.js环境( execjs 的后端),并且执行效率可能低于纯Python。它适合作为破解复杂加密的“终极手段”。
5.3 保持耐心与好奇心
逆向分析是一个需要极大耐心和细心的工作。一个分号的遗漏、一个参数顺序的错误,都可能导致前功尽弃。我的经验是,每完成一道题,不要仅仅满足于得到答案,要多问几个“为什么”:为什么这里要用这种加密?这个密钥是怎么来的?还有没有更简单的查看方式?这种追问能帮你深化理解,当下次遇到类似但又不完全相同的保护时,你才能举一反三。
最后,分享一个我自己的小习惯:在分析过程中,我会用一个笔记软件(如Obsidian、Typora)实时记录我的抓包截图、关键代码片段、调试中的变量值、以及尝试过的思路和错误。这份“侦查笔记”不仅能帮你理清思路,在几天后回顾或者遇到相似问题时,它就是你最好的知识库。爬虫与反爬的对抗在不断升级,但只要你掌握了从请求到响应的全链路分析方法,你就始终能站在理解技术本质的一方,找到那条通往数据的路径。
更多推荐
所有评论(0)