> ## 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/qian-ming-suan-fa-huan-yuan/
- Published: 2026-08-28T14:35:12.000Z
- Updated: 2026-08-28T14:58:18.000Z
- Description: 最后更新：2026-03-29 Web/App 接口通常对请求参数做签名（sign），防止参数篡改和重放攻击。常见形式： 逆向目标：找到 sign 的计算逻辑，使用 Python 自行构造相同的 sign。 在 Chrome DevTools 的 Sources 面板，Ctrl+Shift+F 全局搜索： 在 Sources → XHR/fetch Breakpoints 添加请求 URL 关键词，发起请求时自动断到 XHR 发送处，从调用栈往上追溯到 sign 赋值。 对于 App 的 Native 签名，用 Frida 直接 Hook 签名方法： 最
- Author: yellowdog
- Tags: js逆向, 技巧步骤

> 官方文档：<https://developer.mozilla.org/zh-CN/docs/Web/API/SubtleCrypto>  
> 适用场景：分析 Web/App 请求签名的完整还原流程，从参数提取到 Python 复现

最后更新：2026-03-29

---

## 1\. 什么是签名

Web/App 接口通常对请求参数做**签名**（sign），防止参数篡改和重放攻击。常见形式：

```
# URL 或请求体中的签名参数
GET /api/goods?id=100&ts=1711680000&sign=a3f4b2c1...
POST /api/order  {"data": "...", "sign": "...", "nonce": "xxx", "ts": 1711680000}

```

逆向目标：找到 sign 的计算逻辑，使用 Python 自行构造相同的 sign。

---

## 2\. 签名定位流程

```
1. 抓包       → 找到含 sign/token/signature 等参数的请求
2. 定位代码   → 四种方法（见下）
3. 分析逻辑   → 确定参与签名的字段、排序规则、加密算法
4. Python 还原 → 用 Python 复现签名
5. 验证       → 用还原的签名发请求，验证正确性

```

---

## 3\. 定位签名代码的四种方法

### 方法一：全局搜索关键词（最常用）

在 Chrome DevTools 的 Sources 面板，`Ctrl+Shift+F` 全局搜索：

```
sign        signature       _sign
token       X-Token         Authorization
nonce       timestamp       appkey

```

### 方法二：XHR 断点

在 `Sources → XHR/fetch Breakpoints` 添加请求 URL 关键词，发起请求时自动断到 XHR 发送处，从调用栈往上追溯到 sign 赋值。

### 方法三：请求头 / 请求体 Hook

```javascript
// Hook XMLHttpRequest.setRequestHeader（定位自定义请求头的来源）
var _setRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.setRequestHeader = function(header, value) {
    if (header.toLowerCase().includes("sign") || header === "X-Token") {
        console.log("[XHR Header]", header, "=", value);
        debugger;  // 断点，查看调用栈
    }
    return _setRequestHeader.apply(this, arguments);
};

```

```javascript
// Hook JSON.stringify（定位请求体签名的构造位置）
var _stringify = JSON.stringify;
JSON.stringify = function(obj) {
    var result = _stringify.apply(this, arguments);
    if (result && result.includes("sign")) {
        console.log("[JSON.stringify]", result);
        debugger;
    }
    return result;
};

```

### 方法四：Frida 主动调用

对于 App 的 Native 签名，用 Frida 直接 Hook 签名方法：

```javascript
Java.perform(function () {
    var SignUtil = Java.use("com.example.SignUtil");
    SignUtil.sign.implementation = function (params) {
        var result = this.sign(params);
        console.log("[sign] params:", JSON.stringify(params));
        console.log("[sign] result:", result);
        return result;
    };
});

```

---

## 4\. 常见签名模式

### 模式一：参数排序 + MD5

最常见的简单签名：将所有参数按 key 排序后拼接，加上 `appSecret`，取 MD5：

```javascript
// JS 逆向出的签名逻辑
function sign(params) {
    var sortedKeys = Object.keys(params).sort();
    var str = sortedKeys.map(k => k + "=" + params[k]).join("&");
    str += "&key=SECRET_KEY_HERE";
    return md5(str);
}

```

```python
# Python 还原
import hashlib
from urllib.parse import urlencode

def sign(params: dict, secret_key: str) -> str:
    sorted_params = dict(sorted(params.items()))
    query = urlencode(sorted_params) + "&key=" + secret_key
    return hashlib.md5(query.encode()).hexdigest()

```

### 模式二：HMAC-SHA256 + Base64

更安全的签名方式：

