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

# Douyin a_bogus 加密算法完整分析
- URL: https://blog.vercanti.com/douyin-a_bogus-jia-mi-suan-fa-wan-zheng-fen-xi/
- Published: 2026-08-28T14:34:58.000Z
- Updated: 2026-08-28T14:57:45.000Z
- Description: 抖音所有 API 请求的 URL 末尾都带有 a_bogus 参数，例如： a_bogus 约 160 字符，每次请求不同，是服务端验证请求合法性的核心凭据。 在 Chrome DevTools 对 URLSearchParams.prototype.append 打断点，触发条件 key === 'a_bogus'，得到如下调用栈： 结论：a_bogus 由 bdms_1.0.1.19_fix.js 生成，内含自定义字节码虚拟机（VMP）。 bdms 文件分两层： 密码学原语（SM3）作为基础设施单独存在，没有被 VMP 保护。 在文件 offset
- Author: yellowdog
- Tags: js逆向, douyin

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

> 分析目标：`a_bogus` URL 参数的生成算法，重点揭示 SM3 国密哈希的调用结构  
> 日期：2026-03-05  
> 工具：Chrome DevTools MCP、jsdom、gmssl（Python）

---

## 一、问题来源

抖音所有 API 请求的 URL 末尾都带有 `a_bogus` 参数，例如：

```
GET /aweme/v1/web/social/count?aid=6383&...&msToken=xxx&a_bogus=mfUVkz7jxp5ROd%2FS...

```

`a_bogus` 约 160 字符，每次请求不同，是服务端验证请求合法性的核心凭据。

---

## 二、签名来源定位

### 2.1 调用栈追踪

在 Chrome DevTools 对 `URLSearchParams.prototype.append` 打断点，触发条件 `key === 'a_bogus'`，得到如下调用栈：

```
URLSearchParams.append('a_bogus', value)
  ← d()          bdms_1.0.1.19_fix.js:2:131912   VM 指令执行器
  ← X()          bdms_1.0.1.19_fix.js:2:131083   VM 主循环
  ← n()          bdms_1.0.1.19_fix.js:2:130952   XHR open 拦截器

```

**结论**：`a_bogus` 由 `bdms_1.0.1.19_fix.js` 生成，内含自定义字节码虚拟机（VMP）。

### 2.2 三个安全 SDK 的职责区分

| SDK 文件                  | 生成内容                                         | 位置       |
| ----------------------- | -------------------------------------------- | -------- |
| bdms\_1.0.1.19\_fix.js  | a\_bogus                                     | URL 查询参数 |
| webmssdk.es5.js         | X-Bogus（实际请求中不存在）                            | 内部，不发送   |
| runtime\_bundler\_34.js | x-secsdk-web-signature / x-secsdk-csrf-token | 请求头      |

---

## 三、识别 SM3 哈希算法

### 3.1 为什么 VMP 混淆不能隐藏算法

bdms 文件分两层：

```
bdms_1.0.1.19_fix.js
├── webpack 模块（普通 minified JS）
│   └── gr 类 ← SM3 实现，在这里，可读
└── VMP 字节码（base64 编码 + 自定义 VM 解释器）
    └── 业务逻辑 ← 真正被保护的部分

```

密码学原语（SM3）作为基础设施单独存在，没有被 VMP 保护。

### 3.2 识别依据一：初始向量（IV）

在文件 offset \~140000 处的 `gr` 类 `reset()` 方法中：

```javascript
key: "reset", value: function() {
  this.reg[0] = 1937774191,  // 0x7380166F ← SM3 标准 IV[0]
  this.reg[1] = 1226093241,  // 0x4914B2B9 ← SM3 标准 IV[1]
  this.reg[2] = 388252375,   // 0x172442D7 ← SM3 标准 IV[2]
  this.reg[3] = 3666478592,  // 0xDA8A0600 ← SM3 标准 IV[3]
  this.reg[4] = 2842636476,  // 0xA96F30BC ← SM3 标准 IV[4]
  this.reg[5] = 372324522,   // 0x163138AA ← SM3 标准 IV[5]
  this.reg[6] = 3817729613,  // 0xE38DEE4D ← SM3 标准 IV[6]
  this.reg[7] = 2969243214   // 0xB0FB0E4E ← SM3 标准 IV[7]
}

```

验证：

