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

分享

官方文档: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_bogusbdms_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 处的 grreset() 方法中:

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")

FFGG 是 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.toStringArray 构造函数 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 链部分)

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 方案,不需要完整逆向)

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 的初始化常量 0x79cc45190x7a879d8a 在 bdms 里以明文存在,直接搜索常量,立刻定位到 SM3 实现,比从调用栈逐层追踪节省数小时。

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

源码插桩优于 Proxy/prototype Hook:对 bdms 这类 VM 型代码,下载 JS 文件,在关键函数(SM3.writeSM3.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 正常完成初始化。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog