🔒
There are new articles available, click to refresh the page.
Before yesterdayBinary Exploitation

Buffer Overflow w/ Restricted Characters

 Buffer Overflow Vulnerability w/ restricted characters

Kali Linux
Windows Vista
Vulnerable application: vulnserver.exe (LTER)


Vulnserver.exe is meant to be exploited mainly with buffer overflows vulnerabilities. More info about this application and where to download it can be found here:



~~~~~//********//~~~~~~


For the LTER command, there are two ways to exploit the buffer overflow vulnerability, however, both exploits will have similar restricted characters

- Part 1: Vanilla Buffer Overflow w/ Restricted Characters
- Part 2: SEH base Buffer Overflow w/ Restricted Characters.....click here for  Part 2

Let's get started...

Fuzzing

Similar to the GMON write-up, I used boofuzz to do the initial fuzzing.

...and after crashing the program, we recreate the crash using the follow Proof-of-Concept


We get a pretty vanilla buffer overflow where the EIP has been overwritten with 41s

Also note that ESP currently points to our buffer. This is key once we figure out an address to redirect our EIP


Now we will need to determine our offset and see exactly which part of our buffer overwrites the EIP register.

As usual, this is accomplished using Metasploit's patter_create.rb to generate 3000 unique characters.


Update our POC with our unique characters, send the exploit, and examine the crash in immunity debugger.


Here we can see that EIP has been overwritten with the following values: 386F4337


Metasploit's pattern_offset.rb can be used to determine the offset with this value.

Once we determine the offset, we update our POC again



We send the POC one more time and examine the crash...if our offset is correct, EIP should be overwritten with x42s

In this case, we can see 42424242 were successfully loaded into the EIP register


Finding bad characters

Now that we are able to redirect our EIP...we will need to find an address to redirect the EIP. Since we know that ESP register points to our buffer, we will be looking for a JMP ESP address.

However, before we choose an address, we will need to verify if there are any bad characters.

We update the POC with the following 256 unique hex characters


After running a few test, it's verified that anything over 7F is being subtracted by 7F as we can see below in our dump….such that x80 - x7F = x01

This means we will not be able to use any hex characters over 7F

Allowed characters:

x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f



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
Address: 62501203


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.



Final Proof-of-Concept




QuickZip 4.60 SEH Based Buffer Overflow w/ Egghunter

Structured Exception Handling (SEH)Based Buffer Overflow Vulnerability

Kali Linux
Windows Vista
QuickZip 4.60


