抖音 a_bogus 逆向案例总结

本案例对抖音 Web 端 URL 查询参数 a_bogus 完成了从抓包定位到离线复现的全链路逆向,产出了三类成果:方法论教程 JS逆向入门指南(/js-ni-xiang-ru-men-zhi-nan/)、SM3 哈希链算法深挖 a_bogus_sm3_analysis(/douyin-a_bogus-jia-mi-suan-fa-wan-zheng-fen-xi/),以及可运行工件 gen_abogus.mjs(Node + jsdom 生成器)/ abogus.py(Python 封装)/ example.py(调用样板)/ probe_sm3.mj

分享

本案例对抖音 Web 端 URL 查询参数 a_bogus 完成了从抓包定位到离线复现的全链路逆向,产出了三类成果:方法论教程 JS逆向入门指南、SM3 哈希链算法深挖 a_bogus_sm3_analysis,以及可运行工件 gen_abogus.mjs(Node + jsdom 生成器)/ abogus.py(Python 封装)/ example.py(调用样板)/ probe_sm3.mjs(SM3 插桩侦察)。本篇是这三者的总览索引,只收录前两篇成品未覆盖的工程链路、字节码 VM 结构与服务化实现,三篇分工不重叠。


一、案例概述与最终结论

项目 结论
目标参数 a_bogus(URL 查询参数,约 160 字符,base64-url 变体)
生成 SDK bdms_1.0.1.19_fix.js(147,530 字节,webpack 打包)
哈希算法 SM3(国密 GB/T 32905-2016),详见 a_bogus_sm3_analysis
是否确定性 否,含时间戳/计数器,同一 URL 多次调用结果不同,令牌不可复用
离线可行性 可在 Node.js + jsdom 离线产出,无需真实浏览器
版本锁定 锁定 bdms_1.0.1.19_fix,随抖音版本更新失效

定位手段、断点/Hook、识别算法等通用步骤见 JS逆向入门指南,此处不重复。


二、XHR 调用链还原(独有)

抖音对 XMLHttpRequest.prototype.open / send 做了安全包装层(security wrapper),覆盖在 bdms 之上。这一层是成品文档未记录的中间环节。

open 被替换为包装函数,缓存原始参数并打时间戳后再调原生方法:

// XMLHttpRequest.prototype.open(security wrapper)
function () {
  this.secureOpenArgs = arguments;
  this.ucProxyParam = { startTime: Date.now() };
  t.nativeXMLHttpRequestOpen.apply(this, arguments);
}

send 通过 hookConfig(l).needProxy 判定是否走代理,不需代理时直接转发原生 send

// XMLHttpRequest.prototype.send(security wrapper)
function () {
  var h = t.hookConfig(l) || {}, v = h.needProxy;
  if (!p && !y && !b) return t.nativeXMLHttpRequestSend.apply(this, o);
  // ...needProxy 分支
}

完整调用链(从页面代码到原生 XHR):

page code → security wrapper → bdms VM hook → initScript hook → native XHR

关键实证(来自 _pendingXhrUrls 观测):在 open() 时捕获的完整 URL 已经包含 a_bogus。这证明 bdms 在 open() 阶段就生成并附加了 a_bogus,随后才进入 security wrapper 的下一环。成品文档只给出"生成时机 = open()"的结论,本篇补上观测证据。

由此推出捕获正确测试 URL 的方法:从 _pendingXhrUrls 提取真实请求 URL,去掉已有 a_bogus 参数后再喂给 getABogus(url)。用伪造参数(fake params)会因不匹配 bdms 路径过滤而失败。

浏览器阶段验证:Promise 桥接 + 超时兜底

在离线复现前,先在真实浏览器里确认能拿到 a_bogus 是稳妥的做法。难点是签名经由 hook 异步附加,无法直接 return。通用技巧:在 initScript 里定义 window.getABogus(url),用 Promise 把"hook 捕获"桥接成可 await 的同步取值,并加超时兜底防止路径不匹配时永久挂起。这套脚手架可迁移到其他同类 SDK。

