Now we will need a call esp or jmp esp address…this will ultimately call our 'Cs' where our reverse shell will be loaded while Kkeeping in mind the restricted characters. We find the following address using mona.py in immunity debugger
FF E4 = jmp esp
We can verify this address is a JMP ESP by searching it
At this point, we can updated our EIP to redirect to this JMP ESP address
As we follow the crash in immunity, we can see that EIP has been successfully overwritten with our JMP ESP address
...once we take the JMP ESP, we are redirected to the top of our Cs
Reverse shell time!
We will need to create our revere shell to encode it with x86/alpha_mixed in order to avoid the restricted characters
We update our POC one last time
Again we follow the jmp esp and we hit the beginning of our reverse shell. We let the code execution continue and successfully get a reverse shell in our Kali listener.
You can find numerous write-ups about this vulnerability and exploit it. This buffer overflow vulnerability gives us a good mix of shellcode encoding due to restricted characters as well as how to implement an egghunter.
I'm doing this as part of my preparation of Offensive Security Certified Expert (OSCE) certification
0x0 - Setup
Our Proof-of-Concept creates a .zip file that we will access using QuickZip.
0x1 - Fuzzing
As usual, step one in exploit development is fuzzing. This allows us to examine how the program responds when we introduce a buffer (oversized) to it.
We will be using the fuzzer from corelan as shown below.
We examine the crash using immunity debugger and we can see that have successfully overwritten the SEH handler with 41s.
0x2 - Calculating the offset
We will utilize Metasploit's pattern_create.rb and pattern_offset.rb
First, we create unique characters of 4068 bytes and update our POC
We check the SEH chain after the crash to get the offset values
Second, we calculate these values using pattern_offset.rb
Finally, we update our POC and verify if these values are correct.
If everything is correct, we should see Bs at byte 294 and Cs at byte 298.
Our values are correct as we see the 43s and 42s overwrite both SEH and nSEH respectively.
0x3 - Verifying Restricted Characters
At this point, we are now ready to find an address to redirect the SEH handler. However, before we waste our time looking for an address, we will need to verify first if there are restricted characters.
First, I worked on the first 128 bytes (x01 to x7F)
I sent the characters to the program and carefully watching how the program responds to the characters. If it gets mangled/truncated/updated, then it's a bad character.
After numerous tries, I was able to narrow down the restricted characters to the following:
Updated POC below:
Second, I worked with the other 128 bytes (x80 to xff)
Interestingly, hex characters 80 and above are changed to an arbitrary hex character.
I sent the following characters...
...and we see the following conversion in Immunity Debugger
The table below shows the conversion (not a complete list).
0x4 - POP-PO-RET address!
Now that we know which characters are not allowed, we are ready to find a POP-POP-RET address.
Lucky for us, Immunity Debugger's mona.py module enables an easier way to find a POP-POP-RET address.
There were multiple addresses found.
Although not ideal since it has null bytes, we will be using address 00435133
We can verify that this is indeed a POP-POP-RET address by searching for it.
We can update our POC with this address and see if our SEH handler gets redirected to it.
Note that the address has to be in little-endian.
We fire up the POC and we have successfully redirected the SEH handler as shown below:
…once we take the pop-pop-ret, we then get redirected to our nseh values (EBs)
Note that ourDs are nowhere to be found now, however, our As are easily accessible which is just right above our nSEH. This means our initial jump will have to be a reverse jump to the beginning of our As that should get us at least 127 bytes of address space to further jump to a bigger space (Ds location)
0x5 - First Jump
Normally, we would be able to easily to a reverse jump short 80 (EB 80). However, note that any hex character of 80 or over is restricted and converts to a different hex character.
Due to restricted characters, we will need to carve out our egghunter shellcode so I will be using the egghunter shellcode that I used for OSCE prep (I spent way too much time carving them).
Also, note that I did another EB 83 at the beginning of our second jump to allow another 120+ bytes of address space as I noticed that the first jump wasn't enough space
Before we can start carving out the egghunter shellcode, we will need to align the ESP to point to the space below our first EB 08at address 0012FB8D--this is where our egghunter shellcode will be decoded.
We can see that ESP is currently pointed at 012F574 and we want to point to 0012FB8D
We will need to add 1561 to ESP
To align ESP, we need the following instructions:
ADD AX, 619
Keep in mind that we still need to avoid restricted characters...fortunately, the opcodes that correspond to these instructions are not restricted
We execute the following instructions and we can see that our ESP now points to 0012FB8D
Again, we update our POC with our initial ESP realignment which will be the beginning of our egghunter shellcode
With our ESP aligned to where we want it to point....we can start carving out our egghunter shellcode.
Here's the original egghunter shellcode broken down into 4 bytes
#original egg hunter shellcode
We will be using the EAX register to carve out the egghunter. Note that we need to zero out EAX first before we execute SUB instructions. We start from the last 4 bytes and work our way up.
This whole carving thing is straight black magic...it never ceases to amaze me.
#using hex/dword in windows calc
#0 - E7FFE775 = 1800 188B
#7950 5109 + 7950 5109 + 255F7679 = 1800 188B
#0 - 07950 5109 - 79505109 - 255F7679 = E7FFE775
These 4 bytes are encoded by doing some SUB instructions on EAX, then it gets pushed to the stack, and then they get decoded at the ESP address.
We will be doing this for the other 28 bytes
#using hex/dword in windows calc
#0 - AFEA75AF = 5015 8A51
#71094404 + 71094404 + 6E03 0249 = 5015 8A51
#0 - 71094404 - 71094404 - 6E03 0249 = AFEA75AF
#using hex/dword in windows calc
#0 - FA8B 5730 = 0574 A8D0
#55093131 + 55093131 + 5B62 466E = 0574 A8D0
#0 - 55093131 - 55093131 - 5B62 466E = FA8B 5730
#using hex/dword in windows calc
#0 - 3054 B8EF = CFAB 4711
#56316666 +56316666+ 2348 7A45 = CFAB 4711
#0 - 56316666- 56316666- 2348 7A45 = 3054 B8EF
At this point, we have successfully carved out the first 16 bytes of our egghunter shellcode.
Note that we are getting close our second EB 83 jmp…we will need to jump over this to get to the next 127 bytes of address space
I made a mistake on the ESP realignment that resulted in wasted address space. ESP should have been pointed to the end of our 41s as the stack grows bigger, the address becomes smaller.
This means we need ESP to be pointing at address 0012FBAC
Adjusted ESP alignment
This gives us about extra 32 bytes of address space….every byte counts!
We finish up the first 128 bytes by adding the first half of our zero_out_eax instructions and adding a jump short 10 bytes to go over the second EB 83 (I think we could have looped around too and just overwrite the first 16 bytes of our egghunter shellcode).
Our jump short brings us from address 0012FB2F to 0012FB3B and we successfully get over the second EB 83
Now we continue encoding the rest of our egghunter shellcode
#using hex/dword in windows calc
#0 - 745A053C = 8BA5 FAC4
#41214433 + 41214433 + 0963 725E = 8BA5 FAC4
#0 - 41214433 - 41214433 - 0963 725E = 745A053C
#using hex/dword in windows calc
#0 - 2ECD5802 = D132 A7FE
#657F3165 + 657F3165 + 06344534 =D132 A7FE
#0 - 657F3165 - 657F3165 - 06344534 = 2ECD5802
Finally, we hit our last 4 bytes
That completes our egghunter shellcode…with literally 6 bytes left to spare with our address space
To test if our egghunter can find our egg, we add our egg "T00W" to the beginning of our Ds…and see if we can find it
...and we are successful. We found our Ds address space.
0x7 - Shellcode time!
I created a calc shellcode to see if we are able to pop a shellcode.
We go through our egghunter and add a breakpoint right before the jmp edi instruction and we can see that edi points to the beginning of our calc shellcode
Everything looks good, however, our shellcode just keeps on crashing the program and not spawn a calc.exe
After spending research, it looks like the ESP has to be realigned.
We align ESP with the address of EDX....then make it divisible by 4
AND, ESP FFFFFFF0 ----> make ESP divisible by 4
I update the shellcode to a reverse shell…pop the exploit one more time and we get a reverse shell
This blog will go over on how to backdoor windows executable. The intent is to show how a windows PE can be hijacked and introduced a reverse shell while still allowing the executable to maintain its functionality. We will go over how ASLR provides security feature that randomises the base address of executables/DLLs and positions of other memory segments like stack and heap. This prevents exploits from reliably jumping to a certain function/piece of code.
This is why you shouldn't trust any executables that you are introducing to your system without verifying its source or checksum.
tftpd32.exe - is free, open-source TFTP server that is also includes a variety of different services, including DHCP, TFTP, DNS, and even syslog and functions as a TFTP Client as well
PsExec.exe - is a command-line tool that lets you execute processes on remote systems and redirect console applications' output to the local system so that these applications appear to be running locally.
There are basically two (or more?) ways to get a code cave (1) Find available address space and (2) add a new executable section.
"The concept of a code cave is often employed by hackers and reverse engineers to execute arbitrary code in a compiled program. It can be a helpful method to make modifications to a compiled program in the example of including additional dialog boxes, variable modifications or even the removal of software key validation checks. Often using a call instruction commonly found on many CPU architectures, the code jumps to the new subroutine and pushes the next address onto the stack. After execution of the subroutine a return instruction can be used to pop the previous location off of the stack into the program counter. This allows the existing program to jump to the newly added code without making significant changes to the program flow itself."
(1) Finding a code cave using backdoor-factory
(2) Using LordPE to add a new executable section.
For this blog, we will add a new section. As seen above, I added a section with 1000 virtual size, raw size 400. Also note that virtual offset is 0004B000 as we will need this value to calculate the Relative Value Address (RVA). Since ASLR is enabled, we will use RVA in order to dynamically do our jumps and address redirections.
0x1 - Entry point and New Section address
Using immunity debugger, we get the address of the new section (from this on this will be called code cave)
We can see that the address is currently pointing to 010AB000, however, the 2 higher-bytes of the adressare irrelevant due to ASLR.
We can see the entry point once we open up tftpd32.exe in immunity debugger and verify the memoy map. We will be hijacking the first instruction to make the jump to our code cave. Also, note that we will need to reintroduce these two instructions after the backdoor.
If ASLR was not enabled, we could have easily done a jmp010AB000. That said, we will need to do some calculations to always hit the code cave regardless if its address is randomized.
To calculate the RVA, we will need the virtual offset and the entry point.
4B000 (VOffset) - 1208C (EntryPoint) = 3 8F74. This mean that we will do a jmp 38F74.
Using nasm_shell.rb, we generate the following opcodes.
Our first instruction will be updated with E9 6F8F0300 which will do a jump to our code cave.
...if we reload the program, ASLR kicks in as we can see the higher 2 bytes have changed. The same opcodes but different address.
We take the jump and it lands us to the beginning of our code cave.
0x2 - Backdoor/Reverse Shell Code
Once in our code cave, we will add a Metasploit reverse shellcode
We will create the payload in hex format and binary copy it to the program using immunity debugger.
Before the reverse shell can be copied, all the registers and flags have to be said which can be done with PUSHAD and PUSHFD instructions. This is needed to maintain the integrity of the original program execution.
Once reverse shellcode is added, registers and flags are restored to their original state using POPFD and POPAD. However before that, we need to adjust the value of ESP to point it to the original stack/ESP value.
In my case, here are the ESP values
Before shellcode: 0025F908
After shellcode: 0025F70C
We will need to add 1FC to the ESP to align it, then execute the POPFD and POPAD instructions to restore the registers and flags
At this point, if we add a breakpoint at the end of our shellcode and run the program...we should get a reverse shell to our Kali netcat listener.
As you can see that we have successfully hijacked code execution and redirected it to our code cave containing our reverse shellcode.
0x3 - Restoring Original Program Instructions
Remember the two instruction at the entry point before they were hijacked.? We will now need to restore these two instructions so the program can run as intended.
Keep in mind that we are still dealing with ASLR which means we will need calculate the RVA once again.
0096 BF15 - 0095 0000 = RVA 1 BF15 (this will be our CALL RVA_1 BF15) (additional offset by 10000)
0096 2091 - 0095 0000 = RVA 1 2091 (this will be our JMP RVA_1 2091) (additional offset by 10000)
At address 010AB169 or RVA 4B169 (virtual offset + 169)
RVA_4B169 - RVA_1 BF15
At address 010AB16E or RVA 4B16E (virtual offset + 16E)
RVA_4B16E- RVA_1 2091
Add these instructions or opcodes as shown below:
And we are done...somewhat.
Our reverse shell calls the WaitForSingleObject function which pushes an ESI value of -1. As a result, tftpd32.exe will not execute until we exit the reverse shell. This mean that we will need to change the ESI value from -1 to 0.
We will trace code execution in immunity debugger using 'Trace Over' command.
Line 5 has DEC ESI instructions which makes ESI = FFFFFFFF. This means that all we need to do is cancel the DEC ESI instructions (making it NOP works just fine)!
We should now be able to successfully execute the program while it also simultaneously sends a reverse shell to Kali.
We get the initial crash using the following fuzzer.
...and we get a pretty vanilla EIP overwrite buffer overflow.
Calculating the offset as usual using Metasploit's pattern_offset.rb and pattern_create.rb
We have our offset value so at this point we will need to find JMP ESP address and redirect EIP to it. We will use address 0x625011AF.
Note that you can use Immunity Debugger's mona.py or just search it manually.
Updated POC with the offset value and JMP ESP address.
As we examine the crash in Immunity Debugger, we can see the correct offset value and hit our JMP ESP address however, once the jump is taken, our Cs have been truncated. As a result, this only gives us a space of about 20 bytes.
That said, we will use the C buffer space to do a reverse jmp to A's address space which allowed for roughly about 66 bytes of address space.
Reverse jump EB B8 and will jump as from address 0x00D0F9E0 to 0x00D0F99A
We update and send our exploit...then take the jump.
Once again, we examine the crash and see that we have successfully landed on A's address space.
Stolen from purpl3f0xsec...the diagram below shows how the socket connection is established between the client and the vulnserver.
Basically, we will utilize (reuse) the recv() function call. The idea is to utilize the established socket() connection and use the recv() as the first stage payload. Once the recv() function is up and running, we will then send our second stage payload.
The second stage payload will not overflow the buffer so it will not get truncated.
To set up recv(), we need the following parameters.
Socket = socket descriptor
Buffer = memory location for our second stage payload
BufferSize = memory space allocation for our second stage payload
Flags = Socket flags…can be set to Null
These are the parameters will need to be pushed to the stack (don't forget reverse order).
Before I populated the parameters, I grabbed the address of recv() which can be done by going in to the .text entry point and if we scroll down, we will find the functions.
Address 0x00401953 has the recv() but if you double click it...it shows the actual address which is 0x0040252C
I set a breakpoint at address 0x0040252C.
Once we hit our breakpoint, if you look at the stack, we can see the parameters that are currently loaded in the stack.
Note that at this point...both EAX and EBX registers have the socket descriptor value of 0x00000050. This is because of the instructions at these addresses: 0x0040194A and 0x00401950
0x0040194A - MOV EAX, DWORD PTR SS: [EBP-420]
This is a pointer to [EBP-420] so whatever the value loaded in [EBP-420] gets mov into EAX
We can see that EBP currently points to address 0x00EEFF88 so subtracting 420 should get us to the address that holds the socket descriptor.
As expected, if we jump to address 0x00EEFB68...we can see the value 0x00000050 loaded in.
Now, address 0x00401950 has the following instructions..
0x00401950 - MOV DWORD PTR SS:[ESP], EAX
This basically just loads the socket descriptor value stored in EAX, to the the [ESP] pointer. We can verify this by following ESP in the stack which currently points to address 0x011CF9E0
Next, we will load the value of socket descriptor to EAX. ASLR is enabled we will to do some math and ee will use ESP's address as a reference point.
We know that EBP register holds the socket descriptor located at address 0x00EEFB68. This means that we will need to do following:
0x00EEFB68 (EBP) - 0x00EEF9E0 (ESP) = 188 (in hex)
Great..so we will need to add 188 to ESP. We will do this by pushing ESP into stack, and pop it to EAX then add 188 to EAX. If our calculation is correct, we should have the socket descriptor address (0x00EEFB68) loaded to EAX.
We run the following instuctions:
ADD AX, 188
And we can see that address 00EEFB68 is now loaded in EAX which contains the socket descriptor value of 0x00000050 as shown in the stack.
Great...now we have the socket descriptor easily accessible!
At this point, we will have to re-align ESP so that we don't arbitrarily overlap with our first stage instructions. As we push more instructions to the stack, especially in a small address space, we risk overlaping our first stage with ESP.
That said, we need to move our ESP below where our first stage will be loaded.
We use the following instructions to adjust ESP:
SUB ESP, 6C ;SUB 108 from ESP
This makes ESP points to 0x00EEF974
***Note that towards the end of the exploit development, I found that ESP needed to point directly below the recv() parameters. That meant I had to adjust ESP and only subtracted 64 which pointed it to address 0x00EEF97C instead.
At this point, we have accomplished the following:
1. Located the address for the recv() function
2. Located the address that holds the socket descriptor value
3. Loaded the socket descriptor value to EAX
4. Aligned the stack to ensure our first stage does not overlap with ESP
Now we are ready to populate the parameters for our recv() function call.
Note that we need 4 parameters for the this function call pushed to the stack in reverse order.
0x1 - Param #4 (Flags)
Since we are not setting any flags, we will set this parameter to NULL.
Using the EBX register...we zero it out using XOR instruction before pushing it to the stack
XOR, EBX, EBX
0x2 - Param #3 (Buffer Size)
For the buffer size, I picked about 1024 bytes of buffer space which is a lot more than what we need.
Utilizing the EBX register again which is already set to zero...we will add 400 (in hex) to it.
ADD BH, 4
0x3 - Param # 2 (Buffer Location)
I initialy pointed the buffer location to beginning of the Cs (0x00EEF9E4), however, it didn't work so I had to point it directly right after the four parameters. In this case, the buffer location has to be pointed to address 0x00EEF97C
We can use the ESP address again...load the address into EBX then do some math which in this case I had to add 120 to ESP (HEX 78)
ADD EBX, 78
0x4 - Param #1 (Socket Descriptor)
Finally, for the socket descriptor we will need to access the value currently stored in EAX (not EAX address itself)
EAX is still pointing to 0x00EEFB68 that holds the socket descriptor value of 0x00000050
We can simply push the value stored in EAX using the following instruction:
PUSH DWORD PTR DS:[EAX]
...and we are done with the parameters.
At this point, we should have the following parameters.
Socket descriptor = 0x00000050
Buffer location = beginning of our C's (NOTE: this was adjusted to address 0x00EEF97C)
Buffer size = 400 (1024 in dec)
Flags = 0
Calling the RECV()
With the parameters set, we are now ready to call our RECV() function. Note that RECV() function is at adddress 0x0040252C
This one is about an interesting behavior 🤭 I identified in cmd.exe in result of many weeks of intermittent (private time, every now and then) research in pursuit of some new OS Command Injection attack vectors.
So I was mostly trying to:
find an encoding missmatch between some command check/sanitization code and the rest of the program, allowing to smuggle the ASCII version of the existing command separators in the second byte of a wide char (for a moment I believed I had it in the StripQuotes function - I was wrong ¯\(ツ)/¯),
discover some hidden cmd.exe's counterpart of the unix shells' backtick operator,
find a command separator alternative to |, & and \n - which long ago resulted in the discovery of an interesting and still alive, but very rarely occurring vulnerability - https://vuldb.com/?id.93602.
And I eventually ended up finding a command/argument confusion with path traversal ... or whatever the fuck this is 😃
For the lazy with no patience to read the whole thing, here comes the magic trick:
Tested on Windows 10 Pro x64 (Microsoft Windows [Version 10.0.18363.836]), cmd.exe version: 10.0.18362.449 (SHA256: FF79D3C4A0B7EB191783C323AB8363EBD1FD10BE58D8BCC96B07067743CA81D5). But should work with earlier versions as well... probably with all versions.
Some more context
Let's consider the following command line: cmd.exe /c "ping 127.0.0.1", whereas 127.0.0.1 is the argument controlled by the user in an application that runs an external command (in this sample case it's ping). This exact syntax - with the command being preceded with the /c switch and enclosed in double quotes - is the default way cmd.exe is used by external programs to execute system commands (e.g. PHP shell_exec() function and its variants).
Now, the user can trick cmd.exe into running calc.exe instead of ping.exe by providing an argument like 127.0.0.1/../../../../../../../../../../windows/system32/calc.exe, traversing the path to the executable of their choice, which cmd.exe will run instead of the ping.exe binary.
The potential impact of this includes Denial of Service, Information Disclosure, Arbitrary Code Execution (depending on the target application and system).
Although I am fairly sure there are some other scenarios with OS command execution whereas a part of the command line comes from a different security context than the final command is executed with (Some services maybe? I haven't search myself yet) - anyway let's use a web application as an example.
Consider the following sample PHP code:
Due to the use of escapeshellcmd() it is not vulnerable to known command injection vectors (except for argument injection, but that's a slightly different story and does not allow RCE with the list of arguments ping.exe supports - no built-in execution arguments like find's -exec).
And I know, I know, some of you will point out that in this case escapeshellarg()should be used instead - and yup, you would be right, especially since putting the argument in quotes in fact prevents this behavior, as in such case cmd.exe properly identifies the command to run (ping.exe). The trick does not work when the argument is enclosed in single/double quotes.
Anyway - the use of escapeshellcmd() instead of escapeshellarg() is very common. Noticed that while - after finding and registering CVE-2020-12669, CVE-2020-12742 and CVE-2020-12743 ended up spending one more week running automated source code analysis scans against more open source projects and manually following up the results - using my old evil SCA tool for PHP. Also that's what made me fed up with PHP again quite quickly, forcing me to get back to cmd.exe only to let me finally discover what this blog post is mostly about.
I am fairly sure there are applications vulnerable to this (doing OS command injection sanity checks, but failing to prevent path traversal and enclose the argument in quotes).
Also, the notion of similar behavior in other command interpreters is also worth entertaining.
An extended POC
Now, this is what normal use looks like in Sysmon log (process creation event):
So basically the child process (ping.exe) is created with command line equal to the value enclosed between the double quotes preceded by the /c switch from the parent process (cmd.exe) command line.
Now, the same for the above ipconfig.exe hijack:
And it turns out we are not limited to executables located in directories present in %PATH%. We can traverse to any location on the same disk.
Also, we are not limited to the EXE extension, neither to the list of "executable" extensions contained in the %PATHEXT% variable(which by default is .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC - basically these are the extensions cmd.exe will try to add to the name of the command if no extension is provided, e.g. when ping is used instead of explicit ping.exe). cmd.exe runs stuff regardless to the extension, something I noticed long ago (https://twitter.com/julianpentest/status/1203386223227572224).
And one more thing - more additional arguments between the original command and the smuggled executable path can be added.
Let's see all of this combined.
For the demonstrative purposes, the following C program was compiled and linked into a PE executable (it simply prints out its own command line):
Copied the EXE into C:\xampp\tmp\cmd.png (consider this as an example of ANY location a malicious user could write a file).
So we just effectively achieved an equivalent of actual (exec, not just read) PE Local File Inclusion in an otherwise-safe PHP ping script.
But I don't think that our options end here.
The potential for extending this into a full RCE without chaining with file upload/control
I am certain it is also possible to turn this into an RCE even without the possibility of fully/partially controlling any file in the target file system and deliver the payload in the command line itself, thus creating a sort of polymorphic malicious command line payload.
When running the target executable, cmd.exe passes to it the entire part of the command line following the /c switch.
executes c:\windows\system32\calc.exe with command line equal ping 127.0.0.1/../../../../../../../windows/system32/calc.exe.
And, as presented in the extended POC, it is possible to hijack the executable even when providing multiple arguments, leading to command lines like:
ping THE PLACE FOR THE RCE PAYLOAD ARGS 127.0.0.1/../../path/to/lol.bin
This is the command line lol.bin would be executed with. Finding a proxy execution LOLBin tolerant enough to invalid arguments (since we as attackers cannot fully control them) could turn this into a full RCE. The LOLBin we need is one accepting/ignoring the first argument (which is the hardcoded command we cannot control, in our example "ping"), while also willing to accept/ignore the last one (which is the traversed path to itself). Something like https://lolbas-project.github.io/lolbas/Binaries/Ieexec/, but actually accepting multiple arguments while quietly ignoring the incorrect ones.
I expected it to treat the command line as a string of inline commands and run calc.exe after running ping.exe. Yes, I know, a semicolon is used here to separate ping from calc - but the semicolon character is NOT a command separator in cmd.exe, while it is in powershell (on the other hand almost all OS Command Injection filters block it anyway, as they are written universally with multiple platforms in mind - cause obviously the semicolon IS a command separator in unix shells).
A perfect supported syntax here would be some sort of simple base64-encoded code injection like powershell's -EncodedCommand, having found a way to make it work even when preceded with a string we cannot control. Anyway, this attempt led to powershell running in interactive mode instead of treating the command line as a sequence of inline commands to execute.
Anyway, at this point turning this into an RCE boils down to researching the behaviors of particular LOLbins, focusing on the way they process their command line, rather than researching cmd.exe itself (although yes, I also thought about self-chaining and abusing cmd.exe as the LOLbin for this, in hope for taking advantage of some nuances between the way it parses its command line when it does and when it does not start with the /c switch).
Stumbling upon and some analysis
I know this looks silly enough to suggest I found it while ramming that sample PHP code over HTTP with Burp while watching Procmon with proper filters... or something like that (which isn't such a bad idea by the way)... as opposed to writing a custom cmd.exe fuzzer (no, you don't need to tell me my code is far away from elegant, I did not expect anyone would read it neither that I would reuse it), then after obtaining rather boring and disappointing results, spending weeks on static analysis with Ghidra (thanks NSA, I am literally in love with this tool), followed up with more weeks of further work with Ghidra while simultaneously manually debugging with x64dbg while further expanding comments in the Ghidra project 😂
cmd.exe command line processing starts in the CheckSwitches function (which gets called from Init, which itself gets called from main). CheckSwitches is responsible for determining what switches (like /c, /k, /v:on etc.) cmd.exe was called with. The full list of options can be found in cmd.exe /? help (which by the way, to my surprise, reflects the actual functionality pretty well).
I spent a good deal of time analyzing it carefully, looking for hidden switches, logic issues allowing to smuggle multiple switches via the command line by jumping out of the double quotes, quote-stripping issues and whatever else would just manifest to me as I dug in.
If the /c switch is detected, processing moves to the actual command line enclosed in double quotes - which is the most common mode cmd.exe is used and the only one the rest of this write-up is about:
The same mode can be attained with the /r switch:
After some further logic, doing, among other things, parsing the quoted string and making some sanity fixes (like removing any spaces if any found from its beginning), a function with a very encouraging and self-explanatory name is called:
At this point it was clear it was high time for debugging to come into play.
By default x64dbg will set up a breakpoint at the entry point - mainCRTStartup.
This is a good opportunity to set an arbitrary command line:
Then start cmd.exe once again (Debug-> Restart).
We also set up a breakpoint on the top of the SearchForExecutable function, so we catch all its instances.
We run into the first instance of SearchForExecutable:
We can see that the double-quoted proper command line (after cmd.exe skips the preceding cmd.exe /c) along with its double quotes is held in RBX and R15. Also, the value on the top of the stack (right bottom corner) contains an address pointing at CheckSwitches - it's the saved RET. So we know this instance is called from CheckSwitches.
If we hit F9 again, we will run into the second instance of SearchForExecutable, but this time the command line string is held in RAX, RDI and R11, while the call originates from another function named ECWork:
This second instance resolves and returns the full path to ping.exe.
Below we can see the body of the ECWork function, with a call to SearchForExecutable (marked black). This is where the RIP was at when the screenshot was taken - right before the second call of SearchForExecutable:
Now, on below screenshot the SearchForExecutable call already returned (note the full path to ping.exe pointed at with the address held in R14). Fifteen instructions later the ExecPgm function is called, using the newly resolved executable path to create the new process:
So - seeing SearchForExecutable being called against the whole ping 127.0.0.1 string (uh yeah, those evil spaces) suggests potential confusion between the full command line and an actual file name... So this gave me the initial idea to check whether the executable could be hijacked by literally creating one under a name equal to the command line that would make it run:
Uh really? Interesting. I decided to have a look with Procmon in order to see what file names cmd.exe attempts to open with CreateFile:
So yes, the result confirmed opening a copy of calc.exe from the file literally named ping .PNG in the current working directory:
Now, interestingly, I would not see any results with this Procmon filter (Operation = CreateFile) if I did not create the file first...
One would expect to see cmd.exe mindlessly calling CreateFile against nonexistent files with names being various mutations of the command line, with NAME NOT FOUND result - the usual way one would search for potential DLL side loading issues... But NOT in this case - cmd.exe actually checks whether such file exists before calling CreateFile, by calling QueryDirectory instead:
For this purpose, in Procmon, it is more accurate to specify a filter based on the payload's unique magic string (like PNG in this case, as this would be the string we as attackers could potentially control) occurring in the Path property instead of filtering based on the Operation.
"So, anyway, this isn't very useful" - I thought and got back to x64dbg.
"We can only hijack the command if we can literally write a file under a very dodgy name into the target application's current directory... " - I kept thinking - "... Current directory... u sure ONLY current directory?" - and at this point my path traversal reflex lit up, a seemingly crazy and desperate idea to attempt traversal payloads against parts of the command line parsed by SearchForExecutable.
Which made me manually change the command line to ping 127.0.0.1/../calc.exe and restart debugging... while already thinking of modifying the cmd.exe fuzzer in order to throw a set payloads generated for this purpose with psychoPATH against cmd.exe... But that never happened because of what I saw after I hit F9 one more time.
Below we can see x64dbg with cmd.exe ran with cmd.exe /c "ping 127.0.0.1/../calc.exe" command line (see RDI). We are hanging right after the second SearchForExecutable call, the one originating from the bottom of the ECWork function. Just few instructions before calling ExecPgm, which is about to execute the PE pointed by R14. The full path to C:\Windows\System32\calc.exe present R14 is the result of the just-returned SearchForExecutable("ping 127.0.0.1/../calc.exe") call preceding the current RIP:
The traversal appears to be relative to a subdirectory of the current working directory (calc.exe is at c:\windows\system32\calc.exe):
"Or maybe this is just a result of a failed path traversal sanity check, only removing the first occurrence of ../?" - I kept wondering.
So I dug further into the SearchForExecutable function, also trying to find the answer why variants of the argument created by splitting it by spaces are considered and why the most-to-the-right one is chosen first when found.
I narrowed down the culprit code to the instructions within the SearchForExecutable function, between the call of mystrcspn at 14000ff64 and then the call of the FullPath function at 14001005b and exists_ex at 140010414:
In the meantime I received the following feedback from Microsoft:
Cmd.exe first tries to interpret the whole string as a path: "ping 127.0.0.1/../../../../../../../../../../windows/system32/calc.exe” string is being treated as a relative path, so “ping 127.0.0.1” is interpreted as a segment in that path, and is removed due to the preceding “../” this should help explain why you shouldn’t be able to use the user controlled input string to pass arguments to the executable.
There are a lot a cases that would require that behaviour, e.g. cmd.exe /c "....\Program Files (x86)\Internet Explorer\iexplore.exe" we wouldn’t want that to try to run some program “....\Program” with the argument “Files (x86)\Internet Explorer\iexplore.exe”.
It’s only if the full string can’t be resolved to a valid path, that it splits on spaces and takes everything before the first space as the intended executable name (hence why “ping 127.0.0.1” does work).
So yeah... those evil spaces and quoting.
From this point, I only escalated the issue by confirming the possibility of traversing to arbitrary directories as well as the ability to force execution of PE files with arbitrary extensions.
Interestingly, this slightly resembles the common unquoted service path issue, except that in this case the most-to-the-right variant gets prioritized.
Upon discovery I documented and reported this peculiarity to MSRC. After little less than six days the report was picked up and reviewed. About a week later Microsoft completed their assessment, concluding that this does not meet the bar for security servicing:
On one hand, I was little disappointed that Microsoft would not address it and I was not getting the CVE in cmd.exe I have wanted for some time.
On the other hand, at least nothing's holding me back from sharing it already and hopefully it will be around for some time so we can play with it 😃 It's not a vulnerability, it's a technique 😃
I would like thank Microsoft for making all of this possible - and for being nice enough to even offer me a review of this post! Which was completely unexpected, but obviously highly appreciated.
Researching stuff can sometimes appear to be a lonely and thankless journey, especially after days and weeks of seemingly fruitless drudging and sculpturing - but I realized this is just a short-sighted perception, whereas success is exclusively measured by the number of uncovered vulnerabilities/features/interesting behaviors (no point to argue about the terminology here 😃). In offensive security we rarely pay attention to the stuff we tried and failed, even though those failed attempts are equally important - as if we did not try, we would never know what's there (and risk false negatives). Curiosity and the need to know. And software is full of surprises.
Plus, simply dealing with a particular subject (like analyzing a given program/protocol/format) and gradually getting more and more familiar with it feeds our minds with new mental models, which makes us automatically come up with more and more ideas for potential bugs, scenarios and weird behaviors as we keep hacking. A journey through code accompanied by new inspirations, awarded with new knowledge and the peace of mind resulting from answering questions... sometimes ending with great satisfaction from a unique discovery.
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.
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.
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.
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).
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.
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.
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):
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).
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:
Now, we map that region to our 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):
And we will the entire 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:
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:
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,
This shellcode, however, requires one more adjustment.
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.
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.
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.
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):
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 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 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.
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:
The current base address the halacpi.dll module has been loaded (we will get it dynamically by calling NtQuerySystemInformation from our exploit).
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:
Below screenshots show the same process with Ghidra:
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.