Bug found by: corelancod3r (http://corelan.be:8800)
Software Link: http://www.quickzip.org/downloads.html


GitHub:https://github.com/pyt3ra/QuickZip_4_60_SEH-Based-Buffer-Overflow

~~~~~~//******//~~~~~~

This is another SEH based buffer overflow with an egghunter implementation. A more detailed explanation that I wrote about an egghunter implementation can be found here: https://www.pyt3ra.com/2020/03/slae-assignment-3.html

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

SEH: 6B41396A
nSEH: 41386A41


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:

'\x10\x0f\x14\x15\x2f\x3d\x3a\x5b'

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 our  Ds 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.

More info about jump shorts (forward and reverse) can be found here: https://thestarman.pcministry.com/asm/2bytejumps.htm

If we go back to our bad characters table…we can see that hex 89 and 9F translates to EB and 83 respectively.

89
EB
9F
83

Here's how the EB 83 instructions would work as shown in Immunity. 


We can now update our POC with our first jump


We see our EB 83 first jump in SEH chain


We take the pop-pop-ret and we successfully hit our first jump…from 0012FBB0 to 0012FB35


0x6 - Second Jump - Egghunter

Remember that the address space for our Ds is missing now (still in memory). This is where ideally where reverse shellcode would be. 

Note above that our dump doesn't show the address space where are Ds are close to our As.

Just because we can't find it doesn't mean it is missing…this is where an egghunter shellcode comes in to play.

Here's a write-up that I did about egghunter: https://www.pyt3ra.com/2020/03/slae-assignment-3.html

Mona.py also has a module to create an egghunter.

Note: I will be using a different egghunter

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 08  at 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:

PUSH ESP                   
POP EAX
ADD AX, 619
PUSH EAX
POP ESP

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
\x68\x81\xcA\xFF
\x0F\x42\x52\x6A
\x02\x58\xCD\x2E
\x3C\x05\x5A\x74
\xEF\xb8\x54\x30
\x30\x57\x8b\xFA
\xAF\x75\xea\xAF
\x75\xE7\xFF\xE7

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.

zero_out_eax = "\x25\x4a\x4d\x4e\x55\x25\x35\x32\x31\x2a"

This whole carving thing is straight black magic...it never ceases to amaze me.

#\x75\xe7\xff\xe7
#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

#\xaf\x75\xea\xaf
#using hex/dword in windows calc
#0 - AFEA75AF = 5015 8A51
#71094404 + 71094404  + 6E03 0249‬ =  5015 8A51
#0 - 71094404 - 71094404  - 6E03 0249 = AFEA75AF



\x30\x57\x8b\xfa
#using hex/dword in windows calc
#0 - FA8B 5730 = 0574 A8D0‬
#55093131 + 55093131  + 5B62 466E = 0574 A8D0‬
#0 - 55093131 - 55093131  - 5B62 466E = FA8B 5730


\xef\xb8\x54\x30
#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


#\x3c\x05\x5a\x74
#using hex/dword in windows calc
#0 - 745A053C = 8BA5 FAC4
#41214433 + 41214433 + 0963 725E = 8BA5 FAC4
#0 - 41214433 - 41214433 - 0963 725E = 745A053C


#\x02\x58\xcd\x2e
#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

PUSH EDX
POP ESP
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





Final POC in my GitHub.

Thanks for reading!

freeFTPd-1.0.10 SEH Based Buffer Overflow

Kali Linux
Windows Vista
freeFTPD 1.0.10

Original Author, POC, and vulnerable software: https://www.exploit-db.com/exploits/27747


~~~~~~//******//~~~~~~

Vulnerable program:




Initial Proof-of-Concept 


...and we get our initial crash where our SEH handler and nSEH have been overwritten with 41s.



Calculating the offset values

As usual, we use Metasploit's pattern _create.rb and pattern_offset.rb




We update our POC with the following offset values


...and we verify that we hit the correct offset values as shown with the Cs and Bs


POP-POP-RET 


Since this a SEH based buffer overflow, as usual we will need a POP-POP-RET address.

I am using mona.py to find this address.

Also, note that we will need to find an address that SAFESEH and ASLR disabled.

We will use address 0x0041B865



Again, we update our POC with our SEH Handler redirect address


We send our exploit up and we have successfully redirected code execution as we hit our POP-POP-RET address.


We follow the POP-POP-RET and we hit our nSEH which is just right below our As

Note: We currently do not see our Ds.


...however, if we scroll further down, we can see that our Ds are still loaded in memory. Just need find them...aka. egghunter.


First Jump

Since our first jump is limited to just 4 bytes, we will do a 2 byte reverse short jmp

EB 80 or jmp short reverse 128 bytes.

We update our nSEH with EB 80 and added 2 more NOPs (not necessary) to complete the 4 bytes.



We fire up the POC on more time, take the pop-pop-ret, and hit our first jump



Restricted characters 


After going through the 256 hex characters, we found that 0xa is the only restricted character

So far, we have accomplished the following:

1. Successfully crash the program and overwrite the SEH and nSEH
2. Calculated the offset values
3. Found POP-POP-RET address
4. Completed the first jump from nSEH which allows 127 bytes of address space


Egg...hunting!


Reminder...you can create an egghunter using mona.py


Note: I am using a slightly different egghunter shellcode.

We update our POC with our egghunter shellcode and add the egg in front of our Ds



Send the exploit up...take the first jump (EB 80) and we land on our NOP sled.

If we scroll down, our egghunter is just right below our the NOP sled.

We let the egghunter execute while adding a breakpoint at JMP EDI to check the value of EDI.

Here we can see that we have successfully located our egg...all that we need to do now is add our reverse shellcode right after our egg.


At this point we are ready to add our reverse shell





Portable Executable (PE) backdooring and Address Space Layout Randomization (ASLR)

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.

References:

Address Space Layout Randomization (ASLR): https://en.wikipedia.org/wiki/Address_space_layout_randomization  

Executables:
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. 

Tools:

Immunity Debugger (http://debugger.immunityinc.com/ID_register.py) 
LordPE (http://www.malware-analyzer.com/pe-tools) 
XVI32 (http://www.chmaas.handshake.de/delphi/freeware/xvi32/xvi32.htm)

~~~~~~~~~//*******//~~~~~~~~~

0x0 - Adding a New Section...Code Cave

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 jmp 010AB000. 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.

0x4 - WaitForSingleObject

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.



Thanks for reading.

Multi-Stage EIP redirection Buffer Overflow -- Win API / Socket Reuse

Kali Linux
Windows Vista
Vulnerable application: vulnserver.exe (KSTET)


Vulnserver.exe is meant to be exploited mainly with buffer overflows vulnerabilities. More info about this application and where to download it can be found here:

~~~~~~//*****//~~~~~~

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.


Now the fun part begins..thanks to https://purpl3f0xsec.tech/2019/09/04/Vulnserver-KSTET-Socket-Reuse.html for pointing out this type of vector.

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.



More information about the recv() can be found here: https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recv

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:


PUSH ESP

POP EAX

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!


Re-aligning ESP


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
PUSH 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
PUSH EBX


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)

PUSH ESP
POP EBX
ADD EBX, 78
PUSH EBX



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


As you can see the recv() address contains null bytes which we will need to be removed. I learned something new from https://purpl3f0xsec.tech/2019/09/04/Vulnserver-KSTET-Socket-Reuse.htmlas to how to remove these null bytes.

 

We simply use an arbitrary address such as removing the null bytes and adding 90 for the lowest byte: 0x40252C90 instead of 0x0040252C

 

So we move this arbitrary address to EAX:

 

MOV EAX, 40252C90

 

Then we shift to right by 8 bits which removes the last 8 bits (0x90) and adds 00 to the first 8 bits


SHR EAX, 8

 

Finally, we simply CALL EAX

 

Here just right before we execute CALL EAX, we can see that it currently points to the recv() address



That is the entire first stage...and we update our POC as shown below



We fire up the exploit one more, step through the stager, and we can see that we have all 4 parameters ready to go…they are followed with the address of where our payload will be loaded.


Second Stage / Reverse Shell

With our RECV() function ready, we can send our second stage payload.

We update our POC and see if we can add the Ds to memory.

Noe that I added a sleep(5) in between first and second stage. This ensures that our RECV() fucntion is ready before we send the second stage payload.


Here we can see that we have successfully loaded our Ds to memory at address 0x011EFD7C or 0x00EEFD7C (note ASLR).


We then replace the Ds with an actual reverse shell, fire up the exploit, and we get a reverse shell!





Thank you for reading.

Cmd Hijack - a command/argument confusion with path traversal in cmd.exe

10 June 2020 at 05:43

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.

So the full command line becomes:

cmd.exe /c "ping 127.0.0.1/../../../../../../../../../../windows/system32/calc.exe"

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

Normal use:

Abuse:

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).

Action:

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.


For instance:

cmd.exe /c "ping 127.0.0.1/../../../../../../../windows/system32/calc.exe"

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.

Also, I was thinking of powershell.

Running this:

cmd.exe /c "ping ;calc.exe; 127.0.0.1/../../../../../../../../../windows/system32/WindowsPowerShell/v1.0/POWERSHELL.EXE"

makes powershell start with command line of

ping ;calc.exe 127.0.0.1/../../../../../../../../../../windows/system32/WindowsPowerShell/v1.0/POWERSHELL.EXE

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.

The beginning of the CheckSwitches function after some naming editions and notes I took

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:

Disassembly view:

Decompiler view:

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:

We do have a blog post that helps describe the behavior you have documented: https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats.

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.

The disclosure

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.

Some reflections

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.

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
  • There are no more articles
❌