// 注入 initScript:把异步 hook 捕获改造成可 await 的取值
window.getABogus = function (url) {
  return new Promise((resolve) => {
    // 由 URLSearchParams.prototype.append 的 a_bogus hook 调用它 resolve
    window.__resolveABogus = resolve;
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);   // open() 阶段 bdms 即生成并附加 a_bogus
    xhr.send();
    // 路径不匹配过滤规则时不会触发 hook,1s 兜底避免永久 pending
    setTimeout(() => resolve(null), 1000);
  });
};
// 用法:const ab = await getABogus("/aweme/v1/web/...");

三、bdms 字节码 VM 内部结构(独有)

bdms 的核心逻辑跑在自实现的 JavaScript 字节码虚拟机(VMP 混淆)里,由三个函数构成。成品文档只在调用栈里提到 d() / X() 的行号,未记录三函数职责与操作码语义。

函数 职责
D(t, r) VM 包装函数工厂:取 z[t]z[] 为字节码模块数组),返回调用 X 的闭包
X(t, r, e, n) VM 主循环:do { try { d(); } catch {...} } while (y())
d() 指令执行器:o[a++] 取下一条指令,t < 38 时分发约 60+ 种操作码
function D(t, r) {                 // 创建 VM 包装函数
  var e = z[t];                    // z[] = 字节码模块数组
  var n = function () { return X(e, this, arguments, r); };
  return n;
}
function X(t, r, e, n) {            // VM 主循环
  do { try { d(); } catch (t) { /* ... */ } } while (y());
}
function d() {                     // 指令执行器
  var t = o[a++];                  // 读取下一条指令
  if (t < 38) { /* 指令分发 */ }
}

部分操作码(opcode)语义:

操作码 含义
0 函数调用(call)
7 创建空对象 {}(new obj)
8 属性读取 obj[key](getprop)
9 push true
14 属性写入 obj[key] = val(setprop)
15 全局变量写入(setglobal)

字节码块为 base64 编码,约位于文件 offset 120000–130000。

这是纯 JavaScript 自实现的字节码 VM,不是 WebAssembly:文件内无 .wasm 模块、无 WebAssembly.instantiate。其指令分发模型与 WASM 同构,可作为理解 WASM 解释执行的轻量类比,但逆向路径不同——此处密码学原语(SM3)以明文 webpack 模块独立存在、未进 VM,可用魔法常量直接定位(见 魔法数字速查),而 WASM 需反编译/内存断点(WASM 逆向见 wasm特征)。


四、SM3 哈希链(摘要,详见深挖文档)

a_bogus 的核心是一条 5 步 SM3 哈希链。本篇仅列定位线索,IV/轮常数四重证据、dhzx 密钥、CONST 常量、类 HMAC-SM3 结构与 Python 复现见 a_bogus_sm3_analysis

定位线索:在混淆 JS 中搜索 SM3 轮常量即可锁定实现(gr 类)。

