今日头条a_bogus参数加密逆向

概述

字节跳动系的反爬参数一脉相承,x-bogusa_bogus分别适配移动端/部分产品端与PC端核心业务。本文以PC端头条新闻feed流接口为例,分享补全浏览器环境+混淆代码调试的逆向方法,最终生成可通过验证的a_bogus参数,附带Python调用demo。

网页分析(抓包+断点)

我们先通过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. 补全模拟浏览器环境对象(核心)
  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