Skip to main content

Overview

This guide walks you through the basic usage patterns of syscalls-cpp, from initialization to invoking syscalls with proper error handling.

Quick Start Example

Here’s a complete example that allocates memory using NtAllocateVirtualMemory:
#include <iostream>
#include <syscalls-cpp/syscall.hpp>

int main() 
{
    syscall::Manager<syscall::policies::allocator::section, 
                     syscall::policies::generator::direct> syscallManager;
    
    if (!syscallManager.initialize())
    {
        std::cerr << "initialization failed!\n";
        return 1;
    }

    PVOID pBaseAddress = nullptr;
    SIZE_T uSize = 0x1000;

    std::ignore = syscallManager.invoke<NTSTATUS>(
        SYSCALL_ID("NtAllocateVirtualMemory"),
        syscall::native::getCurrentProcess(),
        &pBaseAddress,
        0, &uSize,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE
    );

    if (pBaseAddress)
        std::cout << "allocation successful at 0x" << pBaseAddress << std::endl;

    return 0;
}
Source: examples/basic-usage.cpp:3-28

Step-by-Step Breakdown

1. Include the Header

#include <syscalls-cpp/syscall.hpp>
This single header provides access to all syscalls-cpp functionality.

2. Create a Manager Instance

syscall::Manager<syscall::policies::allocator::section, 
                 syscall::policies::generator::direct> syscallManager;
The Manager is a template class that takes two policy parameters:
  • Allocation Policy: How memory for syscall stubs is allocated (e.g., section, heap, memory)
  • Stub Generation Policy: How syscall stubs are generated (e.g., direct, gadget, exception)
For most use cases, the combination of allocator::section with generator::direct provides a good balance of security and simplicity.

3. Initialize the Manager

if (!syscallManager.initialize())
{
    std::cerr << "initialization failed!\n";
    return 1;
}
Initialization performs several critical tasks:
  • Parses ntdll.dll to resolve syscall numbers
  • Allocates memory for syscall stubs
  • Generates the actual stub code
Always check the return value of initialize(). If it returns false, the manager cannot be used and syscall invocations will fail.

Parsing Multiple Modules

By default, only ntdll.dll is parsed. You can specify additional modules:
if (!syscallManager.initialize({ 
    SYSCALL_ID("ntdll.dll"), 
    SYSCALL_ID("win32u.dll") 
}))
{
    // Handle initialization failure
}
Source: include/syscalls-cpp/syscall.hpp:714

4. Invoke Syscalls

NTSTATUS status = syscallManager.invoke<NTSTATUS>(
    SYSCALL_ID("NtAllocateVirtualMemory"),
    syscall::native::getCurrentProcess(),
    &pBaseAddress,
    0, &uSize,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_READWRITE
);
The invoke method is a variadic template that:
  • Takes the syscall name as a hashed identifier
  • Accepts all syscall arguments
  • Returns the specified return type

Return Type Specification

The template parameter specifies the return type:
// For syscalls returning NTSTATUS
NTSTATUS status = syscallManager.invoke<NTSTATUS>(...);

// For syscalls returning pointers
PVOID result = syscallManager.invoke<PVOID>(...);

// For syscalls returning integers
ULONG value = syscallManager.invoke<ULONG>(...);

5. Check Results

if (pBaseAddress)
    std::cout << "allocation successful at 0x" << pBaseAddress << std::endl;
Always verify the results of your syscalls. For NTSTATUS return values, use the NT_SUCCESS() macro:
NTSTATUS status = syscallManager.invoke<NTSTATUS>(...);
if (NT_SUCCESS(status))
{
    // Success
}
else
{
    // Handle error
}

Common Use Cases

Memory Allocation

PVOID pBaseAddress = nullptr;
SIZE_T uSize = 0x1000;

NTSTATUS status = syscallManager.invoke<NTSTATUS>(
    SYSCALL_ID("NtAllocateVirtualMemory"),
    syscall::native::getCurrentProcess(),
    &pBaseAddress,
    0,
    &uSize,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_READWRITE
);

Process Enumeration

PVOID pBuffer = nullptr;
ULONG uReturnLength = 0;

// First call to get required size
NTSTATUS status = syscallManager.invoke<NTSTATUS>(
    SYSCALL_ID("NtQuerySystemInformation"),
    SystemProcessInformation,
    pBuffer,
    0,
    &uReturnLength
);

if (status == STATUS_INFO_LENGTH_MISMATCH)
{
    // Allocate buffer and call again
    pBuffer = malloc(uReturnLength);
    status = syscallManager.invoke<NTSTATUS>(
        SYSCALL_ID("NtQuerySystemInformation"),
        SystemProcessInformation,
        pBuffer,
        uReturnLength,
        &uReturnLength
    );
}

File Operations

HANDLE hFile = nullptr;
IO_STATUS_BLOCK ioStatus = {};
OBJECT_ATTRIBUTES objAttr = {};
UNICODE_STRING fileName = {};

// Initialize structures...

NTSTATUS status = syscallManager.invoke<NTSTATUS>(
    SYSCALL_ID("NtCreateFile"),
    &hFile,
    FILE_GENERIC_READ,
    &objAttr,
    &ioStatus,
    nullptr,
    FILE_ATTRIBUTE_NORMAL,
    FILE_SHARE_READ,
    FILE_OPEN,
    FILE_SYNCHRONOUS_IO_NONALERT,
    nullptr,
    0
);

Error Handling

The library provides automatic error handling in several ways:

Failed Initialization

If the manager fails to initialize, invoke() will automatically attempt reinitialization once:
if (!m_bInitialized)
{
    if (!initialize())
    {
        if constexpr (std::is_same_v<Ret, NTSTATUS>)
            return native::STATUS_UNSUCCESSFUL;
        return Ret{};
    }
}
Source: include/syscalls-cpp/syscall.hpp:774-783

Syscall Not Found

If a requested syscall is not found in the parsed modules:
if (it == m_vecParsedSyscalls.end() || it->m_key != syscallId)
{
    if constexpr (std::is_same_v<Ret, NTSTATUS>)
        return native::STATUS_PROCEDURE_NOT_FOUND;
    return Ret{};
}
Source: include/syscalls-cpp/syscall.hpp:786-792

Type Safety with SYSCALL_ID

The SYSCALL_ID macro ensures type safety and enables compile-time optimizations:
// Compile-time hash (recommended)
syscallManager.invoke<NTSTATUS>(
    SYSCALL_ID("NtAllocateVirtualMemory"),
    ...
);

// Runtime hash (when string is dynamic)
const char* syscallName = getSyscallName();
syscallManager.invoke<NTSTATUS>(
    SYSCALL_ID_RT(syscallName),
    ...
);
When SYSCALLS_NO_HASH is defined, SYSCALL_ID uses std::string instead of compile-time hashes for easier debugging.

Best Practices

Always Check Initialization

Never skip checking the return value of initialize(). A failed initialization means all subsequent invocations will fail.

Use nullptr, Not NULL

On x64 platforms, always use nullptr instead of NULL to avoid stack corruption. See Security Considerations.

Handle Return Values

Always check NTSTATUS return values using NT_SUCCESS() or compare against specific error codes.

Reuse Managers

Create one manager instance and reuse it. Initialization is expensive, but invocation is fast.

Next Steps

Custom Policies

Learn how to write custom allocation, generation, and parsing policies

Debugging

Techniques for troubleshooting and verifying syscall behavior

Security Considerations

Critical security best practices and threat model considerations

Build docs developers (and LLMs) love