Douyin a_bogus 加密算法完整分析
抖音所有 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
分析目标:
a_bogusURL 参数的生成算法,重点揭示 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() 方法中:
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]
}
验证:
# 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
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 识别依据三:布尔函数名称残留
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 的初始化配置:
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 最小可运行示例
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 源码打补丁:
// 插桩 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"'
);
替换字符串的唯一性验证(重要,避免错误替换):
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/countvs/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 链部分)
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,还需解决最后一步。可行方向:
- 对比法:固定 querystring,多次采样 a_bogus,找出变化字节的规律(定位 timestamp/counter 位置)
- 时间冻结法:在 jsdom 中 mock
Date.now()为常量,看 a_bogus 是否变为确定性 - 字节反推: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 方案,不需要完整逆向)
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 正常完成初始化。