版本:13.1.1.502547155(2026-10-09) 方法:纯静态分析,Electron 渲染层/主进程 bundle 逆向 结论先行:直播伴侣客户端不判定违规,它是一台“接收服务端判决并渲染出来”的终端。
一、先说清楚我分析的是什么,以及不是什么
某音直播伴侣是 Electron 应用,主程序 直播伴侣.exe 之外还带了一整套自研音视频栈(MediaSDK、Lynx 跨端引擎、Byte-Guard 安全 SDK)。本文只覆盖 resources/app/app.content/ 这一层——也就是 JS bundle。
有几个看起来像但其实不是的东西 worth先排除,否则很容易在错误的分支上浪费时间:
| 疑似物 | 真相 |
|---|---|
@bytegecko(原生 addon + gecko.config.json) |
Argus / Byte-Gurd 反篡改 SDK,给 Lynx 动态模板做签名校验。与内容审核无关。全库 534 处 “gecko” 命中全部误报 |
model.zip(59.6MB,algo_*.model) |
美颜算法模型(磨皮、高光) |
loki.zip(46MB,AmgBeautyPrefab / LUT / warpX) |
美颜引擎 + 人脸形状模型 |
| “敏感词”功能 | 实际是屏蔽词——主播自己配的弹幕过滤,服务端执行 |
manifest.json 里的 strict_min_version |
仅校验 JS 资源版本完整性 |
一个 900MB 的安装目录里,能被误判为“审核组件”的东西至少有 5 个。逆向的第一课是排除法。
二、核心发现:违规字段的来源
客户端渲染处罚弹窗的函数(resource/js/9138.0011714a.js @542483)解构了服务端下发的完整判决:
const {
ban_type, // 处罚类型
ban_reason, // 违规原因
ban_adjust_advice, // 整改建议
is_banned_forever, // 是否永久封禁
ban_duration, ban_time, // 处罚时长 / 起始时间
appeal_deadline_time, // 申诉截止
webcast_appeal_status, // 申诉状态
left_un_block_times, // 剩余解封次数
is_during_live, // 是否直播中
ban_time_left, // 剩余封禁秒数
early_warning_text, // 预警文案
ban_detail_schema, // 违规详情页 schema
log_id, // 申诉凭证
} = e;
这十四个字段里没有一个是本地计算出来的。 全部来自 HTTP 接口返回,或 WebSocket 推送。
这决定了整个架构的性质:伴侣是判决的执行终端,不是判决的产生者。
三、开播前的三道闸
客户端调 create_room,服务端返回 status_code,客户端按码分支。真正的审核相关分支:
封禁类:10018 / 20006 / 4003035
ban_type = resp.ban_type
├─ ban_type ∈ [2,19,20,21] → POST /webcast/review/get_live_assistant_window/ (直播助理引导)
└─ 否则:
├─ GET /webcast/review/get_latest_ban_record/ → 取 log_id_str
└─ GET /webcast/review/get_ban_record/ → 取完整判决
附带 early_warning_text / ban_time_left / ban_detail_schema
注意 ban_type 的语义分支:1 是账号封禁(“此账号已被封禁,封禁账号无法直播和登录”),21 对应电商 APP_ID(“直播伴侣”而非“直播”)。封禁文案里还有一条路径:
“可前往手机端「主播中心-违规记录」入口完成直播行为规范学习,可获得自动解封机会”
——即学习抵扣违规次数。
人脸核身:4003028 / 4003173
ipcRenderer.send("TOGGLE_FACE_DETECT", {
url: resp.web_auth_address, ticket, scene, faceType, from: "status_code"
});
return { code: Error, error: FaceRecognitionError };
这是身份核验,不是内容审核。别把它算进“违规检测”。
健康分:4003163
block_dialog_info 里带 scene / type:
HealthScoreCheck+ 普通警告 → 弹“礼物收入功能被关闭”,勾选“我已知悉”后可继续开播LivingEducationCheck+StrongWarning→ 强制观看学习视频,/webcast/review/education/get_task_record_status轮询is_pass
这是“以罚代教”的机制设计。
四、直播中的四条控制指令
直播中走 WebSocket WebcastControlMessage,control_type 分派(@866200 起):
| control_type | 常量 | 客户端行为 |
|---|---|---|
2 |
LIVING_PAUSE |
弹”直播流已屏蔽“,10 秒倒计时,按钮文案”调整完毕,进入人工审核” → POST /webcast/room/auditing/apply/ |
3 |
LIVING_RESUME |
恢复直播,manualReviewResultEvent({result:"pass"}) |
4 |
MESSAGE_TIPS |
警告 / 违规提醒,punishPopupShowEvent({type:"warn"}) |
5 |
EMPTY_COVER |
空封面 |
11 |
DARK_VEST |
马甲 / 隐身 |
14 |
OVER_LIVING |
关播 |
LIVING_PAUSE → LIVING_RESUME 这对指令构成了完整的实时熔断 + 申诉窗口:
服务端检测到风险
↓ WebcastControlMessage(LIVING_PAUSE)
客户端遮蔽直播流,弹违规通知
↓ 主播点击"调整完毕" / 10秒超时
POST /webcast/room/auditing/apply/ → 进入人工审核队列
↓ 审核结果
├─ 通过 → LIVING_RESUME,"屏蔽结束,已恢复直播"
└─ 不通过 → manualReviewResultEvent({result:"notpass"}) + 断播
“直播流已屏蔽”而非“直接断播”是个精妙的产品设计——它给主播 10 秒自我修正窗口,把大部分误伤消化在客户端,不产生处罚记录。
五、所以违规到底谁在判?
把上面所有链路合起来看:
┌─────────────────────────────────────┐
│ 抖音服务端流处理集群 │
│ 画面 / 音频 / 话术 / 商品 识别 │
└──────────────┬──────────────────────┘
│ 判决(HTTP / WebSocket)
┌──────────────▼──────────────────────┐
│ 直播伴侣客户端 │
│ · 人脸核身(身份) │
│ · 协议/实名/学习确认(合规) │
│ · 结果呈现(弹窗、倒计时、申诉) │
└─────────────────────────────────────┘
客户端做的只有三件事:身份核验、合规确认、结果呈现。没有任何一个字节的视频/音频数据在本机被用于内容判定。
六、支线一:第三方工具检测(反作弊)
这是全库最有技术含量的一段。它的判据不是进程名单,而是**“这个房间的比特到底是谁发的”**。
主进程 HackPerformanceHandler(module 87262):
async getKeys(e, {sdk}) {
if (++this.freCounter % 6 !== 0) return []; // 1/6 采样
const { pushUrl, roomId, isLiving, liveMode } = await this.getRoomInfo(e);
if (!pushUrl || this.hackUrlsObj[pushUrl] || !isLiving) return [];
const tcpTable = await this.getTcpTable(); // system/network/tcp_table/ipv4
const cdnIPs = await getDnsAddress(parse(pushUrl).hostname);
const { bitrate, sendFps } = getPerformanceActiveStatistics();
const hits = tcpTable.filter(c =>
cdnIPs.includes(c.remoteIp) && c.state === "ESTABLISHED");
for (const conn of hits) {
const { appName, appPath } = await this.getPidInfo(conn.processId);
if (appName && appPath
&& appName !== "MediaSDK_Server.exe" // ① 不是我
&& bitrate <= 2 // ② 我几乎没在推
&& sendFps <= 2) { // 我几乎没在发
this.hackUrlsObj[pushUrl] = 1; // 同一流只报一次
this.reportHack({roomId, appName, appPath, remotePort: conn.remotePort, ...});
if (!this.isWhitelistPort(conn.remotePort) || liveMode === OBS) {
global.is_injected = 2; // ③ 判定第三方推流
}
}
}
sdk.mediasdk.protosdk.call.msapi.SetHackPushStream({ hackEnabled });
}
算法精髓
- 取自己房间的
rtmp_push_url,DNS 解析出推流 CDN 的 IP 集合 - 枚举全机 IPv4 TCP 连接表,筛出“连到同一批 CDN 且 ESTABLISHED”的
- 关键判据:伴侣自己
bitrate ≤ 2 kbps && sendFps ≤ 2(几乎静默),但别的进程连着同一批 CDN → 那个人才是真正在推流 process/{pid}/path反查进程,剔除自己的MediaSDK_Server.exe
这是纯行为特征检测,伪装进程名、改端口都无效——只要比特在别人机器上往外走,就会暴露。
渲染层还有一道端口闸
if (liveMode !== OBS && r.room_id && r.hack_exe && 1935 === Number(r.hack_remote_port))
1935 是 RTMP 端口。 弹窗只认这个端口。主进程上报所有端口,渲染层筛出 1935 才提示。
两个分支
| 条件 | 行为 |
|---|---|
首次命中 count===1 |
弹“直播风险提示 / 检测到您当前正在使用第三方工具或插件篡改直播流开播”,附官方开播指南链接 |
| 小玩法场景 | 读服务端配置 minigameHack.moreConfig,若 open 且 hack_exe ∉ bls(黑名单)→ 5 秒倒计时后自动关播 |
上报
明确字段(CONNECT_START 埋点):
{ device_id, stream_name, is_third_push: global.is_injected === 2 ? 1 : 0 }
global.is_injected 初值 -1(未知)/ 0(否)/ 2(是)。另有本地遥测 __TEA__.collect("livesdk_hack_push_detect", {hack_exe, hack_exe_path, hack_remote_port, code_rate, send_fps, live_mode})。
七、支线二:OBS 模式与一个反直觉的细节
直播伴侣内置了完整的 OBS 接入:起本地 nginx.exe 做 RTMP 中继,给用户 rtmp://127.0.0.1:1935/live,并轮询 nginx 的 /stat 端点读取 meta.video.codec,强制要求 H264。
入口由服务端白名单控制:
const res = await callAPI({ api: "/webcast/room/get_pc_obs_status/" });
if (res.data.pc_obs_status !== 1) throw new Error("权限校验失败");
setObsVisible(true); // 只有 pc_obs_status===1 才渲染 OBS 选项
但反作弊代码里:
if (!isWhitelistPort(remotePort) || liveMode === LIVE_MODE.OBS) {
global.is_injected = 2;
}
即便官方放行 OBS 模式,is_third_push=1 照样上报。官方 OBS 通路 = 弹窗豁免,不豁免上报。
这个细节比任何规则条文都更能说明平台的真实意图。
八、本地采集面(顺带扒到的)
客户端自研了一套结构化 collector 系统,21 个命名采集组、70 条采集路径:
| 类别 | 代表路径 |
|---|---|
| 网络 | system/network/tcp_table/ipv4、ping/*、mtu/* |
| 进程 | process/{pid}/{name,path,command_line,module_list,subprocess}、system/process_list、system/top/N/* |
| 设备指纹 | system/device/fingerprint、.../original_wmi、system/cpu/smbios/max_clock_speed |
| 硬件 | system/gpu/*、system/display_config/info、system/monitor/info、system/device/drivers |
| 摄像头归属 | system/camera/using_process/{devId}\n{devName} |
| 完整性 | v_live_get_files_hash、v_live_signature_info |
设备指纹抓 baseboardSerialNumber / systemUuid / systemDiskSerialNumber / networkAdapterMac / memoryDevices[],并且同时取一份 original_wmi 做交叉比对(旧 WMI 值 vs 新值,防篡改)。
摄像头归属是第二个第三方工具检测向量——返回码:
CAMERA_CREATE_ERROR_CODE = { IS_OCCUPIED:4, EMPTY_EXE:-5, UNKNOWN_EXE_NULL:-9,
PHYSICAL_NAME:-3, UNINIT:-1, TIMEOUT:-4 }
用户可见文案 "摄像头被【$processName】占用",会点名具体进程。
另有 Metasec(字节安全 SDK)双模式运行:spawn metasec.exe 或加载 electron-metasec.node + metasecml.dll,JSB 通道名 METASEC,每 5 分钟 report({scene_name:"live"})。
九、局限与没做出来的事
必须诚实标注:
- 纯静态分析,无运行时验证。所有结论来自 bundle 字面量与控制流,不是抓包。
- 原生层零覆盖。
metasecml.dll(7.1MB)、MediaSDK_Server.exe(18.7MB)、gecko.node(2.5MB) 一个字节没看——分析环境缺 Ghidra/Hopper/IDA。所以SetHackPushStream的真实实现、metasec 的采集项、Argus 的校验细节全部是未知。 - 对服务端一无所知。 本文说“判定在服务端”,依据是客户端字段来源,不是对服务端的观测。
- 没有复现。 没有实际开播触发过
LIVING_PAUSE或 hack 检测弹窗。 - 未分析 Electron 主进程与渲染进程间的信任边界(
sender_validation_observations: 1)。
十、几点方法论感想
- 排除法比正向搜索省时间。 900MB 目录里 5 个“疑似审核组件”,先确认它们不是,比直接追真身快得多。
- 压缩 bundle 里按字节偏移抠上下文,比正则全局搜索有效。 单行 2.7MB 的文件,正则会命中几千个噪声点;定位到偏移量后 300 字符窗口就够看清意图。
- 判别“谁在决策”要看字段来源。 找到一个函数在处理
ban_reason,第一反应应该是“这个字段从哪来”,而不是“这个函数怎么判的”。 - “屏蔽 10 秒再进人工审核”和“多次同类违规加重处罚”这类产品设计,比技术细节更能解释平台意图。
责任使用声明 本文仅为对本地已安装软件的技术分析记录,用于理解客户端架构与学习逆向方法。文中接口路径、字段名与检测机制不应被用于规避平台风控或绕过第三方工具检测。 平台规则以官方公示为准;如有争议,请通过官方申诉渠道处理(客户端封禁弹窗即提供申诉入口)。 本次逆向仅出于学习目的,不用于任何商业项目,如有侵权请联系删除。630110598@qq.com