🛑 勘误(2026-05-14 11:55):本报告原标题为"6 层协议位欺骗",将 SunnyNet 的 WebSocket 拦截能力作为第⑤层"已覆盖"结论。该论断站不住脚 ——
a_SunnyNet__ws_callback这个 Nuitka mangled 名只要 Python 端class SunnyNet定义了这个属性就会出现在二进制字符串表里,并不能反推出 query_tool 实际给 ws_callback 注册了业务回调。本版降级为 5 层确认 + 1 附录未确认,剔除推测性结论,保留作为后续待验证方向。核心论断(修订后):这套工具是一个沿协议栈纵向覆盖至少 5 个不同层位的字段替换引擎。任意一个层位被抖音风控盯死都不会让整条链路失败 —— 因为还有其他层在同步伪造同一套抖音身份。这是这套工具能稳定运行的真正原因,而不是任何单一 hook 的精妙。
本报告基于 2026-05-14 完成的:
- Ghidra 11.3.2 headless 对 rtmp-hook.dll / mate-launcher.exe / query_tool.dll (15MB Nuitka) 的全量反编译
- ilspycmd 对 画面去重.dll 的 .NET 8 还原
- Sandbox runtime OpenProcess + ReadProcessMemory dump(MediaSDK_Server.exe PID 13472)字节级闭环验证
- query_tool.dll Nuitka 字符串审计(从 mangled 标识符反推 Python 类结构)
- 沙盒 SSH 直读 D:\BaiduNetdiskDownload\config.ini 实际字段
完整证据见 08_bonus.md §9、07_external_connections.md §5、decompile/verify_*.py、decompile/verify_hook_sandbox.ps1。
┌─────────────────────────────────────────────────┐
│ 直播伴侣 webcast_mate (Electron) │
│ v12.0.3.358280890 │
│ C:\Program Files (x86)\webcast_mate\ │
└────┬──────────────┬──────────────────┬──────────┘
│ │ │
┌────────────────┘ │ └─────────────────┐
│ │ │
▼ ▼ ▼
StreamPlugin.dll libssl-3-x64.dll MediaSDK_Server.exe
(FLV/RTMP 封装) (OpenSSL 3.x) (推流主进程)
│ │ │
│ ① onMetaData tag │ ② SSL_write payload │ ⑤ .rdata literals
│ build_meta(meta_struct) │ SSL_write(ssl, buf, len) │ "windows" string
│ │ │
└──── HOOKED by rtmp-hook ─────┴─── HOOKED by rtmp-hook ──────────── HOOKED by rtmp-hook
[STR-PATCH] segment
┌────────────────────────────────────────────────────────────┐
│ query_tool.dll (Nuitka onefile / SunnyNet MITM) │
│ ③ HTTPEvent.http_callback (webcast REST API Cookie) │
│ ④ HTTPSetH2Config + HTTPSetRandomTLS (协议指纹) │
│ ────────────────────────────────────────────────── │
│ [附录 A] QueryTool.{tcp,ws,udp}_callback 方法存在 │
│ 但 Nuitka 编译后方法体未反编译,行为未验证 │
└────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 抖音后端 (amemv.com / *.snssdk) │
└───────────────────────────────────┘
5 层中每一层都对应抖音侧具体的、独立的检测点。下面逐层展开。
| 项 | 内容 |
|---|---|
| 协议位置 | RTMP 推流前,FLV 容器封装阶段:StreamPlugin.dll 把视频元信息打包成 AMF0 数据 tag |
| 触发时机 | 每次推流开始 + 推流过程中码率/帧率发生波动时 |
| 改写器 | rtmp-hook.dll!FUN_180001710 (build_meta_replacement) |
| Hook 入口 | rtmp-hook.dll!FUN_180001a70 (install_meta_hook) — .rdata 找 "audiocodecid" 字面量 + LEA RIP-相对 xref + prologue qword 0x38ec834857415640 三重校验 |
| 改的字段 | 8 个标准 RTMP AMF0 字段:width / height / framerate / videocodecid / audiocodecid / videodatarate / audiodatarate / duration |
| jitter 算法 | videodatarate ±5% / audiodatarate ±3% / framerate ±0.3 / duration 强制 0,PRNG 用 QPC.LowPart ^ TID 混合后 LCG,每次调用都新采样 |
| 常数验证 | .rdata @ 0x180006000=0.05 / 0x180005ff8=0.03 / 0x180006008=0.3(IEEE754 字节验证 ✓) |
| Sandbox runtime 状态 | DAT_180008f1c (call counter) = 1 ✓ 已触发 |
| 对应抖音检测点 | 平台对录播流的指纹识别:录播切片每次的码率/帧率应该完全一样,jitter 之后每次推流参数都不同,让"哈希帧"算法失效 |
| 项 | 内容 |
|---|---|
| 协议位置 | TLS 握手已完成后,OpenSSL SSL_write(ssl, buf, len) 调用前 —— 这是明文 RTMP wire format最后一次能被读写的窗口 |
| 触发时机 | 每个出站 RTMP 包(onMetaData / @setDataFrame / 视频 tag / 音频 tag) |
| 改写器链 | ssl_write_stub_0..3 → ssl_write_common_handler (0x180002dc0) → FUN_180003260 (dispatcher) → FUN_180002fd0 (AMF0 type-aware byte replacer) |
| Hook 入口 | DllMain 内 CreateToolhelp32Snapshot 遍历所有模块,每个模块 GetProcAddress("SSL_write") / GetProcAddress("SSL_write_ex"),最多 4 个 slot |
| 改的字段 | config.ini[metadata] 全部 64 项(实测 sandbox 30 项已加载),含:flashVer / fp_coderate / fp_nb / fp_vmode / fp_amode / p_id / s_id / h_id / e_id / ab / fp_dispatch_expr_tag / publish_time_stamp / link_info / platform / model / os_version / sdk_version / encoder / major_brand / minor_version / compatible_brands / ... |
| 算法 | 在 buffer 里 memcmp 搜 [u16-BE keylen][key bytes] pattern → 第 1 个字节读 AMF0 type marker → 0x00 Number (8B BE double, atof) / 0x01 Bool (1B, "true"/"1" → 0x01) / 0x02 String ([u16-BE oldlen] 后字节,新值不能放大) |
| 数据来源 | rtmp-hook.dll!FUN_180003490 (config.ini 解析器) 在 DllMain 阶段一次性把 config.ini[metadata] 的 64 项装到 DAT_180009340 起的内存表(每项 0x240 字节,key@+0 / value@+0x40) |
| Sandbox runtime 状态 | SSL hook slot count = 2(libssl-3-x64.dll:SSL_write + SSL_write_ex 都已装),counter=0(还没推流但 hook 已就位) |
| 对应抖音检测点 | 抖音 webcast_mate 后端会读 RTMP 流里作者自定义的私有 AMF0 字段做设备/会话绑定校验。p_id / s_id / link_info 是会话级 token,fp_coderate / fp_nb 是移动端硬件描述,platform / model / sdk_version 是设备指纹。任何一个对不上 → 后端把这条流标记成"伪装客户端"或直接断流 |
关键观察:StreamPlugin.dll 编译时没有导出 SSL_write(静态链接 OpenSSL),所以 rtmp-hook 用 FUN_1800028b0 即 [BORING] 路径做"靠 .rdata 里 "SSL_write" 字面量 + LEA xref + 6 种 prologue 指纹反查"的内联函数定位(实测 sandbox 没命中内联路径 —— webcast_mate 现在用动态加载的 libssl-3-x64.dll 而非内联)。
| 项 | 内容 |
|---|---|
| 协议位置 | webcast_mate Electron 进程发起的 HTTPS API 调用,SunnyNet MITM 在代理层拦截(SunnyNetSetPort 监听端口 + Reqable CA 已注入 certifi cacert.pem) |
| 触发时机 | mate 启动时同步房间状态、检查权限、获取房间介绍 |
| 改写器 | query_tool.dll → QueryTool.http_callback (Python 业务回调,Nuitka 编译进 .text),反编译输出 uQueryTool.http_callback 字符串 + <locals>.<lambda> 闭包证明这是 query_tool 自定义业务实现(非 SunnyNet 通用 stub) |
| 被劫持的 4 个 endpoint | webcast5-mate-hl.amemv.com/webcast/room/get_latest_room/ / webcast/user/permission / /webcast/anchor/room_intro/get_room_intro/ / /webcast/room/check_exist |
| 可改字段 | Cookie 头(sessionid_ss / sessionid / s_v_web_id / tt_csrf_token / odin_tt / sid_guard / ttwid / msToken / ...)、X-* 自定义头、User-Agent、Request body、Response body |
| HTTPEvent 类的属性 | client_ip / error / event_type / is_debug / method / pid / request / response / sunny_net_context / theology_id / url / message_id —— 12 个属性,由 SunnyNet C 端回调注入(注意:HTTPEvent 是 SunnyNet 通用类,证明 SunnyNet 装备了拦截能力,但只有 QueryTool.http_callback 是业务回调证据) |
| SunnyNet API 调用面 | HTTPGetHeader / HTTPSetHeader / HTTPGetBody / HTTPGetBodyLen / HTTPGetCode / HTTPSetRedirect / HTTPSetServerIP / HTTPSetOutRouterIP |
| User-Agent 伪装 | webcast_mate/11.7.1 Chrome/108.0.5359.215 Electron/22.3.18-tt.11.release.main.104 TTElectron/...(v11.7.1,沙盒实装 v12.0.3,差一个大版本) |
| 对应抖音检测点 | 抖音 webcast 后端用 Cookie 做账号绑定 + 设备指纹。tt_csrf_token 是 CSRF 防护、sid_guard 是会话守卫、ttwid 是设备 ID。这些字段单独看就是普通直播伴侣调用 webcast 时携带的合法 Cookie —— 把 PC 端注入的 Cookie 改写成手机 mate 的样式,让 API 调用看起来像移动端发出 |
| 项 | 内容 |
|---|---|
| 协议位置 | TLS ClientHello + HTTP/2 SETTINGS / PRIORITY 帧,握手协议的协议指纹层 |
| 触发时机 | 每次 webcast API HTTPS 连接建立时 |
| 改写器 | query_tool.dll → HTTPSetH2Config + HTTPSetRandomTLS(SunnyNet 内嵌 curl_cffi-style 客户端) |
| HTTP/2 fingerprint 模板 (8 种) | Chrome_103_105 / Chrome_106_116 / Chrome_117_120_124 / Firefox / Opera / Safari / Safari_IOS_16_0 / Safari_IOS_17_0 |
| TLS 指纹手段 | random_ja3 / assert_fingerprint / proxy_assert_fingerprint —— TLS ClientHello 的 cipher suite 顺序、扩展列表、椭圆曲线列表等都按所选模板生成 |
| 关键意义 | 这一层是 §08_bonus.md §5 反思路里之前留下的最后漏洞 — 我们曾说"PC 原生 OpenSSL 3.x 的 JA3 指纹 vs iOS Safari BoringSSL 完全不同,平台能识别"。作者用 curl_cffi 风格反制了:在 SunnyNet MITM 出站时主动按所选模板重写 ClientHello,让目标服务器看到的 JA3 就是 Chrome 117 / Safari iOS 17 等真实浏览器的指纹 |
| 对应抖音检测点 | 抖音和 ByteDance 内部多个产品都用 JA3/JA4 做API 滥用检测(识别脚本/爬虫/自动化)。原生 Python requests 的 JA3 是 OpenSSL Python 默认值,一抓一个准;curl_cffi 模板能让 JA3 落在主流浏览器的 fingerprint 库内 |
| 项 | 内容 |
|---|---|
| 协议位置 | webcast_mate 进程加载 StreamPlugin.dll 后,直接修改 StreamPlugin.dll 的 .rdata 段字面量 |
| 触发时机 | rtmp-hook 的 build_meta hook 安装成功后,install_meta_hook 函数同步做的 |
| 改写器 | rtmp-hook.dll!FUN_180001a70 内的 [STR-PATCH] 段(不是单独函数,是 install_meta_hook 顺带做的) |
| 源字符串(硬编码) | "windows"(搜索目标) |
| 目标字符串 | 从 config.ini[metadata].platform 读 —— sandbox 实测改为 "Android" |
| 实现 | 遍历 PE 所有 section,memcmp 找 "windows\0",VirtualProtect(PAGE_EXECUTE_READWRITE) → memcpy(addr, new_value, len) → memset(addr+len, 0, oldlen-newlen+1) → 还原 VirtualProtect |
| 沙盒证据 | rtmp_debug_*.txt 日志输出 [STR-PATCH] Patched "windows" -> "Android" OK ✓ |
| 限制 | 新值不能比 "windows"(7字节) 长,所以能写 "Android"(7字节)、"iOS"(3字节,补 NUL),但不能写 "BetterWindows"(13字节) |
| 对应抖音检测点 | webcast_mate 内部某些场景会把自身识别字符串(OS 名)放到 user-agent、event payload、telemetry 上报中。仅靠 hook onMetaData 改不到这些非 RTMP 路径的"windows",所以要直接 patch .rdata。注意工具只改一处 token,user-agent 里别的位置和 process name 都没碰 —— 这是工具的覆盖盲点(也是 §08_bonus.md §5 列出来的可识破点之一) |
设想抖音风控团队挨个堵每一层:
| 假设抖音封堵 | 工具仍能 work 的理由 |
|---|---|
| 检测 onMetaData 字段(层①) | 第 ② 层 SSL_write payload 替换还在改全部 64 项 metadata;onMetaData 只是 64 项里的 8 个 tip-of-iceberg |
| 检测私有 AMF0 字段对应不上(层②) | 第 ① 层的 jitter 让基础字段每次推流都不同,第 ③ 层 webcast API 已经声明了一套"正确"的会话信息,第 ⑤ 层 .rdata 已经把 OS 名改成 Android —— 多源一致 |
| 检测 webcast Cookie 异常(层③) | 第 ④ 层 HTTP/2 + JA3 模板让 API 调用从协议指纹看就是 iOS Safari,配合 Cookie 一致;第 ② 层的 RTMP 私有字段也声明同一个 device fingerprint |
| 检测 JA3 / JA4 异常(层④) | 第 ③ 层注入的 Cookie + 第 ② 层注入的 RTMP platform=Android 字段,让"协议指纹"和"业务字段"自洽 |
| 检测 .rdata 字面量被改(层⑤) | 进程级内存校验对客户端来说成本极高,且 .rdata 只改了 7 字节 → 抖音根本不会检测这个 |
任何一层失效,其它 4 层仍能维持"伪 Android 移动端 webcast_mate 推流"这个统一身份。这就是它的纵深防御设计 —— 不是任何一处巧妙,而是每一处都覆盖了一个独立的检测维度。
| 抖音可能的检测维度 | 工具覆盖? | 覆盖方式 |
|---|---|---|
| 设备指纹 (device_model / OS / SDK version) | ✅ 全覆盖 | 层①②③⑤ 多源声明 Android SM-S938B |
| 网络指纹 (TLS JA3 / JA4 / HTTP/2 SETTINGS 顺序) | ✅ 覆盖 | 层④ HTTPSetRandomTLS + HTTP/2 fp 模板 |
| 会话指纹 (Cookie 一致性 / token 生命周期) | ✅ 覆盖 | 层③ 改写所有 webcast Cookie |
| RTMP 私有字段 (fp_* / p_id / link_info) | ✅ 覆盖 | 层② SSL_write byte-level 替换 64 项 |
| RTMP 流参数稳定性 (width/height/fps 完全恒定 = 录播) | ✅ 覆盖 | 层① jitter ±5%/±3%/±0.3 |
| 直播间互动行为 (弹幕响应率) | ❓ 未验证 | QueryTool 类有 ws_callback 方法名(见附录 A),但行为未反编译 |
| TCP / UDP / QUIC 应用层指纹 | ❓ 未验证 | QueryTool 类有 tcp_callback / udp_callback 方法名,行为未验证 |
| 视频内容指纹 (帧 hash / 镜头切换序列) | ❌ 不覆盖(不在本工具链协议层范围) | 画面去重.dll 反去重 overlay + 播放器4.1.exe 9 种音视频扰动 |
| 音频内容指纹 (说话人识别 / 静音区分布) | ❌ 不覆盖 | 播放器4.1.exe 三频段 ±20dB EQ + 速率±10% + 音高±0.5 半音 |
process name / window class (MediaSDK_Server.exe 字符串) |
❌ 不覆盖 | webcast_mate 自身固定名 — 任何客户端检测 process tree 都能看见 |
| TCP/IP 层指纹 (TTL / window size / TCP options) | ❌ 不覆盖 | OS 内核层,client side 无法改 |
| 行为时序 (上线时间分布 / 弹幕语义 / 互动模式) | ❌ 不覆盖 | 需要 AI 后端分析,工具改不了 |
结论:工具针对协议+设备身份+握手指纹这三个维度做了深度立体覆盖(5 层),但对内容指纹和行为指纹没有协议层手段(依赖另外 2 个二进制)。对手如果在 TCP/IP 内核层或行为时序上做检测,工具链整体仍会暴露。
这套工具的作者对抖音协议栈的理解极其精细:
fp_* / p_id 字段 —— 所以两个二进制各自 cover 一层(rtmp-hook 改 RTMP TLS 层,query_tool 改 webcast HTTPS 层).rdata 字面量需要单独处理 —— 因为 webcast_mate 内部不只通过 onMetaData 上报 OS,还在别的地方读 .rdata 字符串但作者也留下了清晰的可识别点(被 §08_bonus.md §5 列出来的):
- flashVer = "BytedanceDouyinLive/30.5.0 (Linux; Android 15; SM-S938B …)" —— Android 设备的 flashVer 一般不会写 Linux
- os_version=26.3.1 —— Android 16 最新,26.3.1 是 iOS 版本号(改造时漏改的痕迹,sandbox 实测 config.ini 里仍然是 26.3.1)
- webcast_mate/11.7.1 vs sandbox 实装 v12.0.3 —— 工具落后 1 个大版本,UA 不匹配
- windows → Android 只改 1 处,其它涉及 windows 字符串的位置都没碰
也就是说 —— 工具用纵深覆盖换稳定性,但每个层位的细节都有可识别点。这是个精细但不完美的工程产品。
| 层 | 证据文件 | 关键 offset / 行号 / 字节序列 |
|---|---|---|
| ① | decompile/decompile_out/rtmp-hook.c:264-397 (FUN_180001710) |
jitter 常数 .rdata @ 0x180006000=0.05 ✓ 已字节验证 |
| ② | rtmp-hook.c:1182-1282 (FUN_180003260) + :1072-1178 (FUN_180002fd0) |
qword 立即数 0x7265566873616c66 ("flashVer") @ .text:0x1800032c6 ✓ |
| ③ | decompile/query_tool_business.md §2 + §5 + Nuitka 字符串 uQueryTool.http_callback |
aHTTPSetHeader / aHTTPGetHeader / a_HTTPEvent__url —— 业务证据是 QueryTool.http_callback(自定义类方法,含 <locals>.<lambda> 闭包符号),不是 SunnyNet 通用属性 |
| ④ | query_tool_business.md §3 |
8 个 aHTTP2_fp_Config_* 字符串 + aHTTPSetRandomTLS + arandom_ja3 |
| ⑤ | rtmp-hook.c:560-665 (FUN_180001a70 STR-PATCH 段) |
.rdata @ 0x180005c30="platform" + @ 0x180005c40="windows" + rtmp_debug_*.txt: [STR-PATCH] Patched "windows" -> "Android" OK |
字节级运行时闭环验证(层① 的 14B JMP-abs64 patch chain):
real build_meta @ 0x7FFE31EB1F50 (StreamPlugin.dll 内), patch:
ff 25 00 00 00 00 ← JMP rip+0
10 17 03 32 fe 7f 00 00 ← abs64 = 0x7FFE32031710 = rtmp-hook+0x1710
= FUN_180001710 = build_meta_replacement
90 90 90 ← NOP padding (stolen 17 - JMP 14)
完整 dump 见 decompile/verify_hook_sandbox.ps1 输出。
为什么单独放附录:本附录所述的"潜在层位"在 query_tool.dll 字符串表里有方法名证据,但方法体在 Nuitka 编译后的 .const_data blob 里,Ghidra 反编译未解出实际行为。可能是空 stub,也可能是真实 spoof —— 当前证据不足以分辨。严谨起见不列入"5 层已覆盖"主结论,但记录在此供后续验证。
最初的"6 层"版本错误地把 SunnyNet 通用类 SunnyNet 的 mangled 属性 _SunnyNet__ws_callback / _SunnyNet__tcp_callback / _SunnyNet__udp_callback 当作业务证据。SunnyNet 是开源 MITM 库(qtgolang/SunnyNet),这些属性是 Python 端 class 定义自带的 —— 只要 import SunnyNet,Nuitka 编译时就会把这些 mangled 名字写入字符串表,与 query_tool 是否实际使用无关。
QueryTool 类自己的方法槽query_tool.dll 字符串表中有:
uQueryTool.http_callback
uQueryTool.http_callback.<locals>.<lambda>
uQueryTool.tcp_callback
uQueryTool.ws_callback
uQueryTool.udp_callback
uQueryTool.script_log_callback
uQueryTool.script_code_callback
uQueryTool.start_proxy
uQueryTool.start_proxy_thread
uQueryTool.do_query
uQueryTool.manual_query
uQueryTool.extract_text
uQueryTool.update_info
uQueryTool.update_ui_started
uQueryTool.update_ui_stopped
uQueryTool.setup_ui
uQueryTool.show_log_menu
uQueryTool.clear_log
uQueryTool.log
uQueryTool.on_closing
uQueryTool.__init__
uQueryTool.run
uQueryTool.stop_proxy
QueryTool 是 query_tool.dll 自定义的业务类(不是 SunnyNet 库的)。它确实定义了 tcp_callback / ws_callback / udp_callback 三个方法 —— 这是 query_tool 可能给 SunnyNet 注册了非 HTTP 层回调的暗示。
| 问题 | 状态 |
|---|---|
这三个 callback 方法是空 def pass 还是有实际业务代码? |
❓ 未知 — Nuitka 编译后的方法体在 Ghidra C 输出里以 byte-array 形式出现,未解为可读代码 |
是否调用 SunnyNetSetCallback 给 ws/tcp/udp 注册了? |
❓ 未知 — 需要动态 hook 验证 |
| 即使注册了,是否实际修改字段?还是仅 log? | ❓ 未知 — 同上 |
| 抖音 webcast IM 链路是否经 SunnyNet 代理? | ❓ 未知 — 需要 Reqable 抓 WebSocket 流量验证 |
注意 http_callback 同时还有 <locals>.<lambda> 符号(说明该方法内部至少创建了一个 lambda 闭包,是有逻辑的,不是空 stub);而 tcp_callback / ws_callback / udp_callback 没有同样的 <locals> 符号,间接暗示这三个可能是更简单的方法(甚至 def cb(event): pass)。这只是辅助线索,仍非确定证据。
Frida-trace SunnyNetSetCallback 看 ws/tcp/udp 是否真的注册了非 None 回调impl_<module>__QueryTool__ws_callback 这种命名规则的 C 函数,看是不是只有 prologue + return None 的几行完成任一项验证后即可把层⑤"复活"或永久关闭。
get_latest_room 请求/响应,对照 fp_* / p_id 在 webcast Cookie 和 RTMP AMF0 两处的具体值是否完全一致这套工具的本质不是"把 PC 改成手机",而是"沿着抖音协议栈的每一个会被风控读到的字段位置都做一次伪造"。5 层位 × 字段一致性 × 算法多样性(jitter / type-aware 替换 / fingerprint 模板) —— 这才是它能稳定运行的真正原因。
| 版本 | 日期 | 修订内容 | 触发 |
|---|---|---|---|
| v1 | 2026-05-14 09:30 | 初版"6 层架构"发布,将 SunnyNet WebSocket 拦截能力作为第⑤层"已覆盖" | 综合 Ghidra 反编译 + Nuitka 字符串审计 |
| v2 (当前) | 2026-05-14 11:55 | 降级为"5 层架构 + 1 附录未验证":删除原层⑤"WebSocket / IM" 主结论(仅凭 SunnyNet 通用类 mangled name 不构成业务证据),原层⑥ 升为层⑤;新增附录 A 记录 QueryTool.{tcp,ws,udp}_callback 三个自定义方法名作为"潜在层位"待验证 |
用户指正:"SunnyNet 是开源方案,内置的是通用 WebSocket" |
作者:Claude (Opus 4.7),2026-05-14 依据:Ghidra 11.3.2 + ilspycmd + Nuitka 字符串审计 + Sandbox runtime ReadProcessMemory 字节级验证