Lets Make Malware – Bypassing Heuristics

Preamble
This week we are going to cover heuristics bypasses. Last time we went over how AV and EDR solutions can detect malware before it has even begun to run. We also went over ways to get around these static detections.
So, the next step is executing our malware. However, just because we have made it this far does not mean smooth sailing.
Usually, if unknown software is trying to execute for the first time, AV will run it inside a sandbox.
Inside the sandbox, any potential damage can be minimized, and the AV can determine if the software should be removed or not.
Unfortunately, sandboxes cannot be maintained indefinitely. At some point, the AV must make its decision. And it must make that decision as soon as possible.
This is one possible way to bypass them. Simply wait.
There is one other critical piece of information we need. Sandboxes generally stand out from live systems.
They have very minimal resources like RAM, CPU cores, and hard drive space.
Likewise, typical processes you would see running on a live system won’t be there. And often enough, you will even see process names that are indicative of a sandbox.
Furthermore, sandboxes don’t tend to be domain-joined.
So, as we can see, there are several checks we can run to verify whether or not our malware is executing inside a sandbox.
Now, let’s take a look at what some of these checks look like when implemented.
Sleep
As we said before, the simplest thing we can do to bypass heuristics is simply wait. In other words, we can use the sleep or SleepEx function. Sleeping for 30 seconds upon initial execution is usually a safe bet. However, of course, AV vendors have caught on to this little trick. Because they have control over what happens in the sandbox, what will usually happen is time is “fast-forwarded” inside the sandbox environment when a sleep function occurs in order to see what happens next.
But if all we need to do is waste time, there are many other ways to accomplish that. One alternative is putting our sleep function inside a loop and choosing values so small that it would not make sense to “fast-forward” past it.
For example, we could do something like this:
for(int i = 0; i < 300; i++) {
Sleep(100)
}
It makes no sense to go forward in time 300 milliseconds, so the assumption here is that the AV will simply allow it to occur. However, because it is within the for-loop, Sleep() will happen 300 times. With some basic math here, we can see this should effectively take 30 seconds (30,000 milliseconds).
It is also worth noting we can detect if time has been manipulated (if only we could do so in real life) by taking a timestamp just prior to our sleep function and again afterwards. If there is a discrepancy between the time we gave our sleep function and the timestamp differences, we know we are inside a sandbox.
Encryption
There are numerous other things we can do without using Sleep at all. For instance, we could do some encryption. We briefly mentioned bruteforcing a multibyte XOR key (also called repeating key) last time, and this is part of the benefit of doing so. Although, in this case, we do not actually need to succeed in bruteforcing the key. We only care about the time it takes to attempt to do so.
We can use the following to encrypt a string:
#include <iostream.h>
int main()
{
char ptString[11]="A nice str";
char xorKey[11]="AbCdEfG^I&";
for(int i=0; i<10; i++)
{
ptString[i] = ptString[i] ^ xorKey[i];
cout << ptString[i];
}
return 0;
}
And then we take our cipher text and run through every combination of characters until we get the key (or not). Note that we should not include the above code in our malware as it contains the key!
We also do not need to use XOR specifically. We could use any encryption algorithm. If we want to ensure our malware runs longer than the sandbox lasts, we could brute force an AES-256 key. Although, we highly recommend including a kill switch if you choose to go down that route.
Primes
One other approach is by calculating large (and we do mean large) prime numbers. Prime numbers, for those of us who have long since forgotten math concepts, are numbers that are only divisible by themselves and 1.
We recommend reading this post by GeeksForGeeks for ideas on implementing this.
Characteristics
Finally, as we mentioned in the beginning, we can check for key characteristics like RAM size, CPU cores, process/module/host names, etc etc. There are a high number of things we could look for.
If we want to check for RAM size:
#define DIV 1073741824
MEMORYSTATUSEX statex;
Statex.dwLength = sizeof (statex);
GlobalMemoryStatusEx(&statex);
if(statex.ullTotalPhys/DIV < 4) {
cout << "[-] RAM less than 4GB!" << endl;
}
If we want to get the number of processors, there are several ways of doing so. A quick and easy way (but not so common) of doing so is using the GetActiveProcessorCount() function. This can be done like so:
DWORD processorCount = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
if(processorCount < 2) {
cout << "[-] Less than 2 cores found" << endl;
}
More commonly, you will see the GetSystemInfo() function used.
SYSTEM_INFO sysinfo;
GetSystemInfo(&sysinfo);
int numCPU = sysinfo.dwNumberOfProcessors;
Wrapping up
While we can go with any of these checks separately, it is recommended we use a combination of these checks to determine if we are inside a sandbox. For example, we can check CPU count, RAM size, system uptime (it would be strange, after all, if our malware just so happens to execute on a system that has only just been powered on), and sleep time discrepancies.
If 2 or 3 of these checks fail, our malware could then either exit or perform mundane tasks to waste time until it is released from the sandbox. Any of these checks done independently could result in a false positive.
And with that, we now know how to bypass heuristics detections!