如何在Python爬虫中模拟App的Header签名算法_使用PyCryptodome库重现加密逻辑
App端Header签名失效是因为其为基于时间戳、随机数、请求体、密钥等动态参数实时计算的HMAC-SHA256或AES-CBC结果,并非固定字符串;硬编码抓包值必然失败,需通过Frida Hook还原真实key/data/iv输入并用PyCryptodome精准复现拼接与加解密逻辑。
为什么直接复制App的Header签名会失效
App端签名通常不是固定字符串,而是基于时间戳、随机数、请求体、密钥等动态参数实时计算的HMAC-SHA256或AES-CBC结果。抓包看到的X-Sign或Signature字段每次都不一样,硬编码必然失败。
关键点在于:签名逻辑往往藏在so库或混淆后的Java/Kotlin代码里,但多数情况下,核心加密流程仍依赖标准算法——PyCryptodome能覆盖绝大多数场景,前提是还原出正确的输入拼接方式和密钥处理逻辑。
- 常见错误现象:
Signature verification failed或401 Unauthorized,即使时间戳、nonce、body都对得上 - 根本原因:密钥被二次处理(如base64解码后再取前16字节)、原始数据未按约定排序/拼接/编码(比如要求UTF-8 bytes但用了str)、缺少预置IV或padding方式不匹配
- 调试建议:用Frida Hook住签名函数,打印出实际传入的
key、data、iv三元组,这是最可靠的输入依据
用PyCryptodome实现HMAC-SHA256签名(最常见类型)
多数App用HMAC-SHA256生成签名,密钥通常是硬编码在APK里的字符串,但可能经过base64.b64decode()或bytes.fromhex()转换。
注意:HMAC不要求IV,但必须严格对齐密钥长度(SHA256推荐32字节密钥)。若原始密钥不足32字节,有些App会用PKCS#7 padding补足,更多情况是直接截断或重复填充——需逆向确认。
from hashlib import sha256 from hmac import HMAC假设逆向得到原始密钥为 base64 字符串,且需解码
raw_key = "dGhpcyBpcyBhIHNlY3JldA==" key = base64.b64decode(raw_key)
拼接规则示例:method + \n + path + \n + timestamp + \n + nonce + \n + body_md5
message = f"POST\n/api/v1/user\n1717023456\nabc123\n{md5(body.encode()).hexdigest()}".encode()
Python 3.14.3下载微软官方的 Python 扩展,是 VS Code 安装量最高的扩展(209M+)。集成 IntelliSense(通过 Pylance)、调试(通过 Python Debugger)、代码检查、格式化、重构和单元测试等功能。支持 Jupyter Notebook、虚拟环境管理和多 Python 版本切换。
signature = HMAC(key, message, sha256).hexdigest() headers["X-Sign"] = signature
- 务必检查App是否对
body做了MD5(或SHA1)哈希后再参与拼接;有些只取body非空时的原始bytes - 时间戳单位要一致(秒 or 毫秒),且服务端常有5分钟容错窗口,本地时间偏差过大会直接拒签
- Python 3.9+ 的
HMAC默认接受bytes,传str会报TypeError: key: expected bytes or bytearray, but got 'str'
AES-CBC签名(带IV的加密型签名)
少数App把拼接后的字符串用AES-CBC加密再hex,作为签名。此时必须还原出IV——它可能是固定值(如b"0102030405060708"),也可能是从请求头或响应中动态提取的字段(如X-Nonce前8字节)。
PyCryptodome要求明文长度是16字节整数倍,PKCS#7是最常用padding方式。别用pad(data, 16)这种模糊写法,必须显式调用PKCS7模块或手写补位逻辑。
from Crypto.Cipher import AES from Crypto.Util.Padding import padkey = b"your-32-byte-key-here-123456789012" iv = b"0102030405060708" # 必须16字节 data = "timestamp=1717023456&nonce=abc123&body={}".format(body).encode()
cipher = AES.new(key, AES.MODE_CBC, iv) encrypted = cipher.encrypt(pad(data, AES.block_size)) signature = encrypted.hex() headers["X-Sign"] = signature
- 如果服务端返回
Invalid IV或解密乱码,优先怀疑IV来源不对;可用Wireshark对比真实App请求中的X-IV或类似字段 - 某些App会把AES输出再做一次base64或反转字节序(
encrypted[::-1]),这类变形必须靠Hook日志确认 - PyCryptodome安装命令是
pip install pycryptodome,不是pycrypto(已废弃且不兼容)
签名验证失败时的快速定位路径
当签名始终不通过,不要反复改密钥长度或算法——先确认输入是否完全一致。真实App签名函数的输入,永远比你从代码里猜出来的更“脏”。
- 用Frida Hook
javax.crypto.Mac.getInstance("HmacSHA256")或javax.crypto.Cipher.getInstance("AES/CBC/PKCS7Padding"),打印doFinal()前的input和key - 对比Python与App端的
message十六进制dump(message.hex()),一个换行符、一个空格、一个URL编码差异都会导致签名不同 - 服务端有时会校验
User-Agent或X-App-Version,这些虽不参与签名,但缺失会导致整个请求被拦截,误判为签名错误
逆向签名逻辑最难的从来不是算法本身,而是搞清那几行不起眼的字符串拼接和编码转换——它们藏在层层嵌套的工具类里,且没有日志输出。
