Solve Summary

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.

SSID Rockar dig!
PSK bestfriends4eva
Flag Recovered from decrypted TCP data
Step 1

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
Step 2

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.

The original OpenOFDM scripts were not ready to run as-is. Python 2 syntax, vendored dependency issues, and the missing 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 wltrace gracefully and still expose raw bytes.
  • Add a wrapper to load capture.cfile directly.
  • Patch vendored commpy compatibility 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]
Step 3

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!"
Clue: the SSID Rockar dig! is not the flag. It is the first useful pivot and strongly hints at rockyou later.
Step 4

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:

  1. Message 1 from AP to client with ANonce.
  2. Message 2 from client to AP with SNonce and MIC.
  3. Message 3 from AP to client.
  4. 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
Step 5

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
The recovered WPA2 passphrase is 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]
Step 6

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}
Step 7

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.

Final Flag undut{Ma5t4r3_4v_4vK0d4n1ng_0ch_D3krypt3r1ng_4v_W1F1}
Short version: identify Wi-Fi, extract handshake, crack PSK bestfriends4eva, decrypt CCMP traffic, recover the flag from the TCP payload.
Appendix

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