Normal view

There are new articles available, click to refresh the page.
Before yesterdayHACKINGISCOOL

HEVD StackOverflowGS x86 Win7 - exploitation analysis

5 October 2021 at 06:00

Introduction

This post is about kernel mode exploitation basics under Windows. It operates on assumptions that the reader is familiar with terms such as process, thread, user and kernel mode and the difference between user and kernel mode virtual address range. One could use this post as an introduction to HEVD.

Even though I came across at least one good write up about Hacksys Extreme Vulnerable Driver StackOverflowGS (https://klue.github.io/blog/2017/09/hevd_stack_gs/, highly recommend it), after reading it I still felt that I did not understand the entire exploitation process (did not notice the link to the source code at the time :D), so I fell back on the PoC provided by HEVD (https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Exploit/StackOverflowGS.c), analyzed it and learned a few things, now I am just sharing my insights and tips.

Setup

There are numerous resources on how to set up Windows kernel debugging and install HEVD (e.g. https://hshrzd.wordpress.com/2017/05/28/starting-with-windows-kernel-exploitation-part-1-setting-up-the-lab/ and https://hshrzd.wordpress.com/2017/06/05/starting-with-windows-kernel-exploitation-part-2/).

I personally prefer using my host OS as the debugger and a VirtualBox VM as the debuggee (Windows 7, x86).

VM setting of the serial port for debugging

To attach to the VM, I run the following command (make sure windbg.exe is in your %PATH%):

windbg -k com:pipe,port=\.\pipe\com_1,resets=0,reconnect

When successfully attaching a debuggee, windbg output will look like this:

I myself have experienced issues when rebooting the debuggee (which happened a lot with all the crashes resulting from my attempts at exploitation) with windbg running; it just didn't want to attach to the named pipe and thus there was no connection between windbg and the debuggee. Also, trying to attach to a VM that was already running didn't work this way either. I figured that for me everything always works as should when I first boot the VM and then, once the OS loading progress bar pops up, I run the command to spawn windbg and make it connect to the named pipe created by VirtualBox.

Also, don't forget to load the symbols, e.g.:

.sympath C:\Users\ewilded\HACKING\VULNDEV\kernel\windows\HEVD\HEVD.1.20\drv\vulnerable\i386;SRVC:\SymbolsServerhttps://msdl.microsoft.com/download/symbols

The vulnerability

StackOverflowGS (code here https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HEVD/Windows/BufferOverflowStackGS.c) is a vanilla stack-based buffer overflow, just like StackOverflow (code here https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HEVD/Windows/BufferOverflowStack.c). The only difference is that in this case stack smashing is detected via a stack canary/stack cookie (a good introduction to the subject can be found here).

All HEVD exercises have the same structure and are all called in the same manner.

Whenever a user wants to interact with the module, they send the driver a data structure -  IRP (https://docs.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/i-o-request-packets). This data structure is our malicious input vector.

On line 128 of the HackSysExtremeVulnerableDriver.c main driver source file, we can see that IrpDeviceIoCtlHandler function is assigned to IRP_MJ_DEVICE_CONTROL packets:

That function can be found in the same file, starting with line 248:

Depending on the IOCTL code (an unsigned long integer argument, part of the IRP), IrpDeviceIoCtlHandler runs a different function:

Constants like HEVD_IOCTL_BUFFER_OVERFLOW_STACK are numeric variables predefined in HackSysExtremeVulnerableDriver.h.

So each exercise has its corresponding function with "IoctlHandler" suffix in its name (BufferOverflowStackIoctlHandler, BufferOverflowStackGSIoctlHandler and so on). Let's see what this function looks like in our case (https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HEVD/Windows/BufferOverflowStackGS.c):

So there is another function, named TriggerBufferOverflowStackGS, run from BufferOverflowStackGSIoctlHandler. So the function call tree, starting from IrpDeviceIoCtlHandler, is now:

Finally, the function is pretty simple too:

UserBuffer is a pointer to the user mode memory block (valid in the address space of the process that is currently interacting with the driver). Kernel mode code will be reading data from this location.

Size is an integer telling HEVD how many bytes we want it to read from the UserBuffer memory block - and write to kernel memory, starting at KernelBuffer. KernelBuffer is a local variable (defined on line 72 visible in the screenshot above), so it resides on the stack.

Both the UserBuffer pointer and the Size are delivered with the IRP and controlled by the user mode program that created it and triggered an interrupt to communicate with this driver (we'll get to that code too, shortly).

Then we get to the bottom of this:

So basically it's a vanilla stack-based buffer overflow. We can overwrite KernelBuffer with UserBuffer, with Size bytes (we control both UserBuffer and Size).

Let's set up a breakpoint in windbg, at HEVD!TriggerStackOverflowGS:

By listing the breakpoint (bl) we can see the current kernel mode address of the function (87e3f8da), which will vary between platforms and system boots.

View the disassembly of the entire function we can notice two important points in the code:

First is our vulnerable memcpy call, the second is the SEH_epilog4_GS, the function responsible for checking the saved stack canary and preventing from normal returning if the stack was detected to be smashed (if the cookie doesn't match), aimed at preventing exploitation.

Naturally, a breakpoint at 87e3f964 e871c8ffff      call    HEVD!memcpy (87e3c1da) would be more precise, as we could see directly how the stack buffer looks like before and after memcpy executes. Let's set it:

By listing the existing breakpoints again, we can see that windbg neatly displays both addresses using properly resolved symbols, so our second breakpoint set using the address 87e3f964 got nicely resolved to HEVD!TriggerStackOverflow+0x8a. I personally prefer to save these, so I can use them later when running again, just to remember where the actual breakpoint I am interested in is.

Now, we need to interact with the driver in order to see how the buffer we supply is stored on the stack, what do we overwrite and how error conditions we cause this way will differ depending on the buffer size.

For this purpose, I assembled a simple piece of C code based on other existing HEVD PoCs (I use Dev-C++) https://gist.github.com/ewilded/1d015bd0387ffc6ee1284bcb6bb93616:

  • it offers two payload types; a string of A-s or up to 3072 bytes of de Brujin sequence,
  • it asks for the size argument that will be sent over to the driver.

Below screenshot demonstrates running it in order to send a 512-byte buffer filled with 'A':

At this point we should hit the first breakpoint. We just let it go (g) and let it hit the second breakpoint (just before memcpy):

Let's see the stack:

Now, let's just step over once (p), so we get to the next instruction after the memcpy call, and examine the stack again:

So we can clearly our 512-byte buffer filled with 'A'. Now, at this point there is no buffer overflow.

Now, the next value on stack, right after that buffer (in this case 070d99de), is the stack cookie.

By the way, this is a good opportunity to notice the call stack (function call tree):

We can see that our saved return address is 87e3f9ca (HEVD!TriggerStackOverflowGS+0x8f)(red). The SEH handler pointer we will overwrite is sitting between the stack cookie and the saved RET (green):

If we let it running further (g), we can see nothing happens and fuzz.exe returns:

Good, as the buffer was 512, there was no overflow, everything returned cleanly.

Now, let's see what happens when we increase the buffer size by just one:

First two breakpoints hit, nothing to see yet:

Now, let's step over (p or F10) and see the stack again. This time we overwrote the stack cookie, by one byte (0d9bb941):

Now, let's let the debuggee go and see what happens (also, note the !analyze -v link generated in windbg output - click on it/run the command to see more details about the crash):

We end up with a fatal error 0x000000f7 (DRIVER_OVERRAN_STACK_BUFFER), which means that the __SEH_epilog4_GS function detected the change in the cookie saved on the stack and triggered a fatal exception.

Just as expected.

It is important to pay close attention to the error code, especially in this case: 0x000000f7 (DRIVER_OVERRAN_STACK_BUFFER) looks a lot like 0x0000007f (DOUBLE_TRAP), whereas the second one basically means that some sort of exception was triggered while already executing some exception handler - in other words, it means that after one exception, the code handling the exception encountered another exception. Distinguishing between these two (easy to mix up) is crucial while developing this exploit, as while the first one indicates that the stack cookie was overwritten and that the SEH __SEH_epilog4_GS has executed and detected the tampering to prevent exploitation. On the other hand, 0x0000007f (DOUBLE_TRAP) indicates that we triggered an exception and that afterwards another exception was raised. We can trigger an access violation by providing sufficiently large value of the Size argument in an IRP, causing the kernel-mode memcpy call to either read beyond the page of the user-mode process working set, or write beyond the kernel stack, depending on which happens first).

Exploitation approach

When it comes to stack cookies, there are several bypass scenarios.

The stack cookie could be leaked by exploiting another vulnerability (chaining, just like in one of my previous write ups) and then used in the payload to overwrite the original value of the canary cookie with the original value, making the entire stack-smashing invisible to the stack canary-checking routine called in the function's epilogue.

Another chaining method involves overwriting the process-specific pseudo-random value of the current cookie in the process memory, wherever it is stored (depending on the OS and compiler).

And then finally there is the third exploitation approach, abusing the fact that exception handlers are executed before the stack cookie is checked. Sometimes it is possible to abuse exception handling code - in this case a SEH handler pointer, which is also stored on the stack in a location we can overwrite. The idea is to abuse the memory corruption vulnerability in such a way that we overwrite a pointer to an exception handler and then we trigger an exception within the same function, before the stack checking routine in the function's epilogue is executed. This way we redirect the execution to our payload (our shellcode), which first elevates our privileges (in this case, as it's a local kernel EoP exploit), then returns to the parent function (the function that called the function we are exploiting - the parent in the call stack/call tree), without ever running the stack cookie-checking routine.

Again, please refer to https://dl.packetstormsecurity.net/papers/bypass/defeating-w2k3-stack-protection.pdf for more details on the general subject of defeating stack cookies under Windows.

HEVD official PoC

The tricky part in this exercise is that we have to do both things with one input (one device interaction, one IRP with a buffer pointer and size, one call of the TriggerStackOverflowGS function); overwrite the pointer to the SEH exception handler AND cause an exception that the handler would be used for.

The only viable option here is to cause the vulnerable memcpy call itself first  overwrite the buffer along with the saved stack cookie and the SEH handler pointer AND trigger an access violation exception - either due to exceeding the size of the user mode buffer and reading past the memory page that holds it, or by writing past the stack boundary (whichever happens first). Now, writing down the stack would completely wipe out all the older (parent) stack frames, making it super hard to return from the shellcode in a way that would avoid crashing the system. Thus, having the kernel code read past the user-supplied user mode buffer is a much better option - and I really like the way this has been solved in the original HEVD PoC (https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Exploit/StackOverflowGS.c).

The entire payload that is introduced into the kernel buffer (bytes we write to the stack) is 532 bytes long. It's 512 bytes of the original buffer, 4 bytes of the stack cookie, 12 bytes of other 3 DWORDs that we don't care about (in the payload referred to as junk) and then finally 4 bytes of the SEH handler. 512 + 4 + 12 + 4 = 532. This is the exact number of bytes that need to be written to the stack for the SEH handler pointer to be overwritten with a value we control.

Now, in order to trigger an access violation exception in the same operation (memcpy), just after our 532 bytes from our user mode buffer were copied into the kernel mode stack, we want to place our 532-byte payload at the end of a page (the basic memory allocation unit provided by the OS memory manager, 4096 bytes by default). So from our user mode program, we allocate a separate page (4096-byte buffer). Then we put our payload into its tail (last 532 bytes) - so our payload starts on the 3565-th byte and ends on the 4096-th (the last 4 bytes being the pointer to our shellcode).

Finally, to trigger an access violation, we adjust the buffer size parameter sent encapsulated in the IRP, to exceed the size of our payload (so it must be bigger than 532, e.g. 536). This will cause memcpy running in kernel mode to attempt reading four bytes beyond the page our payload is located in. To make sure this causes an access violation, the page must not have an adjacent/neighbor page. So for example, if the virtual address of the user mode page allocated for the buffer with our payload is 0x00004000, with page size being 0x1000 (4096), the valid address range for this page will be 0x00004000 <--> 0x00004fff. Meaning that accessing address 0x00005000 or higher would mean accessing another page starting at 0x00005000 (thus we call it an adjacent/neighbor page). Since we want to achieve an access violation, we need to make sure that no memory is allocated for the current (exploit) process in that range. So we want just one, alone page allocated, reading past which causes an access violation.

There are a few ways to cause such a violation. For example, two adjacent pages can be allocated, then the second one could be freed, then the read operation is triggered on the first one, with the size operand making it read beyond the first page, entering the second one. And this is the method employed by klue's PoC: https://github.com/klue/hevd, with his mmap and munmap wrappers around NtAllocateVirtualMemory and NtFreeVirtualMemory.

Another one is to allocate the page in a way that ensures nothing else is allocated in the adjacent address space, which is what the official HEVD exploit does by using an alternative memory allocation method supported by Windows.

Let's  analyze the code (https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Exploit/StackOverflowGS.c).

First, we have declarations. hFile is used for opening the driver object (in order to then send the IRP) . PageSize is 0x1000 (4096). MemoryAddress is the pointer to the special page we are going to allocate our stack-smashing payload (528 bytes of junk, 4 bytes overwriting the SEH handler pointer, pointing at our shellcode, located at the page's tail, starting at 3565-th byte). SuitableMemoryForbuffer is the pointer we are going to pass to HEVD as the UserBuffer. It will point at the 3565-th byte of the 4096-byte page allocated at MemoryAddress. EopPayload is another pointer, another location in user mode, containing our shellcode (so the shellcode is in a separate user mode buffer than the special page we are allocating for the stack-smashing payload):

Variable declarations

Finally, there is SharedMemory - a handle to the mapped file object we are going to create (as an alternative way of allocating memory). Instead of requesting a new page allocation with VirtualAlloc, an empty, non-persisted memory mapped file is created. Memory-mapped files are basically section objects (described properly in Windows Internals, Part 1, "Shared memory and mapped files" section), a mechanism used by Windows for sharing memory between processes (especially shared libraries loaded from the disk), also please see the official Microsoft manual to find out more about https://docs.microsoft.com/en-us/dotnet/standard/io/memory-mapped-files).

In this case, we are going to request creation of a "mapped-file" object without any file, by providing an INVALID_HANDLE_VALUE as the first argument to CreateFileMappingA - this scenario is mentioned in the manual page of this function (https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createfilemappinga):

So it's basically a section ("mapped file") object only backed by the system paging file - in other words, a region in the system paging file that we can map to our process's address space and use just like (almost) a regular page:

Creation of a mapped file object

 Now, we map that region to our address space:

Mapping the object to the current process address space

Now, we're setting the SuitableMemoryForBuffer pointer at 3565-th byte of the SharedMemoryAddress region (this is where we will locate our 532-byte payload that will be then copied by the driver to a buffer on its stack):

Setting the payload pointer at 3565-th byte of the 4096 memory region

And we will the entire region with 'A':

Filling the entire 4096-byte region with 'A'

Then eventually, the payload is finished by setting its last 4 bytes to contain the user mode address of the shellcode (these bytes will overwrite the SEH handler). This is done in a bit indirect way, as first the pointer (MemoryAddress) is set at offset 0x204 (516) - right past the xored stack cookie - and overwrites 3 of the following junk pointers, only to eventually set the new value for the SE handler:

Grooming the buffer - this is tricky

It seems that simply setting the MemoryAddress to point at SuitableMemoryForBuffer + 0x210 directly (to point it at the location that will overwrite the SE handler pointer) would do the trick as well - other locations on the stack would be overwritten with meaningless 'A's anyway:

Then finally, we trigger the creation of our IRP and send it to the driver along with pointers to the UserBuffer (SuitableMemoryForBuffer - 3656-th byte of the 4096-byte region) and the Size argument; SeHandlerOverwriteOffset + RAISE_EXCEPTION_IN_KERNEL_MODE. SeHandlerOverwriteOffset is just the size of our payload (532). Then, a constant RAISE_EXCEPTION_IN_KERNEL_MODE is added to the size - it's just a numeric constant of 0x4 - and it's only to make the size argument exceed 4096 when added to the 3656-th byte being provided as the beginning of the buffer to read from:

Finally, talking to the driver

Shellcode

Our shellcode being a separate buffer in user mode, which will get executed by kernel mode HEVD code, instead of the legitimate exception handler - on modern kernels this would not get executed due to SMEP, but we're doing the very basics here.

First of all, let me recommend ShellNoob. It's a neat tool I always use whenever I want to:

  • analyze a shellcode (a sequence of opcodes) or a just some part of it,
  • write shellcode.

In this case we will use a slightly modified version of the publicly available, common Windows7 token-stealing payload (https://github.com/hasherezade/wke_exercises/blob/master/stackoverflow_expl/payload.h):

After converting the shellcode to ascii-hex and pasting it to shellnoob input (opcode_to_asm), this is what we get:

Our shellcode, executing in kernel mode, finds the SYSTEM process and then copies its access token over the token of the exploit process. This way the exploit process becomes NT AUTHORITY/SYSTEM. Have a look into https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Exploit/Payloads.c to see descriptions of all individual assembly instructions in this payload. Pay attention to the fact that while shellnoob output presents assembly in AT&T syntax, Payloads.c contain assembly in Intel syntax (this is why it's worth to know both, http://staffwww.fullcoll.edu/aclifton/courses/cs241/syntax.html).

This shellcode, however, requires one more adjustment.

Clean return

Now, the problem is, if we simply use this shellcode to exploit this particular vulnerability, the kernel will crash right after modifying relevant access token. The reason for this is the return process and messed up stack. The problem - and the solution - are already well described at https://klue.github.io/blog/2017/09/hevd_stack_gs/. I myself had to get my head around the process my own way to fully understand it and confirm (instead of just blindly running it and trusting it would work), that in fact the return stub provided by klue is going the correct one:

mov 0x78c(%esp), %edi
mov 0x790(%esp), %esi
mov 0x794(%esp), %ebx
add $0x9b8, %esp
pop %ebp
ret $0x8

So, the following return stub

had to be replaced. Again, I used shellnoob to obtain the opcodes:

Basically the entire problem boils down to the fact that we need to return to somewhere - and when we do, the stack needs to be aligned the same way as it would normally be during normal execution.

The entire process of aligning the stuck boils down to three things. First, identifying, where we will be returning - and taking notice of what the stack and the registers look like when return to that location is made normally. Second, setting a breakpoint in our shellcode, to again take notice of what the stack and the registers look like when our shellcode executes (it's convenient to use hardcoded software breakpoint in the shellcode itself - just append it with 0xcc (int3) instead of the return stub). Third, comparing the state of the registers and the stack between the two stages, finding where the register values to restore are in memory, restore them, then finally adjust the last one of them (ESP) and make the return.

Running

Source code can be found here.

Improving the write-what-where HEVD PoC (x86, Win7)

17 October 2021 at 20:12

Introduction

This one is about another HEVD exercise (look here to see the my previous HEVD post); the arbitrary write (https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HEVD/Windows/ArbitraryWrite.c). The main reason I decided to write up my experience with it is the fact that it instantly occurred to me that the official exploitation process, used both in the original PoC as well as described here, leaves the kernel in an unstable state with high probability of crash anytime after the exploit is run. So, this post is more about the exploitation technique, the problem it creates and the solution it asks for, rather than the vulnerability itself. It also occurred to me that doing HEVD exercises fully (like understanding exactly what and how) is quite helpful in improving the general understanding of how the operating system works.

When it comes to stuff like setting up the environment, please refer to my earlier HEVD post. Now let's get started.

The vulnerability

This one is a vanilla write-what-where case - code running in kernel mode performs a write operation of an arbitrary (user-controlled) value into an arbitrary (user-controlled) address. In case of a x86 system (we keep using these for such basic exercises as they are easier while debugger output with 32-bit addresses is more readable), it usually boils down to being able to write an arbitrary 32-bit value into an arbitrary 32-bit address. However, it is also usually possible to trigger the vulnerability more than once (which we will do in this case, by the way, just to fix the state of the kernel after privilege escalation), so virtually we control  data blocks of any size, not just four bytes.

First of all, we have the input structure definition at https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Exploit/ArbitraryOverwrite.h - it's as simple as it could be, just two pointers:

Then, we have the TriggerArbitraryWrite function in https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HEVD/Windows/ArbitraryWrite.c (screenshot below). First, we have a call to ProbeForRead on the input pointer, to make sure that the structure itself is located in user space (both ProbeForRead and ProbeForWrite methods throw an access violation exception if the address provided turns out to belong to the kernel space address range). Then, What and Where values held by the structure (note that these are both pointers and there are no additional checks here whether the addresses those pointers contain belong to kernel or  user space!) are copied into the local kernel mode function variables:

Then, we have the vulnerable write-what-where:

Now, let's see how this C code actually looks like after it's compiled (disassembly view in windbg):

Exploitation

So, as always, we just want to run our shellcode in kernel mode, whereas the only thing our shellcode does is overwriting the security token of our exploit process with the one of the SYSTEM process (token-stealing shellcode).  Again, refer to the previous blog post https://hackingiscool.pl/hevd-stackgs-x86-win7/ to get more details on the shellcode used.

To exploit the arbitrary write-what-where to get our shellcode executed by the kernel, we want to overwrite some pointer, some address residing in the kernel space, that either gets called frequently by other processes (and this is what causes trouble post exploitation if we don't fix it!) or is called by a kernel-mode function that we can call from our exploit process (this is what we will do to get our shellcode executed). In this case we will stick to the HalDispatchTable method - or to be more precise, HalDispatchTable+0x4. The method is already described here https://poppopret.blogspot.com/2011/07/windows-kernel-exploitation-basics-part.html (again, I recommend this read), but let's paraphrase it.

First, we use our write-what-where driver vulnerability to overwrite 4 bytes of the the nt!HalDispatchTable structure (nt!HalDispatchTable at offset 0x4, to be exact). This is because the NtQueryIntervalProfile function - a function that we can call from user mode - results in calling nt!KeQueryIntervalProfile (which already happens after switching into kernel mode), and that function calls whatever is stored at nt!HalDispatchTable+0x4:

So, the idea is to first exploit the arbitrary write to overwrite whatever is stored at nt!HalDispatchTable+0x4 with the user-mode address of our shellcode, then call the NtQueryIntervalProfile only to trick the kernel into executing it via calling HalDisaptchTable+0x4 - and it works like a charm on Windows 7 (kernel mode execution of code located in user mode buffer, as no SMEP in place).

The problem

The problem is that nt!HalDispatchTable is a global kernel structure, which means that once we tamper it, it will still be there if any other program refers to it (e.g. calls NtQueryIntervalProfile). And it WILL affect whatever we will be doing enjoying our SYSTEM privileges, because it WILL crash the entire system.

Let's say that the buffer holding our shellcode in our user mode exploit is at 00403040. If we overwrite the original value of nt!HalDispatchTable+0x4 with it, that shellcode will only be reachable and thus callable if the current process being executed is our exploit. Once the scheduler interrupt switches the current CPU core to another process, in the context of that process the user mode virtual address of 00403040 will either be invalid (won't even fall into any committed/reserved virtual address range within the virtual address space used by that process) or it will be valid as an address, but in reality it will be mapped to a different physical address, which means it will hold something completely different than our shellcode. Remember, each process has its own address space, separate from all other processes, whereas the address space of the kernel is global for the entire system. Therefore every kernel space address makes sense to the entire system (kernel and all processes), whereas our shellcode at 00403040 is only accessible to our exploit process AND the kernel - but only when the process currently being executed is our exploit. The same address referred to from a different process context will be invalid/point at something completely different.

So, after we tamper HalDispatchTable+0x4 by overwriting it with the address of the shellcode residing in the memory of the current process (our exploit) and call NtQueryIntervalProfile to get the shellcode executed, our process should now have SYSTEM privileges (and so will any child processes it spawns, e.g. a cmd.exe shell).

Therefore, if any other process in the system, after we are done with privilege escalation, calls NtQueryIntervalProfile, it will as well trick the kernel into trying to execute whatever is located under the 00403040 address. But since the calling process won't have this address in its working set or will have something completely different mapped under it, it will lead to a system crash. Of course this could be tolerated if we performed some sort of persistence immediately upon the elevation of privileges, but either way as attackers we don't want disruptions that would hurt our customer or potentially tip the defenders off. We don't want system crashes.

This is not an imaginary problem. Right after running the initial version of the PoC (which I put together based on the official HEVD PoC), all of the sudden I saw this in windbg:

Obviously whatever was located at 0040305b  at the time ( 000a - add byte ptr [edx],cl), was no part of my shellcode. So I did a quick check to see what was the process causing this - by issuing the !vad command to display the current process VADs (Virtual Address Descriptors), basically the memory map of the current process, including names of the files mapped into the address space as mapped sections - which includes the path to the original EXE file:

One of svchost.exe processes causing the crash by calling HalDispatchTable+0x4

One more interesting thing is that - if we look at the stack trace (two screenshots above) - the call of HalDispatchTable+0x4 did not originate from KeQueryIntervalProfile function, but from nt!EtwAddLogHeader+0x4b. Which suggests that  HalDispatchTable+0x4 is called from more places than just NtQueryIntervalProfile, adding up to the probability of such a post-exploitation crash being very real.

The solution

So, the obvious solution that comes to mind is restoring the original HalDispatchTable+0x4 value after exploitation. The easiest approach is to simply trigger the vulnerability again, with the same "where" argument ( HalDispatchTable+0x4) and a different "what" argument (the original value as opposed to the address of our user mode shellcode).

Now, to be able to do this, first we have to know what that original value of nt!HalDispatchTable+0x4 is. We can't try to read it in kernel mode from our shellcode, since we need to overwrite it first in order to get the shellcode execute in the first place. Luckily, I figured out it can be calculated based on information attainable from regular user mode execution (again, keep in mind this is only directly relevant to the old Windows 7 x86 I keep practicing on, I haven't tried this on modern Windows yet, I know that SMEP and probably CFG would be our main challenges here).

First of all, let's see what that original value is before we attempt any overwrite. So, let's view nt!HalDispatchTable:

The second DWORD in the memory block under nt!HalDispatchTable contains 82837940. Which definitely looks like an address in kernel mode. It has to be - after all, it is routinely called from other kernel-mode functions, as code, so it must point at kernel mode code. Once I called it up with dt command, windbg resolved it to HaliQuerySystemInformation. Running disassembly view command uu on it, revealed the full symbol name (hal!HaliQuerySystemInformation) and showed that in fact there is a function there (just based on the first few assembly lines we can see it is a normal function prologue).

OK, great, so we know that nt!HalDispatchTable+0x4, the pointer we abuse to turn arbitrary write into a privilege escalation, originally points to a kernel-mode function named hal!HaliQuerySystemInformation (which means the function is a part of the hal module).

Let's see more about it:

Oh, so the module name behind this is halacpi.dll. Now we both have the function name and the module name. Based solely on this information, we can attempt to calculate the current address of hal!HaliQuerySystemInformation dynamically. To do this, we will require the following two values:

  1. The current base address the halacpi.dll module has been loaded (we will get it dynamically by calling NtQuerySystemInformation from our exploit).
  2. The offset of the HaliQuerySystemInformation function within the halacpi.dll module itself (we will pre-calculate the offset value and hardcode it into the exploit code - so it will be version-specific). We can calculate this offset in windbg by subtracting the current base address of the halacpi.dll kernel-mode module (e.g. taken from the lmDvmhal command output) from the absolute address of the  hal!HaliQuerySystemInformation function as resolved by windbg. We can also calculate (confirm) the same offset with static analysis - just load that version of halacpi.dll into Ghidra, download the symbols file, load the symbols file, then find the static address of the function with its address within the binary and subtract the preferred module base address from that address.

Below screenshot shows the calculation done in windbg:

Calculating the offset in windbg

Below screenshots show the same process with Ghidra:

Preferred image base - 00010000
Finding the function (symbols must be loaded)
HaliQuerySystemInformation static address in the binary (assembly view)

Offset calculation based on information from Ghidra: 0x2b940 - 0x10000 = 0x1b940.

So, during runtime, we need to add 0x1b940 (for this particular version of halacpi.dll - remember, other versions will most likely have different offsets) to the dynamically retrieved load base address of halacpi.dll, which we retrieve by calling NtQuerySystemInformation and iterating over the buffer it returns (see the PoC code for details). The same function, NtQuerySystemInformation, is used to calculate the runtime address of the HalDispatchTable - the "what" in our exploit (as well as the  original HEVD PoC code and many other exploits of this sort). In all cases  NtQuerySystemInformation is called to get the current base address of the ntoskrnl.exe module (the Windows kernel). Then, instead of using a hardcoded (fixed) offset to get HalDispatchTable, a neat trick with LoadLibraryA and GetProcAddress is used to calculate it dynamically during runtime (see the full code for details).

The reason I could not reproduce this fully dynamic approach of calculating the offset from the base (calling LoadLibrary(halacpi.dll) and then GetProcAddress(HaliQuerySystemInformation)) to calculate hal!HaliQuerySystemInformation and used a hardcoded, fixed, manually precalculated 0x1b940 offset instead, is because the HaliQuerySystemInformation function is not exported by halacpi.dll - whereas GetProcAddress only works for functions that have their corresponding entries present in the DLL Export Table.

Full PoC

The full PoC I put together can be found here: https://gist.github.com/ewilded/4b9257b552c6c1e2a3af32879f623803.

nt/system shell still running after the exploit process's exit
The original HalDispatchTable+0x4 restored after exploit execution

A case of DLL Side Loading from UNC via Windows environmental variable

5 July 2022 at 15:51

About a month ago I decided to take a look at JetBrains TeamCity, as I wanted to learn more about CVE-2022-25263 (an authenticated OS Command Injection in the Agent Push functionality).

Initially I just wanted to find the affected feature and test the mitigation put in place, eventually I ended up searching for other interesting behaviors that could be considered security issues- and came across something I believed was a vulnerability, however upon disclosure the vendor convinced me that the situation was considered normal in TeamCity's context and its thread model. Since the feature I was testing allowed me to set some of the environmental variables later passed to the given builder step process (in my case it was python.exe).

During that process I accidently discovered that Python on Windows can be used to side-load an arbitrary DLL named rsaenh.dll, placed in a directory named system32, located in a directory pointed by the SystemRoot environment variable passed to the process (it loads %SystemRoot%/system32/rsaenh.dll).

For the purpose of testing, I installed TeamCity on Windows 10 64-bit, with default settings, setting both the TeamCity Server and the TeamCity Build Agent to run as a regular user (which is the default setting).

I used the same system for both the TeamCity Server and the Build Agent.
First, as admin, I created a sample project with one build step of type Python.
I installed Python3 (python3.10 from the Microsoft App Store, checked the box to get it added to the PATH), so the agent would be compatible to run the build. I also created a hello world python build script:

From that point I switched to a regular user account, which was not allowed to define or edit build steps, but only to trigger them, with the ability to control custom build parameters (including some environmental variables).

I came across two separate instances of UNC path injection, allowing me to attack the Build Agent. In both cases I could make the system connect via SMB to the share of my choosing (allowing me to capture the NTLM hash, so I could try to crack it offline or SMB-relay it).

In case of build steps utilizing python, it also turned out possible to load an arbitrary DLL file from the share I set up with smbd hosted from the KALI box.


The local IP address of the Windows system was 192.168.99.4. I ran a KALI Linux box in the same network, under 192.168.99.5.

Injecting UNC to capture the hash / NTLM-relay

On the KALI box, I ran responder with default settings, like this:

Then, before running the build, I set the teamcity.build.checkoutDir parameter to \\192.168.99.5\pub:

I also ran Procmon and set up a filter to catch any events with the "Path" attribute containing "192.168.99.5".
I clicked "Run Build", which resulted in the UNC path being read by the service, as shown in the screenshot below:

Responder successfully caught the hash (multiple times):

I noticed that the teamcity.build.checkoutDir was validated and eventually it would not be used to attempt to load the build script (which was what I was trying to achieve in the first place by tampering with it), and the application fell back on the default value C:\TeamCity\buildAgent\work\2b35ac7e0452d98f when running the build. Still, before validation, the service interacted with the share, which I believe should not be the case.

Injecting UNC to load arbitrary DLL

I discovered I could attack the Build Agent by poisoning environmental variables the same way as I attacked the server, via build parameter customization.
Since my build step used python, I played with it a bit to see if I could influence the way it loads DLLs by changing environmental variables. It turned out I could.

Python on Windows can be used to side-load an arbitrary DLL named rsaenh.dll, placed in a directory named system32, located in a directory pointed by the SystemRoot environment variable passed to the process.

For example, by setting the SystemRoot environmental variable to "\\192.168.99.5\pub" (from the default "C:\WINDOWS" value):

In case of python3.10.exe, this resulted in the python executable trying to load \\192.168.99.5\pub\system32\rsaenh.dll:

With Responder running, just like in case of attacking the TeamCity Server, hashes were captured:

However, since python3.10 looked eager to load a DLL from a path that could be controlled with the SystemRoot variable, I decided to spin up an SMB share with public anonymous access and provide a copy of the original rsaenh.dll file into the pub\system32\ directory shared with SMB.
I used the following /etc/samba/smb.config:

[global]

workgroup = WORKGROUP
log file = /var/log/samba/log.%m
max log size = 1000
logging = file
panic action = /usr/share/samba/panic-action %d
server role = standalone server
map to guest = bad user
[pub]
comment = some useful files
read only = no
path = /home/pub
guest ok = yes
create mask = 0777
directory mask = 0777

I stopped Responder to free up the 445 port, I started smbd:

service smbd start

Then, I ran the build again, and had the python3.10 executable successfully load and execute the DLL from my share, demonstrating a vector of RCE on the Build Agent:

Not an issue from TeamCity perspective

About a week after reporting the issue to the vendor, I received a response, clarifying that any user having access to TeamCity is considered to have access to all build agent systems, therefore code execution on any build agent system, triggered from low-privileged user in TeamCity, does not violate any security boundaries. They also provided an example of an earlier, very similar submission, and the clarification that was given upon its closure https://youtrack.jetbrains.com/issue/TW-74408 (with a nice code injection vector via perl environmental variable).

python loading rsaenh.dll following the SystemRoot env variable

The fact that python used an environmental variable to load a DLL is an interesting occurrence on its own, as it could be used locally as an evasive technique alternative to rundll32.exe (https://attack.mitre.org/techniques/T1574/002/, https://attack.mitre.org/techniques/T1129/) - to inject malicious code into a process created from an original, signed python3.10.exe executable .

POC

The following code was used to build the DLL. It simply grabs the current username and current process command line, and appends them to a text file named poc.txt. Whenever DllMain is executed, for whatever reason, the poc.txt file will be appended with a line containing those details:

First, let's try to get it loaded without any signatures, locally:

Procmon output watching for any events with Path ending with "rsaenh.dll":

The poc.txt file was created in the current directory of  C:\Users\ewilded\HACKING\SHELLING\research\cmd.exe\python3_side_loading_via_SystemRoot while running python:

Similar cases

There must be more cases of popular software using environmental variables to locate some of the shared libraries they load.

To perform such a search dynamically, all executables in the scope directory could be iterated through and executed multiple times, each time testing arbitrary values set to individual common environmental variables like %SystemRoot% or %WINDIR%. This alone would be a good approach for starters, but it would definitely not provide an exhaustive coverage - most of the places in code those load attempts happen are not reachable without hitting proper command lines, specific to each executable.

A more exhaustive, and but also demanding approach, would be static analysis of all PE files in the scope that simply indicate the usage of both LoadLibrary and GetEnv functions (e..g LoadLibraryExW() and _wgetenv(), as python3.10.exe does) in their import tables.

Intel PowerGadget 3.6 Local Privilege Escalation

27 March 2024 at 05:09

Vulnerability summary: Local Privilege Escalation from regular user to SYSTEM, via conhost.exe hijacking triggered by MSI installer in repair mode
Affected Products: Intel PowerGadget
Affected Versions: tested on PowerGadget_3.6.msi (a3834b2559c18e6797ba945d685bf174), file signed on ‎Monday, ‎February ‎1, ‎2021 9:43:20 PM (this seems to be the latest version), earlier versions might be affected as well.
Affected Platforms: Windows
Common Vulnerability Scoring System (CVSS) Base Score (CVSSv3): 7.8 HIGH
Risk score (CVSSv3): 7.8 HIGH  AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H (https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H&version=3.1)

I have reported this issue to Intel, but since the product has been marked End of Life since October 2023, it is not going to receive a security update nor a security advisory. Intel said that they are OK with me making this finding public, under the condition that I would emphasize that the product is EOL.

Description and steps to replicate:
On systems where Intel PowerGadget is installed from an MSI package, a local interactive regular user is able to run the MSI installer file in the "repair" mode and hijack the conhost.exe process (which is created by an instance of sc.exe the installer calls during the process) by quickly left-clicking on the console window that pops up for a split second in the late stage of the process. Left-clicking on the conhost.exe console window area freezes the console (meaning it prevents the sc.exe process from exiting). That process is running as NT AUTHORITY/SYSTEM. From there, it is possible to run a web browser by clicking on one of the links in the small GUI window that can be called by right-clicking on the console window bar and entering "properties". Once a web browser is spawn, attacker can call up the "Open" dialog and in that way get a fully working escape to explorer. From there they can, for example, browse through C:\Windows\System32 and right-click on cmd.exe and run it, obtaining as SYSTEM shell.

Now - an important detail - on most recent builds of Windows neither Edge nor Internet Explorer will spawn as SYSTEM (this is a mitigation from Microsoft); thus for successful exploitation another browser has to already be present in the system. As you can see I pick Chrome and then spawn an instance of cmd.exe, which turns out to be running as SYSTEM. Also, when doing this, DO NOT check "always use this app" in that dialog, as if you pick the wrong one (e.g. Edge or IE), it will be saved as the default http/https handler for SYSTEM and from then further attacks like this won't work if you want to repeat the POC - unless you reverse that change somewhere in the registry.

This class of Local Privilege Escalations is described by Mandiant in this article: https://www.mandiant.com/resources/blog/privileges-third-party-windows-installers.

To run the installer in repair mode, one needs to identify the proper MSI file. After normal installation, it is by default present in C:\Windows\Installer directory, under a random name. The proper file can be identified by attributes like control sum, size or "author" information - just as presented in the screenshot below:

The exploitation process is illustrated in the screenshots below, reflecting the the steps taken to attain a SYSTEM shell (no exploit development is required, the issue can be exploited using GUI).

Just for the record, the versions of Chrome and Windows this was successfully performed on:

Recommendation:
Technically, as per the reference, it is recommended to change the way the sc.exe is called, using the WixQuietExec() method (see the second reference). In such case the conhost.exe window will not be visible to the user, thus making it impossible to perform any GUI interaction and an escape.
I am, however, aware that this product is no longer maintained since October 2023 (https://www.intel.com/content/www/us/en/developer/articles/tool/power-gadget.html) and that includes security updates. Still, I believe a security advisory and CVE should be released just to make users and administrators aware why they need to replace PowerGadget with Intel Performance Counter Monitor.
Another possible (short-term) mitigation is to disable MSI (https://learn.microsoft.com/en-us/windows/win32/msi/disablemsi).

References:
https://www.mandiant.com/resources/blog/privileges-third-party-windows-installers
https://wixtoolset.org/docs/v3/customactions/qtexec/
https://www.intel.com/content/www/us/en/developer/articles/tool/power-gadget.html)
https://learn.microsoft.com/en-us/windows/win32/msi/disablemsi

❌
❌