ManageEngine CVE-2022-47966 Technical Deep Dive

ManageEngine CVE-2022-47966 Technical Deep Dive #windows #research #xml #saml #CVE-2022-47966 #ManageEngine

Original text by James Horseman

Introduction

On January 10, 2023, ManageEngine released a security advisory for CVE-2022-47966 (discovered by Khoadha of Viettel Cyber Security) affecting a wide range of products. The vulnerability allows an attacker to gain remote code execution by issuing a HTTP POST request containing a malicious SAML response. This vulnerability is a result of  using an outdated version of Apache Santuario for XML signature validation.

Patch Analysis

We started our initial research by examining the differences between ServiceDesk Plus version 14003 and version 14004. By default, Service Desk is installed into

C:\Program Files\ManageEngine\ServiceDesk

. We installed both versions and extracted the jar files for comparison.

While there are many jar files that have been updated, we notice that there was a single jar file that has been completely changed.

libxmlsec

from Apache Santuario was updated from 1.4.1 to 2.2.3. Version 1.4.1 is over a decade old.

Jar differences

That is a large version jump, but if we start with the 1.4.2 release notes we find an interesting change:

  • Switch order of XML Signature validation steps. See Issue 44629.

Issue 44629 can be found here. It describes switching the order of XML signature validation steps and the security implications.

XML Signature Validation

XML signature validation is a complex beast, but it can be simplified down to the the following two steps:

  • Reference Validation – validate that each
<Reference>

element within the

<SignedInfo>

  • element has a valid digest value.
  • Signature Validation – cryptographically validate the 
<SignedInfo>

element. This assures that the

<SignedInfo>
  • element has not been tampered with.

While the official XML signature validation spec lists reference validation followed by signature validation, these two steps can be performed in any order. Since the reference validation step can involve processing attacker controlled XML

Transforms

, one should always perform the signature validation step first to ensure that the transforms came from a trusted source.

SAML Information Flow Refresher

Applications that support single sign-on typically use an authorization solution like SAML. When a user logs into a remote service, that service forwards the authentication request to the SAML Identity Provider. The SAML Identity Provider will then validate that the user credentials are correct and that they are authorized to access the specified service. The Identity Provider then returns a response to the client which is forwarded to the Service Provider.

The information flow of a login request via SAML can been seen below. One of the critical pieces is understanding that the information flow uses the client’s browser to relay all information between the Service Provider (SP) and the Identity Provider (IDP). In this attack, we send a request containing malicious SAML XML directly to the service provider’s Assertion Consumer (ACS) URL.

Information flow via https://cloudsundial.com/

The Vulnerability

Vulnerability Ingredient 1: SAML Validation Order

Understanding that SAML information flow allows an attacker to introduce or modify the SAML data in transit, it should now be clear why the Apache Santuario update to now perform signature validation to occur before reference validation was so important. This vulnerability will abuse the verification order as the first step in exploitation. See below for the diff between v1.4.1 and v.1.4.2.

1.4.1 vs 1.4.2

In v1.4.1, reference validation happened near the top of the code block with the call to

si.verify()

. In v1.4.2, the call to

si.verify()

was moved to the end of the function after the signature verification in

sa.verify(sigBytes).

Vulnerability Ingredient 2: XSLT Injection

Furthermore, each 

<Reference>

element can contain a

<Transform>

element responsible for describing how to modify an element before calculating its digest. Transforms allow for arbitrarily complex operations through the use of XSL Transformations (XSLT).

These transforms are executed in

src/org/apache/xml/security/signature/Reference.java

which is eventually called from

si.verify()

from above.

Reference transforms

XSLT is a turing-complete language and, in the ManageEngine environment, it is capable of executing arbitrary Java code. We can supply the following snippet to execute an arbitrary system command:

<ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xslt-19991116">
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime" xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object">
        <xsl:template match="/">
            <xsl:variable name="rtobject" select="rt:getRuntime()"/>
            <xsl:variable name="process" select="rt:exec($rtobject,'{command}')"/>
            <xsl:variable name="processString" select="ob:toString($process)"/>
            <xsl:value-of select="$processString"/>
        </xsl:template>
    </xsl:stylesheet>
</ds:Transform>

Abusing the order of SAML validation in Apache Santuario v1.4.1 and Java’s XSLT library providing access to run arbitrary Java classes, we can exploit this vulnerability in ManageEngine products to gain remote code execution.

SAML SSO Configuration

Security Assertion Markup Language (SAML) is a specification for sharing authentication and authorization information between an application or service provider and an identity provider. SAML with single sign on allows users to not have to worry about maintaining credentials for all of the apps they use and it gives IT administrators a centralized location for user management.

