toutiao-abogus-reverse

概述

字节跳动系产品的反爬策略高度统一,x-bogusa_bogus 系列参数分别服务于移动端/部分产品与 PC 端核心业务。本文以 PC 端头条新闻 feed 流接口为例,分享一种补全浏览器环境 + 调试混淆代码的逆向方法,最终在 Node.js 环境中生成通过验证的 a_bogus 参数,并提供 Python 调用示例。

网页分析(抓包与断点)

通过 Chrome 开发者工具可以梳理出加密调用的完整链路:

  1. 定位加密参数
    打开 PC 端首页并滚动加载,在 Network 面板中过滤 feed 找到目标接口,可以看到 Query String Parameters 中包含动态生成的 a_bogus

    F12 Network面板:找到feed接口并定位a_bogus加密参数

  2. 全局搜索定位调用点
    在 Sources 面板中全局搜索 a_bogus,会发现它出现在某个发起网络请求的封装函数里。打上断点后继续调试,可以追踪到它最终由另一个混淆函数返回。

  3. 找到混淆核心文件
    继续沿调用栈追踪,最终会锁定到动态加载的 bdms.js 文件——这正是字节跳动常用的混淆指纹与加密脚本。

    全局搜索a_bogus定位调用函数

技术要点

核心难点

  • a_bogus 是基于浏览器指纹 + 请求参数动态生成的值
  • bdms.js 使用了高度混淆的 JavaScript 代码
  • 混淆逻辑会主动检测当前是否为真实浏览器环境

解决方案

  1. 在 Node.js 中补全模拟浏览器环境对象(最关键)
  2. 逆向梳理加密调用链的关键参数
  3. 使用 Proxy 代理监控属性/方法调用,辅助补全缺失环境

环境补全实现

基础环境配置

首先初始化 Node.js 环境下的核心全局对象 window,并模拟基础的窗口属性、SDK 版本等静态信息:

window = global;
// 补全基础浏览器回调与标签构造函数
window.requestAnimationFrame = function() {};
window.HTMLSpanElement = function() {};
window.EventSource = function() {};
window.XMLHttpRequest = function() {};

// 补全窗口尺寸与位置(需要与抓包时的真实浏览器保持一致)
window.innerWidth = 1920;
window.innerHeight = 331;
window.outerWidth = 1920;
window.outerHeight = 1040;
window.screenX = 0;
window.screenY = 0;
window.pageYOffset = 0;

// 补全 SDK 版本信息(固定值或从真实页面获取)
window._sdkGlueVersionMap = {
    "sdkGlueVersion": "1.0.0.55",
    "bdmsVersion": "1.0.1.7",
    "captchaVersion": "4.0.2"
};

存储对象模拟

localStoragesessionStorage 中存储的缓存数据(特别是 __tea_* 开头的头条统计 token)对环境的完整性验证非常重要:

// 补全 localStorage(token 类数据从真实页面复制即可,无需动态更新)
span = { classList: {} };
localStorage = {
    "__tea_cache_first_2018": "1",
    "__tea_cache_tokens_2018": "{\"web_id\":\"7530833203905971739\",\"user_unique_id\":\"verify_mdi6arfb_JhCmehzG_uZlV_4Uni_95YM_SHlmdFBh8evy\",\"timestamp\":1755420203978,\"_type_\":\"default\"}",
    "__tea_cache_tokens_24": "{\"web_id\":\"7530833195744642610\",\"user_unique_id\":\"7530833195744642610\",\"timestamp\":1755422052831,\"_type_\":\"default\"}",
    "ttcid": "7ddaeb4ae85c4ad3a97a1f5a20f3128a13",
    // 其余非核心缓存可以简化或省略
    getItem: function() {},
    removeItem: function() {}
};

// 补全 sessionStorage
sessionStorage = {
    "__tea_session_id_24": "{\"sessionId\":\"ea368e47-fcc8-4784-b10c-cc8bfe4b1072\",\"timestamp\":1755422548963}",
    getItem: function() {},
    removeItem: function() {}
};

DOM 与其他对象模拟

继续补全 documentnavigator 等 DOM 核心对象中必需的属性与方法:

document = {
    cookie: 'ttcid=7ddaeb4ae85c4ad3a97a1f5a20f3128a13; csrftoken=d50380b8fd876691377e1784f520d987; tt_scid=QqnVBu5PLTT5IYFV0LBt6D0i8IEcZ17Aebn86VJcNa3TruYKbChQ7VMnJCqf4e3-2205',
    createElement: function(tag) {
        if (tag === 'span') return span;
        return {};
    },
    referrer: 'https://www.toutiao.com/?wid=1753408727185',
    documentElement: {},
    createEvent: function() {},
    all: {}
};

navigator = {
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36'
};

screen = {};
history = {};
location = {};

代理监控系统

为什么需要代理?

混淆后的 bdms.js 会悄悄访问大量浏览器属性(例如 canvas.toDataURL()window.screen.colorDepth 等)。如果只补充静态值,很容易遗漏某些被动态调用的对象。利用 Proxy 可以实时打印出所有被访问的对象、属性及方法,帮助快速定位缺失的环境。

