中兴机顶盒固件及调试协议分析与 STB3.0 开启 ADB 方法
为了给家里的电信定制中兴 ZXV10 B863AV3.2-T 机顶盒(S905L3A-B,Android 9)开启 ADB,笔者从公开渠道找到了网上流传的「stb3.0 工具.exe」。该工具号称只需指定机顶盒的 IP,便可一键开启网络 ADB,无须拆机,也无须通过遥控器进入工厂菜单。经过分析,笔者发现它实际上由 Python 编写,而且似乎还经历过被破解、再传播的过程(目前在某鱼上售价 10 元左右)。这类工具在恩山等论坛流传已久,却几乎没人说清它究竟发送了什么。出于赛博考古的兴趣,笔者分析了工具代码及中兴机顶盒固件中的相关实现,并将结果整理成本文。
首先给出「stb3.0 工具」的核心代码,笔者重新整理了:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84#!/usr/bin/env python3
# 中兴机顶盒 10042 端口 RemoteSetCfg / CfgAgent 协议解密、构造与重放
import argparse
import socket
import struct
import time
from Crypto.Cipher import AES
# libgetkey.so .data 0x11004 处的出厂默认密钥,SecretKey() 用 snprintf "%s" 读出它
KEY = b'|!jFa8o4' + b'\x00' * 8
AFFIRM = '16_AeKg_2t7z_@01_17_@A_$oMjO6fg.P_18_GdpR8jO30!d-rR_20_jD9!2y-dW8'
TOOLTYPE = 'ToolType:16,licindex:yuduanhun'
def pad16(b: bytes) -> bytes:
return b + b'\x00' * (-len(b) % 16)
def enc_frame(ftype: int, payload: bytes) -> bytes:
"""加密帧0x10 | 帧类型 | u16密文长BE | 0x0002 | 18个零字节 | AES-128-ECB密文"""
ct = AES.new(KEY, AES.MODE_ECB).encrypt(pad16(payload))
return bytes((0x10, ftype, len(ct) >> 8, len(ct) & 0xFF, 0, 2)) + b'\x00' * 18 + ct
def cmd_msg(cmd: int, payload: bytes) -> bytes:
"""内层命令头u16命令号BE | u16零 | u32数据长BE,补零到16字节"""
return enc_frame(0x01, struct.pack('>HHI', cmd, 0, len(payload))) + (
enc_frame(0x10, payload) if payload else b'')
def decrypt_dump(blob: bytes) -> None:
"""按帧解析一段报文,解密并逐帧打印"""
i = 0
while i < len(blob):
magic = int.from_bytes(blob[i:i + 2], 'big')
if magic == 0x1020: # 明文 hello
ln = int.from_bytes(blob[i + 4:i + 8], 'big')
print(f' [0x{magic:04x}] hello: {blob[i + 8:i + 8 + ln]!r}')
i += 8 + ln
else: # 0x1001 命令头 / 0x1010 数据帧,均为 AES-128-ECB
ln = int.from_bytes(blob[i + 2:i + 4], 'big')
pt = AES.new(KEY, AES.MODE_ECB).decrypt(blob[i + 24:i + 24 + ln])
if magic == 0x1001:
cmd, _, dlen = struct.unpack('>HHI', pt[:8])
print(f' [0x1001] cmd 0x{cmd:04x}, datalen {dlen}')
else:
print(f' [0x1010] data: {pt.rstrip(bytes(16))!r}')
i += 24 + ln
def build_messages():
hello = bytes((0x10, 0x20, 0, 0)) + struct.pack('>I', 4) + b'V6.0'
return [
('1. 版本协商(明文)', hello),
('2. affirm 鉴权', cmd_msg(5, AFFIRM.encode())),
('3. 工具类型声明', cmd_msg(0x15, TOOLTYPE.encode()) + cmd_msg(0x11, b'')),
('4. 开启网络ADB', cmd_msg(0x2036, b'cmdtype=ADB_ZTE,cmdparams=Open')),
]
def send(host: str) -> None:
with socket.create_connection((host, 10042), timeout=5) as s:
for name, msg in build_messages():
s.sendall(msg)
print(f'已发送 {name}({len(msg)} 字节)')
time.sleep(1) # 原工具在每段报文之间等待约1秒
def main() -> None:
ap = argparse.ArgumentParser(description='中兴机顶盒 10042 协议工具')
ap.add_argument('--send', metavar='HOST', help='向机顶盒发送报文')
args = ap.parse_args()
if args.send:
send(args.send)
print(f'完成。可尝试: adb connect {args.send}:5555')
return
for name, msg in build_messages():
print(f'{name} — {len(msg)} 字节:')
decrypt_dump(msg)
if __name__ == '__main__':
main()
运行输出:1
2
3
4
5
6
7
8
9
10
11
121. 版本协商(明文) — 12 字节:
[0x1020] hello: b'V6.0'
2. affirm 鉴权 — 144 字节:
[0x1001] cmd 0x0005, datalen 65
[0x1010] data: b'16_AeKg_2t7z_@01_17_@A_$oMjO6fg.P_18_GdpR8jO30!d-rR_20_jD9!2y-dW8'
3. 工具类型声明 — 136 字节:
[0x1001] cmd 0x0015, datalen 30
[0x1010] data: b'ToolType:16,licindex:yuduanhun'
[0x1001] cmd 0x0011, datalen 0
4. 开启网络ADB — 96 字节:
[0x1001] cmd 0x2036, datalen 30
[0x1010] data: b'cmdtype=ADB_ZTE,cmdparams=Open'
这个脚本依赖pycryptodome(pip install pycryptodome)。不带参数运行时,它会构造四段报文并自行解密输出;加上--send IP,则会按照原工具约 1 秒的发送间隔,将报文依次发送给机顶盒。
四段报文的整体语义是:「我是 V6.0 工具」→「口令对上」→「我是 16 号工具,授权标识 yuduanhun」→「打开 ADB」。在 TCP 10042 配置服务可达、且仍支持这套旧协议的固件上,它们已经构成原工具开启 ADB 所需的完整命令链。原工具还会发送第五段ADB_ZTE_KEY,向机顶盒写入一把固定的 RSA 公钥;由于在本文分析的固件中没有发现该文件的消费链路,文章开头的实用脚本出于最小权限原则有意省略了这一步。
为了弄清楚原理,笔者同时分析了固件中的服务端和工具客户端,完整还原了协议:它采用 AES-128-ECB,密钥是固件中的一个出厂常量;所谓「鉴权」,本质上是strstr匹配一个硬编码字符串。报文均可从明文完整构造,并且与工具发出的内容完全一致。下面继续展开完整的分析过程,欢迎感兴趣的读者继续阅读。


