From raw IQ to decrypted Wi-Fi payload.
The challenge gives a raw GNU Radio .cfile IQ recording centered on
2412 MHz. The correct solve path is not just “this is Wi-Fi”, and it is not
just the WPA2 password either. The interesting part is that the capture contains enough
management traffic to identify the network, enough handshake material to recover the
passphrase, and enough protected data frames to decrypt the actual application payload
containing the flag.
Identify the signal
The center frequency is 2412 MHz, which maps directly to Wi-Fi channel 1.
Looking at the burst structure and the 802.11 OFDM preamble confirms that the signal is
legacy 802.11 OFDM traffic.
Why this is Wi-Fi
The bursts decode with a valid 802.11 SIGNAL field, the frame lengths are
consistent, and repeated management frames appear exactly like beacon traffic.
Why the task is more than identification
The challenge text asks what Harriet captured, but the capture also contains a full WPA2 exchange and protected data. The flag is in the decrypted payload, not in the signal label alone.
# 2412 MHz = Wi-Fi channel 1
# First conclusion:
# IEEE 802.11 on channel 1
# Later conclusion:
# WPA2-protected network with enough traffic to fully solve the challenge
Make OpenOFDM usable under Python 3
openofdm was useful as the PHY reference, but the repo is old and assumes
Python 2. The practical move was to port the decoder to Python 3 and add a small wrapper
for GNU Radio .cfile input.
wltrace parser all had to be handled.
Key fixes:
- Convert Python 2 syntax to Python 3 syntax.
- Replace legacy octal literals and integer division.
- Handle missing
wltracegracefully and still expose raw bytes. - Add a wrapper to load
capture.cfiledirectly. - Patch vendored
commpycompatibility issues.
# Example Python 2 to Python 3 style fixes
from io import StringIO
# old:
# print "hello"
# self.polarity.next()
# 0133
# new:
print("hello")
next(self.polarity)
0o133
# Minimal .cfile loader idea
import numpy as np
raw = np.fromfile("capture.cfile", dtype=np.float32)
iq = raw[0::2] + 1j * raw[1::2]
Decode the management frames first
Once the decoder was working, the first clean frames were repeated beacons, one probe request, and one probe response. This established the network identity and gave the first solid clue.
[ 0] 80 00 00 00 ff ff ff ff ff ff 48 22 54 12 34 56
[ 16] 48 22 54 12 34 56 00 00 c9 20 1b 58 8a 45 06 00
[ 32] 64 00 11 04 00 0b 52 6f 63 6b 61 72 20 64 69 67
[ 48] 21 01 08 82 84 8b 96 0c 12 18 24 03 01 01 05 04
The important part is the SSID information element:
00 0b 52 6f 63 6b 61 72 20 64 69 67 21
^^ length = 11
"Rockar dig!"
Rockar dig! is not the flag. It is the first
useful pivot and strongly hints at rockyou later.
Recognize the WPA2 association and 4-way handshake
After the early beacon/probe traffic, the capture contains an association request, association response, and then four EAPOL key frames. That changes the task completely: now the challenge can be solved as a WPA2 crack followed by traffic decryption.
# Representative EAPOL frames
08 02 ... aa aa 03 00 00 00 88 8e ...
08 01 ... aa aa 03 00 00 00 88 8e ...
# 0x888e = EAPOL
The four relevant frames are:
- Message 1 from AP to client with ANonce.
- Message 2 from client to AP with SNonce and MIC.
- Message 3 from AP to client.
- Message 4 from client to AP.
AP / BSSID
48:22:54:12:34:56
Station
74:86:e2:12:34:56
After extracting those frames, I built a minimal raw 802.11 PCAP and fed it to
wpapcap2john.
# Build a raw 802.11 PCAP from decoded frames
import struct
with open("handshake.pcap", "wb") as f:
f.write(struct.pack("<IHHIIII", 0xa1b2c3d4, 2, 4, 0, 0, 65535, 105))
ts = 0
for frame in frames:
ts += 1
f.write(struct.pack("<IIII", ts, 0, len(frame), len(frame)))
f.write(frame)
wpapcap2john handshake.pcap > handshake.hash
Crack the WPA2 PSK
This is where the SSID clue pays off. Rockar dig! strongly suggests
rockyou, and the host already had /usr/share/wordlists/rockyou.txt.
HOME=/tmp/johnhome john \
--wordlist=/usr/share/wordlists/rockyou.txt \
handshake.hash \
--format=wpapsk
Loaded 1 password hash (wpapsk, WPA/WPA2/PMF/PMKID PSK ...)
bestfriends4eva (Rockar dig!)
1g 0:00:00:02 DONE
bestfriends4eva. This is real, but it is
still not the final flag.
I also verified the passphrase manually by recomputing the EAPOL MIC from message 2.
import hashlib
import hmac
ssid = b"Rockar dig!"
passphrase = b"bestfriends4eva"
pmk = hashlib.pbkdf2_hmac("sha1", passphrase, ssid, 4096, 32)
def prf512(key, A, B):
out = b""
i = 0
while len(out) < 64:
out += hmac.new(key, A + b"\\x00" + B + bytes([i]), hashlib.sha1).digest()
i += 1
return out[:64]
ptk = prf512(pmk, b"Pairwise key expansion", B)
kck = ptk[:16]
mic = hmac.new(kck, eapol_with_zeroed_mic, hashlib.sha1).digest()[:16]
Derive the temporal key and decrypt CCMP data
After the handshake, the capture contains protected data frames. These were CCMP frames, so the next job was to derive the temporal key from the recovered PSK and decrypt the payloads.
The pairwise temporal key is the third 16-byte chunk of the PTK:
tk = ptk[32:48]
For each CCMP frame:
- Take the 24-byte 802.11 header.
- Take the 8-byte CCMP header.
- Build the CCMP nonce from priority, transmitter address, and packet number.
- Use AES-CCM to decrypt the protected payload.
from Cryptodome.Cipher import AES
hdr = frame[:24]
ccmp = frame[24:32]
enc = frame[32:-12] # ciphertext, excluding MIC and FCS
# PN = PN5 PN4 PN3 PN2 PN1 PN0
pn = bytes([ccmp[7], ccmp[6], ccmp[5], ccmp[4], ccmp[1], ccmp[0]])
# Non-QoS data uses priority 0
nonce = b"\\x00" + hdr[10:16] + pn
cipher = AES.new(tk, AES.MODE_CCM, nonce=nonce, mac_len=8)
plaintext = cipher.decrypt(enc)
That produced clean LLC/IP/TCP payloads. One of the decrypted frames contains the flag as plaintext at the end of the TCP payload.
IDX 74 PT ...
...
756e6475747b4d6135743472335f34765f34764b3064346e316e675f3063685f44336b727970743372316e675f34765f573146317d
ASCII:
undut{Ma5t4r3_4v_4vK0d4n1ng_0ch_D3krypt3r1ng_4v_W1F1}
Recover the flag
The actual submission string is not the SSID and not the WPA2 password. It is the plaintext recovered from the decrypted Wi-Fi payload.
undut{Ma5t4r3_4v_4vK0d4n1ng_0ch_D3krypt3r1ng_4v_W1F1}
bestfriends4eva, decrypt CCMP traffic, recover the flag from the TCP
payload.
Command summary
# 1. Clone / patch OpenOFDM
git clone https://github.com/jhshi/openofdm.git
# 2. Decode the raw GNU Radio file
python3 openofdm/scripts/decode_capture.py capture.cfile
# 3. Extract and package a WPA handshake
wpapcap2john handshake.pcap > handshake.hash
# 4. Crack the PSK
HOME=/tmp/johnhome john \
--wordlist=/usr/share/wordlists/rockyou.txt \
handshake.hash \
--format=wpapsk
# 5. Use the PSK to derive PTK/TK and decrypt CCMP traffic
# 6. Recover the flag from the decrypted payload
Codex session and Python scripts
The latest local Codex session for this writeup was stored at
/home/userhonest/.codex/sessions/2026/03/21/rollout-2026-03-21T21-55-54-019d122e-dc63-79b2-be27-c448efe8649d.jsonl.
# Python scripts used in the solve
openofdm/scripts/decode_capture.py
openofdm/scripts/decode.py
# decode_capture.py
# Loads GNU Radio float32 IQ from capture.cfile
# Detects active packet regions
# Feeds each region into the OpenOFDM decoder
#
# decode.py
# Main OpenOFDM PHY decoder used to parse 802.11 OFDM frames
# Patched for Python 3 compatibility during the solve