On 14 April 2017, the “Lost in Translation” leak was announced by The Shadow Brokers group, providing a link to an archive containing a plethora of exploits and hacking tools developed by the NSA and subsequently stolen. While some began incorporating the exploits in their malware, it wasn’t until May 2017 that the first in-the-wild samples were observed spreading.
The WannaCry ransomware affected many thousands of devices by leveraging EternalBlue, an Server Message Block (SMB) exploit that provides remote code execution capabilities, in this instance utilized to provide worm propagation capabilities.
More recently, Petya-like was seen to use a similar methodology, incorporating EternalRomance alongside EternalBlue to worm across networks. In addition, the BitCoin miner Adylkuzz contained an embedded EternalBlue implementation, and pen-testing tools such as Metasploit were swiftly updated to include support for the SMB exploits.
In this article, we will outline all the SMB exploits leaked by “The Shadow Brokers” (EternalBlue/EternalRomance/EternalSynergy/EternalChampion), focusing on the shellcode they use and the DoublePulsar backdoor that is installed by each of the exploits for remotely executing an arbitrary payload DLL.
At the core, this centers around SMB exploits which will allow arbitrary remote code execution on a victim machine. This, by extension, could allow an attacker visibility into potentially sensitive information about the machine itself, its users or its surrounding network environment. That’s bad for the user and the Holy Grail for any attacker.
The leaked exploits have proven an unmitigated success among malware authors. In the few months since the leak, malware has been reported to spread in worm-like fashion either by embedding their own implementation of the exploits or simply packaging the tools as embedded resources. Unlike the discovery of MS08-67, which was known to a few exclusive attackers, MS10-17 came fully weaponized and in a highly refined form. Unfortunately, there is nearly no skill required to leverage these tools and gain unauthorized access to vulnerable systems.
In this analysis, we go into great detail on all the SMB exploits mentioned above. We also take a look at the DoublePulsar backdoor installed by each of the exploits. We hope to inform the reader of the exact extent they are vulnerable and inform their decisions on mitigating these vulnerabilities.
EternalBlue targets a buffer overflow vulnerability in non-paged kernel pool memory that exists due to the way in which the SMBv1 protocol handles File Extended Attributes (FEA) conversion. The exploit subsequently allows the attacker to execute remote code on Windows 7 machines using SMB to perform heap-spraying and trigger shellcode.
To verify if a remote machine is vulnerable to MS17-010, a remote attacker creates a connection the IPC$ share. If the server is in the default configuration, credentials are not normally required, making this method ideal for performing an unauthorized vulnerability scan. An SMB Transaction packet is then sent containing a PeekNamedPipe transaction request (0x23) with the FileID field set to zero:
Figure 1: EternalBlue - Vulnerability Check
If a machine is vulnerable, it will respond to this request with an NTSTATUS error code of STATUS_INSUFF_SERVER_RESOURCES (0xC0000205):
In the case of WannaCry, should a machine respond as being vulnerable, the malware will check to see if the DoublePulsar backdoor is already installed by sending an SMB Transaction2 packet and comparing the MultiplexID field from the response to 0x51 (Note: Different malware can use different codes in order to complete this function):
Figure 2: EternalBlue - Check For DoublePulsar Backdoor
When a machine is found to be vulnerable, the exploit sends a malformed SMB_NT_TRANSACTION packet, which is interpreted by the vulnerable SrvOs2FeaListToNt() function inside the Srv.sys driver, causing a heap overflow.
Figure 3: SMB Conversation that Triggers the Buffer Overflow in Vulnerable Function
The exploit then uses a combination of heap spray and kernel grooming techniques, flooding the victim’s machine with multiple SMB_COM_TRANSACTION2 packets to position the shellcode for execution.
Figure 4: Kernel Grooming Packet
A successful exploitation will result in shellcode being copied to the HAL region of kernel memory and executed (Note: Since Windows 8, HAL memory is no longer executable, so in order to run this exploit successfully on newer systems, attackers have to change the executable permissions for this memory region first):
Figure 5: First Stage Kernel Shellcode on the Wire
EternalRomance, much like EternalBlue, begins by performing the same check to confirm whether the remote system is vulnerable to an SMB buffer overflow (Figure 1). Where EternalBlue targeted Windows 7 SP1 machines using SMBv2, EternalRomance exploits another vulnerability, specifically, the process of handling SMBv1 transactions.
While Windows 7 does use SMBv1, this also allows EternalRomance to target XP, Vista and 7 as well as Windows Server 2003 and 2008. Unlike EternalBlue, this exploit will start by spraying the heap with SMB_COM_TRANSACTION2 packets, after which it will try to obtain information about the sprayed memory layout using an information leak in response to a malformed SMB request:
Figure 6: SMB_COM_TRANSACTION2 Spraying
Once it obtains the address of one of the TRANSACTION objects in memory, it will corrupt the structure by overwriting the pointer to the InData buffer with a pointer to malicious code (known as an out-of-bounds heap write):
Figure 7: Corruption of InData
After corruption, the exploit then delivers the payload by sending multiple “Trans2 Request, SESSION_SETUP” packets. Each of these packets contains different data within the SESSION_SETUP Data field. In the case of Petya-like, this related to the DoublePulsar payload:
Figure 8: Payload Transmission
This exploit uses a packet type confusion vulnerability. It first sends an SMB_COM_TRANSACTION packet to allocate a Transaction structure in memory. Then it will send an SMB_COM_WRITE_ANDX request to the same transaction, and in the process, trigger the type confusion vulnerability.
This will result in moving the InData pointer to point past the start of the buffer, and therefore allow for modification of the object that comes immediately after the buffer. Then malware will send a specially crafted SMB_COM_TRANSACTION_SECONDARY packet, and due to pointer being moved forward, this packet's data will overwrite the neighboring transaction structure, setting its InData pointer to point to an attacker controlled location.
This exploit takes advantage of a race condition in transaction handling, which allows data to be added to a complete transaction that is already scheduled for execution. The memory object that is storing SMB transaction data (received via a self-containing primary transaction request packet) can be modified by sending a secondary transaction request packet before the transaction is executed.
The exploit first sends an NT_RENAME request and that transaction is scheduled for execution. Another thread sends an NT transaction secondary request, containing the same ID as the first request, resulting in the data counter for this transaction being increased. The transaction is executed by the SrvCompleteExecuteTransaction function called by the previous thread and data is copied from InData to OutData.
Due to the increased counter, out-of-bounds data is leaked, containing one of the sprayed transaction structures. Having learned the layout of the Transaction structure in memory, the exploit now can overwrite - using the same technique - the pointer to InData inside this structure with a pointer to arbitrary code.
After exploiting vulnerabilities in the SMB protocol using one of the Eternal methods described above, shellcode will be copied into kernel memory and executed. Currently, all implementations are using the DoublePulsar backdoor, which comprises multiple stages of shellcode:
Stage 1 of the shellcode contains both 32 and 64-bit code that will first check the CPU architecture by executing architecture specific instructions. The opcode sequence 40 90 is interpreted on x86 as "inc eax / nop", and on x64 as “xchg eax, eax”. Therefore, the value of the eax register after the instruction will be “1” for x86 systems and “0” for x64.
Figure 9: Kernel Shellcode - Architecture Check
Next it will set a hook on the kernel-mode entry point by replacing a pointer to KiFastCallEntry stored in the Model Specific Register IA32_SYSENTER_EIP, with a pointer to a malicious handler function:
Figure 10: Kernel Shellcode - SYSENTER Hook
The malicious handler function will execute the following steps:
- Set the KTRAP frame just as the original KiFastCallEntry does
- Restore the original SYSENTER pointer
- Invoke kernel shellcode stage 2
- Return to the original SYSENTER routine
Figure 11: Kernel Shellcode - Malicious Handler
SMB Dispatch Table Hook
Stage 2 will first check the CPU architecture using the same process as stage 1. It will then determine the ntoskrnl.exe base address by reading the first pointer from IDT (which points inside ntoskrnl.exe memory) and iterating backwards in search of an “MZ” signature. Once the base address of ntoskrnl.exe is known, it will proceed to resolve ntoskrnl APIs (ExAllocatePool, ExFreePool, ZwQuerySystemInformation) using name hash comparison:
Figure 12: Kernel Shellcode - Resolving APIs
Next, the code will proceed to obtain a list of loaded drivers by calling ZwQuerySystemInformation with SystemInformationClass set to 0x0B (an undocumented option that requests the system module information structure), searching for Srv.sys in the list using path hash comparison.
Figure 13: Undocumented Module Information Structure
Figure 14: Kernel Shellcode - Locating Srv.sys
Once it locates the base of Srv.sys in memory, it will then traverse the .data section of this driver in search for the SrvTransaction2DispatchTable structure. This structure should match the following patterns:
- An array of DWORD values that contain the same values in positions 9, 11 and 12
- First DWORD in this array (at offset 0) is a pointer to a routine inside the same memory space
- The DWORD at position 18 (offset +0x48) is equal to zero
Figure 15: Kernel Shellcode - Locating the SMB Transaction2 Dispatch Table
It will then replace a pointer to the SrvTransactionNotImplemented routine at position 14 in the dispatch table with a pointer to the DoublePulsar backdoor:
Figure 16: Kernel Shellcode - Hooking Transaction2 Dispatch Table
Figure 17: Structure of the Transaction2 Dispatch Table Inside Srv.sys
Figure 18: Subverted Transaction2 Dispatch Table in Memory
Figure 19: First Stage Shellcode in Memory
If the target system version is NT 6.2 (Windows 8) or higher, then the EternalRomance version of this shellcode will also look for srvnet.sys and check/set the AllowAnonymousAccess value in this driver's memory using the following steps:
- Look for the srvnet!SrvAdminAllowAnonymousAccess function in memory
- Check the contents of the location pointed to at offset + 0x02 from the beginning of this function
- If it's set to 1, return, otherwise overwrite it with 1, save the pointer to this location and the old value (0) to internal structure and return
Figure 20: EternalRomance - Anonymous Access Hook
The DoublePulsar backdoor shellcode presents a simple interface, capable of handling 3 commands:
The EternalRomance version of the DoublePulsar backdoor's "kill" command will also restore the AllowAnonymousAccess bool to the original value.
Figure 21: DoublePulsar Backdoor
Figure 22: DoublePulsar - “Exec” Command
Figure 23: DoublePulsar - "Kill" Command
The command and response codes found in the Petya-like ransomware implementation of DoublePulsar are different from the original implementation, and were possibly changed using a hex editor:
Figure 24: Differences Between Original DoublePulsar Code and the One Used in Petya-like Ransomware
The purpose of the “launcher” part of the shellcode is to locate a specific process in the running processes list, attach to it, copy the user-mode portion of the shellcode (along with a payload DLL) to its memory and execute it. This code is sent to the DoublePulsar backdoor and executed via kernel-mode.
The launcher will first locate the base of ntoskrnl.exe by finding the first interrupt handler in the IDT table and traversing back to the “MZ” header.
Figure 25: Launcher - Finding ntoskrnl Base
Then it will resolve several ntoskrnl.exe APIs using hardcoded hashes of the function names.
Figure 26: Launcher - Resolving APIs
Figure 27: Launcher - API Hash Calculation Algorithm
Next, the shellcode will locate the ThreadListEntry for the current process using the following steps:
- Get the ETHREAD pointer by calling PsGetCurrentProcess
- Save the EPROCESS->ThreadListHead value
- Call KeGetCurrentThread
- Loop trough the ThreadList until a pointer to the current thread is found
Figure 28: Launcher - Finding the Current Thread Structure
The shellcode then proceeds to locate the process it’s going to inject the payload into. The process name hash is hardcoded, and the exploit will try to find such an instance of this process that has at least two active threads:
- In a loop call PsLookupProcessByProcessId with increasing PID value
- For each process, check the thread count stored in _EPROCESS->ActiveThreads structure
- Call PsGetProcessImageFileName to get the process name
- Compute hash and compare to hardcoded value of 0x8543E98 (lsass.exe)
- Perform additional checks on process command line string (stored at _PEB->ProcessParameters->CommandLine)
Figure 29: Launcher - Looking for a Process to Inject to
Once the appropriate process is found, the exploit will attach itself to this process by calling KeStackAttachProcess, so it can allocate memory inside the target process address space and copy the user-mode shellcode and payload DLL to the allocated buffer.
Figure 30: Launcher - Allocating Memory for the User-mode Shellcode
Next, the shellcode will look for an active thread by iterating through all threads in the target process and checking their flags:
- Call PsGetThreadTeb
- Check if _PEB->ActivationContextStackPointer is set
- Check if the Alert bit is set in ETHREAD->Tcb->MiscFlags
Figure 31: Launcher - Looking for Active Thread
Finally, it will schedule the next stage shellcode for execution using APC:
- Build APC structure with pMappedCode pointing to the user-mode shellcode location in target process memory and dummy ApcKernelRoutine (containing only RET 14h opcode)
- Call KeInitializeApc
- Call KeInsertQueueApc
Figure 32: Launcher - APC Scheduling
This will result in the user-mode shellcode being executed in the context of the lsass.exe process.
Reflective DLL loader
The user-mode portion of the shellcode is a reflective DLL loader, and is responsible for loading the payload DLL into memory and running the specified export. It is injected into the user-land process from kernel space by the launcher shellcode sent to the DoublePulsar backdoor (and executed via APC in the context of this process).
As with the other shellcodes, the usermode code will first resolve all necessary APIs by finding specified DLLs in PEB_LDR_DATA->InLoadOrderModuleList and then traversing the IAT table of these DLLs looking for matching function names. It uses a hardcoded array of name hashes.
Figure 33: Loader - Resolving APIs
Figure 34: Loader - API Hash Calculation Algorithm
Instead of calling an API directly, the shellcode will use these code snippets to jump to a stub function, that in turn will jump to the appropriate API:
Figure 35: Loader - API Jumps
Figure 36: Loader - API Stub Function
The shellcode will then move the payload DLL to another memory location and overwrite the memory at the old location with zeroes:
Figure 37: Loader - Moving Shellcode
The DLL is then injected into memory via the following process:
- Parse the header of the payload DLL
- Allocate readable/writeable/executable memory where the payload DLL is going to be loaded; try to use the preferred base address, as specified in the DLL header
- Copy DLL header and sections to relevant positions in the newly allocated memory
- Resolve all DLL imports and rebuild IAT
- Adjust the ImageBase value in the DLL header to reflect the actual address at which the DLL was loaded
- Resolve relocations
- Set permissions on loaded DLL sections according to the section characteristic stored in the DLL header
Figure 38: Loader
Once the DLL is loaded, the shellcode will initialize it by calling the DllMain function with DLL_PROCESS_ATTACH as the reason parameter. Then it will resolve the address of the exported function whose ordinal was specified by the kernel-mode shellcode and call the DLL export:
Figure 39: Loader - Initializing the DLL
Figure 40: Loader - Calling DLL Export
After executing the payload, the shellcode will unload the DLL by calling DllMain with DLL_PROCESS_DETACH as the reason parameter and wipe from memory by overwriting the DLL and the shellcode with zeroes:
Figure 41: Loader - Cleaning the Memory
- Install the patch: MS17-010: Security update for Windows SMB Server: March 14, 2017
- The indication of vulnerability occurs entirely over the network. To spot the potential exploitation check, look for any PeekNamedPipe transactions containing an IPC$ TreeID Path where FID is set to 0x0000. Any machine returning STATUS_INSUFF_SERVER_RESOURCES (0xC0000205) is vulnerable.
The Eternal-based SMB exploits are very well crafted, providing comprehensive coverage of Windows platforms and a common backdoor interface. The DoublePulsar backdoor has also been well designed, developed and tested, proving to be extremely reliable in the wild.
The modular composition of the exploits, ease of use, reliable/robust nature and near guaranteed success due to the high exposure of unpatched vulnerable systems meant malware authors were quick to leverage the exploits in their code, resulting in several widespread global outbreaks.
After the initial successes of the WannaCry/Adylkuzz/Petya-like malware campaigns, there has been great media attention surrounding these vulnerabilities, leading Microsoft to release patches for previously unsupported/end-of-life Operating Systems.
However, even with increased awareness and the availability of patches, the vulnerabilities are unlikely to disappear soon (much like MS08-067), and will no doubt be employed successfully by future malware, as well as being a treasured weapon in any pen-testers arsenal, for many years to come.