
Ubiquiti AirMAXワイヤレスプロトコルフレーム用のデコーダー/エンコーダー。pcap/ライブキャプチャを解析し、デバイスを発見し、脆弱なファームウェアをスキャンし、不正なパケットを生成し、AirMAX AC/Mターゲットをエミュレートします。
Ubiquiti AirMAX ワイヤープロトコルフレーム用のデコーダおよびエンコーダで、AC(WA ファームウェア)シリーズと M(XW/XM ファームウェア)シリーズの両方に対応しています。
ksy/ 配下の
Kaitai Struct スキーマによって駆動されます。AC ペイロード
(msg_type で切り替わるバージョンゲート付き、deauth XOR アンマスクを含む)は
ac.py に手書きで実装されています — このロジックはパース専用の Kaitai には適合しません。| バリアント | デコード | エンコード | pcap イテレーション |
|---|---|---|---|
| AC | ✅ 全 5 メッセージタイプ(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
に従います([P] は ubnt_poll_host.ko に対して確認済み)。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 ビルダーは
バージョン 9 のみを出力するため、低バージョン(タグなし
名前、末尾フィールド欠落)のパスは仕様から実装されていますが、
ワイヤー上では未検証です。unknown_rest の 9 バイト以降) —
現時点では不透明。pcap.FrameMeta に radiotap メタデータを公開:チャネル、RSSI、
レート。現在はタイムスタンプ/MAC/BSSID のみが設定されています。tests/samples/ の実キャプチャフィクスチャ — 現在:
airmax_ac_beacon.pcap(1 フレーム、beacon)および
airmax_m_probe_response.pcap(1 フレーム、probe response)。さらなるバリアント
(assoc req/resp、マルチフレームキャプチャ)も歓迎します。scan(アクティブ force-assoc)および 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` によるライブ無線インターフェースの**いずれか**を受け取ります。この2つは
相互排他的です。`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 は終了時に集約されたデバイスサマリーを出力します。
終了コード (両コマンドと両ソースモードで共通): 成功時 (「AirMAXフレームなし」を含む — これは有効な結果) は 0、キャプチャ形式エラー (リンクタイプの誤り、ファイルの破損、インターフェースを開けない) は 1、ファイルが見つからないかソース引数が無効な場合は 2。
parse — フレーム単位のダンプ```shpython -m pyrmax parse capture.pcap
AirMAX ベンダー IE を保持する各 802.11 管理フレームは、同じフレーム内で見つかった AC パケット、M パケット、および Routerboard.com コンパニオン IE を含む 1 つのブロックを生成します。```
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を含むフレームごとに1つの
AirmaxRecord(meta, ac, m, routerboard)を生成するため、
M IEとRouterboard IEを手動で関連付ける必要はありません。
discover — デバイス概要```shpython -m pyrmax discover capture.pcap
フレームは802.11送信元MACごとにグループ化され、ピアが蓄積され、ACペイロード / Mペイロード / Routerboardデバイス名の観測結果は、デバイスごとに1つのブロックにまとめられます。```
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 Vendor Specific IEのバイト列が**OUI
から始まる**ことを想定しています — IEラッパー(Element 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 のいずれかです。バージョンゲートされた末尾フィールド (field_9c, rssi, fwname, txpower,
…) は、フレームの version がしきい値未満の場合は None になります。Deauth
は、整合性チェックの前に src_mac を jiffies nonce で XOR 解除し、
16 バイトの enc_token を不透明なバイト列として表面化します (そのキーは
まだ逆算されていないため、検証できません)。
ネーム TLV を運ぶボディ(beacon、assoc_req)は、ストリームを —
末尾の Padding エントリも含めて — body.tlvs にそのまま保持します。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フィールドがない — 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