Direct Syscall Execution in Windows

Introduction

In the world of Windows security and malware analysis, syscall hooking and evasion techniques are becoming increasingly sophisticated. Traditional Windows API calls are typically intercepted by security solutions such as AVs and EDRs, which makes it necessary for red teamers and malware developers to explore alternative methods of interacting with the Windows kernel. One such method is the use of direct syscalls. This article will guide you through implementing direct syscall execution in Windows using C++ and assembly, providing a practical example and code explanations.

What are Syscalls?

Syscalls, short for "system calls," are the fundamental interface between user-space applications and the Windows kernel. When a program wants to interact with hardware, access system resources, or perform privileged operations, it invokes a syscall, which is handled by the kernel.

Typically, when you use a high-level Windows API function, such as CreateFile, it eventually calls a corresponding syscall like NtCreateFile. Security products often monitor these high-level API calls, making them potential points of detection. By invoking syscalls directly, you can potentially bypass these monitoring mechanisms.

Why Use Direct Syscalls?

Using direct syscalls can help evade security mechanisms that hook or monitor Windows API calls. This method allows the execution of kernel-level operations without the overhead of API functions, thereby reducing the chances of detection. It also provides greater control over the execution flow, which is critical in offensive security and exploit development.

Implementing Direct Syscalls

The following example demonstrates how to implement direct syscall execution using a combination of C++ and assembly. This example focuses on two syscalls: NtOpenFile and NtClose.

Code Overview

  1. DirectSyscall.cpp: The main C++ file that sets up and invokes the syscalls.

  2. syscalls.h: Header file defining the prototypes and necessary structures.

  3. syscall_direct.asm: Assembly code that defines the syscall procedures.

DirectSyscall.cp

This file contains the core logic for setting up and calling the syscalls.

#include <windows.h>
#include <winternl.h>
#include <stdio.h>
#include "syscalls.h"

#pragma comment(lib, "ntdll.lib")

// Define the syscall numbers
DWORD wNtOpenFile;
DWORD wNtClose;

// Declare the RtlInitUnicodeString function
extern "C" void RtlInitUnicodeString(PUNICODE_STRING DestinationString, PCWSTR SourceString);

int wmain(int argc, wchar_t* argv[]) {
    if (argc != 2) {
        wprintf(L"Usage: %s <file-path>\n", argv[0]);
        return -1;
    }

    // Get handle to ntdll.dll and cast it to HMODULE
    HMODULE hNtdll = (HMODULE)GetModuleHandleA("ntdll.dll");

    // Get syscall numbers
    UINT_PTR pNtOpenFile = (UINT_PTR)GetProcAddress(hNtdll, "NtOpenFile");
    if (!pNtOpenFile) {
        printf("Failed to get address of NtOpenFile\n");
        return -1;
    }
    wNtOpenFile = ((unsigned char*)(pNtOpenFile + 4))[0];

    UINT_PTR pNtClose = (UINT_PTR)GetProcAddress(hNtdll, "NtClose");
    if (!pNtClose) {
        printf("Failed to get address of NtClose\n");
        return -1;
    }
    wNtClose = ((unsigned char*)(pNtClose + 4))[0];

    HANDLE fileHandle;
    OBJECT_ATTRIBUTES objAttr;
    IO_STATUS_BLOCK ioStatusBlock;

    // Define the file path and initialize the OBJECT_ATTRIBUTES
    UNICODE_STRING filePath;
    wchar_t fullPath[MAX_PATH];
    swprintf(fullPath, MAX_PATH, L"\\??\\%s", argv[1]);
    wprintf(L"Full Path: %s\n", fullPath);

    RtlInitUnicodeString(&filePath, fullPath);
    InitializeObjectAttributes(&objAttr, &filePath, OBJ_CASE_INSENSITIVE, NULL, NULL);

    // Call NtOpenFile
    NTSTATUS status = NtOpenFile(&fileHandle, GENERIC_READ, &objAttr, &ioStatusBlock, FILE_SHARE_READ, FILE_NON_DIRECTORY_FILE);
    if (status != 0) {
        printf("NtOpenFile failed with status: 0x%lx\n", status);
        return -1;
    }

    // Call NtClose
    status = NtClose(fileHandle);
    if (status != 0) {
        printf("NtClose failed with status: 0x%lx\n", status);
        return -1;
    }

    printf("NtOpenFile and NtClose executed successfully\n");
    return 0;
}

syscalls.h

This header file defines the necessary structures and function prototypes required to make the syscalls.

#ifndef _SYSCALLS_H
#define _SYSCALLS_H

#include <windows.h>
#include <winternl.h>  // Include for OBJECT_ATTRIBUTES and related types

#ifdef __cplusplus
extern "C" {
#endif

    typedef long NTSTATUS;
    typedef NTSTATUS(NTAPI* pfnNtOpenFile)(
        PHANDLE FileHandle,
        ACCESS_MASK DesiredAccess,
        POBJECT_ATTRIBUTES ObjectAttributes,
        PIO_STATUS_BLOCK IoStatusBlock,
        ULONG ShareAccess,
        ULONG OpenOptions
        );
    typedef NTSTATUS(NTAPI* pfnNtClose)(HANDLE Handle);

#ifdef __cplusplus
}
#endif

#endif // _SYSCALLS_H

syscall_direct.asm

This assembly file contains the implementation of the syscalls using the syscall instruction, which directly interacts with the Windows kernel.

; Definition of syscall numbers
EXTERN wNtOpenFile:DWORD
EXTERN wNtClose:DWORD

PUBLIC NtOpenFile
PUBLIC NtClose

.CODE  ; Start of the code section

; Procedure for the NtOpenFile syscall
NtOpenFile PROC
    mov r10, rcx               ; Move the contents of rcx to r10, necessary for the syscall in 64-bit Windows
    mov eax, wNtOpenFile       ; Move the syscall number into eax
    syscall                    ; Execute the syscall
    ret                        ; Return from the procedure
NtOpenFile ENDP

; Procedure for the NtClose syscall
NtClose PROC
    mov r10, rcx
    mov eax, wNtClose
    syscall
    ret
NtClose ENDP

END  ; End of the module

How It Works

  1. Getting the Syscall Numbers: In DirectSyscall.cp, we load the ntdll.dll library and retrieve the addresses of NtOpenFile and NtClose using GetProcAddress. The syscall numbers are then extracted by accessing the fourth byte of these addresses.

  2. Assembly Code for Direct Syscalls: The assembly file syscall_direct.asm defines the procedures for NtOpenFile and NtClose. These procedures directly invoke the corresponding syscalls using the syscall instruction, bypassing any high-level API hooks.

  3. Executing the Syscalls: The C++ code initializes the necessary structures and then invokes the syscalls using the procedures defined in the assembly file. This allows the file to be opened and closed directly through kernel calls, reducing the risk of detection by security software.

Conclusion

Direct syscall execution is a powerful technique for evading detection by security solutions that rely on monitoring API calls. By using the method outlined in this guide, you can interact with the Windows kernel directly, potentially bypassing certain security mechanisms. This approach is particularly useful in advanced red teaming scenarios and malware development.

For more details and to access the complete code, visit the GitHub repository.

This guide is just a starting point, and the implementation can be further extended to include more syscalls and different use cases.

Last updated