常量 十六进制 角色
1937774191 0x7380166F IV reg[0]
1226093241 0x4914B2B9 IV reg[1](完整 IV 八元组见 a_bogus_sm3_analysis
2043430169 0x79CC4519 轮常量 T0(轮 0–15)
2055708042 0x7A879D8A 轮常量 T1(轮 16–63)

参与签名的是 querystring,路径本身不参与;只有匹配路径过滤规则的请求才会被签名。


五、离线运行方案(jsdom)

bdms 是独立 JS SDK,不依赖 DOM 渲染,因此可在 jsdom 离线运行。补环境思路见 补环境

选型对比(为何选 jsdom 而非 Playwright):

维度 jsdom Playwright
启动时间 约 3s 约 10s
依赖 jsdom npm 包(已装) 完整浏览器
兼容性 仅纯 JS SDK 全部场景
canvas 不支持 支持
适用 无 canvas 依赖的 SDK 需完整渲染

落地三要点:

  1. 在 bdms 加载安装最小化 mock XHR(bdms 会在 init() 时快照 XMLHttpRequest.prototypeURLSearchParams.prototype.append,晚于此的 hook 会被绕过)。
  2. 必须调用 bdms.init(config) 触发路径过滤规则匹配,否则 getABogus 返回空。
  3. jsdom 无 canvas,bdms 会降级处理,不阻断签名;jsdom 构造时传 pretendToBeVisual 防止 canvas 相关代码崩溃。
  4. 结束时 process.exit(0),避免 jsdom timer 挂起进程。

路径过滤规则(仅以下前缀的 URL 被签名):

^/webcast/   ^/aweme/v1/   ^/aweme/v2/   /douplus/
/v1/message/send   ^/live/   ^/captcha/   ^/ecom/   ^/luna/pc

最小 mock 与 init 配置:

// 最小化 mock XHR(必须在 bdms 加载前)
class MockXHR {
  open(method, url) { this._url = String(url); }
  send() {} setRequestHeader() {}
  addEventListener() {} removeEventListener() {}
}
window.XMLHttpRequest = MockXHR;

// bdms.init 配置(取自页面 sdk-glue.js)
bdms.init({ aid: 6383, pageId: 6241, paths: [/* 见上 */], boe: false, ddrt: 8.5, ic: 8.5 });

六、工程实现:Node + Python 服务化(独有)

abogus.py 的服务模式通过 subprocess 拉起 Node server,并用一套握手协议确认就绪——这套 ready 握手与进程生命周期管理是成品文档未记录的内部实现。

握手时序:

Python process ──spawn──▶ Node.js server
                            │ stdout: {"status":"ready"}
Python(readline 等到 ready)──▶ HTTP GET /sign?url=...
                            │ JSON response
Python(解析)──▶ a_bogus token

gen_abogus.mjs 提供 CLI 与 --server 两种模式;abogus.pysubprocess.Popen 启动 Node,循环 readline 直到 stdout 出现 {"status":"ready"} 才发请求,并用 __enter__/__exit__ 管理进程关闭:

class ABogus:
    def _start_server(self):
        self._proc = subprocess.Popen(cmd, stdout=PIPE, ...)
        for _ in range(30):                      # 最多等约 6s
            line = self._proc.stdout.readline()
            if '"ready"' in line:
                self._ready.set(); return
            time.sleep(0.2)

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

性能:服务模式启动约 3s(jsdom + bdms 初始化),之后约 0.03s/请求(实测 3 请求 0.08s)。单次 CLI 模式每次都需重新初始化(约 3s),仅适合低频调用。


七、维护与风险

说明
版本锁定 锁定 bdms_1.0.1.19_fix.js,抖音更新后需重新下载
监控点 监控 /tos-cn-i-9r5gewecjs/bdms_*.js URL 变化以发现版本更替
canvas 降级 jsdom 无 canvas,bdms 设备指纹退化,长期存在被检测风险
令牌特性 a_bogus 不可复用,每次请求需重新生成
配套参数 需配合 msTokenwebidverifyFpuifidfp 等真实参数使用
签名参数辨析 X-Bogus(webmssdk 内部生成但不发送)、x-secsdk-web-signature / x-secsdk-csrf-token(runtime_bundler 生成的请求头)与 a_bogus(bdms 生成的 URL 参数)是不同体系,勿混淆

八、文件清单与参见

工件(位于 js逆向/douyin/):

文件 作用
lib/bdms_1.0.1.19_fix.js 逆向对象本体(版本锁定)
gen_abogus.mjs Node + jsdom 生成器,CLI 与 --server 模式
abogus.py Python 封装,含 subprocess 服务模式与 ready 握手
example.py 配合 msToken/webid 等参数发请求的样板
probe_sm3.mjs SM3 插桩侦察脚本,对应 a_bogus_sm3_analysis 第五节

参见:JS逆向入门指南a_bogus_sm3_analysis魔法数字速查补环境

阅读更多

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