Bypass

Overload Mapping vs. Memory Scanners

Investigations and further thoughts on in-memory execution and detection.


Sometimes, as an attacker in cyberspace, the goal is not to completely defeat a defense mechanism, but only to minimize the chance of detection. Since the defense side does not sleep and continuously develops new detections and mechanisms, research must also be done by the attacker side. At avantguard cyber security, we are always looking to evolve our tools and processes to keep up with the threats of today, as well as tomorrow. One of our most important and favorite tools is Covenant, the open source part of our Command & Control (C2) infrastructure, which is also used e.g. by the APT group HAFNIUM. Covenant already offers an operator many ways to execute tools only in memory. This method has become popular with attackers in recent years because it means that nothing has to be written to disk and antivirus programs cannot scan the malicious files at all. As a result, defenders now also need to monitor memory for potentially malicious processes. More and more EDRs are therefore using so-called memory scanners.

This blog post describes our current research on Covenant and memory scanners, and is aimed specifically at Blue Teams and other Red Teams. Many concepts are not explained beforehand, otherwise the post would take on too large a scope. A possible approach is explained that could make certain functionalities in Covenant less discoverable by memory scanners, without having already created a working PoC for it. Questions and feedback are encouraged and can be sent to research@avantguard.io.

Covenant uses SharpSploit, which in turn uses DInvoke. DInvoke allows developers to implement certain functionality in C# in a way that makes it harder for EDR agents to detect. One such functionality is loading and executing functions in a DLL. This is made possible by two different ways:

  • Manual mapping: the required DLL is written to a free memory region (mapping) and the functions can then be called. The advantage of this procedure is that it is relatively "simple" and understandable and can nevertheless remain sufficiently undetected on many systems. The disadvantage is that the DLL actually has no reason to be in memory. The memory region is filled with a DLL which is either not present on the underlying filesystem (but as a reference in the executable which started the process), or the DLL is already in memory once and has now been loaded a second time from the filesystem to bypass API hooks. Both cases are indications of malicious code for a memory scanner. Therefore, DInvoke offers an additional type of mapping.
  • Overload mapping: Here, before loading the required DLL, a legitimate DLL of at least the same size is searched for, usually in C:\Windows\System32. Legitimate here means that the DLL on the file system is present and not yet used in the process. Further it can also be required that the DLL must be signed, since signed files are checked sometimes less strictly than unsigned. The legitimate DLL is then loaded into memory. That is still nothing unusual in itself, however now the actual overload mapping happens: The DLL, whose functions are actually needed, is overwritten now into the memory region of the legitimate DLL. Therefore, the legitimate DLL must be at least as large as the needed DLL, so that sufficient space for overwriting is present. The process is shown with some screenshots in a reply to an issue on Covenant's GitHub page.

Manual Mapping vs. Overload Mapping

For memory scanners, this already makes it more difficult to determine whether a DLL in a process is legitimate or being used for malicious purposes. Fortunately, there are tools that can determine this. Examples of these, which we have already used, are pe-sieve and Moneta. Both are open source and available on GitHub. As an example, let's examine a running process of an implant (Covenant calls these Grunts) with pe-sieve before and after a DLL is added to process memory via overload mapping.

pe-sieve before Mimikatz

The above snippet shows the results of pe-sieve for a process that was started via double-clicking a Grunt-implant. The antivirus on the test system does not block the file or the resulting process. Pe-sieve reports three suspicious findings, which are not yet relevant for the current investigation. Now the C2 server sends the command to the implant to run Mimikatz. Specifically, this means that methods in SharpSploit's Mimikatz.cs and Overload.cs are used to load a DLL from the file system and the referenced "powerkatz_x64.dll" DLL is then overwritten into memory. Then the desired command is executed using the DLL. After this procedure, a scan with pe-sieve returns the following values:

pe-sieve after Mimikatz

It is noticeable that the value of "Replaced" has changed from zero to one. This makes sense, since a DLL was loaded and overwritten with another DLL. How exactly the change is detected is not yet clear to us from initial research in the source code. It looks as if the tool compares the exported functions of the DLL on the file system with those of the DLL in the memory (could there be another way of concealment lurking here?), this would have to be tested however with a first PoC more exactly. In summary, these were our first findings.

To get to the actual point of this article, we have to go back one step. As you can see from the two screenshots above, pe-sieve reports not only "Replaced" but also other suspicious findings. Although the implant can be used relatively undetected as it is at the moment, there is potential for improvement that we want to exploit. One approach to fool memory scanners has been published by mgeeky as a PoC on his GitHub as ShellcodeFluctuation. While the PoC relates to the C2 Cobalt Strike, it also got us thinking for Covenant. Cobalt Strike is closed source, but very modular and forms another part of our C2 infrastructure. ShellcodeFluctuation roughly does the following: While the implant is not in use, i.e. no commands are executed, but the implant simply "sleeps" in memory, the implant's code is encrypted. When the implant "wakes up", i.e. when it is to be used again, the code is decrypted and can be executed again. Once executed, it is encrypted again and put to sleep. The memory region of the code is also marked as non-executable. A memory scanner, which checks the memory during the sleep phase, thus only sees unreadable code in a non-executable memory region. This may seem strange, but is not considered suspicious by pe-sieve, for example. Since the sleep phase also often lasts much longer than the actual execution of commands, the risk of detection is therefore significantly reduced, although not completely eliminated.

ShellcodeFluctuation's solution is so effective and elegant that we asked ourselves: Can't something like this be done for overload mapping? And with our current state of knowledge we claim: Yes, for overload mapping it should be possible to significantly reduce the detection risk by changing the memory during runtime too. A possible solution could look like the following:

  • We assume as an example that there is the DLL "win.dll" on the file system on which our implant runs. This DLL is signed, is sometimes used by processes and all in all is not conspicuous or known for malicious purposes.
  • From the C2 server, the implant is sent code to run that references "powerkatz.dll", which is not signed and is known to be used for malicious purposes.
  • The implant executes the code. Via overload mapping, win.dll is to be loaded and overwritten with powerkatz.dll. However, before overwriting, another step is to be taken: win.dll and powerkatz.dll are to generate a key via XOR. The following is a simplified representation of what is to happen in memory:

DLLs XOR in memory

The key is the same size as powerkatz.dll. At the moment there is still win.dll in the memory region which should be overwritten. Thanks to the properties of XOR, win.dll XOR key can now be executed to write powerkatz.dll to the desired memory region.

XORing process visualized

This is only done if we need an exported function of powerkatz.dll. Once the function has been performed, powerkatz.dll XOR key is now executed again. What is finally back in the memory region is win.dll.

Procedure in memory region and memory scanner results

As already mentioned, a PoC has not yet been implemented. The corresponding places in the source code should be known, but the development effort has not yet been done at the time of this blog post. Further testing would then be required to evaluate the actual effectiveness of this approach. Open questions include:

  • Does transforming the original DLL back really fool the memory scanner?
  • Does the dynamic overwriting of the DLL reveal any other suspicious behavior?
  • How do different memory scanners react to this procedure?

Avantguard cyber security is happy and proud to give high priority to research and further development. This is the only way to understand and simulate today's threats. We would like to thank everyone who has read this far despite the rather dry topic and will soon publish another article that addresses a larger target group. Here we would like to ask again for questions and feedback to research@avantguard.io, because research benefits from and is most fun with knowledge and experience sharing!

Similar posts