抖音a_bogus参数加密逆向

概述

抖音Web端的核心API(如视频列表、用户主页)请求中,a_bogus是一个动态生成的校验码,负责“拦截”非浏览器发起的非法访问。本文将拆解环境模拟、反检测Hook、SDK加载、XHR模拟生成参数的完整流程,给出可复用的Python+JS方案。


网页分析

(注:原图片保留路径,此处仅简要总结关键步骤)

  1. 打开Chrome开发者工具,筛选XHR/Fetch找到含a_bogus的请求(如/aweme/v1/web/aweme/post/
  2. 在请求发起前下XHR断点或搜索a_bogus关键词,定位加密入口在bdms(字节跳动安全SDK)模块
  3. 分析bdms的依赖项:大量DOM API、浏览器环境检测、函数Native代码验证

核心技术路线

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

  1. 全局环境模拟:补全window/document/navigator等浏览器全局对象
  2. 函数Native伪装:重写Function.prototype.toString绕过函数源检测
  3. 动态代理监控:用Proxy监听bdms未覆盖的属性访问,按需补全
  4. SDK加载与调用:注入bdms代码,初始化后模拟XHR请求触发加密

核心代码实现

1. 浏览器环境与反检测Hook(JS文件:env.js

window = global;
console_log = console.log;

// -------------------------- 第一步:函数Native伪装 --------------------------
(() => {
  'use strict';
  const $toString = Function.toString;
  // 随机生成Symbol,避免冲突
  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的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 --------------------------
// 必须伪装的定时器/异步API
setInterval = function setInterval(){}; safefunction(setInterval);
setTimeout = function setTimeout(){}; safefunction(setTimeout);
window.requestAnimationFrame = function requestAnimationFrame(){}; safefunction(window.requestAnimationFrame);
window.addEventListener = function addEventListener(){}; safefunction(window.addEventListener);

// 窗口/屏幕尺寸(尽量匹配真实浏览器)
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');

// -------------------------- 第五步:动态代理兜底(按需补全bdms访问的属性) --------------------------
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代码保存为bdms.js
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() {
  // 加密后a_bogus会挂载到window上
  console_log('触发XHR.send,等待加密...');
};
safefunction(XMLHttpRequest.prototype.send);

function get_ab(fullUrlWithParams) {
  const xhr = new XMLHttpRequest();
  // bdms会读取这两个数组获取请求信息
  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;
}

2. Python调用JS生成a_bogus

import requests
import execjs
from urllib.parse import urlencode

# 从抖音页面复制的请求头和Cookie(需替换为自己的有效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代码中的webid/sec_user_id/Cookie为自己的有效信息
  3. 运行Python脚本:调用JS生成a_bogus,发起API请求获取数据

技术难点与解决方案

难点解决方案效果
函数toString检测重写Function.prototype.toString,用随机Symbol存储伪造的native标签完美绕过函数源检测
Object.prototype.toString.call环境检测伪装全局对象的Symbol.toStringTag符合浏览器环境的类型判断
动态未覆盖的属性访问Proxy监听核心对象的属性访问,按需补全减少环境补全的工作量
加密逻辑封装在SDK中模拟XHR的bdmsInvokeListinvokeList触发加密无需深入解析SDK的加密算法

注意事项

  1. bdms代码更新:抖音会不定期更新bdms,需及时重新提取
  2. Cookie有效性:需使用登录后的Cookie,且不能频繁请求
  3. WebID和UserAgent匹配:WebID是根据UserAgent和设备信息生成的,需保持一致