再探Alti-2高度表:通信协议还原与月计数器溢出BUG

再探Alti-2高度表:通信协议还原与月计数器溢出BUG

上一篇逆向分析记录里,通过MITM串口桥抓到了Alti-2高度表和PC之间的加密通信流量,确认了跳伞日志数据没有丢失。
这次还原了整个通信协议,写了一个跨平台的Python库 pyaltitool,可以直接在macOS/Linux/Windows上连接高度表读取数据。顺便也搞清楚了2025年8月开始困扰所有Alti-2用户的那个BUG到底是怎么回事。

通信协议

Alti-2的Atlas系列高度表通过FTDI芯片提供USB串口通信,物理层参数是57600 baud, 8N1, RTS/CTS硬件流控。整个通信过程分五步:

1. DTR唤醒

高度表插入USB后默认只充电,处于休眠状态。PC侧需要Toggle DTR信号线来唤醒设备进入通信模式。

2. ASCII握手

唤醒后发送ASCII握手串018080,设备响应一个32字节的Type 0 Record,包含产品型号、固件版本、序列号、总跳数、Logbook内存起始地址等关键信息。

3. Session Key协商

Session Key的派生基于Type 0 Record和产品型号对应的预置seed。具体来说是取Type 0中的特定字段,与seed组合后生成一组16字节XTEA密钥。这个过程完全在PC侧完成,设备侧用同样的算法从自己的数据派生出相同的密钥,不需要额外的密钥交换通信。

上一篇提到的”旧协议解密失效”就是因为新型号更换了seed,算法本身没有变。从官方固件更新工具的反编译结果中可以直接得到新的seed值。

4. 加密通信

后续所有指令都是32字节的XTEA加密包,逐字节发送,每发一字节都要polling CTS流控信号。指令集沿用了旧的CATI-2协议格式,包括READ_MEMORYWRITE_MEMORY、读取设备时钟等。数据区域是FRAM,Logbook的每条跳伞记录占22字节,按地址顺序排列。

XTEA解密后的指令结构很简单,以上一篇抓到的那条READ_MEMORY为例:

1
2
3
4
07 A0 0E 00 00 00 02 B0 00 00 00 00 00 00 00 00 ...
│ └──┘ └──┘
│ addr len
└─ cmd (READ_MEMORY)

5. 退出

发送\x01EXIT结束会话,设备回到充电休眠模式。

设备有10-15秒的空闲超时,超时后断开通信。pyaltitool在后台跑了一个keepalive线程,定期发datetime ping维持连接。大批量读取Logbook时,每100条记录需要主动重连一次,因为持续传输会导致设备串口状态不稳定。

月计数器溢出BUG

这就是2025年8月开始Alti-2高度表集体出问题的根本原因,一个 Y2K like 溢出BUG。

数据结构

每条22字节的跳伞记录中,byte 2的设计是这样的:

1
byte 2:  [bit 7: deleted flag] [bits 6-0: month counter]

Month counter是一个从2015年1月开始的月份计数器,7-bit范围0-127,对应2015年1月到2025年7月。bit 7被保留为deleted标志位,用于标记记录是否已删除。

问题

固件的写入代码把month counter当作完整的8-bit值写入,完全无视了bit 7的保留用途。当计数器走到128(2025年8月),bit 7被置1:

1
2
3
mc = 127 (2025-07):  byte 2 = 0x7F = 0_1111111  →  deleted=0, mc=127  ✓
mc = 128 (2025-08): byte 2 = 0x80 = 1_0000000 → deleted=1, mc=0 ✗
mc = 129 (2025-09): byte 2 = 0x81 = 1_0000001 → deleted=1, mc=1 ✗

于是从2025年8月起,所有新记录都被设备自己当成了”已删除”,日期信息也因为截断到7-bit而错乱。设备尝试显示这些记录时直接crash——大概是mc=0落到了某个无效的数组索引或空指针。这就是为什么在高度表上点View会卡一下然后退回来。

官方修复

1.0.10固件更新修了crash问题,但并没有恢复受影响记录的日期信息——这些记录在官方工具中仍然显示为deleted。

pyaltitool的修复

每条跳伞记录里存了它被记录时的固件版本号。利用这个信息,可以针对不同固件版本做条件解析:

1
2
3
4
5
6
7
8
sw_major = (word3 >> 11) & 0x0F  # firmware major version from the record

if sw_major < 1: # firmware 0.x.x — has the overflow bug
month_counter = w1 & 0xFF # full 8 bits, bit 7 is part of month counter
deleted = False # bit 7 is NOT a reliable deleted flag
else: # firmware >= 1.0 — bug fixed
month_counter = w1 & 0x7F # 7 bits as originally designed
deleted = bool(w1 & 0x80) # bit 7 is a real deleted flag

对旧固件写入的记录,取完整的8-bit值作为month counter,不把bit 7当deleted flag。空记录(jump_number == 0)仍然识别为deleted。这样就能正确恢复所有溢出影响的跳伞日期数据了。

影响窗口

8-bit month counter的范围是0-255,对应2015年1月到2036年3月。如果到那时候这个表还在用,同样的溢出会在month counter 256处再来一次。彻底修复需要扩展字段位宽或者改用其他日期存储格式。

pyaltitool

基于以上协议还原和BUG修复,写了 pyaltitool,纯Python实现,仅依赖pyserial,跨平台支持macOS/Linux/Windows。

功能包括:读取设备信息、导出跳伞日志(支持CSV)、读取设备时钟、读取自定义名称表(飞机、DZ、告警)、原始FRAM内存读写。自动检测串口,后台keepalive,断线自动重连。

下面是从一台Atlas 2上读取到的一条实际跳伞记录,日期和各项数据均已正确恢复:

pyaltitool logbook record

连上设备后可以直接用交互式命令行:

1
2
3
4
5
altitool> info                           # 设备信息
altitool> logbook all # 全部跳伞记录
altitool> logbook last 10 # 最近10跳
altitool> logbook csv all # 导出CSV
altitool> datetime # 设备时钟

也可以作为库使用:

1
2
3
4
5
6
from pyaltitool import AltitoolDevice, auto_detect_port

port = auto_detect_port()
with AltitoolDevice(port) as dev:
info = dev.connect()
print(f"{info['product_name']} S/N {info['serial_number']}, {info['total_jumps']} jumps")

源码和完整文档在 GitHub

References

  1. 5BytesHook. pyaltitool - Alti-2 Altimeter Communication Library. GitHub. https://github.com/5BytesHook/pyaltitool

  2. Lobanov, A. alti2reader - Alti-2 Neptune Data Reader. GitHub. https://github.com/evilwombat/alti2reader

  3. Wheeler, D. & Needham, R. Correction to XTEA. Computer Laboratory, Cambridge University. https://www.movable-type.co.uk/scripts/xxtea.pdf