> ## 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.

# JS 逆向入门指南
- URL: https://blog.vercanti.com/js-ni-xiang-ru-men-zhi-nan/
- Published: 2026-08-28T14:34:58.000Z
- Updated: 2026-08-28T14:57:44.000Z
- Description: 1. 逆向的核心思路(#1-%E9%80%86%E5%90%91%E7%9A%84%E6%A0%B8%E5%BF%83%E6%80%9D%E8%B7%AF) 2. 第一步：抓包定位目标参数(#2-%E7%AC%AC%E4%B8%80%E6%AD%A5%EF%BC%9A%E6%8A%93%E5%8C%85%E5%AE%9A%E4%BD%8D%E7%9B%AE%E6%A0%87%E5%8F%82%E6%95%B0) 3. 第二步：找到签名来源（断点 + Hook）(#3-%E7%AC%AC%E4%BA%8C%E6%AD%A5%EF%BC%9A%E6%89%B
- Author: yellowdog
- Tags: js逆向, douyin

> 官方文档：<https://developer.chrome.com/docs/devtools/>

> 以抖音 `a_bogus` 逆向为实战案例，从零讲解思路、工具与 Hook 片段

---

## 目录

1. [逆向的核心思路](#1-%E9%80%86%E5%90%91%E7%9A%84%E6%A0%B8%E5%BF%83%E6%80%9D%E8%B7%AF)
2. [第一步：抓包定位目标参数](#2-%E7%AC%AC%E4%B8%80%E6%AD%A5%EF%BC%9A%E6%8A%93%E5%8C%85%E5%AE%9A%E4%BD%8D%E7%9B%AE%E6%A0%87%E5%8F%82%E6%95%B0)
3. [第二步：找到签名来源（断点 + Hook）](#3-%E7%AC%AC%E4%BA%8C%E6%AD%A5%EF%BC%9A%E6%89%BE%E5%88%B0%E7%AD%BE%E5%90%8D%E6%9D%A5%E6%BA%90%EF%BC%88%E6%96%AD%E7%82%B9-hook%EF%BC%89)
4. [第三步：在浏览器环境里验证](#4-%E7%AC%AC%E4%B8%89%E6%AD%A5%EF%BC%9A%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E7%8E%AF%E5%A2%83%E9%87%8C%E9%AA%8C%E8%AF%81)
5. [第四步：识别加密算法](#5-%E7%AC%AC%E5%9B%9B%E6%AD%A5%EF%BC%9A%E8%AF%86%E5%88%AB%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95)
6. [第五步：在 Node.js 中还原签名](#6-%E7%AC%AC%E4%BA%94%E6%AD%A5%EF%BC%9A%E5%9C%A8-nodejs-%E4%B8%AD%E8%BF%98%E5%8E%9F%E7%AD%BE%E5%90%8D)
7. [第六步：插桩（Instrumentation）深入分析](#7-%E7%AC%AC%E5%85%AD%E6%AD%A5%EF%BC%9A%E6%8F%92%E6%A1%A9%EF%BC%88instrumentation%EF%BC%89%E6%B7%B1%E5%85%A5%E5%88%86%E6%9E%90)
8. [常见坑与解决方案](#8-%E5%B8%B8%E8%A7%81%E5%9D%91%E4%B8%8E%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88)
9. [常用 Hook 片段速查](#9-%E5%B8%B8%E7%94%A8-hook-%E7%89%87%E6%AE%B5%E9%80%9F%E6%9F%A5)
10. [工具链推荐](#10-%E5%B7%A5%E5%85%B7%E9%93%BE%E6%8E%A8%E8%8D%90)

---

## 1\. 逆向的核心思路

逆向不是破解，是**追踪数据流**。

```
浏览器发出一个神秘参数
       ↓
这个参数从哪来？（定位生成点）
       ↓
生成它需要哪些输入？（追踪输入）
       ↓
中间做了什么运算？（识别算法）
       ↓
用 Python/Node.js 复现同样的运算

```

**黄金原则**：

- **先观察，再动手**。不要一上来就扣代码，先用 DevTools 观察几个请求，感受参数的规律。
- **缩小范围**。JS 文件往往几十万行，不要试图全部读懂，只找签名相关的那一小块。
- **验证要及时**。每往前推进一步就立刻验证，避免在错误路径上走太远。

---

## 2\. 第一步：抓包定位目标参数

### 2.1 找到含签名的请求

打开 Chrome DevTools → Network 面板，过滤关键词：

```
a_bogus        # 抖音签名参数
sign           # 通用签名
X-Bogus
token

```

点击那个请求，查看：

- **Headers** → 有没有自定义请求头（`X-Sign`、`x-secsdk-web-signature` 等）
- **URL** → 查询参数里有没有签名字段（`a_bogus=...`）

### 2.2 观察参数规律

多刷新几次，对比多个请求的签名值：

- 每次都不一样？→ 含时间戳或计数器
- 同一个 URL 每次相同？→ 纯哈希，可离线复现
- 长度固定？→ 猜算法（32字节=MD5/SM3，64字节=SHA-512...）

---

## 3\. 第二步：找到签名来源（断点 + Hook）

### 3.1 最简单的方法：XHR/Fetch 断点

DevTools → Sources → 右侧 "XHR/fetch Breakpoints" → 添加 URL 关键词

```
/aweme/v1/     ← 抖音 API 路径

```

刷新页面，断点触发后查看调用栈，往上翻就能找到签名函数。

### 3.2 Hook URLSearchParams.append

很多签名是最后通过 `URLSearchParams` 拼到 URL 上的。在 DevTools Console 运行：

```javascript
// 在 Console 里运行，拦截所有 append 调用
const _orig = URLSearchParams.prototype.append;
URLSearchParams.prototype.append = function(key, value) {
  if (key === 'a_bogus' || key === 'sign') {
    console.log('[Hook] append:', key, '=', value);
    console.trace();  // 打印调用栈！
  }
  return _orig.call(this, key, value);
};

```

> **注意**：如果 SDK 在页面加载时就 hook 了原型，Console 里注入已经太晚了。  
> 需要用 initScript（见下面）。

### 3.3 Hook fetch / XMLHttpRequest

```javascript
// 拦截 fetch，查看请求 URL
const _fetch = window.fetch;
window.fetch = function(...args) {
  const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
  if (url?.includes('aweme')) {
    console.log('[Hook] fetch:', url);
    console.trace();
  }
  return _fetch.apply(this, args);
};

// 拦截 XHR
const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
  if (String(url).includes('aweme')) {
    console.log('[Hook] XHR.open:', url);
    console.trace();
  }
  return _open.apply(this, arguments);
};

```

### 3.4 initScript：在所有脚本之前注入（最强）

适用于 SDK 在页面加载时就读取原型引用的情况。

**使用 Chrome DevTools Protocol (CDP) 或 Playwright 的 addInitScript：**

```javascript
// Playwright:
await page.addInitScript(() => {
  const _orig = URLSearchParams.prototype.append;
  URLSearchParams.prototype.append = function(key, value) {
    if (key === 'a_bogus') {
      window.__captured_bogus = value;
      console.log('[initScript] a_bogus:', value);
    }
    return _orig.call(this, key, value);
  };
});

```

**或用 Chrome DevTools MCP / Puppeteer：**

```javascript
// 在页面加载前通过 CDP 注入
await session.send('Page.addScriptToEvaluateOnNewDocument', {
  source: `
    const _orig = URLSearchParams.prototype.append;
    URLSearchParams.prototype.append = function(k, v) {
      if (k === 'a_bogus') window.__ab = v;
      return _orig.call(this, k, v);
    };
  `
});

```

---

## 4\. 第三步：在浏览器环境里验证

找到签名函数后，**先在 DevTools Console 里手动调用它**，确认能复现签名，再考虑搬到 Node.js。

```javascript
// 假设找到了签名函数叫 bdms.sign
bdms.sign('https://www.douyin.com/aweme/v1/web/social/count?aid=6383')
// 看看能不能返回一个 a_bogus 值

```

如果函数依赖 `this` 或闭包变量，在 Console 里直接调用可能报错，这很正常，继续往调用栈上层找包装函数。

---

## 5\. 第四步：识别加密算法

### 5.1 搜索魔法常量

每种哈希/加密算法都有**固定的初始化常量**，这些常量不会被混淆。

```
MD5:    0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476
SHA-1:  0x67452301, 0xEFCDABCD, 0x98BADCFE
SHA-256: 0x6a09e667, 0xbb67ae85, 0x3c6ef372
SM3:    0x7380166f, 0x4914b2b9  ← 中国国密算法
AES:    0x63, 0x7c, 0x77, 0x7b (S-box 前几个值)

```

**在 DevTools 搜索（Ctrl+Shift+F）：**

```
2043430169    ← SM3 的轮常量 Tj (十进制)
1937774191    ← SM3 的 IV[0] (十进制)

```

### 5.2 看函数名残留

混淆工具有时保留部分函数名，搜索：

```
SM3, sm3, Sm3
FF, GG           ← SM3 的轮函数名
SHA, sha
MD5, md5
AES, aes
HMAC
pbkdf2, bcrypt

```

### 5.3 看输出长度

| 长度               | 可能算法          |
| ---------------- | ------------- |
| 32 hex 字符 (16B)  | MD5           |
| 40 hex 字符 (20B)  | SHA-1         |
| 64 hex 字符 (32B)  | SHA-256 / SM3 |
| 128 hex 字符 (64B) | SHA-512       |

### 5.4 用 Python 快速验证猜测

```python
import hashlib, hmac

data = b"your_input_here"

# 测试各种算法
print("MD5:    ", hashlib.md5(data).hexdigest())
print("SHA1:   ", hashlib.sha1(data).hexdigest())
print("SHA256: ", hashlib.sha256(data).hexdigest())

# SM3 (需要 gmssl 库: pip install gmssl)
from gmssl import sm3, func
print("SM3:    ", sm3.sm3_hash(func.bytes_to_list(data)))

```

对比浏览器里捕获到的哈希值，看哪个匹配。

---

## 6\. 第五步：在 Node.js 中还原签名

### 6.1 最简单的方式：直接扣 SDK 在 Node.js 里跑

如果 SDK 是纯 JS（不依赖 DOM），可以直接用 Node.js 运行：

```javascript
// run_sdk.mjs
import { readFileSync } from 'fs';

// 加载 SDK 代码
const sdk_code = readFileSync('./lib/some_sdk.js', 'utf-8');

// 创建沙箱全局对象
const globalObj = {
  window: {},
  document: { createElement: () => ({}) },
  navigator: { userAgent: 'Mozilla/5.0...' },
  location: { href: 'https://www.example.com/' },
};

// 用 Function 构造器在沙箱里执行
const fn = new Function('window', 'document', 'navigator', 'location',
  sdk_code + '\n return typeof sign !== "undefined" ? sign : typeof module !== "undefined" ? module.exports : null;'
);
const sign = fn(globalObj.window, globalObj.document, globalObj.navigator, globalObj.location);

```

### 6.2 用 jsdom 模拟完整浏览器环境

适用于 SDK 依赖 DOM API 的情况：

```javascript
import { JSDOM } from 'jsdom';
import { readFileSync } from 'fs';

const src = readFileSync('./lib/sdk.js', 'utf-8');

const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
  url: 'https://www.example.com/',   // 必须设置，SDK 会读取 location
  pretendToBeVisual: true,           // 重要！防止 canvas 相关代码崩溃
  runScripts: 'dangerously',         // 允许执行脚本
});

const win = dom.window;

// 在加载 SDK 之前安装 Mock
class MockXHR {
  open(method, url) { this._url = url; }
  send() {}
  setRequestHeader() {}
  addEventListener() {}
  removeEventListener() {}
}
win.XMLHttpRequest = MockXHR;

// 加载 SDK
const scr = win.document.createElement('script');
scr.textContent = src;
win.document.head.appendChild(scr);

// 调用 SDK 的初始化函数
win.bdms.init({ aid: 6383, paths: ['^/aweme/v1/'] });

// Hook URLSearchParams 捕获签名（必须在 init 之前，或用 initScript）
const origAppend = win.URLSearchParams.prototype.append;
win.URLSearchParams.prototype.append = function(k, v) {
  if (k === 'a_bogus') {
    console.log('Captured a_bogus:', v);
  }
  return origAppend.call(this, k, v);
};

```

### 6.3 封装为 HTTP 服务（Python 调用）

Node.js 启动一个本地 HTTP 服务，Python 调用它：

```javascript
// sign_server.mjs
import http from 'http';
// ... 初始化 SDK（同上）...

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, 'http://localhost');
  const target = url.searchParams.get('url');

  const a_bogus = await getSignature(target);
  res.end(JSON.stringify({ a_bogus }));
});

server.listen(8888, () => {
  // Python 端监听这行输出来判断服务已就绪
  process.stdout.write(JSON.stringify({ status: 'ready' }) + '\n');
});

```

```python
# Python 端
import subprocess, requests, json, time

class Signer:
    def __init__(self, port=8888):
        self._proc = subprocess.Popen(
            ['node', 'sign_server.mjs'],
            stdout=subprocess.PIPE, text=True
        )
        # 等待服务就绪
        for _ in range(30):
            line = self._proc.stdout.readline()
            if '"ready"' in line:
                break
            time.sleep(0.2)
        self._port = port

    def sign(self, url):
        r = requests.get(f'http://localhost:{self._port}/sign',
                         params={'url': url}, timeout=5)
        return r.json()['a_bogus']

    def close(self):
        self._proc.terminate()

    def __enter__(self): return self
    def __exit__(self, *_): self.close()

# 使用
with Signer() as s:
    token = s.sign('https://www.douyin.com/aweme/v1/...')

```

---

## 7\. 第六步：插桩（Instrumentation）深入分析

当算法藏在 VM 里看不懂时，**不必读懂 VM，只需在关键函数的入口/出口插入日志**。

### 7.1 源码字符串替换插桩

最稳定的方法：加载 SDK 前，用字符串替换在源码里直接插入日志。

```javascript
let src = readFileSync('./lib/sdk.js', 'utf-8');

// 目标：在 someFunc(t) 的开头插入日志
// 原代码片段：
//   function someFunc(t){var r=...
// 替换为：
//   function someFunc(t){globalThis.__logs.push({fn:'someFunc',in:t});var r=...

src = src.replace(
  'function someFunc(t){var r=',
  'function someFunc(t){if(globalThis.__logs)globalThis.__logs.push({fn:"someFunc",in:typeof t==="string"?t:Array.from(t)});var r='
);

```

**关键技巧**：

- 用 `globalThis.__logs` 而不是局部变量，避免变量名冲突
- 用 `if(globalThis.__logs)` 做开关，只在需要时开启
- 对 byte array 用 `Array.from(t)` 转换，方便打印
- 替换的代码片段要足够独特，不能在文件里有多处匹配

### 7.2 验证替换成功

```javascript
// 检查替换是否生效
if (!src.includes('globalThis.__logs')) {
  console.error('替换失败！原始代码片段未找到');
  process.exit(1);
}
console.log('替换成功，共', (src.match(/globalThis\.__logs/g) || []).length, '处');

```

### 7.3 控制日志开关

```javascript
win.__logs = null;  // 默认关闭

// 需要分析时打开
win.__logs = [];
// ... 触发目标操作 ...
const log = [...win.__logs];
win.__logs = null;  // 关闭

// 分析日志
log.forEach((entry, i) => {
  console.log(`[${i}] ${entry.fn}: input=`, entry.in);
});

```

### 7.4 完整的哈希函数插桩示例

以抖音 SM3 分析为例：

```javascript
// 插桩 write() 入口，记录所有输入
src = src.replace(
  'key:"write",value:function(t){var r="string"==typeof t?',
  'key:"write",value:function(t){' +
  'if(globalThis.__sm3Calls)globalThis.__sm3Calls.push({op:"W",d:typeof t==="string"?t:Array.prototype.slice.call(t)});' +
  'var r="string"==typeof t?'
);

// 插桩 sum() 输出，记录哈希值
src = src.replace(
  'return this.reset(),u}},{key:"_compress"',
  'if(globalThis.__sm3Calls)globalThis.__sm3Calls.push({op:"S",d:u});return this.reset(),u}},{key:"_compress"'
);

```

---

## 8\. 常见坑与解决方案

### 坑 1：Hook 时机太晚

**现象**：在 Console 里 hook 了 `URLSearchParams.prototype.append`，但签名时没有触发。

**原因**：SDK 在初始化时就把原型方法的引用存到了内部变量里，之后的修改不影响它。

```javascript
// SDK 内部（简化）：
var _append = URLSearchParams.prototype.append;  // 启动时存了引用
// ... 之后调用 _append(key, value)  ← 不受后来的 hook 影响

```

**解决**：使用 initScript（在 SDK 加载前注入），或用 Playwright/Puppeteer 的 `addInitScript`。

---

### 坑 2：Hook 内部抛异常破坏 VM

**现象**：某些 hook 注入后，SDK 的 VM 报各种奇怪错误（`TypeError: undefined is not a function` 等）。

**原因**：VM 有 try/catch，hook 抛出的错误被 VM 捕获，导致 VM 状态错乱。

**错误做法**：

```javascript
// 在原生方法上 hook 并抛出 Error 获取堆栈——会破坏 VM
Number.prototype.toString = function() {
  throw new Error('trace');  // ← 危险！VM 内部大量调用 toString
};

```

**正确做法**：hook 里只做**记录**，不抛异常，调用原始方法后正常返回。

---

### 坑 3：测试 URL 不匹配路径过滤规则

**现象**：签名函数调用了，但 `a_bogus` 就是没出现。

**原因**：SDK 只对特定路径签名，测试 URL 不在白名单里。

```javascript
// 抖音 bdms 只签名这些路径：
paths: ['^/aweme/v1/', '^/aweme/v2/', '^/webcast/']

// 用 http://test.com/api/xxx 当然不会签名！
// 必须用真实的 API URL

```

**解决**：在 Network 面板找一个真实请求的 URL 来测试。

---

### 坑 4：jsdom 缺少 canvas 导致崩溃

**现象**：jsdom 里运行 SDK 时报 `TypeError: Cannot read properties of undefined (reading 'getContext')`。

**解决**：`pretendToBeVisual: true` 让 jsdom 声称自己支持 canvas，SDK 一般会降级处理。

```javascript
const dom = new JSDOM('...', {
  pretendToBeVisual: true,  // ← 加上这行
  runScripts: 'dangerously',
});

```

---

### 坑 5：Node.js 进程挂起不退出

**现象**：脚本执行完了，但进程一直不退出。

**原因**：jsdom 内部有 `setTimeout`/`setInterval` 保持了事件循环。

**解决**：在所有输出打印完后强制退出：

```javascript
// 放在文件最末尾
process.exit(0);

```

---

### 坑 6：Windows 终端中文乱码

**现象**：Python 脚本输出中文时报 `UnicodeEncodeError`。

**解决**：

```python
import sys, io
# 强制使用 UTF-8 输出
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

```

或在运行脚本前设置环境变量：

```bat
set PYTHONIOENCODING=utf-8

```

---

## 9\. 常用 Hook 片段速查

### URLSearchParams 签名捕获

```javascript
// 适合 initScript 注入
(function() {
  const _orig = URLSearchParams.prototype.append;
  URLSearchParams.prototype.append = function(key, value) {
    if (['a_bogus', 'sign', 'signature'].includes(key)) {
      window.__signatures = window.__signatures || {};
      window.__signatures[key] = value;
      // 可选：通知外部代码
      window.dispatchEvent(new CustomEvent('sign-captured', { detail: { key, value } }));
    }
    return _orig.call(this, key, value);
  };
})();

```

### fetch 请求拦截

```javascript
(function() {
  const _fetch = window.fetch;
  window.fetch = async function(input, init) {
    const url = typeof input === 'string' ? input : input.url;
    console.log('[fetch]', url?.substring(0, 100));
    const resp = await _fetch.apply(this, arguments);
    return resp;
  };
})();

```

### XHR 全拦截

```javascript
(function() {
  const _open = XMLHttpRequest.prototype.open;
  const _send = XMLHttpRequest.prototype.send;
  const _setHeader = XMLHttpRequest.prototype.setRequestHeader;

  XMLHttpRequest.prototype.open = function(method, url) {
    this.__url = url;
    return _open.apply(this, arguments);
  };

  XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
    console.log('[XHR header]', name, ':', value?.substring(0, 60));
    return _setHeader.apply(this, arguments);
  };

  XMLHttpRequest.prototype.send = function(body) {
    console.log('[XHR send]', this.__url?.substring(0, 100));
    return _send.apply(this, arguments);
  };
})();

```

### 函数调用追踪（不破坏原函数）

```javascript
function wrapFn(obj, fnName, label) {
  const orig = obj[fnName];
  obj[fnName] = function() {
    const args = Array.from(arguments).map(a =>
      typeof a === 'string' ? a.substring(0, 60) : typeof a
    );
    console.log(`[${label}] ${fnName}(`, args.join(', '), ')');
    const result = orig.apply(this, arguments);
    console.log(`[${label}] ${fnName} => `, String(result).substring(0, 60));
    return result;
  };
}

// 使用
wrapFn(window.crypto.subtle, 'digest', 'Crypto');
wrapFn(window, 'btoa', 'Encode');

```

### localStorage/sessionStorage 监控

```javascript
(function() {
  ['localStorage', 'sessionStorage'].forEach(storeName => {
    const store = window[storeName];
    const _setItem = store.setItem.bind(store);
    const _getItem = store.getItem.bind(store);

    Object.defineProperty(window, storeName, {
      get() {
        return new Proxy(store, {
          get(target, prop) {
            if (prop === 'setItem') return function(k, v) {
              console.log(`[${storeName}] set "${k}" =`, String(v).substring(0, 80));
              return _setItem(k, v);
            };
            return typeof target[prop] === 'function'
              ? target[prop].bind(target) : target[prop];
          }
        });
      }
    });
  });
})();

```

### Cookie 变动监控

```javascript
(function() {
  let _cookie = document.cookie;
  Object.defineProperty(document, 'cookie', {
    get() { return _cookie; },
    set(val) {
      console.log('[cookie set]', val.substring(0, 80));
      _cookie = val;
    }
  });
})();

```

---

## 10\. 工具链推荐

### 必装

| 工具              | 用途               |
| --------------- | ---------------- |
| Chrome DevTools | 抓包、断点、Console 注入 |
| Node.js (v18+)  | 运行 JS SDK        |
| jsdom npm 包     | 模拟浏览器 DOM 环境     |
| gmssl pip 包     | Python 里验证 SM3   |
| requests pip 包  | Python HTTP 请求   |

```bash
npm install jsdom          # Node.js 浏览器模拟
pip install gmssl requests # Python 工具

```

### 进阶

| 工具                  | 用途                       |
| ------------------- | ------------------------ |
| Playwright          | 带真实浏览器的自动化，支持 initScript |
| Fiddler / mitmproxy | HTTPS 流量解密抓包             |
| AST Explorer        | 分析混淆 JS 的语法树             |
| de4js.com           | 在线 JS 反混淆                |
| CyberChef           | 在线算法验证工具箱                |

### DevTools 快捷键

```
Ctrl+Shift+F    全局搜索（所有 JS 文件中搜索）
Ctrl+P          按文件名搜索
{}              格式化压缩代码（点击底部花括号图标）
F8              继续执行（断点中）
F10             单步跳过
F11             单步进入
Ctrl+Shift+E    查看 Event Listener

```

---

## 实战流程回顾：抖音 a\_bogus

```
1. Network 找到含 a_bogus 的请求
         ↓
2. 全局搜索 "a_bogus" → 找到 bdms SDK 加载位置
         ↓
3. initScript hook URLSearchParams.append → 确认 a_bogus 从 bdms 产生
         ↓
4. 下载 bdms JS 文件，搜索常量 2043430169 → 发现 SM3 实现
         ↓
5. 在 jsdom 中加载 bdms，Mock XHR，成功生成 a_bogus
         ↓
6. 源码插桩 SM3 的 write/sum 方法 → 记录完整 5 步哈希链
         ↓
7. Python gmssl 验证每一步哈希值完全一致
         ↓
8. 封装 Node.js HTTP 服务 + Python ABogus 类，完成集成

```

---

> **记住**：逆向的本质是调试和观察，而不是猜测。  
> 每一步都要有验证，不要在没有证据的假设上构建逻辑。

---

## 最佳实践

**从网络请求入手，不要从代码入手**：打开 DevTools Network，过滤目标请求，观察哪些参数在刷新后变化、哪些不变。变化的参数就是签名目标，然后再去找代码。直接搜索代码库效率低，因为混淆后的变量名毫无规律。

**XHR/fetch 断点优先于全局搜索**：在 DevTools Sources 面板设置 XHR 断点匹配目标 URL，比全局搜索参数名快得多。断到请求发出的位置后，调用栈里就能看到参数从哪里来。

**Hook 验证参数来源**：通过调用栈定位到疑似生成签名的函数后，在该函数入口 `console.log(arguments)` \+ `console.trace()`，刷新页面验证输入输出，再和 Network 里的实际值比对，确认后才开始分析逻辑。

**魔法数字是识别算法的最快方式**：MD5 的 `0x67452301`、SHA-1 的 `0x67452301`、SM3 的 `0x79cc4519` 这类初始化常量在混淆代码里不会被改变。搜索这些常量立刻定位算法实现，比从函数名、调用链入手快一个数量级。参见 [魔法数字速查](https://blog.vercanti.com/jia-mi-suan-fa-mo-fa-shu-zi-su-cha/)。

**用 Python 做交叉验证，不要只靠浏览器**：还原出算法后，在 Python 里用标准库（`hashlib`、`pycryptodome`）实现同样的计算，用相同输入对比输出。一致才算真的还原，否则可能有字节序、编码或 padding 细节遗漏。

---

## 常见陷阱

### 陷阱：在混淆代码里搜索参数名找不到

**现象：** Network 里看到 `a_bogus=xxx`，全局搜索 `a_bogus` 在 JS 里找不到任何赋值。

**原因：** 混淆代码把字符串分散存储，或通过字符串拼接、解密后动态生成键名，静态搜索无法找到。

**解决：** 改用运行时 Hook：`Object.defineProperty(URLSearchParams.prototype, 'append', ...)` 拦截所有 URLSearchParams 添加操作，或 Hook `Object.defineProperty(document, 'cookie', ...)` 拦截 Cookie 写入，在断点的调用栈里找来源。

### 陷阱：在 Node.js 里跑浏览器代码，window/document 报错

**现象：** 复制 JS 文件到 Node.js 执行，立刻报 `ReferenceError: window is not defined` 或 `document is not defined`。

**原因：** 浏览器 JS 依赖全局的 `window`、`document`、`navigator` 等对象，Node.js 环境没有。

**解决：** 使用 jsdom 提供完整 DOM 环境，或手动补充全局变量。参见 [jsdom完全指南](https://blog.vercanti.com/jsdom-wan-quan-zhi-nan-jsni-xiang-shi-zhan/) 和 [补环境](https://blog.vercanti.com/bu-huan-jing/)。

### 陷阱：逆向出的算法输出和真实值不一致

**现象：** 以为还原了签名算法，Python 里算出的结果和抓包数据不一样。

**原因：** 常见遗漏点：字节序（大端/小端）、字符串编码（UTF-8/GBK/Latin-1）、HMAC 密钥的编码方式、哈希输入是字符串还是字节数组、输出是 hex 还是 Base64。

**解决：** 在浏览器里打印算法每一步的中间值，Python 里逐步对比，定位到第几步开始不一致，就能精确找到差异。

---

## 参见

- [js逆向调试技巧](https://blog.vercanti.com/js-ni-xiang-diao-shi-ji-qiao/)
- [hook](https://blog.vercanti.com/hook-dai-ma-duan/)
- [补环境](https://blog.vercanti.com/bu-huan-jing/)
- [jsdom完全指南](https://blog.vercanti.com/jsdom-wan-quan-zhi-nan-jsni-xiang-shi-zhan/)
- [魔法数字速查](https://blog.vercanti.com/jia-mi-suan-fa-mo-fa-shu-zi-su-cha/)
- [签名算法还原](https://blog.vercanti.com/qian-ming-suan-fa-huan-yuan/)
- [国密SM2\_SM3\_SM4](https://blog.vercanti.com/guo-mi-suan-fa-sm2-sm3-sm4/)