```python
import hmac
import hashlib
import base64
import time
import json

def sign(params: dict, secret: str) -> str:
    timestamp = str(int(time.time()))
    sorted_keys = sorted(params.keys())
    sign_str = "".join(f"{k}{params[k]}" for k in sorted_keys) + timestamp

    mac = hmac.new(secret.encode(), sign_str.encode(), hashlib.sha256)
    return base64.b64encode(mac.digest()).decode()

```

### 模式三：拼接固定盐值 + SHA1

```python
import hashlib

def sign(data: str, salt: str = "fixed_salt_value") -> str:
    raw = data + salt
    return hashlib.sha1(raw.encode()).hexdigest()

```

### 模式四：RSA 私钥签名

App 内置 RSA 私钥，用私钥对请求数据签名：

```python
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
import base64

# 从 App 中提取的私钥（或逆向出来的）
PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----"""

def sign(data: str) -> str:
    key = RSA.import_key(PRIVATE_KEY)
    h = SHA256.new(data.encode())
    signature = pkcs1_15.new(key).sign(h)
    return base64.b64encode(signature).decode()

```

### 模式五：时间戳 + Nonce + HMAC（OAuth 风格）

```python
import hmac
import hashlib
import time
import uuid

def build_signed_headers(url: str, body: str, app_id: str, app_secret: str) -> dict:
    timestamp = str(int(time.time()))
    nonce = uuid.uuid4().hex

    sign_str = "\n".join([timestamp, nonce, url, body])
    signature = hmac.new(
        app_secret.encode(),
        sign_str.encode(),
        hashlib.sha256
    ).hexdigest()

    return {
        "X-App-Id": app_id,
        "X-Timestamp": timestamp,
        "X-Nonce": nonce,
        "X-Signature": signature,
    }

```

---

## 5\. 分析技巧

### 确认参与签名的字段

1. 改变一个参数的值，观察 sign 是否变化 → 确认哪些字段参与签名
2. 去掉某个参数，观察 sign 是否变化 → 确认可选字段
3. 时间戳参与签名时，sign 每次请求都不同

### 识别加密算法

| 现象                    | 可能的算法               |
| --------------------- | ------------------- |
| sign 固定 32 位十六进制      | MD5                 |
| sign 固定 40 位十六进制      | SHA1                |
| sign 固定 64 位十六进制      | SHA256              |
| sign 是 Base64 格式      | HMAC + Base64 / RSA |
| sign 每次相同（无时间戳参与）     | 纯 MD5/SHA，无随机性      |
| sign 每次不同（含时间戳/nonce） | 带随机因子的 HMAC/RSA     |

参考：加密算法/魔法数字速查

### 定位密钥（secret key）

密钥常见藏身位置：

1. JS 文件中的字符串常量（全局搜索 `secret`、`key`、`salt`）
2. 接口返回值中（有些应用从服务端下发 key）
3. App 的 SharedPreferences / 本地配置文件
4. so 库（需要 Native 逆向）
5. 根据设备信息动态生成（如 `md5(deviceId + appVersion)`）

---

## 6\. 完整 Python 还原模板

```python
"""
接口签名还原模板
目标接口：GET /api/goods/list?category=1&page=1&sign=xxx
签名算法：MD5(sorted_params + &key=SECRET)
"""
import hashlib
import time
import requests
from urllib.parse import urlencode

SECRET_KEY = "your_secret_key"
BASE_URL = "https://api.example.com"

session = requests.Session()
session.headers.update({
    "User-Agent": "Mozilla/5.0 ...",
    "Content-Type": "application/json",
})

def sign(params: dict) -> str:
    """构造签名"""
    filtered = {k: v for k, v in params.items() if k != "sign"}
    sorted_params = dict(sorted(filtered.items()))
    query = urlencode(sorted_params) + "&key=" + SECRET_KEY
    return hashlib.md5(query.encode()).hexdigest().upper()

def build_params(extra: dict = None) -> dict:
    """构造带公共参数的请求参数"""
    params = {
        "ts": str(int(time.time())),
        "v": "1.0",
        "appid": "com.example.app",
    }
    if extra:
        params.update(extra)
    params["sign"] = sign(params)
    return params

def get_goods_list(category: int, page: int = 1) -> dict:
    params = build_params({"category": category, "page": page})
    r = session.get(f"{BASE_URL}/api/goods/list", params=params)
    r.raise_for_status()
    return r.json()

if __name__ == "__main__":
    result = get_goods_list(category=1, page=1)
    print(result)

```

---

## 7\. 时间戳防重放对抗

有些接口校验时间戳，超过一定时间（如 5 分钟）就拒绝请求：

```python
import time
import ntplib

def get_accurate_timestamp() -> int:
    """获取精确时间戳（防止本地时钟偏差）"""
    try:
        c = ntplib.NTPClient()
        response = c.request("pool.ntp.org", version=3)
        return int(response.tx_time)
    except Exception:
        return int(time.time())

```

