
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 메타데이터 노출: channel, RSSI,
rate. 현재는 타임스탬프/MAC/BSSID만 채워집니다.tests/samples/의 실제 캡처 픽스처 — 현재:
airmax_ac_beacon.pcap (1프레임, beacon) 및
airmax_m_probe_response.pcap (1프레임, probe response). 추가 변형
(assoc req/resp, 다중 프레임 캡처)은 언제든 환영합니다.scan(능동 강제 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`를 통한 라이브 무선 인터페이스 **중 하나**를 받습니다. 이 두 가지는
상호 배타적입니다. `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("no AirMAX frames" 포함 — 유효한 결과), 캡처 형식 오류 시 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 Vendor Specific IE의 바이트가 **OUI에서
시작하는** — IE 래퍼(Element ID `0xDD` + Length)는 이미 제거되어
있어야 합니다. `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