- dsl.py: use NDArray type annotations, fix None narrowing - cli.py: add type annotations, fix import sorting - strategy.py: fix UP038 isinstance, add noqa for I() method name - tests: fix E712 bool comparison assertions Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.9 KiB
TDX 协议逆向工程全景
本文记录通达信 (TDX) 私有二进制 TCP 协议的逆向研究过程,帮助理解 easy-tdx 项目中各层协议实现的设计原理。
第一层:抓包 — 发现帧结构
通达信客户端和服务器之间是裸 TCP(端口 7709),没有 HTTP 包装。研究者通过 Wireshark 抓包发现了一个固定的帧格式:
响应帧 = 16字节固定头 + 可变长度 body
codec/frame.py 记录了这个发现:
偏移 0: I (4字节) — 未知
偏移 4: I (4字节) — 未知
偏移 8: I (4字节) — 未知
偏移 12: H (2字节) — zipsize(body 实际长度)
偏移 14: H (2字节) — unzipsize(解压后长度)
关键发现:当 zipsize == unzipsize 时 body 未压缩,否则需要 zlib 解压。这是通过对比抓包数据和 zlib 解压结果验证的。
第二层:握手 — 三条神秘命令
commands/setup.py 中的三条握手命令是硬编码的原始字节:
SETUP_CMD1: "0c0218930001030003000d0001"
SETUP_CMD2: "0c0218940001030003000d0002"
SETUP_CMD3: "0c031899000120002000db0f..." # 30字节,含GBK编码的"申银万国"
注释写得很清楚:"从 pytdx 源码移植,已在真实服务器验证"。这说明握手命令不是这个项目原创的,而是从开源项目 pytdx 继承来的。pytdx 的作者 rainx 通过逆向通达信 Windows 客户端(用 IDA Pro 等反汇编工具)得到了这些字节序列。
第三条命令特别有意思,里面包含了 d5d0c9ccd6a4a8af,这是 GBK 编码的"申银万国"(一家券商名),说明这条握手命令里包含了客户端标识信息。
第三层:请求构造 — 猜解命令格式
每个命令的请求都是一个固定格式的二进制包。以 security_list.py 为例:
header = bytes.fromhex("0c0118640101060006005004")
payload = struct.pack("<HHH", market, start, 0)
这些 magic number(如 0c0118640101060006005004)是怎么来的?有两种途径:
- 反汇编客户端:在通达信的 exe/dll 中找到构建这些请求包的函数
- 协议模糊测试:修改已知命令的某些字节,观察服务器响应变化
比如 K 线命令 security_bars.py 的头部:
struct.pack("<HIHHHH6sHHHHIIH",
0x010C, # 固定魔数
0x01016408, # 命令 ID(K线数据的标识)
0x001C, # payload 长度 = 28
0x001C, # 重复的 payload 长度
0x052D, # 子命令/协议版本
market, # 市场
code, # 股票代码(6字节)
category, # K线周期
1, # 未知,可能是数据类型
start, # 起始位置
count, # 请求数量
0, 0, 0, # 三个保留字段
)
通过逐个修改这些字段发送给服务器,观察返回数据的变化,就能推断每个字段的含义。
第四层:响应解析 — 最难的部分
响应的解析才是真正的逆向难点。以实时行情 (security_quotes.py) 为例:
差分编码:服务器返回的不是绝对价格,而是差值(delta)。所有价格字段都是相对于 price_raw 的差分:
pre_close = (price_raw + last_close_diff) / 100.0
open = (price_raw + open_diff) / 100.0
high = (price_raw + high_diff) / 100.0
# 五档买卖也是差分:
bid1 = (price_raw + bid1_d) / 100.0
K 线数据更复杂,使用了累积差分 (security_bars.py):
open_abs = open_diff + pre_diff_base
close_abs = open_abs + close_diff
high_abs = open_abs + high_diff
low_abs = open_abs + low_diff
pre_diff_base = open_abs + close_diff # 下一根K线的基准
第五层:编解码 — 自定义数据格式
价格编码 (codec/price.py) — 类 LEB128 变长有符号整数
首字节: [bit7=继续][bit6=符号][bit5-0=低6位数据]
后续字节: [bit7=继续][bit6-0=7位数据]
这不是标准 LEB128,而是通达信自己的变体。推断过程:
- 抓包看到价格附近的数据长度不固定(1-4字节)
- 注意到 bit7 似乎是"还有更多字节"的标记
- 发现 bit6 是符号位
- 通过已知价格验证:发送请求时知道某个价格是 15.50,在响应中找到对应的字节序列,反推编码规则
成交量编码 (codec/volume.py) — 完全自定义的 4 字节浮点
logpoint = (ivol >> 24) & 0xFF # 指数
hleax = (ivol >> 16) & 0xFF # 高精度
lheax = (ivol >> 8) & 0xFF # 中精度
lleax = ivol & 0xFF # 低精度
这个格式完全不是 IEEE 754,是通达信自创的。逆向方法:拿到已知成交量的 hex dump,用不同的拆分方式穷举,直到找到能还原正确数值的公式。
第六层:未知字段 — 逆向的痕迹
代码中大量保留了 unknown_0 到 unknown_8 的字段,这本身就是逆向过程的痕迹——解析出格式但不确定含义的字段。比如:
unknown_0后来被发现是服务器时间戳("小时 + 百万分之一小时"编码)unknown_1推测等于-price_raw- 指数 K 线比股票 K 线多 4 字节(上涨/下跌家数),是通过对比同一市场的指数和股票响应长度差异发现的
总结:逆向方法论
这个项目体现的逆向研究路线是:
- 抓包 (Wireshark) → 发现 TCP 帧结构、握手序列
- 反汇编 (IDA Pro) → 得到请求构造函数、编解码算法
- 黑盒验证 → 修改参数发请求,对比响应确认字段含义
- 差分分析 → 利用已知数据(如已知价格)反推编码公式
- 对比分析 → 指数 vs 股票、不同市场、不同周期的响应差异
- 开源传承 → pytdx 打下基础,easy-tdx 在此之上修复 bug 并重构
核心依赖是 pytdx 项目做的基础逆向工作(反汇编通达信客户端),easy-tdx 则是在已有协议知识上的高质量 Python 重写。