```python
# SM3 标准（GB/T 32905-2016）规定的初始值
IV = [0x7380166F, 0x4914B2B9, 0x172442D7, 0xDA8A0600,
      0xA96F30BC, 0x163138AA, 0xE38DEE4D, 0xB0FB0E4E]
# 对应十进制：[1937774191, 1226093241, 388252375, 3666478592, ...]

```

这 8 个常数是 SM3 专有的，SHA-256 的 IV 完全不同（`0x6a09e667`, `0xbb67ae85`, ...）。

### 3.3 识别依据二：轮常数 Tj

```javascript
function yr(t) {
  return 0  <= t && t < 16 ? 2043430169   // 0x79CC4519 ← SM3 T_j (j<16)
       : 16 <= t && t < 64 ? 2055708042   // 0x7A879D8A ← SM3 T_j (j≥16)
       : void console.error("invalid j for constant Tj")
}

```

SM3 标准定义两个轮常数，十进制值唯一对应 SM3，不可能是 SHA 系列。

### 3.4 识别依据三：布尔函数名称残留

```javascript
console.error("invalid j for bool function FF")
console.error("invalid j for bool function GG")

```

**FF** 和 **GG** 是 SM3 标准文档中布尔函数的原始名称，SHA 系列用的是 Ch/Maj，这是最直接的文字证据。

### 3.5 识别依据四：结构特征

| 特征    | 代码实现                     | SM3 标准            |
| ----- | ------------------------ | ----------------- |
| 寄存器数量 | this.reg = new Array(8)  | 8 个 32-bit 字 ✓    |
| 总轮数   | for (n = 0; n < 64; n++) | 64 轮 ✓            |
| 分组大小  | 64 - this.chunk.length   | 512 bit = 64 字节 ✓ |
| 轮函数切换 | t < 16 / t >= 16 两套逻辑    | SM3 在第 16 轮切换 ✓   |
| 输出长度  | new Array(32)            | 256 bit = 32 字节 ✓ |

---

## 四、在 jsdom 中运行 bdms

### 4.1 发现 bdms 可以在无浏览器环境运行

bdms 只依赖：

- `XMLHttpRequest.prototype.open`（可 mock）
- `URLSearchParams`（jsdom 原生支持）
- Canvas API（用于设备指纹，失败时降级）

### 4.2 关键配置：bdms.init()

从页面 sdk-glue.js 中提取到 bdms 的初始化配置：

```javascript
window.bdms.init({
  aid: 6383,
  pageId: 6241,
  paths: [          // 只对这些路径的请求签名
    "^/webcast/",
    "^/aweme/v1/",
    "^/aweme/v2/",
    "/douplus/",
    "/v1/message/send",
    "^/live/",
    "^/captcha/",
    "^/ecom/",
    "^/luna/pc"
  ],
  boe: false,
  ddrt: 8.5,
  ic: 8.5
});

```

没有调用 `init()` 时，bdms 不对任何请求签名（这是之前 `getABogus` 返回 null 的原因）。

### 4.3 最小可运行示例

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

const dom = new JSDOM('<html></html>', {
  url: 'https://www.douyin.com/',
  pretendToBeVisual: true,   // 重要：模拟有 canvas，防止 VM 报错
  runScripts: 'dangerously',
});
const win = dom.window;

// Hook：捕获 a_bogus 生成
const origAppend = win.URLSearchParams.prototype.append;
win.URLSearchParams.prototype.append = function(k, v) {
  if (k === 'a_bogus') console.log('a_bogus:', v);
  return origAppend.call(this, k, v);
};

// 最小 XHR mock
class MockXHR {
  open() {} send() {} setRequestHeader() {} addEventListener() {} removeEventListener() {}
}
win.XMLHttpRequest = MockXHR;
win.__ac_referer = '';

// 加载并初始化 bdms
const s = win.document.createElement('script');
s.textContent = readFileSync('lib/bdms_1.0.1.19_fix.js', 'utf-8');
win.document.head.appendChild(s);
win.bdms.init({ aid: 6383, pageId: 6241, paths: ['^/aweme/v1/'], boe: false, ddrt: 8.5, ic: 8.5 });

// 触发签名
const xhr = new win.XMLHttpRequest();
xhr.open('GET', 'https://www.douyin.com/aweme/v1/web/social/count?aid=6383');
xhr.send();
// 控制台输出：a_bogus: DJ-nfzywYq...

