CSGOCheatBase: Anatomy of an Internal Cheat

CSGOCheatBase is a C++ cheat framework built as a DLL that demonstrates handle hijacking, native NT API memory operations, and an ImGui-based overlay. Despite its name, the codebase actually targets ac_client.exe (AssaultCube) rather than CS:GO, making it a general-purpose cheat base that could be adapted to any game. This is a historical, deprecated project. CS:GO itself was replaced by CS2 in September 2023 [1], and this codebase dates to roughly 2022.

This post walks through each layer: the DLL entry point, the handle hijacking system that avoids calling OpenProcess directly, the native NT API wrappers for reading and writing memory, the memory utility class, and the DirectX 11 + ImGui rendering pipeline.

DLL Entry and Thread Bootstrapping

The project compiles as a DynamicLibrary (DLL), confirmed by the .vcxproj configuration type. When injected into a host process, the standard DllMain entry point fires on DLL_PROCESS_ATTACH and spawns the main logic on a separate thread:

// From: CheatBase/dllmain.cpp
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)MainThread, hModule, 0, nullptr);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

DllMain must return quickly [2]. Long-running work inside DllMain can cause deadlocks because the OS loader lock is held during the call. Spawning a thread and returning immediately is the standard pattern for internal cheats.

The MainThread function allocates a console for debug output, locates the target process, hijacks a handle to it, and then starts the UI render loop on yet another thread:

// From: CheatBase/dllmain.cpp
DWORD WINAPI MainThread(HMODULE hModule) {
    AllocConsole();
    FILE* f;
    freopen_s(&f, "CONOUT$", "w", stdout);
    std::vector<DWORD> possibleSources;
    do {
        possibleSources = GetALlPID("discord.exe");
    } while (possibleSources.size() == 0);

    const char* target = "ac_client.exe";
    // ...
    DWORD pid = GetPID(target);
    // ...
    HANDLE myHandle = NULL;
    myHandle = HijackExistingHandle(pid, possibleSources);
    // ...
    CreateThread(nullptr, NULL, (LPTHREAD_START_ROUTINE)UI::Render, nullptr, NULL, nullptr);

    while (globals::active) {
        if (GetAsyncKeyState(VK_END) & 1) {
            globals::active = false;
        }
    }
    // ...
    FreeLibraryAndExitThread(hModule, 0);
    return 0;
}

A few things stand out. The cheat polls for discord.exe PIDs in a spin loop, waiting until at least one Discord process is running. These PIDs serve as “source” processes for the handle hijacking step, which is covered next. The main thread then monitors the VK_END key as a kill switch. When pressed, it waits for the render thread to finish, closes the hijacked handle, and calls FreeLibraryAndExitThread to unload the DLL cleanly from the host process [3].

Handle Hijacking: Borrowing Another Process’s Access

The most technically interesting part of this codebase is its handle hijacking system. Rather than calling OpenProcess on the target (which anti-cheats can monitor [4]), the cheat finds a handle that another legitimate process already holds to the target and duplicates it into its own process.

The idea, credited to Apxaey in the source comments, is to inject the cheat DLL into a legitimate program like Discord. Discord may already hold handles to various processes for its overlay, game detection, or Rich Presence features. By duplicating one of those existing handles, the cheat gets access to the target process without ever creating a suspicious new handle.

The NT API Foundation

Before any handle work begins, the globals module resolves six undocumented NT functions from ntdll.dll at load time:

// From: CheatBase/globals.cpp
HMODULE globals::Ntdll = GetModuleHandleA("ntdll");
_RtlAdjustPrivilege globals::RtlAdjustPrivilege =
    (_RtlAdjustPrivilege)GetProcAddress(globals::Ntdll, "RtlAdjustPrivilege");
_NtQuerySystemInformation globals::NtQuerySystemInformation =
    (_NtQuerySystemInformation)GetProcAddress(globals::Ntdll, "NtQuerySystemInformation");
_NtDuplicateObject globals::NtDuplicateObject =
    (_NtDuplicateObject)GetProcAddress(globals::Ntdll, "NtDuplicateObject");