SAML uses XML signature verification to ensure the secure transfer of messages passed between service providers and identity providers.

We can enable SAML SSO by navigating to

Admin -> Users & Permissions -> SAML Single Sign On

where we can enter our identity provider information. Once properly configured, we will see “Log in with SAML Single Sign On” on the logon page:

Service Desk SAML logon

Proof of Concept

Our proof of concept can be found here.

After configuring SAML, the Assertion Consumer URL will now be active at

https://<hostname>:8080/SamlResponseServlet

and we can send our malicious SAML Response.

python3 CVE-2022-47966.py --url https://10.0.40.64:8080/SamlResponseServlet --command notepad.exe

Since ServiceDesk runs as a service, there is no desktop to display the GUI for

notepad.exe

so we use ProcessExplorer to check the success of the exploit.

Notepad running

This proof of concept was also tested against Endpoint Central and we expect this POC to work unmodified on many of the ManageEngine products that share some of their codebase with ServiceDesk Plus or EndpointCentral.

Notably, the AD-related products (AdManager, etc) have additional checks on the SAML responses that must pass. They perform checks to verify that the SAML response looks like it came from the expected identity provider. Our POC has an optional

--issuer

argument to provide information to use for the

<Issuer>

element. Additionally, AD-related products have a different SAML logon endpoint URL that contains a guid. How to determine this information in an automated fashion is left as an exercise for the reader.

python3 CVE-2022-47966.py --url https://10.0.40.90:8443/samlLogin/<guid> --issuer https://sts.windows.net/<guid>/ --command notepad.exe

Summary

In summary, when Apache Santuario is <= v1.4.1, the vulnerability is trivially exploitable and made possible via several conditions:

  • Reference validation is performed before signature validation, allowing for the execution of malicious XSLT transforms.
  • Execution of XSLT transforms allows an attacker to execute arbitrary Java code.

This vulnerability is still exploitable even when Apache Santuario is between v1.4.1 and v2.2.3, which some of the affected ManageEngine products were using at the time, such as Password Manager Pro. The original research, Khoadha, documents further bypasses of validation in their research and is definitely worth a read.

Exploring ZIP Mark-of-the-Web Bypass Vulnerability (CVE-2022-41049)

Exploring ZIP Mark-of-the-Web Bypass Vulnerability (CVE-2022-41049)

Original text by breakdev

Windows ZIP extraction bug (CVE-2022-41049) lets attackers craft ZIP files, which evade warnings on attempts to execute packaged files, even if ZIP file was downloaded from the Internet.

In October 2022, I’ve come across a tweet from 5th July, from @wdormann, who reported a discovery of a new method for bypassing MOTW, using a flaw in how Windows handles file extraction from ZIP files.

Will Dormann
@wdormann
The ISO in question here takes advantage of several default behaviors: 1) MotW doesn’t get applied to ISO contents 2) Hidden files aren’t displayed 3) .LNK file extensions are always hidden, regardless of the Explorer preference to hide known file extensions.

So if it were a ZIP instead of ISO, would MotW be fine? Not really. Even though Windows tries to apply MotW to extracted ZIP contents, it’s really quite bad at it. Without trying too hard, here I’ve got a ZIP file where the contents retain NO protection from Mark of the Web.

https://twitter.com/wdormann/status/1544416883419619333

This sounded to me like a nice challenge to freshen up my rusty RE skills. The bug was also a 0-day, at the time. It has already been reported to Microsoft, without a fix deployed for more than 90 days.

What I always find the most interesting about vulnerability research write-ups is the process on how one found the bug, what tools were used and what approach was taken. I wanted this post to be like this.

Now that the vulnerability has been fixed, I can freely publish the details.

Background

What I found out, based on public information about the bug and demo videos, was that Windows, somehow, does not append MOTW to files extracted from ZIP files.

Mark-of-the-web is really another file attached as an Alternate Data Stream (ADS), named 

Zone.Identifier
, and it is only available on NTFS filesystems. The ADS file always contains the same content:

[ZoneTransfer]
ZoneId=3

For example, when you download a ZIP file 

file.zip
, from the Internet, the browser will automatically add 
file.zip:Zone.Identifier
 ADS to it, with the above contents, to indicate that the file has been downloaded from the Internet and that Windows needs to warn the user of any risks involving this file’s execution.

This is what happens when you try to execute an executable like a JScript file, through double-clicking, stored in a ZIP file, with MOTW attached.

Clearly the user would think twice before opening it when such popup shows up. This is not the case, though, for specially crafted ZIP files bypassing that feature.