```

---

## 五、SM3 调用结构的完整侦察

### 5.1 插桩方案

由于 SM3 的 `gr` 类在 bdms 的 IIFE 闭包内，无法从外部直接访问。采用**源码字符串替换**方案，在加载前对 bdms 源码打补丁：

```javascript
// 插桩 1：在 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?'
);

// 插桩 2：在 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"'
);

```

**替换字符串的唯一性验证**（重要，避免错误替换）：

```bash
grep -o 'key:"write",value:function(t){var r="string"' bdms_1.0.1.19_fix.js | wc -l
# 输出：1  ✓ 唯一
grep -o 'return this.reset(),u}},{key:"_compress"' bdms_1.0.1.19_fix.js | wc -l
# 输出：1  ✓ 唯一

```

> **失败的尝试**：曾尝试 Hook `Number.prototype.toString`、`Array` 构造函数 Proxy、`encodeURIComponent` 等方案，均因影响 VM 内部操作而导致 `TypeError: undefined is not a function`。源码替换是最干净的方案。

### 5.2 侦察结果

对两个不同 URL 分别签名，记录所有 SM3 调用：

#### URL A：`?aid=6383`（只有 aid 参数）

```
[0]  write  "aid=6383dhzx"                           ← querystring + 密钥
[1]  sum    → 6c32a19917eaa8b78219ee4e5207a726...    ← h1
[2]  write  [32B] 6c 32 a1 99 17 ea a8 b7 82 19...   ← h1 的字节
[3]  sum    → d0cc81a2bb8ba6595db7354702cdbcf7...    ← h2
[4]  write  "dhzx"                                   ← 只有密钥
[5]  sum    → 54058183087f8ce187d426f8b1b00f54...    ← h3（固定）
[6]  write  [32B] 54 05 81 83 08 7f 8c e1 87 d4...   ← h3 的字节
[7]  sum    → 40fd9cf02c609f961b7a5234c578ea77...    ← h4（固定）
[8]  write  "T8c7Qub0SliGOfVZg2evTNNYdWlDLDSf..."   ← 固定常量（base64）
[9]  sum    → 5ca3b44e7e8d6e102ae6392fd3906ae1...    ← h5（固定）

```

#### URL B：`?aid=6383`（相同 querystring，不同路径）

```
[0]  write  "aid=6383dhzx"     ← 相同！
[1~9]       全部相同！

```

#### 多参数测试对比

| URL querystring               | write\[0\] 输入                     | h1（SM3 输出前4字节） |
| ----------------------------- | --------------------------------- | -------------- |
| aid=6383                      | aid=6383dhzx                      | 6c32a199...    |
| aid=6383&source=6             | aid=6383&source=6dhzx             | e4ade0ea...    |
| aid=6383&source=6&extra=hello | aid=6383&source=6&extra=hellodhzx | 4beb2ff5...    |
| xxx=999                       | xxx=999dhzx                       | c4799285...    |

**关键发现**：

- URL 路径（`/aweme/v1/web/social/count` vs `/aweme/v1/web/user/profile/`）**不参与哈希**
- 只有 **querystring**（`?` 后面的部分，不含 `a_bogus`）参与哈希
- 固定密钥 `"dhzx"` 拼接在 querystring 末尾

---

## 六、算法结构还原

### 6.1 5 步 SM3 哈希链

```
输入：querystring = URL 中 ? 后面的部分（不含 a_bogus）
      KEY = "dhzx"（硬编码密钥，藏在 VMP 字节码中）
      CONST = "T8c7Qub0SliGOfVZg2evTNNYdWlDLDSf2Z8Dj+0IuzuDhB6Ph6gn1M6IZv8Ne0bkjm+oDt634d9QX+xzZ3vrMKvA8eYoPMR="（固定常量）

h1 = SM3(querystring + KEY)        ← 随请求变化
h2 = SM3(h1_bytes)                 ← 随请求变化
h3 = SM3(KEY)                      ← 固定：54058183087f8ce1...
h4 = SM3(h3_bytes)                 ← 固定：40fd9cf02c609f96...
h5 = SM3(CONST_bytes)              ← 固定：5ca3b44e7e8d6e10...

a_bogus = f(h2, h4, h5, timestamp, counter, ...)  ← f() 还在 VMP 字节码中

```

### 6.2 类 HMAC 结构

这个构造类似于自定义的 **HMAC-SM3**：

```
标准 HMAC：H((K ⊕ opad) || H((K ⊕ ipad) || message))