_NtOpenProcess globals::NtOpenProcess =
    (_NtOpenProcess)GetProcAddress(globals::Ntdll, "NtOpenProcess");
_NtReadVirtualMemory globals::NtReadVirtualMemory =
    (_NtReadVirtualMemory)GetProcAddress(globals::Ntdll, "NtReadVirtualMemory");
_NtWriteVirtualMemory globals::NtWriteVirtualMemory =
    (_NtWriteVirtualMemory)GetProcAddress(globals::Ntdll, "NtWriteVirtualMemory");

Using native NT functions instead of their Win32 wrappers (ReadProcessMemory, WriteProcessMemory, etc.) is a common anti-cheat evasion technique. The Win32 functions are thin wrappers around these NT syscalls [5], and anti-cheats often hook the Win32 layer. Calling the NT functions directly can bypass usermode hooks placed on the higher-level API. Peregrine’s MinHook-based interception hooks both the Win32 and NT layers to catch exactly this pattern.

Each function is resolved via GetProcAddress and cast to a manually defined function pointer type. The type definitions in handle.h mirror the actual NT function signatures:

// From: CheatBase/handle.h
typedef NTSTATUS(NTAPI* _NtReadVirtualMemory)(
    HANDLE               ProcessHandle,
    PVOID                BaseAddress,
    PVOID               Buffer,
    ULONG                NumberOfBytesToRead,
    PULONG              NumberOfBytesReaded
    );

typedef NTSTATUS(NTAPI* _NtDuplicateObject)(
    HANDLE SourceProcessHandle,
    HANDLE SourceHandle,
    HANDLE TargetProcessHandle,
    PHANDLE TargetHandle,
    ACCESS_MASK DesiredAccess,
    ULONG Attributes,
    ULONG Options
    );

Enumerating Every Handle on the System

The hijacking function starts by enabling SeDebugPrivilege via RtlAdjustPrivilege [6]. This privilege allows the process to open handles to any other process, including SYSTEM-level processes:

// From: CheatBase/handle.cpp
boolean OldPriv;
globals::RtlAdjustPrivilege(SeDebugPriv, TRUE, FALSE, &OldPriv);

The constant SeDebugPriv is defined as 20 in handle.h, matching the privilege number for SE_DEBUG_NAME [7].

Next, the cheat queries every open handle on the system using NtQuerySystemInformation with the SystemHandleInformation class (value 16). Because the total size of handle data is unknown ahead of time, the code uses a growing allocation pattern:

// From: CheatBase/handle.cpp
DWORD size = sizeof(SYSTEM_HANDLE_INFORMATION);
hInfo = (SYSTEM_HANDLE_INFORMATION*) new byte[size];
ZeroMemory(hInfo, size);
NTSTATUS NtRet = NULL;

do
{
    delete[] hInfo;
    size *= 1.5;
    try
    {
        hInfo = (PSYSTEM_HANDLE_INFORMATION) new byte[size];
    }
    catch (std::bad_alloc)
    {
        CleanUpAndExit(const_cast<LPSTR>("Bad Heap Allocation"), hInfo, &procHandle);
    }
    Sleep(1);
} while ((NtRet = globals::NtQuerySystemInformation(
    SystemHandleInformation, hInfo, size, NULL)) == STATUS_INFO_LENGTH_MISMATCH);

The STATUS_INFO_LENGTH_MISMATCH return code means the buffer was too small. The code increases the allocation by 50% each iteration and retries until the entire handle table fits. On a typical Windows system, tens of thousands of handles may be open at any given time [8].

Filtering and Duplicating the Target Handle

With the full handle table in memory, the code iterates through every entry looking for a process handle (type 0x7) that belongs to one of the Discord PIDs and points to the target process:

