douyin-abogus-reverse

概述

抖音Web端在核心API(如视频列表、用户主页接口)中普遍使用了一个名为 a_bogus 的签名参数。它本质上是一个动态生成的校验值,用来阻挡非浏览器发起的请求。简单来说,服务器会用一套隐藏在安全SDK里的规则来验证这个值,如果验证不通过,请求就会被拒绝。

本文会从一个完整的逆向实践角度出发,拆解环境模拟、反检测Hook、SDK加载、XHR模拟触发加密等关键步骤,最终给出可复用的 Python + JavaScript 方案,让你能够在本地生成合法的 a_bogus 参数。


网页分析

在开始逆向之前,我们先从浏览器端确认 a_bogus 的生成位置和依赖关系。这里以抖音个人主页视频列表接口为例:

GET /aweme/v1/web/aweme/post/?...&a_bogus=XXXX
  1. 定位加密请求
    打开 Chrome 开发者工具,切换到 Network 面板,筛选 XHR/Fetch 类型的请求,可以很容易找到携带 a_bogus 参数的接口(如 /aweme/v1/web/aweme/post/)。

  2. 寻找加密入口
    这类加密通常不会放在业务代码里,而是在请求发起前由安全模块拼装上去。直接在请求上打 XHR 断点,或全局搜索 a_bogus 关键词,就会发现加密入口位于一个名为 bdms(字节跳动安全SDK)的模块中。

  3. 分析依赖
    bdms 并不是一段可以独立运行的代码。它加载时会大量检测浏览器环境,包括:

    • DOM API 是否存在(如 document.createElement
    • 浏览器全局对象(windownavigatorscreen 等)
    • 各种函数是否为原生的(通过 Function.prototype.toString 判断)

这说明,单纯抠出 bdms 的代码是无法直接在 Node.js 或 Python 环境里运行的——我们必须先搭建一个足够逼真的浏览器沙箱。


核心技术路线

本次逆向采用「环境补全优先 + 动态代理兜底」的思路:

  1. 全局环境模拟
    补全 windowdocumentnavigator 等浏览器全局对象的关键属性和方法,让 bdms 不至于一启动就报错。

  2. 函数 Native 伪装
    bdms 会通过 fn.toString() 检查某个函数是否仍为 [native code] 状态。我们需要重写 Function.prototype.toString,并给伪造的函数打上 native 标记。

  3. 动态代理监控
    Proxy 监听 bdms 对环境对象的属性访问。当遇到尚未补全的属性时,可以在控制台看到日志,然后按需补充,避免一开始就写死大量用不到的属性。

  4. SDK 加载与调用
    注入整理好的 bdms 代码,调用它的初始化方法,再通过模拟 XMLHttpRequest 的方式触发加密流程,最终从 window.a_bogus 中拿到生成的参数。


核心代码实现

下面是一份完整的浏览器环境模拟脚本 env.js,它结合了环境补全、Native 函数伪装、动态代理以及 SDK 初始化逻辑。

注意:你需要从抖音页面中提取 bdms 的核心代码,保存为 bdms.js 并放在相同目录下,env.js 会在最后通过 require('./bdms') 加载它。

window = global;
console_log = console.log;

// ==================== 第一步:函数 Native 伪装 ====================
(() => {
  'use strict';
  const $toString = Function.toString;
  // 使用随机 Symbol 作为 native 标签,避免被检测到固定属性名
  const fakeNativeTag = Symbol('('.concat('', ')_', (Math.random() + '').toString(36)));

  // 自定义 toString:如果函数已经打上标签,就返回伪造的 native 代码,否则沿用原始行为
  const myToString = function () {
    return (typeof this === 'function' && this[fakeNativeTag]) || $toString.call(this);
  };

  function set_native(obj, key, value) {
    Object.defineProperty(obj, key, {
      enumerable: false,
      configurable: true,
      writable: true,
      value: value
    });
  }

  // 替换 Function.prototype.toString
  delete Function.prototype['toString'];
  set_native(Function.prototype, 'toString', myToString);
  // 保护自定义 toString 本身,使其看起来像原生函数
  set_native(Function.prototype.toString, fakeNativeTag, 'function toString() { [native code] }');

  // 导出一个工具函数,用来给任意函数打上“原生”标记
  globalThis.safefunction = (func) => {
    set_native(func, fakeNativeTag, `function ${func.name || ''}() { [native code] }`);
  };
}).call(globalThis);

// ==================== 第二步:补全基础全局 API ====================
// 定时器相关(这些在 bdms 中经常被检测)
setInterval = function setInterval(){}; safefunction(setInterval);
setTimeout = function setTimeout(){}; safefunction(setTimeout);
window.requestAnimationFrame = function requestAnimationFrame(){}; safefunction(window.requestAnimationFrame);
window.addEventListener = function addEventListener(){}; safefunction(window.addEventListener);

// 窗口及屏幕尺寸(与你提取 bdms 时的浏览器保持一致)
window.outerWidth = 1920;
window.outerHeight = 1040;
window.innerWidth = 1920;
window.innerHeight = 937;

// 抖音安全 SDK 的版本标记(从页面环境中提取得到)
window._sdkGlueVersionMap = {
    "sdkGlueVersion": "1.0.0.64-fix.01",
    "bdmsVersion": "1.0.1.19-fix.01",
    "captchaVersion": "4.0.10"
};

// ==================== 第三步:补全 DOM 相关对象与第三方 SDK ====================
// 极简 DOM 对象实现
span = {classList: {}};
canvas = {getContext: function getContext(){}}; safefunction(canvas.getContext);
document = {
  createElement: function (tag) {
    if (tag === 'span') return span;
    if (tag === 'canvas') return canvas;
    console_log('未处理的 DOM 元素创建:', tag);
    return {};
  },
  documentElement: {},
  createEvent: function createEvent(){}, safefunction(document.createEvent),
  addEventListener: function addEventListener(){}, safefunction(document.addEventListener),
  all: {},
  location: {
    "ancestorOrigins": {},
    "href": "https://www.douyin.com/",
    "origin": "https://www.douyin.com",
    "protocol": "https:",
    "host": "www.douyin.com"
  }
};
safefunction(document.createElement);

// 第三方 SDK 存根(抖音可能会检测浏览器插件或开发环境)
window.CefSharp = { isMock: true, BindObjectAsync: () => Promise.resolve() };
window.eoapi = { mock: true, invoke: () => {} };

// ==================== 第四步:补全其他浏览器对象并伪装 toStringTag ====================
location = document.location;
navigator = {
  "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
  "platform": "Win32",
  "storage": {}
};
history = {};
screen = {availWidth: 1920, availHeight: 1040, width: 1920, height: 1080, colorDepth: 24};
localStorage = {getItem: () => {}, setItem: () => {}}; safefunction(localStorage.getItem);
sessionStorage = {};

// 伪装 Symbol.toStringTag,绕过 Object.prototype.toString.call 的环境检测
const fakeToStringTag = (obj, tag) => {
  Object.defineProperty(obj, Symbol.toStringTag, {
    value: tag,
    configurable: true,
    writable: true
  });
};
fakeToStringTag(document, 'HTMLDocument');
fakeToStringTag(location, 'Location');
fakeToStringTag(navigator, 'Navigator');
fakeToStringTag(history, 'History');
fakeToStringTag(screen, 'Screen');
fakeToStringTag(localStorage, 'Storage');
fakeToStringTag(sessionStorage, 'Storage');

// ==================== 第五步:动态代理兜底 ====================
function createProxy(objName) {
  return new Proxy(window[objName] || {}, {
    get(target, prop) {
      console_log(`[GET] 对象: ${objName}, 属性: ${String(prop)}`);
      return Reflect.get(target, prop);
    },
    set(target, prop, val) {
      console_log(`[SET] 对象: ${objName}, 属性: ${String(prop)}, 值: ${JSON.stringify(val)}`);
      return Reflect.set(target, prop, val);
    }
  });
}
// 只对核心对象挂代理,既能监控缺失属性,又不会严重影响性能
['window', 'document', 'location', 'navigator', 'screen'].forEach(name => {
  window[name] = createProxy(name);
});

// ==================== 第六步:注入 bdms 并初始化 ====================
require('./bdms');  // 将你从抖音提取的 bdms 代码放在同目录下
window.bdms.init({
  aid: 6383,                  // 抖音 Web 应用 ID
  pageId: 6241,               // 页面 ID
  paths: ['^/webcast/', '^/aweme/v1/'],  // 需要签名的接口路径(正则)
  boe: false,
  ddrt: 8.5,
  ic: 8.5
});

// ==================== 第七步:模拟 XHR 触发 a_bogus 生成 ====================
XMLHttpRequest = function XMLHttpRequest() {};
safefunction(XMLHttpRequest);
XMLHttpRequest.prototype.send = function send() {
  console_log('触发 XHR.send,等待加密...');
};
safefunction(XMLHttpRequest.prototype.send);

function get_ab(fullUrlWithParams) {
  const xhr = new XMLHttpRequest();
  // bdms 会通过这两个数组读取请求信息来构造 a_bogus
  xhr.bdmsInvokeList = [
    { args: ['GET', fullUrlWithParams, true] },
    { args: ['Accept', 'application/json, text/plain, */*'] }
  ];
  xhr.invokeList = [
    { name: 'addEventListener', args: ['load', null] },
    { name: 'addEventListener', args: ['error', null] }
  ];
  xhr.send(null);
  return window.a_bogus;
}

Python 端调用

上面的 env.js 在 Node.js 环境下运行并暴露出 get_ab 方法。Python 端可以使用 PyExecJS 来调用它,并构造完整的请求。

import requests
import execjs
from urllib.parse import urlencode

# 从抖音页面中复制的请求头与 Cookie(需要替换为你自己的有效信息)
headers = {
    "accept": "application/json, text/plain, */*",
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
    "referer": "https://www.douyin.com/"
}
cookies = {
    # 替换成你登录后的 Cookie
    "s_v_web_id": "verify_xxx",
    "ttwid": "xxx"
}

# 抖音接口本身的业务参数
base_params = {
    "device_platform": "webapp",
    "aid": "6383",
    "channel": "channel_pc_web",
    "sec_user_id": "MS4wLjABAAAAPs96_XYppoAye-VK57HhjucNZfR_mTyR4KKqDBHdt5k",  # 替换为目标用户ID
    "count": "18",
    "webid": "7486005262718158363"  # 替换为自己当前的 webid
}
url = "https://www.douyin.com/aweme/v1/web/aweme/post/"

# 构建不含 a_bogus 的完整 URL
full_url = url + "?" + urlencode(base_params)

# 编译 JS 并调用生成 a_bogus
ctx = execjs.compile(open('env.js', 'r', encoding='utf-8').read())
a_bogus = ctx.call("get_ab", full_url)
base_params['a_bogus'] = a_bogus

# 发起最终请求
response = requests.get(url, headers=headers, cookies=cookies, params=base_params)
print("响应状态码:", response.status_code)
print("响应内容:", response.text[:500])

完整工作流程

  1. 提取 bdms 代码
    在 Chrome 开发者工具的 Sources 面板中搜索 bdms.init,将相关代码块复制出来,保存为 bdms.js。注意不要遗漏任何依赖模块(若代码被拆分成多个文件,需要合并在同一作用域)。

  2. 配置环境信息
    将上面 env.js 和 Python 脚本中的 webidsec_user_id 以及 Cookie 替换成你自己的有效数据。这些都可以在浏览器中登录抖音后从 Network 面板里直接复制。

  3. 运行生成
    确保 Node.js 和 Python 环境正确,执行 Python 脚本即可输出携带有效 a_bogus 的接口响应。


技术难点与解决方案

难点解决方案效果
函数 toString 被检测重写 Function.prototype.toString,用随机 Symbol 存储伪造的 native 标签完美绕过所有 [native code] 校验
Object.prototype.toString.call 检测环境类型为所有全局对象伪装 Symbol.toStringTag返回类型与真实浏览器一致
安全 SDK 访问了未定义的属性对核心对象挂载 Proxy,在控制台实时打印未覆盖的属性,按需补全显著降低环境补全工作量,并提高稳定性
加密逻辑封装在 SDK 内部,难以剥离通过模拟 XHR 的 bdmsInvokeListinvokeList 触发加密流程无需理解内部算法,即可获取有效签名

注意事项

  1. bdms 版本更新
    抖音会不定期升级安全 SDK。一旦发现 a_bogus 签名失效,需要重新从页面提取最新的 bdms 代码并可能调整环境脚本。

  2. Cookie 有效性
    签名依赖于登录状态的 Cookie(如 s_v_web_idttwid 等)。这些 Cookie 有一定的有效期且与服务端会话关联,过期后需要重新获取。

  3. WebID 与 User‑Agent 一致性
    webid 是根据浏览器指纹(含 User‑Agent、屏幕分辨率等)生成的。替换时必须确保 env.js 中的 User‑Agent 等信息与生成该 webid 时的浏览器完全一致,否则签名可能校验失败。

  4. 请求频率控制
    即使有了正确的签名,过快的请求频率仍然可能触发风控。建议在请求之间加入随机延迟,并尽量避免在高并发场景下使用。