visit
Research by*: Eyal Itkin and Itay Cohen*
This isn’t the first documented case of a Chinese APT repurposing an Equation Group exploit. In the Bemstour case, discussed by both and , the main assumption was that APT3 (Buckeye) sniffed the EternalRomance exploit from network traffic, and later upgraded it to the equivalent of EternalSynergy using an additional APT3 vulnerability. In the present case, however, we have strong evidence that APT31 had access to the actual exploit files of Equation Group, in both their 32-bits and 64-bits versions.
Armed with these two leads, and already familiar with the packer used by these exploits, we set out to find the described exploit of CVE-2017-0005.
Jian, the exploit of CVE-2017-0005, was shipped in a DLL named Add.dll
. It contained an interesting PDB path suggesting that it was written in 2015 under a project named “rundll32_getadmin”.
F:\code\2015\rundll32_getadmin\Add\x64\Release\Add.pdb
The decryption routine is very straightforward. The packer starts by allocating memory for the encrypted code and copies it to the newly allocated buffer. It then allocates a buffer with PAGE_EXECUTE_READWRITE
protection to store the decrypted code. After the buffers are allocated, the packer checks if a string argument, which will be used as a decryption key, was passed to the AddByGod
function. Next, the packer uses the AES256 algorithm with a SHA1 derived key of the passed argument to decrypt the encrypted code. If the decryption is successful, the decrypted code is executed and a second stage payload runs. Luckily, we managed to obtain the password that was needed to execute the binary and decrypt the encrypted payload.
rundll32.exe Add.dll AddByGod [password]
The second stage begins with a typical shellcode technique, searching the module’s header for the address of kernel32.dll
and dynamically retrieving a pointer to the GetProcAddress
export function. Next, the program decompresses another Portable Executable (PE) and jumps to its entry point. The decompressed PE, which is the 3rd stage in the loading sequence, has intentionally corrupted headers. It does basic loading operations and then begins with a reflective loading of an embedded executable (yes, another one). The loaded PE is the last stage in the loading sequence and is responsible for executing the exploit.
As can be seen in the figure above, the packer used for CVE-2019-0803 is very similar to the one used in CVE-2017-0005. In fact, the flow is almost identical. The file was compiled on September 18, 2018, and is also internally named “Add.dll”. Like the previously packed exploit, CVE-2019-0803 also has an export function named “AddByGod” and contains debug information:
C:\Users\sms2056\Desktop\Add(未修改dll‘)\x64\Release\Add.pdb
Just for comparison, the exploit of CVE-2019-0803 supported only a single Windows version and used the hardcoded version-dependent constants for Windows Server 2008 R2. Alibaba even reported that the tool’s file name was 2008.dll, leaving no doubt about the tool’s intended target.
Mcl_NtElevation_EpMe_GrSa.dll (x86) – 292fe1fc7d350cc7b970da0f308d22089cd36ab552e5659e3cfb0d9690166628
Mcl_NtElevation_EpMo_GrSa.dll (x64) – 1537cad1d2c5154e142af775ed555d6168d528bbe40b31f451efa92c9e4f02de
One of the main components in this leak is DanderSpritz, Equation Group’s post-exploitation framework that contains a wide variety of tools for persistence, reconnaissance, lateral movement, bypassing Antivirus engines, and more. The framework is very modular and provides the operator many capabilities to access victims’ computers. During the recent months, we revisited the DanderSpritz framework, reverse-engineered some of its modules and implants, and plan to publish a detailed publication dedicated to the framework and our findings.
Note: The CVEs listed here don’t match those mentioned originally by Kaspersky in their report. First, Kaspersky’s researchers weren’t sure about the CVE-IDs to begin with, marking them as “possibly.” Second, we found additional information regarding the latter two exploits, which helped shed light on more probable CVE-IDs for each. More details about the CVE-ID identification can be found later on under the respective sections describing each exploit.
These privilege escalation modules are the ones we caught when we queried for Jian’s global configuration table. And they were not alone. We also found a couple of more Local Privilege Escalation exploits from the NtElevation series.
While the Eternal* exploits received a lot of attention, and rightly so, mentions of the NtElevation exploits were somehow missing. We couldn’t find any online reference that points to the existence of the NtElevation module as part of the Equation Group arsenal or even as part of the “Shadow Brokers Exploits”, nor any reference to the following 4 exploit code names.
ElEi
ErNi
EpMo
EpMe
Note: Equation Group’s exploits are known to have code names that are abbreviated using 4 letters. For example, Eternal Blue and Eternal Romance are internally referred to as ETBL and ETRO. Similarly, the Local Privilege Escalation exploits we discussed have their own code names, as listed above.
Despite our attempts, we couldn’t manage to trace back the full code names for these exploits. However, the naming convention suggests that EpMo
and EpMe
are of the same type or that they exploit vulnerabilities in the same module, just like the Eternal* exploits (EternalBlue, EternalRomance, etc). This conclusion does make sense as our single search query found both of these exploits.
Houston Disk: 3rd in the execution order. Supported DanderSpritz Windows Versions: Windows 2000 to Windows 7, inclusive.
The exploit also contains an additional check that win32k.sys
is dated to before November 23, 2011. This is a clear indication of CVE-2011-3402, which is the only font vulnerability that was fixed in December 2011. The gap in the dates is explained by the fact that Microsoft compiled the patched driver on the mentioned date.
Houston Disk: 2nd in the execution order. Supported DanderSpritz Windows Versions: Only Windows 2000.
The exploit also contains an additional check that ATMFD.dll
is of the exact version “5.0.2.227”. As the Houston Disk exploit supported additional versions, we aren’t fully sure why the version range was narrowed down in DanderSpritz. Compared to ElEi, there is no indicative patch check, which may be because the DanderSpritz files are dated to mid-2013, which is prior to the patch that was identified by Kaspersky and is dated to October 2013.
Basic analysis of both EpMo
and EpMe
found them to be GDI User-Mode-Print-Driver (UMPD) related, which explains why we found them when searching for a GDI UMPD related artifact from Jian. Before diving into the exploits, we first provide some background on what exactly is a User-Mode-Print-Driver.
Supporting such a data flow dictates that the kernel is aware of the user’s UMPD device and can forward it a set of requests, depending on the types that the driver declared to support. As is explained in more detail in this excellent talk focusing on UMPD, allowing for user-mode callbacks, invoked from the kernel, is a sure recipe for security vulnerabilities.
Houston Disk: N/A. Supported DanderSpritz Windows Versions: Windows 2000 to Windows Server 2008 R2, inclusive.
As this is a NULL-Deref vulnerability, we can immediately rule out CVE-2017-0005, as the stack trace shown in Microsoft’s blog has nothing to do with the NULL page. This means that this is possibly another vulnerability found and exploited by Equation Group in 2013. With that out of the way, it is time to understand what triggers this NULL-Deref vulnerability.
After the version-dependent index of the callback is fetched, the callback itself is replaced with the attacker’s fake ClientPrinterThunk callback.
Bingo! The exploit indeed makes use of a fake ClientPrinterThunk
. Let’s dig in and analyze the exploit logic inside this fake callback.
The callback itself is a thin wrapper that forwards the gdi_ctx
and the original argument to a function that is very similar to Windows’s own GdiPrinterThunk
. As a matter of fact, the code of the exploit is very modular, and each supported driver command is handled by its own virtual handler implemented in the gdi_ctx
class. Aside from the chosen set of implemented handlers, there is no real logic in this function.
While analyzing this function, we stumbled upon the GDI configuration array that originally pointed us to this exploit sample. Now, placed in the right context, we can easily deduce the role of this configuration array. It holds the print driver’s INDEX_LAST value for each version of the target operating system.
As can be seen above, this configuration value is crucial for the driver’s logic as it represents the total number of dispatched functions that should be handled by the driver.
On top of these commands, there is one additional supported command, with the os-dependent value of INDEX_LAST + 4:
In this command, we initialize an array that tells the operating system which function handlers we support. The attackers chose to mark all of the functions as “supported” except for 3 specific function handlers:
DrvStartDoc (0x23)
DrvEnableSurface (0x03)
DrvDisableSurface (0x04)
Please pay close attention to DrvEnableSurface
. The syscall that triggers the vulnerability is NtGdiStartDoc
, which is responsible for starting the print job. However, to do so, the vulnerable function win32k!PDEVOBJ::bMakeSurface()
is invoked and tries to create a surface, exactly the operation that isn’t supported by our driver. Here is a debugging output from the vulnerable function:
While the entry for DrvDisablePDEV
(0x02) exists, and points to the correct Windows function, the adjacent entry for DrvEnableSurface
(0x03) contains only zeros.
00 8aeb8c6c 8fa42330 0x0
01 8aeb8c88 8fbae3f6 win32k!PDEVOBJ::bMakeSurface+0x43
02 8aeb8cb0 8fbae94c win32k!GreStartDocInternal+0x7e
03 8aeb8d1c 82a4f42a win32k!NtGdiStartDoc+0x2ff
04 8aeb8d1c 000f0813 (T) nt!KiFastCallEntry+0x12a
05 0022eed0 10008118 (T) 0xf0813
06 0022ef18 10006f53 Mcl_NtElevation_EpMo_GrSa+0x8118
07 0022ef28 10006fc3 Mcl_NtElevation_EpMo_GrSa+0x6f53
08 0022ef44 10007af4 Mcl_NtElevation_EpMo_GrSa+0x6fc3
09 0022ef74 10006ce5 Mcl_NtElevation_EpMo_GrSa+0x7af4
0a 0022efcc 10004a31 Mcl_NtElevation_EpMo_GrSa+0x6ce5
0b 0022f324 100038c0 Mcl_NtElevation_EpMo_GrSa+0x4a31
0c 0022f338 10003a7b Mcl_NtElevation_EpMo_GrSa+0x38c0
0d 0022f370 10002adf Mcl_NtElevation_EpMo_GrSa+0x3a7b
0e 0022f3d8 77d5af24 Mcl_NtElevation_EpMo_GrSa+0x2adf
Microsoft fixed this vulnerability in May 2017, one month after the Shadow Brokers’s Lost in Translation leak. However, we failed to find a CVE-ID that refers to this fix. The patch itself addresses the exact flaw in the vulnerable function win32k!PDEVOBJ::bMakeSurface()
. It adds a sanity check after the handler is fetched from the struct, and before it is invoked by the function. If the entry is NULL, the function aborts.
To conclude, the EpMo
Equation Group exploit is a NULL-Deref in GDI’s UMPD module, and is therefore not an exploit for CVE-2017-0005. It was patched by Microsoft in May 2017, and we couldn’t find a clear CVE-ID associated with it.
Now that we better understand the framework’s API, and have a better understanding of the UMPD module, it is time to focus on the next exploit – EpMe
.
Just to recap, we found both of these exploits when searching for an artifact from Jian, APT31’s exploit for CVE-2017-0005. As EpMo
is a different vulnerability that originates from the same module, our hope was that EpMe
does indeed exploit CVE-2017-0005. Time to analyze it and find out.
Houston Disk: N/A. Supported DanderSpritz Windows Versions: Windows XP to Windows 8, inclusive.
Armed with our newly acquired knowledge about the UMPD module, we started analyzing the EpMe exploit. The exploit itself shares a lot of code with EpMo, thus allowing us to easily focus on the exploit-specific logic that is unique to EpMe. While the initialization phase of this exploit is longer and involves a lot of GDI-related bootstrapping (DrawStream
(), finding the wanted Display, etc.), the actual flow that hijacks the control flow is relatively simple.
After the bootstrap is finished, the exploit triggers a call to the NtGdiBitBlt
syscall. This initiates a chain of events in the Windows kernel and eventually passes the flow back to our user-mode callback (DrvBitBlt
()) registered by our UMPD. Here lies the heart of the exploit.
Our function allocates a new Rbrush
using NtGdiBRUSHOBJ_pvAllocRbrush()
, whose sole purpose is to allow UMPD implementations to allocate themselves an Rbrush
and couple it with a BRUSHOBJ
. As a direct result, it also means that the Rbrush
is allocated in user-mode, using EngAllocUserMemEx
(). Storing it in user-mode means that we can access it and craft the struct’s content. And so, the attackers corrupted the Rbrush
to point at a set of fake GDI objects that were forged on a local stack buffer inside the callback.
To hijack the control flow, the attackers chose to use a Palette and crafted it so that PALETTE.pfnGetNearestFromPalentry
points to their shellcode, exactly as Microsoft pointed out in their blog on the caught-in-the-wild exploit. After everything is built, the callback invokes the NtGdiEngBitBlt
syscall with a rop4
parameter of 0xCCAA.
This specific syscall was chosen because of two key features:
The user passes to it a BRUSHOBJ
.
A rop4
value of 0xCCAA
means the kernel directly accesses the user-controlled Rbrush
from within the supplied BRUSHOBJ
.
More specifically, a Stream is extracted from the fully-controlled Rbrush
and is forwarded on to EngDrawStream()
, causing the unsuspecting kernel function to use our fully crafted Stream.
This chain of functions gradually uses all of the GDI objects that we’ve crafted in our user-mode callback, eventually leading to XLATEOBJ_iXlate()
. This last function invokes our crafted PALETTE.pfnGetNearestFromPalentry
function pointer, thus hijacking the control flow and triggering the execution of our shellcode.
To summarize, the root cause for this vulnerability is based on the complex design involved in supporting UMPD, and the need to allocate objects for it in user-mode. The vulnerability itself lies inside EngBitBlt(),
which blindly trusts and directly uses our crafted Rbrush
and the set of fake GDI objects it points to. Not only does this vulnerability give an attacker a powerful exploit primitive, but it also points at a design issue in the Windows kernel. As long as there is a function somewhere in the kernel that directly accesses a user-supplied Rbrush
, it also blindly trusts all the values that it points to and that are fully controlled by the user.
The patch itself is rather straightforward: EngBitBlt()
with a rop4
value of 0xCCAA
no longer supports the option to draw a Stream, an action that demands extracting a Stream from the user-supplied Rbrush
. By removing this feature altogether, Microsoft completely eliminated the vulnerable code flow.
Before:
After:
It is important to remember that two APTs exploiting the same vulnerability (CVE-2017-0005) could just be a coincidence. When this happens to security researchers, such a case is often referred to as a “bug-collision.” It’s possible that researchers on both sides found this vulnerability independently, and it doesn’t necessarily mean that there is a real connection between the tools.
Now, when we review the version context used by all exploits shared by the DanderSpritz framework, we can see the following, very similar, structure:
The fields that are marked in red in Jian were marked again in the sample from the Equation Group exploits. As can be seen, one field is still unused in all 4 DanderSpritz exploits, but the other field is heavily used and holds the handle for the mapped version of NTOS kernel. It is hard to miss the wide similarity between the two structures, up to the order and size of the first 9 fields, even including the size of the unused field in between.
At the heart of the exploits lies a single function that populates a buffer with the various fake GDI objects, which is pointed to by our user-mode Rbrush
object. Not only do both exploits use a single function for the construction of all of these fake objects, but the memory layout of the objects in the argument buffer is also identical.
When we analyzed the code of the Equation Group exploit, we used it to recreate a source code Proof-Of-Concept (POC). The result is the following beautified and labeled code:
void populate_buffer_and_brush(char * pBuffer, HBRUSH hbrush)
{
memset(pBuffer, 0, 0x200);
memset(&pBuffer[0x200], 0, 0xA8);
memset(&pBuffer[0x2A8], 0, 0x30);
memset(&pBuffer[0x2D8], 0, 0x10);
// 0x00: PALETTE
*(DWORD *)( pBuffer + 0x18) = 2; // 0x14 - 0x1C: flPal
*(size_t *)(pBuffer + 0x80) = pBuffer + 0x2A8; // 0x80 - 0x88: apalColor
// Brush (DRAWSTREAMINFO)
size_t * pBrush = (size_t*)hbrush;
pBrush[3] = pBuffer + 0x29C; // pptlDstOffset - Pointer to 0
pBrush[4] = pBuffer + 0x208; // pxloSrcToBGRA
pBrush[5] = pBuffer + 0x208; // pxloDstToBGRA
pBrush[6] = pBuffer + 0x208; // pxloBGRAToDst <-- We use this one (offset 0x30)
pBrush[7] = 60; // ulStreamLength - 60
pBrush[8] = pBuffer + 0x260; // pvStream - Pointer to our built "Stream"
// Second Struct
*(size_t *)(pBuffer + 0x200) = hbrush;
// 0x208: _XLATE
*(size_t *)(pBuffer + 0x230) = pBuffer; // ppalSrc
*(size_t *)(pBuffer + 0x238) = pBuffer; // ppalDst <-- We use this one (offset 0x30)
*(size_t *)(pBuffer + 0x240) = pBuffer; // ppalDstDC
// 0x260 - 0x2A8: Our "Stream"
*(DWORD *)(pBuffer + 0x260) = 9; // DS_NINEGRIDID (ulCmdID)
*(DWORD *)(pBuffer + 0x26C) = 1; // rclDst.right = 0x01
*(DWORD *)(pBuffer + 0x270) = 1; // rclDst.bottom = 0x01
*(DWORD *)(pBuffer + 0x27C) = 80; // rclSrc.right = 0x50
*(DWORD *)(pBuffer + 0x280) = 80; // rclSrc.bottom = 0x50
*(DWORD *)(pBuffer + 0x284) = 4; // ngi.flFlags := DSDNG_PERPIXELALPHA (4)
// 0x2A8: Palette Color Table
*(DWORD *)(pBuffer + 0x2CC) = 100;
// Fourth Struct
*(size_t *)(pBuffer + 0x2D8) = AllocMemoryPage(0x10000);
*(size_t *)(pBuffer + 0x2E0) = g_pRtlCopyUnicodeString;
}
Aside from the added struct at the end of the buffer, which uses RtlCopyUnicodeString
, the memory layout of the objects inside the argument buffer was completely identical.
Needless to say, none of the above were related to the vulnerability itself, and changing them didn’t affect the exploit at all. They are simply hardcoded constants chosen by the original developers of the exploit.
The interesting thing is, both EpMe and the Jian use the exact same hardcoded constants. The fact that all of these constants are shared between the two samples, even the weird looking Unicode string above, just shows that one of the exploits was most probably copied from the other.
It is also possible that both parties were inspired by some unknown 3rd-party implementation that used all of these constants. Alas, we failed to find any evidence for the existence of such a module. We must say that the odds of this scenario are rather slim, especially when taking into account the weird-looking Unicode string.
41 5C pop r12
5F pop rdi
C3 retn
This is one small indication that the original authors of the exploits could indeed have been Equation Group. However, as this artifact could also be just a coincidence, we now review additional aspects of the exploits.
Despite Windows 2000 not being vulnerable, the UMPD code in Jian has special cases for Windows 2000, and Windows 2000 is part of the OS Version enum.
The interesting issue is that according to the exploit configurations of Equation Group, EpMe doesn’t support Windows 2000. The minimal supported version is Windows XP, which aligns perfectly with the vulnerable Windows versions.
If we assume for a moment that APT31 was the original developer of the exploit for CVE-2017-0005, why would they even attempt to add support for Windows 2000? Windows 2000 was never vulnerable in the first place. To be clear, we have no indication that the actors had their own version of the EpMo exploit or anything similar, meaning we have no indication they ever needed such Windows 2000 support for any other tool / exploit.
The problem is, if we check the actual syscall numbers for Windows XP Service Pack 0 and Service Pack 1, we can see that the condition for setting the value of SP_delta is flawed. It was correct for the Equation Group exploit, but is not correct here as APT31 modified the wSpMajor_refined value during the exploit’s initialization.
This value update is even stranger, as the Equation Group exploits have no mention of it whatsoever:
You might ask, “Why would someone perform the above value rotation?” The answer is actually rather simple. From our past analyses of several exploits attributed to Chinese-affiliated actors, we saw that the developers have a habit of using the value 0 as a marker for “illegal value.” This can be clearly seen as all Service Pack values above 6 are mapped back to the value 0, which marks them as “illegal.” This was also the case for the OS Version Enum, which was fully incremented by 1, making Windows NT use a value of 1 instead of 0, and reserving the sacred value of 0 to mark an error state.
And yet, this time the developers used only a partial rotation for the Service Pack value, causing a collision with the legitimate value for Service Pack 0 which for some reason they didn’t remap to “1”. This means that 0 is simultaneously an illegal value that shouldn’t be supported, and a legitimate value that is crucial for configuring the correct syscall numbers. The correct adjustment should probably have been to increment all values by 1, map the illegal values to 0, and adjust the syscall check to wSpMajor_refined != 1.
Once again, we see a weird pattern. Even under the premise that Chinese exploits should reserve the value 0 for illegal values, the code still looks odd. A developer writing this exploit from scratch would probably have just incremented the wSpMajor_refined
value by 1, while remembering that in future checks Service Pack 0 is marked with the value 1. Instead, as if not to break an existing piece of code, the syscall initialization still checks for 0, and this value is simultaneously both valid and illegal at the same time.
Jian contains a debug string “int the overflow!!!” that can be found inside UMPD’s DrvBitBlt(), the callback responsible for triggering the vulnerability.
As we already established, the exploited vulnerability is not an “overflow” vulnerability. While the string may hint at either a “buffer overflow” or an “integer overflow”, none of them have any connection to the user-mode callback design issue that was actually exploited.
In greater detail:
We began with analyzing “Jian”, the Chinese (APT31 / Zirconium) exploit for CVE-2017-0005, which was reported by Lockheed Martin’s Computer Incident Response Team. To our surprise, we found out that this APT31 exploit is in fact a reconstructed version of an Equation Group exploit called “EpMe”. This means that an Equation Group exploit was eventually used by a Chinese-affiliated group, probably against American targets.
The case of EpMe / Jian is different, as we clearly showed that Jian was constructed from the actual 32-bits and 64-bits versions of the Equation Group exploit. This means that in this scenario, the Chinese APT acquired the exploit samples themselves, in all of their supported versions. Having dated APT31’s samples to 3 years prior to the Shadow Broker’s “Lost in Translation” leak, our estimate is that these Equation Group exploit samples could have been acquired by the Chinese APT in one of these ways:
Finally, although EpMo was indeed patched by Microsoft in May 2017, we couldn’t trace the CVE-ID that was assigned to the patched vulnerability. Not only that, to our knowledge, our publication is the first to even mention the existence of this Equation Group exploit, even though it was publicly accessible on GitHub
for the last 4 years.
Jian: AE512F13136774B4AAB79EBCC378927143BE77181E3B256E6F9940CE73696DE4
tools.dll: 68A3710765DA1886F00E40F2D5E02776D224C77AEA114CD22C3A6204A7FAD363
2008.dll: 279320EE5C3B2DA4364AFBACBE5286EC4EED9AB5E887D4E0B9AAB2EB618BC539
Note: There are several variants of each exploit in the leak. The following are single examples of each exploit.
Houston Disk: 868EB363F32BEACD8BCDC7A114E020D4CFE67913A15275F4E7493D87DB643FF2
DanderSpritz – ElEi: C99FFACBA6D6689F7934E6E912E36EFCC4BD6A09C8A4D1E43BB27C3AFD131882
DanderSpritz – ErNi: E4FBF75ABF928CD1C9073656A61755FD3F0C25DC2E7922FB5073E1F64E5E9161
DanderSpritz – EpMo: 1537CAD1D2C5154E142AF775ED555D6168D528BBE40B31F451EFA92C9E4F02DE
DanderSpritz – EpMe (CVE-2017-0005): 634A80E37E4B32706AD1EA4A2FF414473618A8C42A369880DB7CC127C0EB705E