Let’s find the cause of the bug.

Identifying the culprit

What I knew already from my observation is that the bug was triggered when 

explorer.exe
 process handles the extraction of ZIP files. I figured the process must be using some internal Windows library for handling ZIP files unpacking and I was not mistaken.

ProcessHacker revealed 

zipfldr.dll
 module loaded within Explorer process and it looked like a good starting point. I booted up IDA with conveniently provided symbols from Microsoft, to look around.

ExtractFromZipToFile
 function immediately caught my attention. I created a sample ZIP file with a packaged JScript file, for testing, which had a single instruction:

WScript.Echo("YOU GOT HACKED!!1");

I then added a MOTW ADS file with Notepad and filled it with MOTW contents, mentioned above:

notepad file.zip:Zone.Identifier

I loaded up 

x64dbg
 debugger, attached it to 
explorer.exe
 and set up a breakpoint on 
ExtractFromZipToFile
. When I double-clicked the JS file, the breakpoint triggered and I could confirm I’m on the right path.

CheckUnZippedFile

One of the function calls I noticed nearby, revealed an interesting pattern in IDA. Right after the file is extracted and specific conditions are meet, 

CheckUnZippedFile
 function is called, followed by a call to 
_OpenExplorerTempFile
, which opens the extracted file.

Having a hunch that 

CheckUnZippedFile
 is the function responsible for adding MOTW to extracted file, I nopped its call and found that I stopped getting the MOTW warning popup, when I tried executing a JScript file from within the ZIP.

It was clear to me that if I managed to manipulate the execution flow in such a way that the branch, executing this function is skipped, I will be able to achieve the desired effect of bypassing the creation of MOTW on extracted files. I looked into the function to investigate further.

I noticed that 

CheckUnZippedFile
 tries to combine the TEMP folder path with the zipped file filename, extracted from the ZIP file, and when this function fails, the function quits, skipping the creation of MOTW file.

Considering that I controlled the filename of the extracted ZIP file, I could possibly manipulate its content to trigger 

PathCombineW
 to fail and as a result achieve my goal.

PathCombineW
 turned out to be a wrapper around 
PathCchCombineExW
 function with output buffer size limit set to fixed value of 
260
 bytes. I thought that if I managed to create a really long filename or use some special characters, which would be ignored by the function handling the file extraction, but would trigger the length check in 
CheckUnZippedFile
 to fail, it could work.

I opened 010 Editor, which I highly recommend for any kind of hex editing work, and opened my sample ZIP file with a built-in ZIP template.

I spent few hours testing with different filename lengths, with different special characters, just to see if the extraction function would behave in erratic way. Unfortunately I found out that there was another path length check, called prior to the one I’ve been investigating. It triggered much earlier and prevented me from exploiting this one specific check. I had to start over and consider this path a dead end.

I looked if there are any controllable branching conditions, that would result in not triggering the call to 

CheckUnZippedFile
 at all, but none of them seemed to be dependent on any of the internal ZIP file parameters. I considered looking deeper into 
CheckUnZippedFile
 function and found out that when 
PathCombineW
 call succeeds, it creates a 
CAttachmentServices
 COM objects, which has its three methods called:

CAttachmentServices::SetReferrer(unsigned short const * __ptr64)
CAttachmentServices::SetSource(unsigned short const * __ptr64)
CAttachmentServices::SaveWithUI(struct HWND__ * __ptr64)

 realized I am about to go deep down a rabbit hole and I may spend there much longer than a hobby project like that should require. I had to get a public exploit sample to speed things up.

Huge thanks you @bohops & @bufalloveflow for all the help in getting the sample!

Detonating the live sample

I managed to copy over all relevant ZIP file parameters from the obtained exploit sample into my test sample and I confirmed that MOTW was gone, when I extracted the sample JScript file.

I decided to dig deeper into 

SaveWithUI
 COM method to find the exact place where creation of 
Zone.Identifier
 ADS fails. Navigating through 
shdocvw.dll
, I ended up in 
urlmon.dll
 with a failing call to 
<a href="https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-writeprivateprofilestringw">WritePrivateProfileStringW</a>
.

This is the Windows API function for handling the creation of INI configuration files. Considering that 

Zone.Identifier
 ADS file is an INI file containing section 
ZoneTransfer
, it was definitely relevant. I dug deeper.

The search led me to the final call of 

<a href="https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile">NtCreateFile</a>
, trying to create the 
Zone.Identifier
 ADS file, which failed with 
ACCESS_DENIED
 error, when using the exploit sample and succeeded when using the original, untampered test sample.