// From: CheatBase/handle.cpp
for (unsigned int i = 0; i < hInfo->HandleCount; ++i)
{
    if (!IsHandleValid((HANDLE)hInfo->Handles[i].Handle))
        continue;

    if (hInfo->Handles[i].ObjectTypeNumber != ProcessHandleType)
        continue;

    clientID.UniqueProcess = (DWORD*)hInfo->Handles[i].ProcessId;

    // check if pid of procHandle is in sources
    if (std::find(sources.begin(), sources.end(),
        (DWORD)clientID.UniqueProcess) == sources.end())
        continue;

    procHandle = OpenProcess(PROCESS_DUP_HANDLE, false, (DWORD)clientID.UniqueProcess);
    // ...

    NtRet = globals::NtDuplicateObject(procHandle,
        (HANDLE)hInfo->Handles[i].Handle,
        NtCurrentProcess, &HijackedHandle,
        PROCESS_ALL_ACCESS, 0, 0);
    // ...

    if (GetProcessId(HijackedHandle) != dwTargetProcessId) {
        CloseHandle(HijackedHandle);
        continue;
    }

    hProcess = HijackedHandle;
    break;
}

The filtering logic works in three stages:

  1. Type check: Only handles with ObjectTypeNumber == 0x7 are considered. This filters out file handles, registry key handles, and everything else that is not a process handle.
  2. Source check: The handle’s owning process must be one of the Discord PIDs found earlier.
  3. Target verification: After duplicating the handle with PROCESS_ALL_ACCESS, the code calls GetProcessId to confirm the duplicated handle actually points to the target process.

NtDuplicateObject is the key call. It takes a handle from the source process and creates a duplicate in the current process (NtCurrentProcess, defined as (HANDLE)(LONG_PTR) -1) with whatever access rights are requested [9]. By requesting PROCESS_ALL_ACCESS, the cheat gets full read/write/execute permissions to the target.

The handle table entry structures are defined manually in handle.h since they are not part of the public Windows SDK:

// From: CheatBase/handle.h
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
    ULONG ProcessId;
    BYTE ObjectTypeNumber;
    BYTE Flags;
    USHORT Handle;
    PVOID Object;
    ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, * PSYSTEM_HANDLE;

typedef struct _SYSTEM_HANDLE_INFORMATION
{
    ULONG HandleCount;
    SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;

The Handles[1] member is a common C pattern for a variable-length array at the end of a struct [10]. The actual array extends beyond the struct boundary into the heap allocation.

Memory Read/Write via Native APIs

With the hijacked handle in hand, the Mem class provides a set of utility functions for reading and writing the target process’s memory. All operations go through the native NT functions resolved earlier:

// From: CheatBase/mem.cpp
void Mem::writeMemory(uintptr_t address, void* value, size_t size)
{
    globals::NtWriteVirtualMemory(handle, (void*)address, value, size, nullptr);
}

bool Mem::readMemory(uintptr_t address, void* buffer, size_t size)
{
    auto res = globals::NtReadVirtualMemory(handle, (void*)address, buffer, size, nullptr);
    return res == 0x00000000;
}

The return value 0x00000000 corresponds to STATUS_SUCCESS [11]. A nonzero result means the read failed, likely due to an invalid address or insufficient permissions.

Pointer Chain Resolution

Game cheats commonly need to follow chains of pointers through memory to reach dynamic game state. The getAddress method walks a multi-level pointer chain:

// From: CheatBase/mem.cpp
DWORD Mem::getAddress(DWORD addr, std::vector<DWORD> vect)
{
    for (unsigned int i = 0; i < vect.size(); i++)
    {
        globals::NtReadVirtualMemory(handle, (BYTE*)addr, &addr, sizeof(addr), 0);
        addr += vect[i];
    }
    return addr;
}

This is a standard multi-level pointer dereference. Given a base address and a vector of offsets, it reads the value at each level, adds the next offset, and continues. For example, to reach a player’s health stored at [[base + 0x10] + 0x100] + 0x4, the caller would pass base + 0x10 and offsets {0x100, 0x4}. This pattern is used in nearly every game cheat because games store entities in heap-allocated, pointer-linked structures where addresses change each run [12].

Memory Patching and NOP Slides

Two additional methods support code patching in the target process:

// From: CheatBase/mem.cpp
void Mem::PatchEx(BYTE* dst, BYTE* src, unsigned int size)
{
    DWORD oldprotect;
    VirtualProtectEx(handle, dst, size, PAGE_EXECUTE_READWRITE, &oldprotect);
    globals::NtWriteVirtualMemory(handle, dst, src, size, nullptr);
    VirtualProtectEx(handle, dst, size, oldprotect, &oldprotect);
}

void Mem::NopEx(BYTE* dst, unsigned int size)
{
    BYTE* nopArray = new BYTE[size];
    memset(nopArray, 0x90, size);
    PatchEx(dst, nopArray, size);
    delete[] nopArray;
}

PatchEx temporarily changes the target memory’s page protection to PAGE_EXECUTE_READWRITE [13], writes the new bytes, then restores the original protection. This is necessary because code sections are normally mapped as read-execute only.

NopEx fills a region with 0x90 bytes (the x86 NOP instruction [14]) and uses PatchEx to write them. This is a classic technique for disabling game checks: find the instruction that validates something (recoil, damage falloff, visibility checks) and replace it with NOPs so the check never executes.

The ImGui Overlay: DirectX 11 Rendering

The UI layer creates a standalone DirectX 11 window with ImGui for rendering the cheat menu. The UI::Render method sets up the entire rendering pipeline:

// From: CheatBase/UI.cpp
void UI::Render()
{
    ImGui_ImplWin32_EnableDpiAwareness();
    const WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, WndProc, 0L, 0L,
        GetModuleHandle(nullptr), nullptr, nullptr, nullptr, nullptr,
        _T("ImGui Standalone"), nullptr };
    ::RegisterClassEx(&wc);
    const HWND hwnd = ::CreateWindow(wc.lpszClassName, _T("ImGui Standalone"),
        WS_OVERLAPPEDWINDOW, 100, 100, 50, 50, NULL, NULL, wc.hInstance, NULL);

    if (!CreateDeviceD3D(hwnd))
    {
        CleanupDeviceD3D();
        ::UnregisterClass(wc.lpszClassName, wc.hInstance);
        return;
    }

    ::ShowWindow(hwnd, SW_HIDE);
    ::UpdateWindow(hwnd);
    // ...
}

