rop-syscall-kit is a Windows x64 C++ library for resolving Native API syscall numbers at runtime and invoking them through compact ROP-style syscall stubs with stack spoof(optional).
For educational exchange and authorized security research only. Use these PoCs only in local labs, CTFs, coursework, or systems you own or are explicitly authorized to test. The examples are intentionally benign and are provided to study windows mechanisms, not for unauthorized access or misuse.
- Minimal API: call
rop_syscall::invoke<Ret>(...)orrop_syscall::invoke_spoofed<Ret>(...)directly. - Automatic syscall parsing: scans the local
ntdll.dllexport table and extracts syscall numbers from real Native API stubs. - ROP syscall dispatch: generated stubs transfer control to an existing
syscall; retgadget insidentdll.dll. - Stack spoofing: optional spoofed invocation path builds a configurable return chain before entering the syscall gadget.
- Automatic argument forwarding: variadic C++ templates preserve the requested function signature.
- Automatic stack allocation: the x64 spoofed stub reserves stack space and copies stack arguments into the expected Windows x64 ABI layout.
- Lowercase project layout: directory names are shell-friendly on Windows, Linux, and WSL.
rop-syscall-kit/
include/rop_syscall/
shared.hpp
syscall.hpp
src/
syscall.cpp
image_stub.asm
lib/
rop_syscall_kit.vcxproj
rop_syscall_kit.vcxproj.filters
examples/benign-poc/
main.cpp
benign-poc.vcxproj
benign-poc.vcxproj.filters
rop-syscall-kit.sln
LICENSE
#include <rop_syscall/syscall.hpp>
int main()
{
if (!rop_syscall::initialize()) {
return 1;
}
LARGE_INTEGER system_time = {};
NTSTATUS status = rop_syscall::invoke<NTSTATUS>(
"NtQuerySystemTime",
&system_time);
return NT_SUCCESS(status) ? 0 : 1;
}Spoofed call example:
auto config = rop_syscall::create_spoof_config();
PROCESS_BASIC_INFORMATION info = {};
ULONG returned = 0;
NTSTATUS status = rop_syscall::invoke_spoofed<NTSTATUS>(
"NtQueryInformationProcess",
config,
NtCurrentProcess(),
ProcessBasicInformation,
&info,
sizeof(info),
&returned);Requirements:
- Windows x64
- Visual Studio 2022 C++ toolchain
- Windows SDK 10.0 or newer
- MASM build customization
Build release:
cd /d D:\Code\Security\rop-syscall-kit
msbuild rop-syscall-kit.sln /p:Configuration=Release /p:Platform=x64Build debug:
msbuild rop-syscall-kit.sln /p:Configuration=Debug /p:Platform=x64Output:
x64\Release\rop_syscall_kit.lib
x64\Release\benign_poc.exe
x64\Debug\rop_syscall_kit.lib
x64\Debug\benign_poc.exe
x64\Release\benign_poc.exeThe example performs local calls only:
- Resolves syscall metadata.
- Prints syscall numbers.
- Calls
NtQuerySystemTimeand prints UTC time. - Calls
NtQueryInformationProcesson the current process and prints returned fields. - Calls
NtDelayExecutionfor a short delay. - Creates a local event with
CreateEventWand closes it withNtClose.
Expected output shape:
rop-syscall-kit benign poc
============================
Resolved syscall numbers:
NtQuerySystemTime -> #...
NtQueryInformationProcess -> #...
NtDelayExecution -> #...
NtClose -> #...
Stack-spoofed invocation path: enabled
NtQuerySystemTime returned UTC: ...
NtQueryInformationProcess returned:
current pid : ...
peb address : 0x...
bytes : ...
NtDelayExecution returned after about ... ms
NtClose closed a local event handle successfully
Demo completed successfully.
Windows x64 Native API exports in ntdll.dll commonly use a small syscall entry stub:
mov r10, rcx
mov eax, <syscall_number>
syscall
retThe important value is the immediate loaded into eax. That value is the syscall number for the current Windows build.
rop-syscall-kit parses the local ntdll.dll, finds these stubs, extracts the immediate value, and stores it in an in-memory lookup table.
Instead of emitting a standalone syscall instruction in generated memory, the normal generated stub jumps to a real syscall; ret gadget already present in ntdll.dll:
mov r10, rcx
mov eax, <syscall_number>
mov r11, <ntdll_syscall_ret_gadget>
jmp r11This keeps syscall dispatch compact and reuses the host module's existing instruction sequence.
The public API is built around variadic templates:
template<typename Ret, typename... Args>
inline Ret invoke(const std::string& syscall_name, Args... args)The wrapper resolves the syscall, obtains the correct generated stub, casts it to the requested function signature, then forwards the arguments:
using func_t = Ret(NTAPI*)(Args...);
func_t fn = reinterpret_cast<func_t>(stub);
return fn(std::forward<Args>(args)...);The caller only supplies the return type, Native API name, and normal arguments.
On Windows x64:
- arguments 1-4 are passed in
rcx,rdx,r8, andr9, - additional arguments are passed on the stack,
- each call reserves shadow space,
- stack alignment must remain valid across the call boundary.
The normal path relies on the compiler-generated call sequence for the typed function pointer.
The spoofed x64 path additionally reserves a temporary stack area, copies stack arguments into a new home area, places fake return anchors, performs the syscall gadget call, restores the original return address, then returns to the caller.
create_spoof_config creates a stack_spoof_config containing return anchors. invoke_spoofed uses that config to prepare a stack layout before dispatching the syscall.
Conceptual layout:
higher addresses
caller frames
fake return anchor #2
fake return anchor #1
copied stack arguments
shadow / temporary stack area.
syscall; ret gadget call
lower addresses
The x64 assembly implementation lives in src/image_stub.asm.
Public declarations and template wrappers.
Important structures:
struct syscall_entry {
std::string name;
uint32_t number;
void* gadget_address;
void* stub_normal;
};syscall_entry stores one resolved Native API entry.
struct stack_spoof_config {
void* fake_return_1 = nullptr;
void* fake_return_2 = nullptr;
void* fake_return_3 = nullptr;
bool enabled = false;
};stack_spoof_config controls spoofed invocation.
Primary functions:
bool initialize();
bool get_syscall_number(const std::string& function_name, uint32_t& out_syscall_number);
bool get_gadget_address(const std::string& function_name, void*& out_gadget_address);
stack_spoof_config create_spoof_config(void* ret1 = nullptr, void* ret2 = nullptr, void* ret3 = nullptr);Primary templates:
rop_syscall::invoke<Ret>(name, args...)
rop_syscall::invoke_spoofed<Ret>(name, config, args...)Implementation of parsing, lookup, stub generation, and spoof config creation.
Main flow:
initialize()calls the internal extraction routine.extract_syscalls()readsntdll.dlland parses PE exports.- Exported functions beginning with
4C 8B D1 B8are treated as syscall stubs. - The immediate after
B8is stored as the syscall number. find_syscall_gadget()locates0F 05 C3in the loadedntdll.dllimage.get_or_build_normal_stub()returns a cached generated stub.build_spoofed_stub()prepares the shared image stub context for spoofed calls.
Important byte signatures:
4C 8B D1 B8 mov r10, rcx; mov eax, imm32
0F 05 C3 syscall; ret
x64 MASM stub used for spoofed calls.
Core behavior:
sub rsp, 0B8h
; copy stack arguments into the reserved home area
; place fake return anchors
mov r10, rcx
mov eax, dword ptr [g_image_stub_ctx + 0]
mov r11, qword ptr [g_image_stub_ctx + 8]
call r11
; restore caller return address
add rsp, 0B8h
retThis is where automatic stack space allocation and stack argument copying happen for spoofed invocation.
Small demonstration program that exercises both direct and spoofed calls. It prints values returned by local Native API calls so users can verify the library behavior without additional setup.
Use the static library project:
lib/rop_syscall_kit.vcxproj
Or include these files in another MSBuild project:
include/rop_syscall/syscall.hpp
include/rop_syscall/shared.hpp
src/syscall.cpp
src/image_stub.asm
Then include the public header:
#include <rop_syscall/syscall.hpp>Initialize once:
if (!rop_syscall::initialize()) {
return 1;
}Call Native APIs by name:
uint32_t number = 0;
if (rop_syscall::get_syscall_number("NtClose", number)) {
// number contains the current host's syscall number.
}This project is released under the MIT License. See LICENSE.
Copyright (c) 2026 Fadouse