逆向实战模板
收集常见的逆向分析场景和完整的 Python 还原模板,可直接复制修改使用。 目标:请求中有一个 sign 参数,定位后发现是 AES-CBC 加密结果。 目标:请求头中有 X-Signature,分析为 HMAC-SHA256。 目标:登录时密码被 RSA 加密,公钥从页面 JS 中提取。 目标:每次登录前先请求 /api/getPublicKey 获取 RSA 公钥。 目标:每次请求的 AES key 由时间戳和固定字符串 HMAC 生成。 先用已知真实请求包 100% 验证还原结果,再写采集逻辑:不要"感觉应该对"就接入爬虫;应从 DevTools
官方文档:https://pycryptodome.readthedocs.io/en/latest/ | https://pypi.org/project/httpx/
适用场景:JS 逆向 Python 还原模板(2026-05-07 整理)
收集常见的逆向分析场景和完整的 Python 还原模板,可直接复制修改使用。
场景一:标准 AES-CBC 签名参数还原
目标:请求中有一个 sign 参数,定位后发现是 AES-CBC 加密结果。
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
import json
import time
import hashlib
import requests
# 从源码中提取的 key 和 iv
AES_KEY = '1234567890abcdef' # 16 字节
AES_IV = 'abcdef1234567890' # 16 字节
def aes_encrypt(data: str) -> str:
cipher = AES.new(AES_KEY.encode(), AES.MODE_CBC, AES_IV.encode())
encrypted = cipher.encrypt(pad(data.encode(), 16))
return base64.b64encode(encrypted).decode()
def build_params(user_data: dict) -> dict:
ts = str(int(time.time() * 1000))
payload = json.dumps({**user_data, 'ts': ts}, separators=(',', ':'))
sign = aes_encrypt(payload)
return {'data': payload, 'sign': sign, 'ts': ts}
# 发起请求
session = requests.Session()
session.headers.update({
'User-Agent': 'Mozilla/5.0 ...',
'Content-Type': 'application/json'
})
params = build_params({'uid': '12345', 'action': 'login'})
resp = session.post('https://api.example.com/login', json=params)
print(resp.json())
场景二:HMAC-SHA256 接口签名
目标:请求头中有 X-Signature,分析为 HMAC-SHA256。
import hmac
import hashlib
import time
import json
import requests
SECRET_KEY = 'your_secret_key_here'
def sign_request(method: str, path: str, body: dict) -> dict:
ts = str(int(time.time()))
body_str = json.dumps(body, sort_keys=True, separators=(',', ':'))
# 签名原文:METHOD + \n + path + \n + timestamp + \n + body
raw = f'{method}\n{path}\n{ts}\n{body_str}'
sig = hmac.new(SECRET_KEY.encode(), raw.encode(), hashlib.sha256).hexdigest()
return {
'X-Timestamp': ts,
'X-Signature': sig,
'Content-Type': 'application/json'
}
path = '/api/v1/data'
body = {'page': 1, 'size': 20}
headers = sign_request('POST', path, body)
resp = requests.post(f'https://api.example.com{path}', json=body, headers=headers)
场景三:RSA 加密密码
目标:登录时密码被 RSA 加密,公钥从页面 JS 中提取。
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
import base64
import requests
# 从源码中提取的公钥(或从接口动态获取)
PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"""
def rsa_encrypt_password(password: str) -> str:
key = RSA.import_key(PUBLIC_KEY_PEM)
cipher = PKCS1_v1_5.new(key)
return base64.b64encode(cipher.encrypt(password.encode())).decode()
def login(username: str, password: str) -> dict:
encrypted_pwd = rsa_encrypt_password(password)
payload = {'username': username, 'password': encrypted_pwd}
resp = requests.post('https://example.com/login', json=payload)
return resp.json()
场景四:动态获取公钥再加密
目标:每次登录前先请求 /api/getPublicKey 获取 RSA 公钥。
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
import base64
import requests
session = requests.Session()
session.headers['User-Agent'] = 'Mozilla/5.0 ...'
def get_public_key() -> str:
resp = session.get('https://example.com/api/getPublicKey')
return resp.json()['data']['publicKey']
def login(username: str, password: str) -> dict:
pub_key_pem = get_public_key()
key = RSA.import_key(pub_key_pem)
cipher = PKCS1_v1_5.new(key)
encrypted_pwd = base64.b64encode(cipher.encrypt(password.encode())).decode()
return session.post('https://example.com/login', json={
'username': username,
'password': encrypted_pwd
}).json()
场景五:AES key 由 HMAC 动态生成
目标:每次请求的 AES key 由时间戳和固定字符串 HMAC 生成。
import hmac
import hashlib
import time
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
HMAC_SECRET = 'fixed_secret_string'
def generate_aes_key(timestamp: str) -> bytes:
"""用时间戳生成 16 字节 AES key"""
full_key = hmac.new(HMAC_SECRET.encode(), timestamp.encode(), hashlib.sha256).hexdigest()
return full_key[:32].encode() # 取前 32 字节作为 AES-256 key
def encrypt_payload(data: dict) -> dict:
ts = str(int(time.time()))
key = generate_aes_key(ts)
iv = key[:16] # 用 key 的前 16 字节作为 IV
cipher = AES.new(key, AES.MODE_CBC, iv)
ct = base64.b64encode(cipher.encrypt(pad(
__import__('json').dumps(data).encode(), 16
))).decode()
return {'ts': ts, 'data': ct}
场景六:多重加密(MD5 签名 + AES 加密请求体)
import hashlib
import json
import time
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import requests
APP_KEY = 'app_key_from_js'
AES_KEY = '1234567890abcdef'
AES_IV = '1234567890abcdef'
def md5(s: str) -> str:
return hashlib.md5(s.encode()).hexdigest()
def aes_encrypt(data: str) -> str:
cipher = AES.new(AES_KEY.encode(), AES.MODE_CBC, AES_IV.encode())
return base64.b64encode(cipher.encrypt(pad(data.encode(), 16))).decode()
def build_request(api: str, params: dict) -> dict:
ts = str(int(time.time()))
nonce = 'random_string_16'
body = json.dumps(params, separators=(',', ':'))
# 1. 先加密请求体
enc_body = aes_encrypt(body)
# 2. 再生成签名:md5(appKey + ts + nonce + encryptedBody)
raw_sign = APP_KEY + ts + nonce + enc_body
sign = md5(raw_sign).upper()
return {
'url': f'https://api.example.com{api}',
'headers': {'X-App-Key': APP_KEY, 'X-Timestamp': ts,
'X-Nonce': nonce, 'X-Sign': sign},
'body': {'data': enc_body}
}
通用工具函数集
import hashlib, hmac, base64, json, time
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
# ── 哈希系列 ─────────────────────────────────────────────────
def md5(s, encoding='utf-8'):
return hashlib.md5(s.encode(encoding)).hexdigest()
def sha256(s, encoding='utf-8'):
return hashlib.sha256(s.encode(encoding)).hexdigest()
def hmac_sha256(key, data, output='hex'):
h = hmac.new(key.encode(), data.encode(), hashlib.sha256)
return h.hexdigest() if output == 'hex' else base64.b64encode(h.digest()).decode()
# ── AES ──────────────────────────────────────────────────────
def aes_cbc_encrypt(data, key, iv, output='base64'):
cipher = AES.new(key.encode(), AES.MODE_CBC, iv.encode())
ct = cipher.encrypt(pad(data.encode(), 16))
return base64.b64encode(ct).decode() if output == 'base64' else ct.hex()
def aes_cbc_decrypt(data, key, iv, input_fmt='base64'):
raw = base64.b64decode(data) if input_fmt == 'base64' else bytes.fromhex(data)
cipher = AES.new(key.encode(), AES.MODE_CBC, iv.encode())
return unpad(cipher.decrypt(raw), 16).decode()
# ── 编码转换 ─────────────────────────────────────────────────
def to_base64(s):
return base64.b64encode(s.encode()).decode()
def from_base64(s):
return base64.b64decode(s).decode()
def to_hex(s):
return s.encode().hex()
def from_hex(s):
return bytes.fromhex(s).decode()
# ── 时间戳 ───────────────────────────────────────────────────
def ts_sec(): return int(time.time())
def ts_ms(): return int(time.time() * 1000)
def ts_str(): return str(ts_sec())
# ── 签名模板 ─────────────────────────────────────────────────
def build_sign(params: dict, secret: str, method='md5') -> str:
"""将字典按 key 排序拼接后签名"""
raw = '&'.join(f'{k}={v}' for k, v in sorted(params.items()))
if method == 'md5':
return md5(raw + secret)
elif method == 'sha256':
return sha256(raw + secret)
elif method == 'hmac_sha256':
return hmac_sha256(secret, raw)
调试技巧:验证还原结果
# 用已知的请求包验证实现是否正确
# 1. 从 DevTools Network 面板复制真实请求参数
# 2. 复制真实的加密结果(sign/token 等)
# 3. 用自己的实现生成,对比是否一致
known_plain = 'test_data_12345'
known_key = '1234567890abcdef'
known_iv = 'abcdef1234567890'
known_ct = 'xxxx' # 从真实请求中复制的密文
my_ct = aes_cbc_encrypt(known_plain, known_key, known_iv)
assert my_ct == known_ct, f'加密结果不匹配!\n期望: {known_ct}\n实际: {my_ct}'
print('验证通过')
最佳实践
先用已知真实请求包 100% 验证还原结果,再写采集逻辑:不要"感觉应该对"就接入爬虫;应从 DevTools Network 面板复制真实参数,用自己的实现生成,确认与真实密文完全一致才继续。
在 JS 断点处直接打印中间值,不要依赖推断:key、iv、明文、密文的具体值和编码格式(字符串 vs Hex vs Base64)必须在断点处确认,猜测会浪费大量时间。
处理编码时统一用 bytes 操作,避免字符串/bytes 混用:Python 中 str 与 bytes 是两种类型,混用会抛出 TypeError。确定好输入/输出的编码方式(UTF-8、Hex、Base64)后,在函数入口统一转换。
def normalize_key(key_input):
if isinstance(key_input, str):
return key_input.encode('utf-8')
return key_input # 已是 bytes,直接返回
将还原函数封装为独立模块,与采集代码分离:加密还原逻辑(sign.py)与请求发送逻辑(spider.py)分开,便于单独测试和复用,也便于目标网站更新时快速定位修改点。
常见陷阱
陷阱:key/iv 长度不正确导致 ValueError: AES key must be either 16, 24, or 32 bytes long
现象: Python AES 初始化时抛出 ValueError,提示 key 长度错误。
原因: AES 要求 key 为 16/24/32 字节(对应 128/192/256 bit)。JS 中若 key 是字符串形式(如 Unicode 字符),其字节长度可能与字符数不同;或 JS 中做了截断处理但 Python 端未复现。
解决: 在 JS 断点处打印 key.sigBytes(CryptoJS)或 Buffer.byteLength(key) 确认实际长度,Python 端对应截断或补齐。
raw_key = "my_key_string"
key = raw_key.encode('utf-8')[:16] # 截断到 16 字节
陷阱:Base64 解码前未处理 URL-safe 变体
现象: base64.b64decode(ciphertext) 抛出 binascii.Error: Invalid base64-encoded string,或解码结果错误。
原因: 部分网站使用 URL-safe Base64(- 替代 +,_ 替代 /),标准 b64decode 不能直接处理。
解决: 使用 base64.urlsafe_b64decode,或替换字符后再解码。
import base64
ct = ciphertext.replace('-', '+').replace('_', '/')
decoded = base64.b64decode(ct + '==') # 末尾补齐
陷阱:时间戳签名参数有效期短,复现时已过期
现象: 用真实请求包中的参数直接重放,服务端返回"签名过期"或"请求无效"。
原因: 签名通常包含时间戳,服务端校验时间差(通常 60 秒内有效),调试过慢或直接重放旧参数时会失效。
解决: 用自己的实现实时生成时间戳,而非复制历史请求中的时间戳。还原完成后,以当前时间戳生成参数并发起请求。