It looked like the majority of parameters were constant, as you can see on the screenshot above. The only place where I’d expect anything dynamic was in the structure of 

ObjectAttributes
 parameter. After closer inspection and half an hour of closely comparing the contents of the parameter structures from two calls, I concluded that both failing and succeeding calls use exactly the same parameters.

This led me to realize that something had to be happening prior to the creation of the ADS file, which I did not account for. There was no better way to figure that out than to use Process Monitor, which honestly I should’ve used long before I even opened IDA 😛.

Backtracking

I set up my filters to only list file operations related to files extracted to TEMP directory, starting with 

Temp
 prefix.

The test sample clearly succeeded in creating the 

Zone.Identifier
 ADS file:

While the exploit sample failed:

Through comparison of these two listings, I could not clearly see any drastic differences. I exported the results as text files and compared them in a text editor. That’s when I could finally spot it.

Prior to creating 

Zone.Identifier
 ADS file, the call to 
SetBasicInformationFile
 was made with 
FileAttributes
 set to 
RN
.

I looked up what was that 

R
 attribute, which apparently is not set for the file when extracting from the original test sample and then…

Facepalm

The 

R
 file attribute stands for 
read-only
. The file stored in a ZIP file has the read-only attribute set, which is set also on the file extracted from the ZIP. Obviously when Windows tries to attach the 
Zone.Identifier
 ADS, to it, it fails, because the file has a read-only attribute and any write operation on it will fail with 
ACCESS_DENIED
 error.

It doesn’t even seem to be a bug, since everything is working as expected 😛. The file attributes in a ZIP file are set in 

ExternalAttributes
 parameter of the 
ZIPDIRENTRY
 structure and its value corresponds to the ones, which carried over from MS-DOS times, as stated in ZIP file format documentation I found online.

4.4.15 external file attributes: (4 bytes)

       The mapping of the external attributes is
       host-system dependent (see 'version made by').  For
       MS-DOS, the low order byte is the MS-DOS directory
       attribute byte.  If input came from standard input, this
       field is set to zero.

   4.4.2 version made by (2 bytes)

        4.4.2.1 The upper byte indicates the compatibility of the file
        attribute information.  If the external file attributes 
        are compatible with MS-DOS and can be read by PKZIP for 
        DOS version 2.04g then this value will be zero.  If these 
        attributes are not compatible, then this value will 
        identify the host system on which the attributes are 
        compatible.  Software can use this information to determine
        the line record format for text files etc.  

        4.4.2.2 The current mappings are:

         0 - MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems)
         1 - Amiga                     2 - OpenVMS
         3 - UNIX                      4 - VM/CMS
         5 - Atari ST                  6 - OS/2 H.P.F.S.
         7 - Macintosh                 8 - Z-System
         9 - CP/M                     10 - Windows NTFS
        11 - MVS (OS/390 - Z/OS)      12 - VSE
        13 - Acorn Risc               14 - VFAT
        15 - alternate MVS            16 - BeOS
        17 - Tandem                   18 - OS/400
        19 - OS X (Darwin)            20 thru 255 - unused

        4.4.2.3 The lower byte indicates the ZIP specification version 
        (the version of this document) supported by the software 
        used to encode the file.  The value/10 indicates the major 
        version number, and the value mod 10 is the minor version 
        number.  

Changing the value of external attributes to anything with the lowest bit set e.g. 

0x21
 or 
0x01
, would effectively make the file read-only with Windows being unable to create MOTW for it, after extraction.

Conclusion

I honestly expected the bug to be much more complicated and I definitely shot myself in the foot, getting too excited to start up IDA, instead of running Process Monitor first. I started with IDA first as I didn’t have an exploit sample in the beginning and I was hoping to find the bug, through code analysis. Bottom line, I managed to learn something new about Windows internals and how extraction of ZIP files is handled.

As a bonus, Mitja Kolsek from 0patch asked me to confirm if their patch worked and I was happy to confirm that it did!

https://twitter.com/mrgretzky/status/1587234508998418434

The patch was clean and reliable as seen in the screenshot from a debugger:

I’ve been also able to have a nice chat with Will Dormann, who initially discovered this bug, and his story on how he found it is hilarious:

I merely wanted to demonstrate how an exploit in a ZIP was safer (by way of prompting the user) than that *same* exploit in an ISO.  So how did I make the ZIP?  I:
1) Dragged the files out of the mounted ISO
2) Zipped them. That's it.  The ZIP contents behaved the same as the ISO.

Every mounted ISO image is listing all files in read-only mode. Drag & dropping files from read-only partition, to a different one, preserves the read-only attribute set for created files. This is how Will managed to unknowingly trigger the bug.