bdms 的变体：
  先 SM3(message + K) → h1
  再 SM3(h1)          → h2      ← 这是消息相关的认证码
  独立 SM3(K) → h3
  再 SM3(h3)  → h4              ← 这是密钥派生值（固定）
  SM3(CONST)  → h5              ← 常量哈希（固定）

```

h3/h4/h5 每次都固定，实际上只有 h2 携带了消息（querystring）信息。

### 6.3 Python 完整复现（SM3 链部分）

```python
from gmssl import sm3, func

KEY   = "dhzx"
CONST = "T8c7Qub0SliGOfVZg2evTNNYdWlDLDSf2Z8Dj+0IuzuDhB6Ph6gn1M6IZv8Ne0bkjm+oDt634d9QX+xzZ3vrMKvA8eYoPMR="

def sm3h(data: bytes) -> bytes:
    """计算 SM3 哈希，返回 32 字节"""
    return bytes.fromhex(sm3.sm3_hash(func.bytes_to_list(data)))

def compute_chain(querystring: str) -> dict:
    h1 = sm3h((querystring + KEY).encode())
    h2 = sm3h(h1)
    h3 = sm3h(KEY.encode())          # 固定
    h4 = sm3h(h3)                    # 固定
    h5 = sm3h(CONST.encode())        # 固定
    return {'h1': h1, 'h2': h2, 'h3': h3, 'h4': h4, 'h5': h5}

```

### 6.4 验证结果（全部匹配）

```
[OK] h1 = SM3("aid=6383dhzx")
     = 6c32a19917eaa8b78219ee4e5207a726f2ccc6ff9231c219afc5e02f60b9a6d5
[OK] h2 = SM3(h1_bytes)
     = d0cc81a2bb8ba6595db7354702cdbcf721a34d071c3c19de99274a17a89779a8
[OK] h3 = SM3("dhzx")
     = 54058183087f8ce187d426f8b1b00f544eabc81b19ee83a5202cbb81127b81be
[OK] h4 = SM3(h3_bytes)
     = 40fd9cf02c609f961b7a5234c578ea77f55947b163621c8e05637bc7b00998f0
[OK] h5 = SM3(CONST)
     = 5ca3b44e7e8d6e102ae6392fd3906ae16ae2087d0f3dbbdc4d1b5db08fd56d06

```

---

## 七、已知 vs 未知

### 已完全复现

| 内容     | 值                             |
| ------ | ----------------------------- |
| 哈希算法   | SM3（GB/T 32905-2016）          |
| 硬编码密钥  | "dhzx"                        |
| 固定常量   | "T8c7Qub0..." (88字符 base64)   |
| 5 步哈希链 | 全部匹配，可独立计算                    |
| 哈希输入   | querystring（不含路径，不含 a\_bogus） |

### 仍在 VMP 黑盒中

| 内容             | 说明                          |
| -------------- | --------------------------- |
| 最终编码函数 f()     | 将 h2 + 运行时数据 → a\_bogus 字符串 |
| 时间戳/计数器位置      | 导致相同 URL 每次 a\_bogus 不同     |
| a\_bogus 的字节布局 | 160 字符，约 120 字节，内部结构未知      |
| 自定义编码字母表       | 使用 \- 而不是标准 base64 的 +      |

### 下一步逆向方向

要完全脱离 jsdom，还需解决最后一步。可行方向：

1. **对比法**：固定 querystring，多次采样 a\_bogus，找出变化字节的规律（定位 timestamp/counter 位置）
2. **时间冻结法**：在 jsdom 中 mock `Date.now()` 为常量，看 a\_bogus 是否变为确定性
3. **字节反推**：base64 decode a\_bogus，对比 h2 的字节在其中的位置

---

## 八、完整工具链

```
js_reverse/douyin/
├── lib/
│   └── bdms_1.0.1.19_fix.js     # bdms SDK（147KB）
├── gen_abogus.mjs                # jsdom 版生成器（CLI + HTTP 服务）
├── abogus.py                     # Python 封装
├── example.py                    # 真实 API 调用示例
└── probe_sm3.mjs                 # SM3 侦察工具（本文使用）

```

### 当前推荐用法（jsdom 方案，不需要完整逆向）

```python
from douyin.abogus import ABogus