The window is created with WS_OVERLAPPEDWINDOW but immediately hidden with SW_HIDE. ImGui’s viewport system (enabled via ImGuiConfigFlags_ViewportsEnable) creates its own platform windows for rendering, so the initial window serves only as a device context anchor.

The DirectX 11 device and swap chain are created with standard parameters:

// From: CheatBase/UI.cpp
bool UI::CreateDeviceD3D(HWND hWnd)
{
    DXGI_SWAP_CHAIN_DESC sd;
    ZeroMemory(&sd, sizeof(sd));
    sd.BufferCount = 2;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.OutputWindow = hWnd;
    sd.SampleDesc.Count = 1;
    sd.Windowed = TRUE;
    sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
    // ...
    D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr,
        createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION,
        &sd, &pSwapChain, &pd3dDevice, &featureLevel, &pd3dDeviceContext);
    // ...
}

The render loop itself is a standard ImGui frame cycle. Each frame begins a new ImGui frame, calls Drawing::Draw() for the actual menu content, and presents through the swap chain:

// From: CheatBase/UI.cpp (render loop, trimmed)
while (globals::active)
{
    // ...
    ImGui_ImplDX11_NewFrame();
    ImGui_ImplWin32_NewFrame();
    ImGui::NewFrame();
    {
        Drawing::Draw();
    }
    ImGui::EndFrame();

    ImGui::Render();
    // ...
    pSwapChain->Present(1, 0);
    // ...
}

The Drawing::Draw() method in this base is a placeholder that renders a single text label:

// From: CheatBase/drawing.cpp
void Drawing::Draw()
{
    if (isActive())
    {
        ImGui::SetNextWindowSize(vWindowSize);
        ImGui::SetNextWindowBgAlpha(1.0f);
        ImGui::Begin(lpWindowName, &bDraw, WindowFlags);
        {
            ImGui::Text("Test");
        }
        ImGui::End();
    }

#ifdef _WINDLL
    if (GetAsyncKeyState(VK_INSERT) & 1)
        bDraw = !bDraw;
#endif
}