如果接口要求时间戳是服务器时间，先请求一个公开接口获取服务器时间：

```python
def get_server_timestamp(session: requests.Session) -> int:
    r = session.get("https://api.example.com/ping")
    # 有些接口在响应头中返回服务器时间
    server_time = r.headers.get("X-Server-Time")
    if server_time:
        return int(server_time)
    return int(time.time())

```

---

## 8\. 踩坑与注意事项

### 字段顺序影响签名

部分签名要求参数按特定顺序（如按首字母升序、按字段名 ASCII 码排序、按固定顺序），排序规则错误会导致签名不一致。

用逆向出的 JS 代码验证：

```javascript
// 在浏览器控制台中手动调用逆向出的签名函数，比对结果
sign({category: "1", page: "1", ts: "1711680000"})
// 与 Python 输出对比

```

### 字段类型（数字还是字符串）

`sign({page: 1})` 和 `sign({page: "1"})` 结果不同。抓包时注意参数是数字类型还是字符串类型。

### URL 编码问题

`urlencode` 会对特殊字符编码，但有些签名要求对值不编码或使用特定的编码方式：

```python
# 标准 urlencode
urlencode({"name": "张三"})  # name=%E5%BC%A0%E4%B8%89

# 不编码，直接拼接
"&".join(f"{k}={v}" for k, v in sorted_params.items())  # name=张三

```

---

## 最佳实践

**先用对比法验证理解**：固定一组已知的请求参数和对应签名（从抓包获取），在每个还原步骤后对比中间值，确认每一步都正确，而不是只在最后对比最终签名。

**逐步去掉动态变量定位签名边界**：用固定值替换时间戳、nonce、随机数（如 `timestamp=0`, `nonce='test'`），对比 Python 复现结果与浏览器 Hook 日志，排查是动态变量问题还是算法问题。

**参数排序方式要完全复现**：ASCII 排序、Unicode 排序、按 key 字母序、按 key 长度排序各有不同；JS 对象的 `Object.keys()` 顺序是插入顺序而非字母序，Python `sorted()` 默认 ASCII 序，要对齐。

**特殊字符编码细节决定签名是否成功**：URL 编码大小写（`%2F` vs `%2f`）、`+` 是否编码、中文用 UTF-8 还是 GBK，这些细节常常是最后一步卡住的原因；用 Wireshark 抓包和 Hook 日志双重验证。

**将还原逻辑封装为 Python 函数并写单元测试**：以已知的请求参数和签名为测试用例，`assert sign(params) == expected`，后续签名算法升级时能快速定位变更点。

---

## 常见陷阱

### 陷阱：参数排序后签名仍不对

**现象：** 按字母排序参数后签名结果与目标不符。  
**原因：** 排序规则可能是按 key 的 ASCII 码排序（区分大小写，大写字母排在小写前），而 Python `sorted()` 对字符串默认相同行为，但 JS 的 `localeCompare` 可能返回不同结果。  
**解决：** 在 JS DevTools Console 中直接打印排序后的参数数组对比，确认排序规则；Python 中 `sorted(params.items(), key=lambda x: x[0])` 与 JS `[...obj.keys()].sort()` 对比结果。

### 陷阱：时间戳精度不对导致签名失败

**现象：** 每次请求签名都不对，但算法完全相同。  
**原因：** 目标使用毫秒级时间戳（13位），而 Python `int(time.time())` 是秒级（10位）；或 JS 的 `Date.now()` 返回毫秒，签名时被截断为秒级。  
**解决：** Python 毫秒时间戳：`int(time.time() * 1000)`；对比 Hook 日志确认时间戳格式（位数）。

### 陷阱：Base64 编码方式与预期不符

**现象：** 签名最后一步是 Base64，但 Python 输出比目标多 `=` 或字符不同（`+` vs `-`）。  
**原因：** URL-safe Base64 用 `-` 和 `_` 替换 `+` 和 `/`；部分签名去掉末尾 `=` padding。  
**解决：** 检查 JS 中的 Base64 输出是否做了字符替换；Python 用 `base64.urlsafe_b64encode(data).decode().rstrip('=')` 对应处理。

---

## 参见

[js逆向调试技巧](https://blog.vercanti.com/js-ni-xiang-diao-shi-ji-qiao/)  
[Frida基础](https://blog.vercanti.com/frida-ji-chu-zhi-nan/)  
[HMAC](https://blog.vercanti.com/hmac/)  
[MD5](https://blog.vercanti.com/md5/)  
[hook](https://blog.vercanti.com/hook-dai-ma-duan/)