用于 Ubiquiti AirMAX 线协议帧的解码器和编码器,涵盖 AC(WA 固件)和 M(XW/XM 固件)系列。
ksy/ 下的 Kaitai Struct 模式驱动。AC 载荷(按版本门控,依据 msg_type 切换,带 deauth XOR 去掩码)由 ac.py 手写实现——该逻辑不适合纯解析式 Kaitai。| Variant | Decode | Encode | pcap iteration |
|---|---|---|---|
| AC | ✅ 全部 5 种 msg 类型(beacon、assoc req/resp、probe req、deauth) | ✅ 字节级精确逆操作 + 构建器 + seal | ✅ |
| M | 部分(9 个已记录的字节;其余为 unknown_rest) | ✅ 可往返已记录头部 + unknown_rest | ✅ |
| Routerboard.com IE(M 的伴随项) | ✅ 设备名 + 子 IE 列表 | n/a | ✅ |
AC 线格式遵循
docs/ac_wire_format.md
(已针对 ubnt_poll_host.ko 进行 [P] 确认)。所有 AC 多字节整数均为
大端序。
pyrmax.ac.encode(AcPacket) -> bytes — 手写的 decode() 字节级精确逆操作(外加 seal()、to_ie() 和 build_* 构造器)。pyrmax.m.encode(MPacket) -> bytes — M 同理(外加 build_m、seal、to_ie)。[open] AC 字段 — cap_flags 精确位图、mixed_mode、field_14/field_9c(assoc_req)、sta_field_68/ic_6b8(assoc_resp),以及 deauth enc_token(可提取,但其密钥尚未被逆向工程,因此无法验证)。参见 docs/ac_wire_format.md §11-§12。version < 9 的 AC 解码器 — TX 构建器只会发出 version 9,因此较低版本(无标签名称、缺少尾部字段)的路径是根据规范实现的,但未在线验证。unknown_rest 中第 9 字节及之后)——目前仍不透明。pcap.FrameMeta 上呈现 radiotap 元数据:channel、RSSI、rate。目前仅填充了 timestamp/MAC/BSSID。tests/samples/ 中的真实抓包固定样本 — 当前有:airmax_ac_beacon.pcap(1 帧,beacon)和 airmax_m_probe_response.pcap(1 帧,probe response)。仍欢迎更多变体(assoc req/resp、多帧抓包)。scan(主动强制关联)和 emulate 命令通过 scapy 注入([scan] / [emulate] 附加特性)。参见 scan.py / emulate.py。该包附带一个子命令驱动的 CLI。按如下方式运行: `python -m pyrmax COMMAND ...```` usage: python -m pyrmax [-h] [--version] COMMAND ...
COMMAND parse Print each AirMAX frame in detail. discover Summarize devices observed in the capture. scan Live-scan for AirMAX devices and flag vulnerable firmware. emulate Emulate AirMAX AC/M devices (fake targets for scanners).
`parse` 和 `discover` **要么**接受 pcap/pcapng 文件(位置参数)
**要么**通过 `-i / --iface IFACE` 接受实时无线接口。这两者是
互斥的。`scan` 接受相同的源选项(外加一个
仅适用于实时接口的 `--active` 模式);`emulate` 只能
用于实时接口。```sh
python -m pyrmax parse capture.pcap # offline
python -m pyrmax discover capture.pcap
sudo python -m pyrmax parse -i wlan0mon # live (needs root)
sudo python -m pyrmax discover -i wlan0mon
实时捕获假定接口已经处于监听模式,并且位于目标
信道上——pyrmax 不会对这两者进行配置。它需要可选的
[live] 附加组件(pip install pyrmax[live]),该组件会引入
pcapy-ng。按 Ctrl-C 停止:parse 报告它流式处理了多少帧,
而 discover 在退出时打印聚合的设备摘要。
退出码(两个命令和两种源模式共用):0 表示
成功(包括“没有 AirMAX 帧”——这也是一种有效结果),1 表示
捕获格式错误(链路类型错误、文件损坏、无法打开
接口),2 表示文件缺失或源参数无效。
parse — 逐帧转储```shpython -m pyrmax parse capture.pcap
每个携带 AirMAX 厂商 IE 的 802.11 管理帧都会生成一个
块:AC 数据包、M 数据包,以及在同一帧中找到的任何 Routerboard.com
配套 IE。```
Found 1 AirMAX frame(s) in capture.pcap: 0 AC, 1 M (1 with Routerboard companion).
=== Frame #0 [M] ts=1765494974.955358 ===
802.11 src=04:18:d6:0e:0c:42 dst=24:a4:3c:88:d8:22 bssid=04:18:d6:0e:0c:42
AirMAX M
version 15
msg_type BEACON (raw=1)
src_mac 04:18:d6:0e:0c:42
enable 1
unknown_rest b700000000000000000000040418d60e0c420000000000 (23B)
Routerboard.com IE
oui_type 0
unknown 0000
device_name 'AP Sur HY1315'
sub_ie subtype=1 (30B) 040000001f660902ff0f4150205375722048593133313500000000000000
同一个单遍遍历器也以编程方式暴露为
pyrmax.pcap.iter_airmax(path) — 它为每个携带 AirMAX 的帧生成一个
AirmaxRecord(meta, ac, m, routerboard),因此
你无需手动关联 M 和 Routerboard IE。
discover — 设备摘要```shpython -m pyrmax discover capture.pcap
帧按 802.11 源 MAC 分组,对等节点累积,AC 载荷 / M 载荷 / Routerboard 设备名观测结果汇总为每个设备单个块。```
2 device(s) observed in capture.pcap across 12 AirMAX frame(s).
24:5a:4c:44:57:fd (AC)
radioname 'LB1'
ssid 'labalUBI2'
ac_msg_types BEACON
ac_version 9
cap_flags 0x0000003e
mixed_mode 0
frames 8
first seen 1767046123.708745
last seen 1767046129.012448
peers (broadcast only)
04:18:d6:0e:0c:42 (M)
device_name 'AP Sur HY1315'
msg_types BEACON
m_version 15
m_enable 1
frames 4
first seen 1765494974.955358
last seen 1765494980.341110
peers 24:a4:3c:88:d8:22
相同的聚合操作也是一个公共函数:```python from pyrmax.devices import summarize from pyrmax.pcap import iter_airmax
devices = summarize(iter_airmax("capture.pcap")) for mac, dev in devices.items(): print(mac.hex(":"), dev.device_name or dev.radioname, dev.peers)
## 使用
### 解码单条 AirMAX AC 消息
解码器期望的是 802.11 厂商特定 IE 的字节,**从
OUI 开始** —— IE 包装(元素 ID `0xDD` + 长度)必须已经
剥离。`src_mac` / `dst_mac` 取自外层 802.11 帧的
SA / DA。```python
from pyrmax import ac
packet = ac.decode(
data, # bytes starting at b"\x00\x27\x22"
src_mac="aa:bb:cc:dd:ee:ff", # accepts str ("aa:bb:..." or "aa-bb-..."
# or "aabbcc...") and raw 6-byte bytes
dst_mac="ff:ff:ff:ff:ff:ff", # optional — defaults to broadcast
)
packet.msg_type # <MsgType.BEACON: 1>
packet.version # 9 (the wire-format epoch / version gate)
packet.src_mac # b'\xaa\xbb\xcc\xdd\xee\xff' (integrity-checked)
packet.radioname # "lab-rx-1" (convenience prop, delegates to body)
packet.ssid # "NetA"
packet.cap_flags # 0x3e (None for msg types that have no cap_flags)
# The per-message-type fields live on packet.body, one of:
# BeaconBody | AssocReqBody | AssocRespBody | ProbeReqBody | DeauthBody
body = packet.body
if isinstance(body, ac.BeaconBody):
body.mac_0c # the radio's own MAC / BSSID
body.cap_flags # u32 capability bitfield (§11)
body.mixed_mode # u32 [open]
msg_type 是 BEACON / ASSOC_REQ / ASSOC_RESP / PROBE_REQ /
DEAUTH 之一。当帧的 version 低于其阈值时,受版本门控的尾部字段(field_9c、rssi、fwname、txpower、
…)为 None。Deauth 在完整性检查之前将 src_mac 与 jiffies nonce 进行异或解算,
并将 16 字节的 enc_token 作为不透明字节呈现(其密钥尚未被逆向,
因此无法验证)。
携带名称 TLV 的报文主体(beacon、assoc_req)会在 body.tlvs 上逐字保留该流——包括末尾的 Padding 条目。radioname、ssid 和 fwname 作为便捷属性暴露在报文上;其余内容保持原始状态。```python
body = packet.body
for tlv in getattr(body, "tlvs", ()):
if tlv.tag == ac.TlvTag.PADDING:
continue
print(f"{tlv.tag.name:<10} ({len(tlv.data)} bytes): {tlv.data!r}")
### 解码单条 AirMAX M 消息
形状相同,表面更小——M 载荷中仅有 9 个字节有文档记录;
其余部分原样保留在 `unknown_rest` 中。**注意:** M 载荷
本身没有 SSID 字段——为此,请查看外层信标或探测响应中的标准 802.11 SSID IE
(通过 `pyrmax.pcap` 迭代时会以
`FrameMeta.ssid` 形式呈现)。```python
from pyrmax import m
packet = m.decode(data, src_mac="aa:bb:cc:dd:ee:ff")
packet.version # 1
packet.msg_type # <MsgType.BEACON: 1>
packet.src_mac # b'\xaa\xbb\xcc\xdd\xee\xff'
packet.enable # 1
packet.unknown_rest # b'\xde\xad\xbe\xef...' # opaque, RE pending
encode(packet, dst_mac=…) 是 decode 的字节级精确逆操作 —
对于格式正确的帧,encode(decode(x)) == x。它将主体序列化
(重新应用版本门控,对于 deauth 还重新应用 src_mac XOR 掩码),填充到
AES 块大小,使用由 packet.src_mac 派生的密钥进行加密,并输出
从 OUI 开始的字节。用 to_ie() 包装即可得到完整的 0xDD 厂商 IE。
build_* 构造函数可免去你手工组装嵌套主体的麻烦:```python
from pyrmax import ac
pkt = ac.build_beacon(src_mac="24:5a:4c:44:57:fd", radioname="LB1", ssid="labalUBI2", cap_flags=0x3e) ie = ac.to_ie(ac.encode(pkt)) # full 802.11 vendor IE, ready to embed
deauth = ac.build_deauth(src_mac="24:5a:4c:44:57:fd", jiffies_nonce=0xdeadbeef) raw = ac.encode(deauth, dst_mac="24:a4:3c:88:d8:22")