url = "https://www.douyin.com/aweme/v1/web/social/count?" \
      "aid=6383&device_platform=webapp&..."

# 单次
token = ABogus().get(url)

# 批量（服务模式）
with ABogus(server=True) as s:
    token = s.get(url)    # ~0.03s/次

```

---

## 最佳实践

**用魔法常量定位算法实现，不要逐行读混淆代码**：SM3 的初始化常量 `0x79cc4519`、`0x7a879d8a` 在 bdms 里以明文存在，直接搜索常量，立刻定位到 SM3 实现，比从调用栈逐层追踪节省数小时。

**initScript 注入时机早于 prototype 修改**：bdms 在初始化时会快照 `URLSearchParams.prototype.append` 等原生方法引用，后续通过快照调用而不经过 prototype 链。必须用 Chrome DevTools 的 `initScript` 在 bdms 脚本加载之前注入 Hook，才能覆盖到快照前的原始函数。

**源码插桩优于 Proxy/prototype Hook**：对 bdms 这类 VM 型代码，下载 JS 文件，在关键函数（`SM3.write`、`SM3.sum`）的入口/出口手动插入 `console.log`，比 Proxy 拦截可靠得多，且不会破坏 VM 内部状态。

**用 Python 逐步交叉验证每一轮 SM3 哈希**：SM3 有 5 步迭代，用 `gmssl` 库在 Python 里复现每一步，和插桩日志的中间值逐一比对。一旦某步不一致，说明那步有字节序或输入格式问题，精准定位而不是盲猜。

**jsdom 用 `pretendToBeVisual: true` 避免 canvas 崩溃**：bdms init 期间会做 canvas 指纹操作，若 jsdom 未开启 `pretendToBeVisual`，canvas 方法返回 null 导致 VM 在错误处理路径上产生级联崩溃，表现为 `window.bdms` 加载后为 undefined。

---

## 常见陷阱

### 陷阱：URLSearchParams hook 被 bdms 绕过

**现象：** Hook `URLSearchParams.prototype.append` 后，捕获不到 `a_bogus` 写入。

**原因：** bdms 在 `init()` 时保存了 `append` 的引用快照，后续通过快照调用，绕过 prototype 修改。

**解决：** 使用 Chrome DevTools 的 `initScript` 在 bdms 脚本执行前注入 Hook，覆盖原始引用。

### 陷阱：Array Proxy 或 prototype Hook 破坏 bdms VM 执行

**现象：** 注入 `Proxy` 包装 `Array` 构造函数或 Hook `Number.prototype.toString` 后，bdms 报 `TypeError: undefined is not a function`，a\_bogus 无法生成。

**原因：** bdms 包含一个自定义 VM，大量创建 Array 对象或调用 `toString`。Proxy/prototype 修改被 VM 内部的 try/catch 捕获后导致状态异常，VM 之后的调用全部失败。

**解决：** 放弃侵入性 Hook，改用源码字符串替换：下载 bdms JS 文件，在目标函数（`SM3.sum`）末尾直接拼接 `console.log` 代码，加载修改后的文件。

### 陷阱：jsdom 加载 bdms 后 `window.bdms` 为 undefined

**现象：** jsdom 环境里执行 bdms，不报错但 `window.bdms` 是 undefined，无法调用生成函数。

**原因：** bdms init 期间调用 `document.createElement('canvas')`，`pretendToBeVisual: false` 时 canvas 行为异常，VM 走到错误处理分支后静默退出，`window.bdms` 未被赋值。

**解决：** JSDOM 构造时加 `pretendToBeVisual: true`，使 canvas API 返回虚假值而不报错，VM 正常完成初始化。

---

## 参见

- [国密SM2\_SM3\_SM4](https://blog.vercanti.com/guo-mi-suan-fa-sm2-sm3-sm4/)
- [魔法数字速查](https://blog.vercanti.com/jia-mi-suan-fa-mo-fa-shu-zi-su-cha/)
- [jsdom完全指南](https://blog.vercanti.com/jsdom-wan-quan-zhi-nan-jsni-xiang-shi-zhan/)
- [JS逆向入门指南](https://blog.vercanti.com/js-ni-xiang-ru-men-zhi-nan/)
- [补环境](https://blog.vercanti.com/bu-huan-jing/)
- [js逆向调试技巧](https://blog.vercanti.com/js-ni-xiang-diao-shi-ji-qiao/)