> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# 逆向实战模板
- URL: https://blog.vercanti.com/ni-xiang-shi-zhan-mo-ban/
- Published: 2026-08-28T14:35:06.000Z
- Updated: 2026-08-28T14:58:03.000Z
- Description: 收集常见的逆向分析场景和完整的 Python 还原模板，可直接复制修改使用。 目标：请求中有一个 sign 参数，定位后发现是 AES-CBC 加密结果。 目标：请求头中有 X-Signature，分析为 HMAC-SHA256。 目标：登录时密码被 RSA 加密，公钥从页面 JS 中提取。 目标：每次登录前先请求 /api/getPublicKey 获取 RSA 公钥。 目标：每次请求的 AES key 由时间戳和固定字符串 HMAC 生成。 先用已知真实请求包 100% 验证还原结果，再写采集逻辑：不要"感觉应该对"就接入爬虫；应从 DevTools
- Author: yellowdog
- Tags: js逆向, 加密算法

> 官方文档：<https://pycryptodome.readthedocs.io/en/latest/> | <https://pypi.org/project/httpx/>  
> 适用场景：JS 逆向 Python 还原模板（2026-05-07 整理）

收集常见的逆向分析场景和完整的 Python 还原模板，可直接复制修改使用。

---

## 场景一：标准 AES-CBC 签名参数还原

目标：请求中有一个 `sign` 参数，定位后发现是 AES-CBC 加密结果。

```python
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。

```python
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 中提取。

```python
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 公钥。

```python
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 生成。

```python
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 加密请求体）

```python
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}
    }

```

---

## 通用工具函数集

```python
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)

```

---

## 调试技巧：验证还原结果

```python
# 用已知的请求包验证实现是否正确
# 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）后，在函数入口统一转换。

```python
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 端对应截断或补齐。

```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`，或替换字符后再解码。

```python
import base64
ct = ciphertext.replace('-', '+').replace('_', '/')
decoded = base64.b64decode(ct + '==')  # 末尾补齐

```

### 陷阱：时间戳签名参数有效期短，复现时已过期

**现象：** 用真实请求包中的参数直接重放，服务端返回"签名过期"或"请求无效"。

**原因：** 签名通常包含时间戳，服务端校验时间差（通常 60 秒内有效），调试过慢或直接重放旧参数时会失效。

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

---

## 参见

- [AES](https://blog.vercanti.com/aes/)
- [DES\_3DES](https://blog.vercanti.com/des-3des/)
- [MD5](https://blog.vercanti.com/md5/)
- [SHA](https://blog.vercanti.com/sha-xi-lie/)
- [RSA](https://blog.vercanti.com/rsa/)
- [HMAC](https://blog.vercanti.com/hmac/)
- [Base64与编码](https://blog.vercanti.com/base64-yu-bian-ma/)
- [魔法数字速查](https://blog.vercanti.com/jia-mi-suan-fa-mo-fa-shu-zi-su-cha/)