Rubik's Cube

869 Words · 3 Minutes, 57 Seconds

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 be brunner{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.

The actual cube

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:

offsetmeaning
[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 2CRC16/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 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


forensicspcapblebluetoothaesecb

Forensics