Challenge Name: forensics_rubiks-cube
Category: Forensics
Difficulty: Medium
Author: Vincent
Challenge Description
Our coworker just got a new Rubik’s Cube and it is driving everyone in the office crazy. But more importantly, it is decreasing their productivity and thereby shareholder value.
Figure out what moves they executed and write them in standard notation separated by underscores and wrapped in
brunner{}.Example: If the sequence was
R B B U F' F' D', the flag would bebrunner{R_B2_U_F2_D'}.
I was SO excited to take this challenge as I am a bit of a cuber myself (modest 18 sec PB on 3x3)…
We get a single rubiks.pcapng. The example shows the flag is a move sequence, and adjacent identical turns collapse (B B → B2, F' F' → F2). So we need to recover the turns a Bluetooth “smart cube” reported over the air.

Support my friends over at MasterCubeStore and start solving faster!
Approach
Step 1 — Identify the capture
file says it’s a pcapng; scapy shows every frame is HCI_Mon — a Bluetooth HCI (BLE) monitor capture. The protocol hierarchy (tshark) shows GATT/ATT traffic, and the advertised device names include:
QY-QYSC-S-0D1E
QY-QYSC-S is the advertising name of a QiYi Smart Cube (QYSC). GATT service is fff0, and the cube’s data characteristic is fff6 (ATT handle 0x001a).
Step 2 — Find the move traffic
Dumping ATT packets, the cube→app Handle Value Notifications (opcode 0x1b) on handle 0x1a carry fixed 16-byte-aligned blobs:
tshark -r rubiks.pcapng -Y "btatt.opcode==0x1b && btatt.handle==0x001a" -T fields -e btatt.value
31 of them. One blob even repeats a 16-byte block internally (aadd489de230471c… twice) — the tell-tale signature of AES-ECB (identical plaintext blocks → identical ciphertext blocks). So the payloads are AES-128-ECB encrypted.
Step 3 — The key and protocol (QiYi is not custom-secure)
The QiYi cube uses a fixed, publicly reverse-engineered AES-128 key (Flying-Toast’s qiyi_smartcube_protocol, cstimer, and many others all agree):
57 b1 f9 ab cd 5a e8 a7 9c b9 8c e7 57 8c 51 08 (AES-128, ECB, NoPadding)
Decrypted message layout:
| offset | meaning |
|---|---|
[0] | 0xFE magic |
[1] | message length |
[2] | opcode — 0x02 = full state (hello), 0x03 = state change (a move) |
[7:34] | 27 bytes = 54 facelet nibbles ("LRDUFB"[nibble]) |
[34] | move byte (opcode 0x03 only) |
| last 2 | CRC16/MODBUS over the whole message (valid ⇒ CRC of message == 0) |
The move byte raw ∈ 1..12 decodes as (per every reference implementation):
face = ['L','L','R','R','D','D','U','U','F','F','B','B'][raw-1]
prime = "'" if raw % 2 == 1 else "" # odd raw = CCW
Step 4 — Decrypt and read the moves
Decrypting all 31 notifications in frame order: all 31 pass the CRC16/MODBUS check — proof the key and parsing are correct. The first is opcode 0x02 (the cube’s current, already-scrambled state), followed by 30 opcode-0x03 move events:
F F U U B' R R B' L D' D' R F F U B' B' D R R B' B' D' B' B' U F F U B
Step 5 — Verify the moves are real turns (geometry-independent)
Since the cube didn’t start solved, we can’t check against a solved cube — but each 0x03 packet also carries the full facelet state after the move, so we can cross-validate:
- The positions that stay fixed across all 30 transitions are exactly
[4, 13, 22, 31, 40, 49]— the centre sticker of each of the six 9-sticker faces. A rigid cube rotation must fix all centres. ✔ - For every move token, a single consistent rigid permutation
after[i] = before[perm[i]]explains all its occurrences. The frequent moves (B'×8,F×6,R×5,U×5) each leave exactly54 − 20 = 34stickers unchanged — precisely a quarter turn (which cycles 20 stickers). ✔
The decode is airtight.
Step 6 — Standard notation
Collapsing adjacent identical quarter-turns into half-turns (X X → X2, dropping the prime, exactly as the example specifies), the 30 turns become 20 tokens:
F2 U2 B' R2 B' L D2 R F2 U B2 D R2 B2 D' B2 U F2 U B
Step 7 — Watch the solve
Below is the recovered sequence animated. The setup is the inverse of the recovered alg, so the cube starts scrambled and the coworker’s twenty moves drive it back to solved — press play and step through:
Flag
brunner{F2_U2_B'_R2_B'_L_D2_R_F2_U_B2_D_R2_B2_D'_B2_U_F2_U_B}
Reflections and Learnings
- A repeated ciphertext block is a free tell: one notification that repeated a 16-byte block gave away AES-ECB (identical plaintext → identical ciphertext) before any key was in hand. Scanning the raw bytes for structure pointed straight at the cipher mode.
- “Custom-secure” rarely is — identify the device first: the QiYi cube ships a fixed, publicly reverse-engineered AES-128 key. Recognising commodity hardware turned the crypto into a lookup rather than a break; always check whether the target is a known device with a documented protocol.
- Validate the decode independently of the answer: since the cube didn’t start solved, correctness was proven two ways — all 31 messages passing CRC16/MODBUS, and every move’s full facelet state fitting one rigid permutation with all six centres fixed. Independent cross-checks turn “probably right” into “airtight.”
- Let the format’s own redundancy check you: each
0x03packet carried the post-move state, so the recovered turns could be verified against cube geometry instead of trusted blindly.