逆向实战模板

收集常见的逆向分析场景和完整的 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 中 strbytes 是两种类型,混用会抛出 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 秒内有效),调试过慢或直接重放旧参数时会失效。

解决: 用自己的实现实时生成时间戳,而非复制历史请求中的时间戳。还原完成后,以当前时间戳生成参数并发起请求。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog