1. 项目概述:一次针对Token加密机制的深度逆向之旅
最近在做一个数据采集项目时,遇到了一个相当“硬核”的坎儿:目标平台的核心API请求,其身份验证Token不再是简单的明文或Base64编码,而是被一套复杂的客户端加密算法保护了起来。直接抓包拿到的请求参数,里面的Token字段是一串毫无规律、每次请求都在变化的密文。这意味着,传统的爬虫手段——复制Cookie、复制Authorization头——在这里完全失效了。这激起了我的技术好奇心,也迫使我必须深入客户端内部,去搞清楚这个Token到底是怎么生成的。今天,我就把这次完整的“逆向分析+Python还原”实战过程记录下来,这不仅仅是一个爬虫案例,更是一次对现代Web应用安全机制的理解与挑战。
这个项目适合所有对网络爬虫进阶、Web安全、JavaScript逆向以及Python自动化有兴趣的朋友。无论你是遇到了类似的技术壁垒,还是单纯想了解前端加密与后端验证是如何“斗智斗勇”的,这篇文章都将提供一个非常详实的参考路径。整个过程涉及静态分析、动态调试、算法还原和代码复现,我会尽量用通俗的语言把每个环节讲清楚,并提供可以直接“抄作业”的代码片段。当然,我们必须时刻牢记
合规底线
:所有的分析仅用于学习交流与技术研究,务必尊重网站的
robots.txt
协议,控制请求频率,避免对目标服务器造成压力,更不得用于非法获取和滥用数据。毕竟,把正规爬虫的带宽都挤占了的做法,是损人不利己的。
2. 逆向分析的核心思路与工具准备
面对一个加密的Token,我们的核心目标很明确:找到生成这个Token的原始逻辑,并用Python将其复现出来。这听起来像在破解一个黑盒,而我们的武器就是逆向工程。整个思路可以概括为“由外及内,动静结合”。
2.1 逆向分析的基本逻辑链条
首先,我们需要建立一个清晰的逻辑链条。一个典型的客户端加密Token的生成流程,通常离不开以下几个关键点:
-
入口定位
:Token在何处被生成并设置到请求中?通常是在发起网络请求(如
XMLHttpRequest或fetch)之前,由某个JavaScript函数处理请求参数时加入的。 - 关键函数定位 :找到负责生成或加密Token的核心函数。这个函数可能被混淆、被压缩,但它的输入(通常是用户信息、时间戳、随机数等)和输出(加密后的Token)是确定的。
- 算法识别 :分析该函数内部使用的加密算法。是常见的AES、RSA,还是自定义的哈希、编码组合?参数(如密钥、加密模式、填充方式)是什么?
- 依赖追踪 :加密函数可能依赖其他函数或全局变量(例如,一个动态生成的密钥)。需要理清这些依赖关系。
-
环境还原
:有些加密逻辑依赖于浏览器环境特有的对象或属性(如
window、document的一些特性),在Node.js或Python中需要模拟。
2.2 必备工具栈
工欲善其事,必先利其器。以下是本次逆向分析中我用到的主要工具,它们构成了从探查到复现的完整流水线。
-
浏览器开发者工具(Chrome DevTools)
:这是我们的主战场。尤其是
Sources
面板和
Network
面板。
- Network面板 :用于捕获所有网络请求,重点关注携带加密Token的请求。查看其请求头(Headers)和发起者(Initiator),可以快速定位到是哪个脚本文件发起了这个请求,从而缩小代码搜索范围。
-
Sources面板
:用于静态查看和动态调试JavaScript代码。可以在这里搜索关键词(如Token的字段名
token、sign、encrypt等),设置断点(Breakpoint),单步执行(Step into/over)来观察变量状态。
- Overrides功能 :Chrome DevTools的一个神器。它允许你将在线JS文件映射到本地副本。这样你就可以在本地随意修改、格式化(美化)混淆的代码,刷新页面后浏览器会加载你修改过的本地文件,极大方便了动态调试和代码分析。
-
Python环境
:用于最终算法还原和爬虫编写。需要准备一些关键库:
-
requests:用于发送HTTP请求。 -
execjs:一个执行JavaScript代码的库。在完全用Python还原算法困难时,可以“偷懒”直接调用关键的、已提取的JS函数。 -
PyExecJS:execjs的一个后端,需要安装一个JavaScript运行环境,如Node.js。 -
Crypto相关库:如pycryptodome,用于实现AES、RSA等标准加密算法。
-
-
代码格式化工具
:如果目标JS代码被压缩成一行,你需要一个格式化工具。在线工具(如
beautifier.io)或编辑器的插件(如Prettier)都可以。格式化后的代码可读性会大大提升。
注意 :在开始逆向之前,一定要在Network面板中勾选“Preserve log”(保留日志),并禁用缓存(Disable cache),以确保能捕获到页面加载过程中的所有请求,尤其是第一次生成Token的请求。
2.3 初步探查与关键词搜索
打开目标网站,进行登录或触发需要Token的API操作。在Network面板中,找到那个返回数据或提交数据的XHR/Fetch请求。查看其请求头或请求体,找到那个加密的Token字段,记住它的名字,比如叫
encryptedToken
或
signature
。
然后,在Sources面板中,使用
Ctrl+Shift+F
进行全局搜索。搜索关键词可以包括:
-
Token字段名本身:
encryptedToken -
可能的方法名:
encrypt,sign,getToken,generateSign -
常见的加密库标识:
CryptoJS,encrypt,AES,RSA -
设置请求头的代码:
setRequestHeader
搜索的结果可能会指向一个被压缩的JS文件。这时,首先点击左下角的
{}
(Pretty-print)按钮格式化代码,然后在该文件中继续搜索,逐步缩小核心函数的位置。
3. 动态调试与关键函数定位实战
静态搜索往往只能找到线索,真正的突破来自于动态调试。我们需要让代码在生成Token的那一刻暂停下来,让我们有机会观察所有的输入、输出和中间状态。
3.1 断点策略与调用栈分析
找到疑似生成Token的代码行后,在其左侧行号处点击设置断点。更高效的方法是使用
“事件监听器断点”
(Event Listener Breakpoints)。在Sources面板的右侧,展开“Event Listener Breakpoints”,勾选“XHR/Fetch”下的
send
或
fetch
。这样,当任何JavaScript代码发起网络请求时,执行都会自动暂停,我们可以直接看到是哪个函数在发起请求,并顺着调用栈(Call Stack)向上回溯,找到封装和加密参数的逻辑。
当断点触发后,右侧的“Call Stack”区域会显示当前执行位置的函数调用链。点击调用栈中的上层函数,可以跳转到对应的代码位置,观察参数是如何一层层传递和加工的。我们的目标,就是找到那个最终将原始数据(如
{“userId”: 123, “timestamp”: …}
)转换成加密字符串的函数。
3.2 关键变量监控与作用域分析
在断点暂停的状态下,调试器的“Scope”区域显示了当前作用域内的所有变量。你可以将鼠标悬停在代码中的变量上,或者直接在Console面板中输入变量名来查看其当前值。这是理解算法输入的关键。
例如,你可能会发现一个名为
rawData
的对象,里面包含了时间戳、用户ID和一些固定参数。然后,看到这个
rawData
被传递给一个名为
encryptData
的函数,该函数返回的结果最终被赋值给了请求头中的
X-Token
字段。那么,
encryptData
就是我们需要重点攻克的核心函数。
3.3 跟踪函数执行与参数提取
进入
encryptData
函数内部,可以继续使用单步调试(F11 Step into)。观察每一步执行后,数据发生了怎样的变化。是进行了字符串拼接?还是调用了某个加密库的方法?特别注意函数调用时传入的参数,以及函数返回的结果。
一个非常实用的技巧是:在Console中,你可以尝试
手动执行你怀疑的核心函数
。首先,在代码中找到这个函数的定义,通常是一个
function encryptData(data) {…}
或者
var encrypt = function(){…}
。然后,在Console中,复制这个函数的完整代码(包括其依赖的任何全局变量或工具函数),定义一个同名函数。接着,构造一个你认为可能的输入参数,手动调用这个函数,看输出是否与网络请求中捕获的Token一致或类似。如果一致,恭喜你,你已经成功提取了加密逻辑。
实操心得 :很多加密函数会依赖一个“密钥”(key)。这个密钥可能是硬编码在JS文件里的一个字符串,也可能是通过另一个函数动态生成的,甚至是从服务器端响应中获取的。在调试时,一定要找到这个密钥的源头。如果密钥是动态生成的,你需要把生成密钥的逻辑也一并提取出来。
4. 算法还原与Python代码实现
经过动态调试,我们假设已经定位到了核心加密函数
generateToken(payload)
。通过观察和手动执行,我们分析出它的逻辑是:
-
将载荷
payload(一个对象)按固定键顺序排序后,转换为key1=value1&key2=value2格式的字符串。 -
在这个字符串末尾拼接一个固定的密钥
secret。 - 对拼接后的字符串进行MD5哈希(也可能是SHA1、SHA256等)。
- 将MD5结果进行Base64编码,得到最终的Token。
4.1 纯Python还原实现
对于这种标准算法(MD5、Base64),我们可以用Python的
hashlib
和
base64
库轻松还原。
import hashlib
import base64
import urllib.parse
import time
import json
def generate_token_pure(payload_dict, secret='your_secret_key_here'):
"""
纯Python实现Token生成算法
Args:
payload_dict: 字典,包含需要加密的数据,如userId, timestamp等。
secret: 字符串,从JS中分析得到的固定密钥。
Returns:
str: 加密后的Token字符串。
"""
# 1. 对字典按键进行排序,并转换为查询字符串格式
# 注意:排序是为了保证顺序与JS端一致,这是很多签名算法的常见要求
sorted_items = sorted(payload_dict.items(), key=lambda x: x[0])
query_string = '&'.join([f'{k}={v}' for k, v in sorted_items])
# 2. 拼接密钥
sign_string = query_string + secret
# 3. 计算MD5哈希(注意编码)
# JS中通常处理的是UTF-8或特定编码的字符串,Python需保持一致
md5_hash = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
# 4. 进行Base64编码
# 注意:有些JS的Base64实现可能对结果有处理(如替换字符),需观察确认
token = base64.b64encode(md5_hash.encode('utf-8')).decode('utf-8')
# 示例中可能还会去掉末尾的‘=’
token = token.rstrip('=')
return token
# 示例用法
payload = {
'userId': '123456',
'timestamp': int(time.time() * 1000), # JS常用毫秒时间戳
'nonce': 'abcdefg'
}
secret = 'this_is_a_hardcoded_secret' # 此密钥需从JS中分析得出
my_token = generate_token_pure(payload, secret)
print(f"生成的Token: {my_token}")
4.2 使用execjs调用提取的JS函数
有些加密算法非常复杂,或者大量使用了浏览器环境特有的对象,完全用Python重写成本很高。这时,
execjs
就派上用场了。我们可以把从JS文件中提取出来的、包含核心加密逻辑的代码片段保存到一个
.js
文件中,然后用Python去调用它。
假设我们提取出的关键JS代码保存为
crypto_logic.js
:
// crypto_logic.js
// 这里是从目标网站JS中提取并可能稍作修改的加密函数
function complexEncrypt(data, dynamicKey) {
// ... 这里是非常复杂的、依赖浏览器Crypto API或特定库的加密逻辑 ...
// 例如,使用了 window.crypto.subtle 或某个混淆过的第三方库
var encrypted = someObfuscatedFunction(data, dynamicKey);
return encrypted;
}
// 暴露给外部调用的接口
function getEncryptedToken(payloadJsonStr, extraParams) {
var payload = JSON.parse(payloadJsonStr);
var key = generateDynamicKey(extraParams); // 可能还有动态密钥生成逻辑
return complexEncrypt(payload, key);
}
对应的Python调用代码:
import execjs
import json
# 1. 读取包含加密逻辑的JS文件
with open('crypto_logic.js', 'r', encoding='utf-8') as f:
js_code = f.read()
# 2. 创建JS执行上下文
ctx = execjs.compile(js_code)
# 3. 准备参数
payload = {'userId': 123, 'action': 'query'}
extra = {'version': '1.0'}
# 4. 调用JS函数
# 注意:JS函数接收字符串,所以需要将字典序列化
token = ctx.call('getEncryptedToken', json.dumps(payload), extra)
print(f"通过execjs生成的Token: {token}")
这种方法的好处是还原速度快,几乎可以100%模拟原逻辑。缺点是部署环境需要安装Node.js,并且执行效率比纯Python慢。
4.3 处理动态密钥与环境依赖
这是逆向中最棘手的部分之一。密钥可能不是硬编码的,而是:
- 由服务器下发的 :在某个初始化接口的响应中返回。这时你的爬虫需要先模拟这个初始化请求,拿到密钥。
-
根据时间或用户信息计算的
:例如,
key = md5(用户ID + 当日日期)。你需要用Python复现这个计算过程。 -
依赖浏览器指纹
:使用了
navigator.userAgent、屏幕分辨率等生成一个种子。在Python中,你需要用requests库的headers来模拟一个固定的UA,并保证每次计算时使用的“指纹”一致。
对于环境依赖,比如使用了
window.btoa
(Base64编码)或
CryptoJS
库。对于
btoa
,可以用Python的
base64.b64encode
替代。如果使用了
CryptoJS
,你可以尝试在Python中用
pycryptodome
实现相同算法,或者更简单地将
CryptoJS
的源码片段一起打包到你的
execjs
执行环境中。
5. 完整爬虫集成与请求模拟
成功还原Token生成算法后,剩下的工作就是将其集成到一个健壮的爬虫流程中。
5.1 爬虫流程设计
一个典型的、需要处理加密Token的爬虫流程如下:
-
会话维持
:使用
requests.Session()来维持Cookie,模拟浏览器会话状态。 - 前置请求 :访问登录页或主页,获取必要的初始Cookie或CSRF Token。
- 登录模拟 :如果需要登录,模拟登录请求。登录成功后,Session会自动保存登录态Cookie。
-
获取动态参数
:如果Token生成依赖服务器下发的动态参数(如一个临时的
keyId),先请求相关接口获取这些参数。 -
构造载荷与生成Token
:根据API要求,构造
payload字典(包含业务参数、时间戳、随机数等),调用我们还原的generate_token函数生成加密Token。 -
发起目标请求
:将生成的Token放入请求头(通常是
Authorization: Bearer <token>或自定义头如X-Sign)或请求体中,发起最终的API请求。 - 处理响应与错误 :解析响应数据。特别注意处理Token过期(返回401/403状态码)的情况,设计重试或重新登录的逻辑。
5.2 请求头与参数模拟
为了让请求更像来自真实浏览器,除了Token,还需要精心设置请求头。
import requests
import time
import random
import string
def get_random_string(length):
"""生成随机字符串,用于模拟nonce等参数"""
letters = string.ascii_letters + string.digits
return ''.join(random.choice(letters) for i in range(length))
session = requests.Session()
# 设置通用请求头,模拟Chrome浏览器
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Content-Type': 'application/json;charset=UTF-8', # 根据实际情况调整
'Origin': 'https://target-website.com',
'Referer': 'https://target-website.com/some/page',
}
session.headers.update(headers)
# 1. 可能的前置请求,获取初始Cookie
session.get('https://target-website.com/login')
# 2. 模拟登录(如果需要)
login_data = {
'username': 'your_username',
'password': 'your_password',
# 可能还有从登录页HTML中解析出的CSRF token
}
login_resp = session.post('https://target-website.com/api/login', json=login_data)
if login_resp.status_code != 200:
print('登录失败')
exit()
# 3. 获取动态密钥(如果需要)
key_resp = session.get('https://target-website.com/api/getKey')
dynamic_secret = key_resp.json().get('secretKey')
# 4. 构造业务请求载荷并生成Token
payload = {
'page': 1,
'size': 20,
'timestamp': int(time.time() * 1000), # 毫秒时间戳
'nonce': get_random_string(8), # 随机数,防重放
'userId': login_resp.json().get('userId')
}
# 假设我们使用纯Python还原的函数
from token_generator import generate_token_pure
encrypted_token = generate_token_pure(payload, dynamic_secret) # 使用动态密钥
# 5. 发起最终请求,携带加密Token
api_headers = {
'X-Token': encrypted_token,
# 其他业务头...
}
target_url = 'https://target-website.com/api/targetData'
resp = session.post(target_url, json=payload, headers=api_headers)
if resp.status_code == 200:
data = resp.json()
print('数据获取成功!')
# 处理数据...
else:
print(f'请求失败,状态码:{resp.status_code}')
print(resp.text)
5.3 应对反爬策略
目标平台可能不止Token加密这一道防线。常见的还有:
-
频率限制
:在代码中主动添加延迟,
time.sleep(random.uniform(1, 3)),避免请求过快。 -
IP封禁
:考虑使用代理IP池。
requests库可以通过proxies参数设置代理。 -
请求头校验
:确保
User-Agent、Referer、Accept等头信息与浏览器行为一致。 -
JavaScript挑战
:有些网站会先返回一段JS代码,要求客户端执行并返回结果才能继续。这通常需要
execjs或Selenium等工具来配合解决,复杂度更高。
6. 常见问题排查与调试技巧实录
在实际操作中,你几乎一定会遇到各种问题。下面是我踩过的一些坑以及排查思路。
6.1 Token生成结果不一致
这是最常见的问题。你的Python代码生成的Token和浏览器生成的Token对不上。
-
排查步骤1:检查输入一致性
。这是最关键的。将浏览器中断点处的
payload对象和secret值完整地复制出来,确保你的Python函数输入的每一个键值对、每一个字符都完全一致。特别注意:-
数据类型
:JS中数字
123和字符串”123″的MD5结果不同。 -
编码
:中文字符串的编码。JS内部通常使用UTF-16,但在进行网络传输或哈希计算时,可能会转换为UTF-8。在Python中统一使用
.encode(‘utf-8’)。 -
空格与不可见字符
:字符串首尾是否有空格?换行符是
\n还是\r\n?
-
数据类型
:JS中数字
- 排查步骤2:验证中间结果 。在JS调试器中,在加密函数的每一步,都把中间变量的值打印到Console。在你的Python代码中,也对应地打印出每一步的结果。对比从原始字符串拼接,到第一次哈希/加密前的数据,再到最终编码前的数据,看哪一步开始出现差异。
-
排查步骤3:算法细节
。
- 哈希算法 :你确定是MD5吗?会不会是SHA-1、SHA-256?哈希结果是否截取了部分字符(如前16位)?
-
Base64变种
:标准的Base64编码结果包含
+/和末尾的=。有些JS实现会进行URL安全的替换(+->-,/->_)并去掉填充=。Python的base64.urlsafe_b64encode可以处理前者,但需要手动去掉=。 - 自定义编码 :可能根本不是标准Base64,而是自定义的字符映射表。
6.2 动态密钥获取失败
- 问题 :模拟获取密钥的请求返回错误或空数据。
-
排查
:
-
检查该请求是否需要特定的Cookie或Header(如
X-Requested-With: XMLHttpRequest)。 - 检查请求方法(GET/POST)和参数是否正确。
- 该密钥接口本身可能也有防爬措施,需要先完成某种验证。
-
检查该请求是否需要特定的Cookie或Header(如
6.3 请求返回403/401错误
即使Token生成正确,请求也可能被拒绝。
-
Token过期
:Token很可能有时效性。检查
payload中是否包含时间戳timestamp,服务器端会验证这个时间戳是否在允许的时间窗口内(如±5分钟)。确保你的系统时间准确,并且时间戳格式(秒还是毫秒)与服务器一致。 -
Nonce重复
:随机数
nonce可能被服务器记录,短时间内重复使用会被拒绝。确保每次请求都生成新的随机数。 - 请求签名范围 :有些签名算法不仅对参数签名,还会把HTTP方法、请求路径甚至部分请求头也纳入签名计算。你需要仔细阅读JS代码,看签名原始串是如何组装的。
6.4 execjs执行环境问题
-
execjs._exceptions.ProgramError:通常是JS代码本身在执行环境中有语法错误或缺少依赖。- 将你的JS代码先在Node.js命令行或浏览器Console中单独运行测试,确保无误。
- 检查是否遗漏了某些全局变量或函数的定义。可能需要将依赖的JS库(如CryptoJS的完整源码)一起打包进执行上下文。
-
性能问题
:
execjs调用JS的速度较慢。如果每个请求都需要调用,会成为性能瓶颈。尽量将算法完全移植到Python,或者将execjs上下文创建一次并重复使用。
6.5 代码混淆与反调试
高级的网站会使用代码混淆工具(如
obfuscator
)和反调试技术。
-
反调试
:在开发者工具打开时,网站可能会检测到并跳转或卡死。可以尝试使用
setTimeout断点、debugger;语句绕过,或者使用无头浏览器工具如Puppeteer进行调试。 -
代码混淆
:变量名变成
a、b、c,逻辑被分割成无数个小函数。这需要极大的耐心。策略是:- 先格式化代码。
-
搜索关键常量字符串(如加密算法名
AES、encrypt,或API端点/api/getData)。 - 找到引用这些字符串的函数,逐步理清逻辑。动态调试(断点)在此刻比静态阅读更有效,因为你可以直接观察真实的数据流。
整个逆向分析的过程,就像一场耐心的狩猎。你需要细心观察、大胆假设、小心验证。每一次成功破解,不仅是为了拿到数据,更是对自身技术能力的一次锤炼。最后再次强调,技术是一把双刃剑,请在法律和道德允许的范围内,合理、克制地使用这些知识。控制你的爬虫速度,尊重网站的服务器压力,这才是技术人应有的素养。

3204

被折叠的 条评论
为什么被折叠?



