pwnable.kr - unlink
- The goal of this challenge is to gain RCE via a variant of unlink macro used by libc.
- In simplified terms this program will release an node out of a double linked list that we fully control.
Main creates three chunks on heap, will crate a double linked list consisting of that chunks and is calling
gets()at the end. Our input will be copied to first chunk and theres no sanitazation of the same.
Thanks to hat overflow vulnerabilty we are able to copy a payload into first chunk, overwrite meta information of heap chunks and spawn shell with use of
shell()routine provided by
The routine that is responsible for unlinking a node out of double linked list gave me flashbacks on famous
unlink() macro implemented in dlmalloc/ptmalloc, but with no security mitigations.
While running main, it creates three chunks (called A, B and C onwards) on the heap. Each chunk is defined as struct the same manner:
So total chunk size will be 0x10 bytes. Attaching with a debugger and setting a break at
0x08048575 reveals, that these chunks will be adjacent in memory:
Looking at disassembly, we observe, that some data gets written into freshly allocated chunks:
After instruction @ 0x0804859f got executed, we got situation graphically demonstrated below:
The following code snippet shows the "unlinking" routine that will be called by main in executable:
Remarkably there is no check for corruption in linked list like the one in malloc.c when using libc's
ptmalloc implementation out of the box:
My first thought while preparing any exploit was:
Ok, just make
B->bkpoint to ret addy on the stack and overwrite it with address of
shell(), which gets written into
I was absolutely wrong.
B-bk have to point to a writeable address, but I have never seen writable .text section before...
That's why we have to find any writable location in process' memory map. But wait...Isn't the binary providing already a stack address to us?
Changing tactics now:
B->bk point to a stack address and
B->fd to an address in first chunk we control, we could gain any progress in our aim in binary demolition...
Check the following code:
0x08048504 push ebp
0x08048505 mov ebp, esp
0x08048507 sub esp, 0x10
0x0804850a mov eax, dword [arg_8h]
0x0804850d mov eax, dword [eax + 4]
0x08048510 mov dword [local_4h], eax
0x08048513 mov eax, dword [arg_8h]
0x08048516 mov eax, dword [eax]
0x08048518 mov dword [local_8h], eax
0x0804851b mov eax, dword [local_8h]
0x0804851e mov edx, dword [local_4h]
0x08048521 mov dword [eax + 4], edx
0x08048524 mov eax, dword [local_4h]
0x08048527 mov edx, dword [local_8h]
0x0804852a mov dword [eax], edx
unlink uses a normal epilogue with
ret instructions. We could use
leave to pop into ebp our controlled address. Control of ebp leads to control of esp as well. Afterwards
ret would do rest for us with redirecting control flow.
After unlinking the following instructions will be executed:
0x080485f2 call sym.unlink
0x080485f7 add esp, 0x10
0x080485fa mov eax, 0
0x080485ff mov ecx, dword [ebp-4]
0x08048603 lea esp, [ecx - 4]
Content of ebp-4 gets copied to ecx. So at the end program will be redirected to wherever ecx-4 points to. If we let ebp point to a location on heap, control flow will be redirected to whatever is written 4 bytes before that location. We have a "write-what-where"-gadget, so why not letting it point to
The address that will be leaked out on stdout is at location ebp-0x14 which can be verified using a debugger:
That means, that leak gives us information about the location of ebp-0x14 => leaked addy + 0x10 will point to ebp-0x4 (our target address).
Now after evaluating out tactics, payload actually will look like this:
There's only one last remaining point to care about. In main+212
lea esp,[ecx-0x4] the address of whats written to ebp-4 will be reduced by 4 bytes. As buffer offset in chunk is at position 8 we have to assign
Check the following script in my github repo