Will also made me realize that 7zip extractor, even though having announced they began to add MOTW to every file extracted from MOTW marked archive, does not add MOTW by default and this feature has to be enabled manually.

I mentioned it as it may explain why MOTW is not always considered a valid security boundary. Vulnerabilities related to it may be given low priority and be even ignored by Microsoft for 90 days.

When 7zip announced support for MOTW in June, I honestly took for granted that it would be enabled by default, but apparently the developer doesn’t know exactly what he is doing.

I haven’t yet analyzed how the patch made by Microsoft works, but do let me know if you did and I will gladly update this post with additional information.

Hope you enjoyed the write-up!

A blueprint for evading industry leading endpoint protection in 2022

original text by vivami

About two years ago I quit being a full-time red team operator. However, it still is a field of expertise that stays very close to my heart. A few weeks ago, I was looking for a new side project and decided to pick up an old red teaming hobby of mine: bypassing/evading endpoint protection solutions.

In this post, I’d like to lay out a collection of techniques that together can be used to bypassed industry leading enterprise endpoint protection solutions. This is purely for educational purposes for (ethical) red teamers and alike, so I’ve decided not to publicly release the source code. The aim for this post is to be accessible to a wide audience in the security industry, but not to drill down to the nitty gritty details of every technique. Instead, I will refer to writeups of others that deep dive better than I can.

In adversary simulations, a key challenge in the “initial access” phase is bypassing the detection and response capabilities (EDR) on enterprise endpoints. Commercial command and control frameworks provide unmodifiable shellcode and binaries to the red team operator that are heavily signatured by the endpoint protection industry and in order to execute that implant, the signatures (both static and behavioural) of that shellcode need to be obfuscated.

In this post, I will cover the following techniques, with the ultimate goal of executing malicious shellcode, also known as a (shellcode) loader:

1. Shellcode encryption

Let’s start with a basic but important topic, static shellcode obfuscation. In my loader, I leverage a XOR or RC4 encryption algorithm, because it is easy to implement and doesn’t leave a lot of external indicators of encryption activities performed by the loader. AES encryption to obfuscate static signatures of the shellcode leaves traces in the import address table of the binary, which increase suspicion. I’ve had Windows Defender specifically trigger on AES decryption functions (e.g. 

CryptDecrypt
CryptHashData
CryptDeriveKey
 etc.) in earlier versions of this loader.

Output of dumpbin /imports, an easy giveaway of only AES decryption functions being used in the binary.

2. Reducing entropy

Many AV/EDR solutions consider binary entropy in their assessment of an unknown binary. Since we’re encrypting the shellcode, the entropy of our binary is rather high, which is a clear indicator of obfuscated parts of code in the binary.

There are several ways of reducing the entropy of our binary, two simple ones that work are:

  1. Adding low entropy resources to the binary, such as (low entropy) images.
  2. Adding strings, such as the English dictionary or some of 
    "strings C:\Program Files\Google\Chrome\Application\100.0.4896.88\chrome.dll"
     output.

A more elegant solution would be to design and implement an algorithm that would obfuscate (encode/encrypt) the shellcode into English words (low entropy). That would kill two birds with one stone.

3. Escaping the (local) AV sandbox

Many EDR solutions will run the binary in a local sandbox for a few seconds to inspect its behaviour. To avoid compromising on the end user experience, they cannot afford to inspect the binary for longer than a few seconds (I’ve seen Avast taking up to 30 seconds in the past, but that was an exception). We can abuse this limitation by delaying the execution of our shellcode. Simply calculating a large prime number is my personal favourite. You can go a bit further and deterministically calculate a prime number and use that number as (a part of) the key to your encrypted shellcode.

4. Import table obfuscation

You want to avoid suspicious Windows API (WINAPI) from ending up in our IAT (import address table). This table consists of an overview of all the Windows APIs that your binary imports from other system libraries. A list of suspicious (oftentimes therefore inspected by EDR solutions) APIs can be found here. Typically, these are 

VirtualAlloc
VirtualProtect
WriteProcessMemory
CreateRemoteThread
SetThreadContext
 etc. Running 
dumpbin /exports &lt;binary.exe&gt;
 will list all the imports. For the most part, we’ll use Direct System calls to bypass both EDR hooks (refer to section 7) of suspicious WINAPI calls, but for less suspicious API calls this method works just fine.

We add the function signature of the WINAPI call, get the address of the WINAPI in 

ntdll.dll
 and then create a function pointer to that address:


typedef BOOL (WINAPI * pVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD  flNewProtect, PDWORD lpflOldProtect);
pVirtualProtect fnVirtualProtect;