The VK_INSERT key toggles the menu visibility, which is the conventional toggle key in game cheats. The _WINDLL preprocessor guard ensures this toggle logic only compiles in the DLL build, not in a standalone executable build (the code supports both via the #ifndef _WINDLL check in the render loop).

The DPI-aware font scaling is a nice touch for usability:

// From: CheatBase/UI.cpp
const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
MONITORINFO info = {};
info.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(monitor, &info);
const int monitor_height = info.rcMonitor.bottom - info.rcMonitor.top;

if (monitor_height > 1080)
{
    const float fScale = 2.0f;
    ImFontConfig cfg;
    cfg.SizePixels = 13 * fScale;
    ImGui::GetIO().Fonts->AddFontDefault(&cfg);
}

On monitors taller than 1080p, fonts are scaled up by 2x to remain readable.

What This Base Does Not Include

It is worth noting what CSGOCheatBase does not contain. Despite its name suggesting a CS:GO cheat, the codebase is a framework, not a finished cheat. There is:

  • No ESP or wallhack. The Drawing class renders a placeholder “Test” label. There is no world-to-screen projection, no player iteration, no bounding box drawing.
  • No aimbot. There is no angle calculation, no target selection, no recoil compensation.
  • No game SDK structures. There are no player entity classes, no bone matrices, no weapon definitions.
  • No offset table. The Mem class provides the pointer-chain walking infrastructure, but no actual game offsets are defined.

The value of this project is in the infrastructure: the handle hijacking bypass, the native API memory layer, and the ImGui rendering scaffold. A developer would extend this by adding game-specific offsets, entity structures, and drawing logic into the Drawing::Draw() method.

Honest Assessment

The codebase has some rough edges typical of educational/hobbyist game hacking projects:

  • The DllMain switch statement has a missing break on the DLL_PROCESS_ATTACH case, causing fall-through into DLL_THREAD_ATTACH. This has no practical effect since those cases do nothing, but it is technically a bug.
  • The GetPID function does not initialize procEntry.dwSize before calling Process32Next, which means the first call will fail. Only szExeFile is zeroed. The function also has unreachable code: CloseHandle(hSnap) appears after return.
  • The handle hijacking technique relies on Discord holding an open handle to the target game, which is not guaranteed and depends on Discord’s overlay or Rich Presence being active.
  • The 32-bit pointer types (DWORD) in Mem::getAddress limit this to 32-bit target processes. A 64-bit game would require uintptr_t throughout.
  • NtWriteVirtualMemory and NtReadVirtualMemory bypass usermode hooks but not kernel-level protections. Any anti-cheat with a kernel driver (EasyAntiCheat, BattlEye, Vanguard) would detect this through ObCallbacks or similar kernel instrumentation [15]. For a kernel-level approach to cross-process memory access that bypasses these protections entirely, see the Medusa kernel driver.

As a learning resource for understanding handle manipulation, NT native APIs, and ImGui overlay setup, the project demonstrates the core patterns. As a practical bypass for modern anti-cheat systems, it would be insufficient.


This post was generated by an LLM based on code from CSGOCheatBase. All code snippets are from the actual repository.

References

  1. Valve, “Moving from CS:GO to CS2,” Counter-Strike Blog, September 2023
  2. Microsoft, “DllMain entry point,” Windows SDK Documentation
  3. Microsoft, “FreeLibraryAndExitThread function,” Windows SDK Documentation
  4. Microsoft, “OpenProcess function,” Windows SDK Documentation
  5. Microsoft, “Windows System Calls,” Windows Internals, 7th Edition
  6. Microsoft, “RtlAdjustPrivilege (undocumented),” ntdll.dll
  7. Microsoft, “Privilege Constants,” Windows SDK Documentation
  8. Microsoft, “NtQuerySystemInformation function,” Windows SDK Documentation
  9. Microsoft, “NtDuplicateObject (undocumented),” ntdll.dll
  10. ISO/IEC 9899:2011, Section 6.7.2.1, “Structure and union specifiers” (flexible array members)
  11. Microsoft, “NTSTATUS Values,” Windows SDK Documentation
  12. Guided Hacking, “Multi-Level Pointers in Game Hacking”
  13. Microsoft, “VirtualProtectEx function,” Windows SDK Documentation
  14. Intel, “NOP instruction,” Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 2
  15. Microsoft, “ObRegisterCallbacks function,” Windows Driver Kit Documentation