January 26th, 2023

Evading Detection: From Inception to Reality

Itamar Brem

Itamar Brem

Antivirus EDR Ethical Hacking Pentera Labs Reflective loading

In this article, we will show how it’s possible to use reflective loading to run Mimikatz while evading detection by Windows Defender. While this is a known attack method, recent improvements in windows defender blocked the method from working properly, so we needed to find a new way to handle dependencies. Read on to see how we did it.

Fileless execution

One way to keep malicious content from being uploaded to the cloud is by simply never having it on disk. To do this, an attacker can turn their tool into shellcode and execute it through a vulnerability already present in the system. If no vulnerabilities are found, an attacker can drop a small executable/script/command, which does basically nothing except receive and execute shellcode from a socket.

This can also make the small executable more obscure by hiding the shellcode execution behind a vulnerability, which triggers only under specific unique conditions.

Evading memory scanning

The next step is to evade EDR memory scanning so our tools aren’t caught and stopped prematurely. 

In order to execute logic on a computer an attacker will usually need to have the code present on the system. Another possibility would be to implement most of the logic offsite and to use a small tool to handle RPCs.  

As the code is going to be on the system, it would make sense to hide it. It’s possible to either implement a Virtual Machine of some kind, which would obscure the logic of the code.. If this sounds complicated, it’s also possible to try to have the code encrypted for as long as possible and decrypt it only when needed.

In this case, we used LIEF to create a new code section in the EXE and injected shellcode into it.

After the shellcode is injected, chosen ranges of the data and code of the EXE are encrypted.
After encryption, inline hooks are added in selected functions to redirect execution to the injected shellcode. The shellcode performs the decryption of selected ranges according to the location the hook is coming from, removes the inline hook and jumps back to where it came from.

This keeps the payload encrypted until the last moment before it needs to be executed. This is similar to the idea of “lazy-loading.”

Reflective loading

Using LoadDLL as a starting point, it is possible to implement a reflective loader to unpack the next stage of the payload from its own resources.

The LoadDLL project doesn’t properly load all DLLs, since it doesn’t handle edge cases such as DLL import redirections and TLS callbacks. As a result, we need to make the tool only reflectively load the payload EXE and a copy of ntdll to evade the most simple EDR hooks.

When we tried it out, it worked for a bit until Defender implemented new techniques to catch maliciously loaded code.

While debugging the tool we noticed that Defender would raise an alarm right after/inside LoadDLL_ResolveImports(). This means the tool can catch a reflectively loaded Mimikatz by monitoring calls to LoadLibrary()/GetProcAddress() and checking against a blacklist.

At first it was possible to bypass this by using MyGetProcAddress() while still using the original LoadLibrary(), however, later we noticed that LoadLibrary() itself was being monitored for a sequence of known DLL imports of widely-used malicious tools.

To bypass this, we implemented namespaces using hashmap lookups in order to preload DLLs with the LoadDLL code instead of just calling the original LoadLibrary().

This made it obvious that the LoadDLL code didn’t implement all the needed functionality to properly load a DLL. Some, but not all, of the problems have to do with redirecting API imports and can be dealt with. Other problems can be harder to deal with such as initialization of dynamic global data, DLLs which create threads upon DllMain(), DLLs which hold and check mutexes, etc.

DLL forwarding

The first problem that occurred was that LoadDLL’s implementation of MyGetProcAddress() lacked code to handle DLL forwarding. This became clear when the original GetProcAddress() returned a different address than MyGetProcAddress(), which returned an address of a string instead of the wanted function.

If the resulting address of the API still points to the IMAGE_EXPORT_DIRECTORY of the module that’s being looked at, it’s a DLL redirection string.

The basic redirection string format is either DLL_NAME.API_NAME or DLL_NAME.#ORDINAL.

(LdrpGetFullPath() and RtlGetFullPathName_Ustr() hint at more complicated import strings, which may be interesting).