unsigned char sVirtualProtect&#91;] = { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t', 0x0 };
unsigned char sKernel32&#91;] = { 'k','e','r','n','e','l','3','2','.','d','l','l', 0x0 };

fnVirtualProtect = (pVirtualProtect) GetProcAddress(GetModuleHandle((LPCSTR) sKernel32), (LPCSTR)sVirtualProtect);
// call VirtualProtect
fnVirtualProtect(address, dwSize, PAGE_READWRITE, &amp;oldProt);

Obfuscating strings using a character array cuts the string up in smaller pieces making them more difficult to extract from a binary.

The call will still be to an 

ntdll.dll
 WINAPI, and will not bypass any hooks in WINAPIs in 
ntdll.dll
, but is purely to remove suspicious functions from the IAT.

5. Disabling Event Tracing for Windows (ETW)

Many EDR solutions leverage Event Tracing for Windows (ETW) extensively, in particular Microsoft Defender for Endpoint (formerly known as Microsoft ATP). ETW allows for extensive instrumentation and tracing of a process’ functionality and WINAPI calls. ETW has components in the kernel, mainly to register callbacks for system calls and other kernel operations, but also consists of a userland component that is part of 

ntdll.dll
 (ETW deep dive and attack vectors). Since 
ntdll.dll
 is a DLL loaded into the process of our binary, we have full control over this DLL and therefore the ETW functionality. There are quite a few different bypasses for ETW in userspace, but the most common one is patching the function 
EtwEventWrite
 which is called to write/log ETW events. We fetch its address in 
ntdll.dll
, and replace its first instructions with instructions to return 0 (
SUCCESS
).


