为了给家里的电信定制中兴 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 import argparseimport socketimport structimport timefrom Crypto.Cipher import AESKEY = 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 : 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 : 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 ) 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 12 1. 版本协商(明文) — 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匹配一个硬编码字符串。报文均可从明文完整构造,并且与工具发出的内容完全一致。下面继续展开完整的分析过程,欢迎感兴趣的读者继续阅读。