For example; SystemFunction007@adviapi32.dll -> “CRYPTSP.SystemFunction007″@adviapi32.dll -> SystemFunction007@cryptsp.dll

Even though the string representation is pretty self explanatory it’s always good to make sure we’re not wrong.

We can follow a call to kernel32_GetProcAddress(), which eventually reaches ntdll_LdrpResolveProcedureAddress() and start looking from there.

In the image above, it’s possible to see the code, which loops until no further DLL forwarding occurs.

When a DLL import is forwarded to another DLL, LdrpGetProcedureAddress() returns the code 0xC000022D. The result is then passed to LdrpParseForwarderDescription() which finds the last ‘.’ character and initializes a UNICODE_STRING struct up to that character. Later LdrpAppendAnsiStringToFilenameBuffer() appends “.DLL” and that is used as the new DLL name. The loop with the blue arrow then jumps back to the top and LdrpGetProcedureAddress() is called again with the forwarded DLL.

As you can see above, the LdrpGetProcedureAddress() returns the error code 0xC000022D when the result points inside the range of the IMAGE_EXPORT_DIRECTORY segment of the module.

ApiSet redirections

Windows implements a feature called ApiSet in order to help with portability issues. This is explained below:

“The APIs in an umbrella library may be implemented across a range of modules. The umbrella library abstracts those details away from you, making your code more portable across Windows versions and devices. Instead of linking to libraries such as kernel32.lib and advapi32.lib, simply link your desktop app or driver with the umbrella library that contains the set of core OS APIs that you’re interested in.”

Using the ApiSet, APIs can be redirected from a “virtual DLL” to a real DLL on disk. This looks like any other DLL import, except that the name starts with either “api-” or “ext-” and looks like “api-ms-win-core-processthreads-l1-1-0.dll”.

For example: 

OpenProcessToken@kernel32.dll -> “api-ms-win-core-processthreads-l1-1-0.OpenProcessToken”@kernel32.dll -> OpenProcessToken@ApiSetResolve(“api-ms-win-core-processthreads-l1-1-0.dll”, “kernel32.dll”) == kernelbase.dll

The logic that handles this is in LdrpPreprocessDllName() / LdrpApplyFileNameRedirection().

Given that LdrpPreprocessDllName() isn’t exported by ntdll, we would need to use an ApiSet library, which parses the structures of the ApiSet, which reside and are accessible in user-mode memory.

(The library is written with a cool trick to prevent code duplication; The pebteb.h file uses a macro which is defined differently before each time pebteb.h is included, causing different code to be generated each time).

Recapping the attack

We set out to check if tools like Mimikatz can evade AV and EDR products like Windows Defender.

We were able to use DLL forwarding and ApiSet improvements to LoadDLL to preload several DLL files from either memory or disk that are imported by the DLLs and EXE instead of using LoadLibrary().

This lets an attacker bypass EDR hooks by stealthily loading their own copy of DLLs, which are likely to be hooked.

Possible security improvements

Both the defensive and offensive sides of AV/EDR R&D are complicated. There might not be a single correct and efficient way to prevent techniques such as reflective loading, but the problem can be made more difficult. Below are some ways to do this (I’m assuming all legitimate syscalls are located in ntdll):

  • Improve memory scanning
  • Have syscalls check the return address and make sure it points to a valid code location such as ntdll
  • Randomize syscall numbers+table per-boot and have the operating system handle the “relocations” when loading ntdll
  • Prevent read/write access to system DLL files and memory (except in the correct context)
  • Make system DLLs hold mutexes on DllMain() to make them harder to reflectively load
  • Move the ApiSet data to kernel-mode to make it more private and obscure

Correctly implementing these features on an operating system level would most likely require any code to pass through ntdll in order to do anything interesting, which could be monitored by EDR products.

For any questions, reach out to itamar.brem@pentera.io.

Liked it? You should share it:

Learn more about our platform