void disableETW(void) {
    // return 0
    unsigned char patch&#91;] = { 0x48, 0x33, 0xc0, 0xc3};     // xor rax, rax; ret
   
    ULONG oldprotect = 0;
    size_t size = sizeof(patch);
   
    HANDLE hCurrentProc = GetCurrentProcess();
   
    unsigned char sEtwEventWrite&#91;] = { 'E','t','w','E','v','e','n','t','W','r','i','t','e', 0x0 };
   
    void *pEventWrite = GetProcAddress(GetModuleHandle((LPCSTR) sNtdll), (LPCSTR) sEtwEventWrite);
   
    NtProtectVirtualMemory(hCurrentProc, &amp;pEventWrite, (PSIZE_T) &amp;size, PAGE_READWRITE, &amp;oldprotect);
   
    memcpy(pEventWrite, patch, size / sizeof(patch&#91;0]));
   
    NtProtectVirtualMemory(hCurrentProc, &amp;pEventWrite, (PSIZE_T) &amp;size, oldprotect, &amp;oldprotect);
    FlushInstructionCache(hCurrentProc, pEventWrite, size);
   
}

I’ve found the above method to still work on the two tested EDRs, but this is a noisy ETW patch.

6. Evading common malicious API call patterns

Most behavioural detection is ultimately based on detecting malicious patterns. One of these patters is the order of specific WINAPI calls in a short timeframe. The suspicious WINAPI calls briefly mentioned in section 4 are typically used to execute shellcode and therefore heavily monitored. However, these calls are also used for benign activity (the 

VirtualAlloc
WriteProcess
CreateThread
 pattern in combination with a memory allocation and write of ~250KB of shellcode) and so the challenge for EDR solutions is to distinguish benign from malicious calls. Filip Olszak wrote a great blog post leveraging delays and smaller chunks of allocating and writing memory to blend in with benign WINAPI call behaviour. In short, his method adjusts the following behaviour of a typical shellcode loader:

  1. Instead of allocating one large chuck of memory and directly write the ~250KB implant shellcode into that memory, allocate small contiguous chunks of e.g. <64KB memory and mark them as 
    NO_ACCESS
    . Then write the shellcode in a similar chunk size to the allocated memory pages.
  2. Introduce delays between every of the above mentioned operations. This will increase the time required to execute the shellcode, but will also make the consecutive execution pattern stand out much less.

One catch with this technique is to make sure you find a memory location that can fit your entire shellcode in consecutive memory pages. Filip’s DripLoader implements this concept.

The loader I’ve built does not inject the shellcode into another process but instead starts the shellcode in a thread in its own process space using 

NtCreateThread
. An unknown process (our binary will de facto have low prevalence) into other processes (typically a Windows native ones) is suspicious activity that stands out (recommended read “Fork&Run – you’re history”). It is much easier to blend into the noise of benign thread executions and memory operations within a process when we run the shellcode within a thread in the loader’s process space. The downside however is that any crashing post-exploitation modules will also crash the process of the loader and therefore the implant. Persistence techniques as well as running stable and reliable BOFs can help to overcome this downside.

7. Direct system calls and evading “mark of the syscall”

The loader leverages direct system calls for bypassing any hooks put in 

ntdll.dll
 by the EDRs. I want to avoid going into too much detail on how direct syscalls work, since it’s not the purpose of this post and a lot of great posts have been written about it (e.g. Outflank).

In short, a direct syscall is a WINAPI call directly to the kernel system call equivalent. Instead of calling the 

ntdll.dll
 
VirtualAlloc
 we call its kernel equivalent 
NtAlocateVirtualMemory
 defined in the Windows kernel. This is great because we’re bypassing any EDR hooks used to monitor calls to (in this example) 
VirtualAlloc
 defined in 
ntdll.dll
.

In order to call a system call directly, we fetch the syscall ID of the system call we want to call from 

ntdll.dll
, use the function signature to push the correct order and types of function arguments to the stack, and call the 
syscall &lt;id&gt;
 instruction. There are several tools that arrange all this for us, SysWhispers2 and SysWhisper3 are two great examples. From an evasion perspective, there are two issues with calling direct system calls:

  1. Your binary ends up with having the 
    syscall
     instruction, which is easy to statically detect (a.k.a “mark of the syscall”, more in “SysWhispers is dead, long live SysWhispers!”).
  2. Unlike benign use of a system call that is called through its 
    ntdll.dll
     equivalent, the return address of the system call does not point to 
    ntdll.dll
    . Instead, it points to our code from where we called the syscall, which resides in memory regions outside of 
    ntdll.dll
    . This is an indicator of a system call that is not called through 
    ntdll.dll
    , which is suspicious.

To overcome these issues we can do the following:

  1. Implement an egg hunter mechanism. Replace the 
    syscall
     instruction with the 
    egg
     (some random unique identifiable pattern) and at runtime, search for this 
    egg
     in memory and replace it with the 
    syscall
     instruction using the 
    ReadProcessMemory
     and 
    WriteProcessMemory
     WINAPI calls. Thereafter, we can use direct system calls normally. This technique has been implemented by klezVirus.
  2. Instead of calling the 
    syscall
     instruction from our own code, we search for the 
    syscall
     instruction in 
    ntdll.dll
     and jump to that memory address once we’ve prepared the stack to call the system call. This will result in an return address in RIP that points to 
    ntdll.dll
     memory regions.

Both techniques are part of SysWhisper3.

8. Removing hooks in 
ntdll.dll

Another nice technique to evade EDR hooks in 

ntdll.dll
 is to overwrite the loaded 
ntdll.dll
 that is loaded by default (and hooked by the EDR) with a fresh copy from 
ntdll.dll
ntdll.dll
 is the first DLL that gets loaded by any Windows process. EDR solutions make sure their DLL is loaded shortly after, which puts all the hooks in place in the loaded 
ntdll.dll
 before our own code will execute. If our code loads a fresh copy of 
ntdll.dll
 in memory afterwards, those EDR hooks will be overwritten. RefleXXion is a C++ library that implements the research done for this technique by MDSec. RelfeXXion uses direct system calls 
NtOpenSection
 and 
NtMapViewOfSection
 to get a handle to a clean 
ntdll.dll
 in 
\KnownDlls\ntdll.dll
 (registry path with previously loaded DLLs). It then overwrites the 
.TEXT
 section of the loaded 
ntdll.dll
, which flushes out the EDR hooks.

I recommend to use adjust the RefleXXion library to use the same trick as described above in section 7.

9. Spoofing the thread call stack

The next two sections cover two techniques that provide evasions against detecting our shellcode in memory. Due to the beaconing behaviour of an implant, for a majority of the time the implant is sleeping, waiting for incoming tasks from its operator. During this time the implant is vulnerable for memory scanning techniques from the EDR. The first of the two evasions described in this post is spoofing the thread call stack.

When the implant is sleeping, its thread return address is pointing to our shellcode residing in memory. By examining the return addresses of threads in a suspicious process, our implant shellcode can be easily identified. In order to avoid this, want to break this connection between the return address and shellcode. We can do so by hooking the 

Sleep()
 function. When that hook is called (by the implant/beacon shellcode), we overwrite the return address with 
0x0
 and call the original 
Sleep()
 function. When 
Sleep()
 returns, we put the original return address back in place so the thread returns to the correct address to continue execution. Mariusz Banach has implemented this technique in his ThreadStackSpoofer project. This repo provides much more detail on the technique and also outlines some caveats.

We can observe the result of spoofing the thread call stack in the two screenshots below, where the non-spoofed call stack points to non-backed memory locations and a spoofed thread call stack points to our hooked Sleep (

MySleep
) function and “cuts off” the rest of the call stack.

Default beacon thread call stack.
Spoofed beacon thread call stack.

10. In-memory encryption of beacon

The other evasion for in-memory detection is to encrypt the implant’s executable memory regions while sleeping. Using the same sleep hook as described in the section above, we can obtain the shellcode memory segment by examining the caller address (the beacon code that calls 

Sleep()
 and therefore our 
MySleep()
 hook). If the caller memory region is 
MEM_PRIVATE
 and 
EXECUTABLE
 and roughly the size of our shellcode, then the memory segment is encrypted with a XOR function and 
Sleep()
 is called. Then 
Sleep()
 returns, it decrypts the memory segment and returns to it.

Another technique is to register a Vectored Exception Handler (VEH) that handles 

NO_ACCESS
 violation exceptions, decrypts the memory segments and changes the permissions to 
RX
. Then just before sleeping, mark the memory segments as 
NO_ACCESS
, so that when 
Sleep()
 returns, it throws a memory access violation exception. Because we registered a VEH, the exception is handled within that thread context and can be resumed at the exact same location the exception was thrown. The VEH can simply decrypt and change the permissions back to RX and the implant can continue execution. This technique prevents a detectible 
Sleep()
 hook being in place when the implant is sleeping.

Mariusz Banach has also implemented this technique in ShellcodeFluctuation.

11. A custom reflective loader

The beacon shellcode that we execute in this loader ultimately is a DLL that needs to be executed in memory. Many C2 frameworks leverage Stephen Fewer’s ReflectiveLoader. There are many well written explanations of how exactly a relfective DLL loader works, and Stephen Fewer’s code is also well documented, but in short a Reflective Loader does the following:

  1. Resolve addresses to necessary 
    kernel32.dll
     WINAPIs required for loading the DLL (e.g. 
    VirtualAlloc
    LoadLibraryA
     etc.)
  2. Write the DLL and its sections to memory
  3. Build up the DLL import table, so the DLL can call 
    ntdll.dll
     and 
    kernel32.dll
     WINAPIs
  4. Load any additional library’s and resolve their respective imported function addresses
  5. Call the DLL entrypoint

Cobalt Strike added support for a custom way for reflectively loading a DLL in memory that allows a red team operator to customize the way a beacon DLL gets loaded and add evasion techniques. Bobby Cooke and Santiago P built a stealthy loader (BokuLoader) using Cobalt Strike’s UDRL which I’ve used in my loader. BokuLoader implements several evasion techniques:

  • Limit calls to 
    GetProcAddress()
     (commonly EDR hooked WINAPI call to resolve a function address, as we do in section 4)
  • AMSI & ETW bypasses
  • Use only direct system calls
  • Use only 
    RW
     or 
    RX
    , and no 
    RWX
     (
    EXECUTE_READWRITE
    ) permissions
  • Removes beacon DLL headers from memory

Make sure to uncomment the two defines to leverage direct system calls via HellsGate & HalosGate and bypass ETW and AMSI (not really necessary, as we’ve already disabled ETW and are not injecting the loader into another process).

12. OpSec configurations in your Malleable profile

In your Malleable C2 profile, make sure the following options are configured, which limit the use of 

RWX
 marked memory (suspicious and easily detected) and clean up the shellcode after beacon has started.


    set startrwx        "false";
    set userwx          "false";
    set cleanup         "true";
    set stomppe         "true";
    set obfuscate       "true";
    set sleep_mask      "true";
    set smartinject     "true";

Conclusions

Combining these techniques allow you to bypass (among others) Microsoft Defender for Endpoint and CrowdStrike Falcon with 0 detections (tested mid April 2022), which together with SentinelOne lead the endpoint protection industry.

CrowdStrike Falcon with 0 alerts.
Windows Defender (and also Microsoft Defender for Endpoint, not screenshotted) with 0 alerts.

Of course this is just one and the first step in fully compromising an endpoint, and this doesn’t mean “game over” for the EDR solution. Depending on what post-exploitation activity/modules the red team operator choses next, it can still be “game over” for the implant. In general, either run BOFs, or tunnel post-ex tools through the implant’s SOCKS proxy feature. Also consider putting the EDR hooks patches back in place in our 

Sleep()
 hook to avoid detection of unhooking, as well as removing the ETW/AMSI patches.

It’s a cat and mouse game, and the cat is undoubtedly getting better.