代理实现函数

function setProxy(proxyObjArr) {
    for (let i = 0; i < proxyObjArr.length; i++) {
        const handler = `{
            get: function(target, property, receiver) {
                console.log("[GET] 对象:", "${proxyObjArr[i]}", "属性:", property);
                return target[property];
            },
            set: function(target, property, value, receiver) {
                console.log("[SET] 对象:", "${proxyObjArr[i]}", "属性:", property, "值:", value);
                return Reflect.set(...arguments);
            }
        }`;
        eval(`try {
            ${proxyObjArr[i]};
            ${proxyObjArr[i]} = new Proxy(${proxyObjArr[i]}, ${handler});
        } catch (e) {
            ${proxyObjArr[i]} = {};
            ${proxyObjArr[i]} = new Proxy(${proxyObjArr[i]}, ${handler});
        }`);
    }
}

// 初始代理配置(先代理 window 和 canvas,后续根据打印结果补充)
proxy_array = ['window', 'canvas'];
setProxy(proxy_array);

加密参数生成

加密调用链梳理

通过断点深入 bdms.js,最终会发现两个核心变量:

  • window._U._v:基础配置参数数组
  • window._U._u:核心加密函数

get_a_bogus 封装

在补全环境并引入混淆文件之后,即可封装生成函数:

// 先加载混淆的 bdms.js 文件(注意路径)
require("./bdms");

function get_a_bogus(queryStr) {
    // 构造加密所需的参数数组(顺序固定)
    const args_1 = [
        0,          // 固定参数1
        1,          // 固定参数2
        14,         // 固定参数3
        queryStr,   // 需要加密的请求参数(URL 编码后的完整 Query String)
        "",         // 空字符串(通常为额外签名,暂不需填写)
        navigator.userAgent // 浏览器 UA
    ];

    // 获取基础配置
    const r = window._U._v;
    // 调用核心加密函数
    const a_bogus = window._U._u(r[0], args_1, r[1], r[2], null);
    return a_bogus;
}

完整 Python 调用流程

将上述的环境补全、代理监控(可选,正式使用时可以注释掉)以及加密函数封装到同一个 env.js 文件中,然后通过 Python 的 execjs 库进行调用。

Python Demo 代码

import requests
import execjs
from urllib.parse import urlencode

# 1. 准备请求参数(与真实页面抓包参数保持一致)
headers = {
    "accept": "application/json, text/plain, */*",
    "accept-language": "zh-CN,zh;q=0.9",
    "cache-control": "no-cache",
    "pragma": "no-cache",
    "priority": "u=1, i",
    "referer": "https://www.toutiao.com/?wid=1753408727185",
    "sec-ch-ua": "\"Not;A=Brand\";v=\"99\", \"Google Chrome\";v=\"139\", \"Chromium\";v=\"139\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\"",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
}
cookies = {
    "tt_webid": "7530833195744642610",
    "ttcid": "7ddaeb4ae85c4ad3a97a1f5a20f3128a13",
    "csrftoken": "d50380b8fd876691377e1784f520d987",
    "s_v_web_id": "verify_mdi6arfb_JhCmehzG_uZlV_4Uni_95YM_SHlmdFBh8evy",
    "ttwid": "1%7CBXcBsVkKaGJhR8z2otQMXZQ5KyemAE5rZCYmMW7X7Yo%7C1755501182%7C4f426bea4b4347b5a88ccf4f952936fe836cf82bc83a9357735980f36356d1ae"
}
url = "https://www.toutiao.com/api/pc/list/feed"
params = {
    "offset": "0",
    "channel_id": "94349549395",
    "max_behot_time": "0",
    "category": "pc_profile_channel",
    "disable_raw_data": "true",
    "aid": "24",
    "app_name": "toutiao_web",
    "msToken": "rbgPJm26nRNMyXMIjHJoEN2mBaX7RApOkC9YcHYHGSprXqztmiSBt7y7Du9SXXLIrPg3UDzjloBJzp8sSNWZbHPDXzz2qjyc3Dryr0WO07LhQf8qR"
}
# 生成 a_bogus
query_string = urlencode(params)
ctx = execjs.compile(open('env.js', encoding='utf-8').read())
a_bogus = ctx.call('get_a_bogus', query_string)

# 补全参数并发起请求
params['a_bogus'] = a_bogus
response = requests.get(url, headers=headers, cookies=cookies, params=params)
print(response.status_code)
print(response.text[:200])  # 只打印部分响应,验证是否成功

小结

逆向 a_bogus 的核心思路在于补全 Node.js 环境的浏览器指纹。通过 Proxy 监控可以高效定位 bdms.js 中访问的所有属性,逐步完善环境后即可在本地稳定生成加密参数。整个流程可以无缝集成到爬虫项目中,实现自动化的数据采集。

提示:由于头条的加密策略会动态更新,本文的示例代码可能随时间失效,请以实际逆向为准,并注意合理使用,遵守平台规则。