One of the oldest ways of bypassing Anti-Virus (AV) detection has been to pack and encode malware contents on disk, and when executed the malicious payload is decoded in memory and never touches disk. With traditional AV’s being predominantly disk-focused, by decoding the malicious payload entirely in memory, this results in disk based AV detection being bypassed.
Attackers and malware authors are always coming up with new techniques for bypassing AV. For example, we recently posted about the rise of fileless malware, whereby the malware stays memory-resident and avoids touching disk at all, nor has a corresponding component on disk. This is something we have been seeing for years but has garnered significant publicity more recently (https://countercept.com/our-thinking/threat-hunting-for-fileless-malware/).
A more trivial example – the topic of this article – includes XOR encoding a malicious binary and adding a decoding function to the beginning of the file, which decodes itself in-memory and executes. This can be achieved with a simple XOR decoding stub, which iterates through the contents in memory and decodes them, before continuing execution.
In this post, we’ll analyse an example to fully understand the techniques used, before comparing how EDR software with memory analysis capabilities can give visibility of it in order to help threat hunting teams find evidence of malicious execution using similar techniques.
Upon initially opening the binary with IDA Pro, only a single function exists. This is a clear indicator that something isn’t quite right with the file, and reviewing the remainder of the binary, it doesn’t look to contain a huge amount of valid assembly.
Figure 1 - Only a single function can be observed when analyzing with IDA Pro
Immunity Debugger will also pop-up a message when loaded and the malicious binary entered, complaining that the main binary module appears to be “either compressed, encrypted or contains [a] large amount of embedded data.”
Figure 2 - Immunity Debugger statistical analysis detects the code is either compressed, encrypted or contains embedded data
Reviewing the assembly at the entry point, only a small stub of code is visible, and the surrounding assembly looks invalid.
Figure 3 - Immunity Debugger is unable to analyse a large amount of the file
Following the initial stack prologue, execution jumps into a loop which clears the EAX register, calls PUSHA/POPA to push all the general purpose registers onto the stack and pop them off again, increments EAX and compares it to 0x10000000 and repeats this until the Zero flag - indicating EAX equals 0x10000000 – is set. See Figure 4.
This function does nothing but waste CPU cycles, and is used as an anti-sandbox technique. AV’s that employ behavioural detection will typically execute a file in a sandbox and observe it for any malicious indicators. However, the longer that AV takes analysing a file, the longer the user must wait to use a file, and so a fine balance must be met between how long to wait, and when to make a decision of the legitimacy of a file. By delaying execution with such techniques it reduces the likelihood of the file being ran fully by the AV sandbox, resulting in it being less likely to be detected as malicious.
Figure 4 - Anti-Sandbox technique used to delay execution
Following the sandbox evasion, the sample next makes a call to the Windows API function LoadLibraryA(), used to load Windows modules/DLLs into the calling process. This call is made with of the argument of “kernel32.dll”, which if successful will load the DLL into the process and return a handle to it.
Figure 5 - kernel32.dll is loaded with LoadLibraryA()
With a handle to kernel32.dll, a call is made to GetProcAddress() with the arguments of ”VirtualProtect” and the handle to kernel32.dll. This returns the address of VirtualProtect(), which is stored in EAX.
Figure 6 – GetProcAddress() is used to return the address of VirtualProtect()
VirtualProtect() is used to change the permissions of pages within the memory space of the calling process. In the case of this sample, VirtualProtect() is used to modify the permissions of the memory page associated with the .text from PAGE_EXECUTE_READ to PAGE_EXECUTE_READWRITE, necessary for writing any changes to the page in question.
Figure 7 – VirtualProtect() is used to change the page permissions of the .text section to RWX
When analysed in Immunity Debugger, it is possible to see the page permissions change before and after this call is made.
Figure 8a - Page Permissions of the .text section BEFORE the call to VirtualProtect()
Figure 8b - Page permissions of the .text section AFTER the call to VirtualProtect()
Following the call to VirtualProtect(), the return address is checked to ensure it is non zero. Execution then jumps into an XOR decoding loop which begins at address 0x401000 and runs until address 0x40610D, decoding the contents with the 2-byte XOR key of 0x0F3C. This is repeated again for the remainder of the binary.
Figure 9 - XOR decode stub with key 0x0F3C
With the binary in memory decoded, execution can continue as normal as if the decoding had never taken place in the first place, and the malicious payload never touches disk.
Figure 10a - .text section BEFORE decoding (garbage assembly)
Figure 10b - .text section AFTER decoding (genuine looking assembly)
Once decoded, as per Figure 10b, it is possible to dump the decoded file back to disk. This can be achieved with Immunity Debugger by selecting the contents of the .text section (Click First Address, SHIFT + Click End Address). In the popup dialogue, it is then possible to right click and ‘Save file’ to save a copy of the current state.
Figure 11 - It is possible to save the decoded binary to disk with Immunity Debugger
Reviewing the decoded binary in IDA Pro, we now see all of the different functions as expected, and the binary looks a little more complete.
Figure 12 - Once decoded, the functions can be observed in IDA Pro
This is just one simple example, but a whole range of different AV-bypass techniques exist which have been developed over the years with the purpose of bypassing AV defences. But with the growing popularity of Endpoint Detection & Response (EDR) agents, the very nature by which these bypass techniques occur often result in other behaviours or anomalies which more flexible EDR agents can detect.
In particular for this example, by avoiding touching disk in the hope of bypassing AV, this sample plays into the hands of EDR agents that target in-memory malware. Two easy indicators can be observed when this sample is run against our own EDR solution.
Firstly, we can see that the .text section of the malware.exe module on disk is very different from that which is in memory. This is due to the self-decoding technique this sample uses, resulting in an anomaly which can be used to detect its presence. This has been highlighted because the EDR software has taken the .text section of the file on disk, relocated it to understand how it should appear in memory and then compared it with the actual corresponding .text section in memory and reported the match percentage along with other useful data.
We may not always expect an exact 100% match for various reasons, including the prevalence of inline hooking techniques used by a range of legitimate software. However, a very low percentage match is often indicative of process hollowing, or self-decrypting/decoding malware samples.
Secondly, the .text section in the malware.exe module is allocated as PAGE_EXECUTE_WRITECOPY, but because the sample needs to decode and re-write itself in memory, it uses the call to VirtualProtect() to change the page permissions to PAGE_EXECUTE_READWRITE. This shows that something has changed the execute permissions for that region to allow them to be modified.
Figure 13 - In-Memory anomalies can be detected with EDR tools
Additionally, by monitoring Windows API calls that are known to be associated with malware, it is possible to detect malicious behaviours. Alongside the typical functions of CreateRemoteThread() and LoadLibraryEx() that are commonly associated with malware, reviewing API functions such as VirtualProtect() can be effective for identifying malicious behaviour, particularly when contextualised. For example:
Why is this file giving itself writeable permissions?
Why does this file need to import these particular kernel32 modules?
Figure 14 - Windows API calls made by the sample
It’s also worth noting that more generic indicators may be used for detecting this, such as least frequency analysis of process execution data or abnormalities in PE header data. We find this can be a highly effective technique for detecting the presence of anomalous processes.
For example, threat hunting teams may consider some of the following questions:
Why does this single process exist on a single host across our entire estate?
Why is it not signed when we have a policy of signing all deployed executables?
Why does it have no publisher information?
Attackers have been bypassing AV for many years, and for as long as AV continues to exist, new techniques will continue to be developed. Interestingly - as demonstrated in this post - the very nature by which AV-bypass techniques typically work result in subsequent anomalies elsewhere that can be detected.
AV is no longer the only host-based defence attackers are coming up against anymore, as the popularity of EDR is continuing to increase. Although attackers have become experienced in bypassing AV, with many targeted and sophisticated attacks able to bypass AV defences, the increase in EDR means attackers will need to start developing ways to avoid detection from hunt teams and security solutions using EDR, and this will likely ultimately lead onto the development of EDR-bypass techniques.
The world of EDR is still technically in its early years, and the different vendors are each taking their own approaches to performing detection. But as the industry becomes more standardised, and the techniques used by EDR tools become more commonplace, it is reasonable to expect that attackers will become more acquainted with bypassing EDR and bypassing detection alike.
There is one particularly key difference between AV and EDR solutions that makes the latter harder to bypass though. AV is a fire-and-forget solution that can be installed and provides immediate protection against a class of threats. However, it is also a static target with a binary decision process and so is easily bypassed by dedicated attackers.
On the other hand, EDR left unattended achieves nothing as it is merely a data provider and a response tool that remains completely passive if not used proactively by a skilled hunt team. When used effectively however, it is very difficult to bypass in the same manner as it is much more difficult to stay invisible to all forms of logs and near impossible to know exactly how an adaptive human hunt team will interpret the data.