Blog / reverse-engineering · · 11 min read · Signed KyTech Research

Reverse Engineering Game Binaries: Offsets, RTTI, and Named-String Global-Singleton Discovery

A working primer on the toolchain, C++ vtable recovery, and the patch-day workflows that keep cheat offsets alive.

Every cheat, trainer, and ESP overlay starts the same way: someone opens a stripped 200MB PE in a disassembler and stares. The game ships without symbols, the class names are gone, the strings are half-obfuscated, and the entry point is buried under CRT init, Steam DRM, and whatever anti-tamper wrapper this quarter's engine chose. The job of the reverser is to turn that opaque blob back into a mental model of the engine: where the local player pointer lives, which vtable holds GetOrigin, which global holds the view matrix, and which of a thousand identical mov rax, [rcx+0x1E8] instructions is the one that matters this patch.

This is a working primer on the toolchain and techniques used to reverse engineer a game binary for cheat development. It focuses on the parts of the pipeline that survive contact with real 2026 shipping titles: RTTI recovery on MSVC-compiled C++, discovering global singletons through named-string cross-references, and the version-diffing workflow that keeps an offset database current on patch days.

The static analysis toolchain

Three disassemblers matter. IDA Pro from Hex-Rays is still the industry default. Its decompiler produces the best commercial C-like output for x86-64, its FLIRT signatures cover most of the MSVC and LLVM runtime, and IDAPython 3 is the de facto scripting language for anything nontrivial. It is also EUR 2,000+ per seat per year for the Pro edition with the x64 decompiler, which is why the free tier of the field runs on Ghidra.

Ghidra, released by the NSA in 2019, is the only free decompiler that competes with Hex-Rays on quality. Its P-code intermediate representation is genuinely nice to work with in scripts, the type manager is arguably better than IDA's, and headless mode makes automation trivial. It is slower on cold-open of a large PE and its GUI is Java Swing, which some reversers find aesthetically offensive but which works.

Binary Ninja from Vector 35 sits in the middle: cheaper than IDA, faster than Ghidra, with the best-in-class MLIL/HLIL IR for people who actually want to write analyses. Its market share among game reversers is small but growing.

For a first look at a shipping game, the disassembly graph view is where you live. Reversers memorize the shape of common patterns: cache miss around a virtual call, prologue with sub rsp, N and register spills, the tell-tale test rax, rax / jz after every operator new. A typical function graph in IDA looks something like this:

+-------------------------+
| loc_140A31C20:          |
|   sub  rsp, 28h         |
|   mov  rbx, [rcx+1E8h]  |
|   test rbx, rbx         |
|   jz   short loc_140... |
+-----------+-------------+
            |
     +------+------+
     |             |
     v             v
+----+----+   +----+---------+
| non-null|   | null branch  |
|   path  |   |   xor  eax, eax
|         |   |   jmp  epilog
+----+----+   +--------------+
     |
     v
+----+------------------+
|   mov  rax,[rbx]      |  ; vtable load
|   call qword ptr [rax]|  ; virtual call
+-----------------------+

That shape (dereference, null check, load vtable, indirect call) is the signature of a virtual method dispatch, and finding one is often the first foothold into a class you care about.

Dynamic analysis: x64dbg, WinDbg, Cheat Engine

Static analysis alone will not tell you which entity in a 4,000-slot pool is the local player. For live inspection, x64dbg is the standard usermode debugger for anti-tamper-heavy targets. It has scriptable conditional breakpoints, a plugin ecosystem (ScyllaHide, xAnalyzer), and a UI that does not fight you when the target loads a hundred DLLs. Its main weakness is anti-debug: any game shipping with a serious protector (BattlEye, EAC, Vanguard, Byfron) will detect a naive attach immediately. If you want the deeper story on how those protectors spot memory reads, our signature-scanning breakdown covers what they hunt for and why static pattern hunts still work on non-obfuscated modules.

WinDbg is the correct tool for kernel-mode work. Analyzing a signed kernel driver, chasing an IOCTL handler in a signed-driver-abuse chain, or reading a full memory dump from an anti-cheat crash all live in WinDbg or its newer WinDbg (previously WinDbg Preview) shell. Microsoft's kernel-mode debugging documentation is the canonical reference and is genuinely useful, not marketing filler.

Cheat Engine deserves a note only because every article about reverse engineering games mentions it. It is a fast memory scanner and its pointer-scan feature is fine for prototyping. It is not a serious reverse engineering tool. Its assembler is buggy, its debugger-detection surface is enormous, and no shipping cheat KyTech has ever built used a CE table as anything but a scratchpad during initial recon. Use it for the first thirty minutes on a new target, then move to IDA and x64dbg.

RTTI recovery: why MSVC C++ leaks its own vtables

Nearly every AAA game engine in 2026 is MSVC-compiled C++ with RTTI enabled. Unreal, Source 2, IW engine, Frostbite: all of them ship dynamic_cast support and typeid operators in retail, because turning RTTI off breaks too many middleware components. That is a gift to reversers.

Every polymorphic class compiled by MSVC with RTTI on has a RTTICompleteObjectLocator sitting exactly sizeof(void*) bytes before each vtable. The layout, documented by countless RE blogs and matching the observable structure in every shipping game binary we have opened, is:

struct RTTICompleteObjectLocator {
    uint32_t signature;         // 0 for x86, 1 for x64
    uint32_t offset;            // offset of vtable in complete class
    uint32_t cdOffset;          // constructor displacement
    uint32_t pTypeDescriptor;   // RVA to TypeDescriptor
    uint32_t pClassHierarchy;   // RVA to ClassHierarchyDescriptor
    uint32_t pSelf;             // RVA to this COL (x64 only)
};

struct RTTITypeDescriptor {
    void*    pVFTable;          // vftable of type_info class
    void*    spare;
    char     name[];            // ".?AV<mangled_name>@@" e.g. ".?AVCBaseEntity@@"
};

The mangled .?AVFoo@@ string sits in .data and is trivially greppable. Cross-reference it once and you have the TypeDescriptor, cross-reference the TypeDescriptor and you have every RTTICompleteObjectLocator for that class, then step back one pointer width and you are staring at the vtable. From the vtable you can walk every virtual method for the class. This is the single most productive technique in modern C++ game RE and it works on every unstripped MSVC binary we have looked at, including current Apex, Overwatch 2, and Call of Duty builds. For a deeper example of applying this to a specific target, see our Apex Legends reversing writeup, which walks through the CPlayer vtable recovery step by step. The CS2 Source 2 post shows the same technique against Valve's rewritten engine, where RTTI is even more aggressively preserved than in the original Source 1 CS:GO builds.

Global-Singleton Discovery via Named-String Cross-References

The second high-value pattern is discovering global singletons through named-string cross-references. Most engines expose their major singletons (entity list, view render, input system, world) through globals that end up either at fixed offsets in .data or behind a handful of accessor functions. Frequently those globals are constructed in a function that logs an error message like "Failed to initialize CClientState" on failure, or writes its own class name into a debug channel on init.

The workflow: grep the .rdata section for suggestive strings, cross-reference each hit, look at the surrounding function for a mov [rip+disp], rax that stores into a global, and record the RVA. In IDAPython:

import idautils, idc, ida_bytes, ida_search

def find_string_xrefs(needle: str):
    """Yield (string_ea, xref_ea) for every .rdata hit of `needle`."""
    ea = 0
    while True:
        ea = ida_search.find_binary(ea + 1, idc.BADADDR,
                                    " ".join(f"{b:02X}" for b in needle.encode()),
                                    16, ida_search.SEARCH_DOWN)
        if ea == idc.BADADDR:
            return
        for x in idautils.XrefsTo(ea, 0):
            yield ea, x.frm

def pattern_scan(module_base: int, size: int, pattern: bytes, mask: str) -> int:
    """AoB scan. mask uses 'x' for match, '?' for wildcard."""
    for off in range(size - len(pattern)):
        for i, (b, m) in enumerate(zip(pattern, mask)):
            if m == 'x' and ida_bytes.get_byte(module_base + off + i) != b:
                break
        else:
            return module_base + off
    return idc.BADADDR

for s_ea, xref in find_string_xrefs("CClientState::Connect"):
    print(f"string @ {s_ea:X} referenced from {xref:X}")

That skeleton is what a first-pass sig generator looks like. Real production scripts add MSVC-specific handling for .?AV type descriptor mangling, RIP-relative displacement resolution for x64 mov instructions, and NOP-tolerant masks so the pattern survives compiler codegen changes across builds. The tradeoff between a hardcoded pattern that breaks weekly and a resolver that scans on load is covered in the polymorphic cheat builds post, which is why our production loaders lean on runtime resolvers over baked offsets.

Symbol recovery: PDBs when you can get them

Microsoft's public symbol server at https://msdl.microsoft.com/download/symbols ships PDBs for every Windows system binary, and you should always point WinDbg and IDA at it before touching kernel-related RE. That gets you full symbol coverage for ntoskrnl.exe, win32kbase.sys, every DirectX runtime DLL, and every C runtime the game statically links. On a modern Windows 11 24H2 build, that is a huge amount of context for free.

Game vendors almost never ship retail PDBs. There are exceptions worth remembering: Valve historically shipped stripped PDBs for the Source engine, some Unreal Engine 4 titles shipped debug builds by accident during early access, and a small number of anti-cheat vendors have leaked internal PDBs in past supply-chain incidents. Any leaked PDB is legally and ethically fraught, so we keep our workflows on clean-room recovery via RTTI and string mining.

Version diffing on patch day

Every game patch invalidates a fraction of your offsets. The question is which ones, and the answer comes from binary diffing. The two production tools are BinDiff (Zynamics, now Google, free since 2016) and Diaphora by Joxean Koret (open source, IDA-native, actively maintained). Both work by comparing function control-flow graphs across two IDBs and producing a mapping from old-function to new-function with a confidence score.

The typical KyTech patch-day pipeline runs something like this:

  1. Steam or Battle.net pushes an update. Our watcher hits within minutes.
  2. Both the pre-patch and post-patch binaries land in a build server.
  3. Headless IDA runs auto-analysis on the new binary. Diaphora exports diff data from both.
  4. Every offset in our internal database is tagged with the function it belongs to. The diffing tool maps old to new function addresses, and offsets that reference known-changed structs get flagged for manual review.
  5. A human reverser resolves anything the diff cannot map with high confidence, usually a few dozen entries out of thousands.
  6. The updated offset table ships to production.

The comparison of the main tools breaks down cleanly:

Tool License Strength Weakness
IDA Pro 9.x + Hex-Rays Commercial, EUR 2K+/seat Best decompiler, largest plugin ecosystem Cost, closed source
Ghidra 11.x Free, Apache 2.0 Excellent decompiler, headless mode, scriptable in Python and Java Slower cold start, Java UI
Binary Ninja Commercial, cheaper than IDA Best IR (MLIL/HLIL), fast reanalysis Smaller plugin ecosystem
BinDiff Free (Google) Cross-tool, mature graph matching Standalone workflow
Diaphora Free, open source Runs inside IDA, SQL-queryable results IDA-only
x64dbg Free, open source De facto usermode debugger No kernel support
WinDbg Free (Microsoft) Kernel debugging, dump analysis, MS symbols Learning curve

A C++ RTTI resolver worth carrying

Because RTTI recovery is the highest-ROI technique in modern game RE, it is worth having a runtime resolver in your cheat's own code so you can find vtables from inside the target process without pre-baking every offset. The following helper walks .data for RTTICompleteObjectLocator structures matching a class name, then returns the associated vtable:

#include <windows.h>
#include <cstdint>
#include <string_view>

static uintptr_t rva_to_va(HMODULE m, uint32_t rva) {
    return reinterpret_cast<uintptr_t>(m) + rva;
}

void* find_vtable_by_rtti(HMODULE module, std::string_view mangled_name) {
    auto* dos  = reinterpret_cast<IMAGE_DOS_HEADER*>(module);
    auto* nt   = reinterpret_cast<IMAGE_NT_HEADERS*>(
                     reinterpret_cast<uint8_t*>(module) + dos->e_lfanew);
    auto* sec  = IMAGE_FIRST_SECTION(nt);

    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++sec) {
        if (memcmp(sec->Name, ".data", 5) != 0) continue;

        auto base = reinterpret_cast<uint8_t*>(module) + sec->VirtualAddress;
        auto end  = base + sec->Misc.VirtualSize - sizeof(void*);

        for (auto p = base; p < end; p += sizeof(void*)) {
            auto col_rva = *reinterpret_cast<uint32_t*>(p);
            if (col_rva == 0) continue;

            auto* col = reinterpret_cast<uint32_t*>(
                rva_to_va(module, col_rva));
            if (col[0] != 1) continue;                    // x64 signature

            auto* td = reinterpret_cast<uint8_t*>(
                rva_to_va(module, col[3]));               // TypeDescriptor
            const char* name = reinterpret_cast<const char*>(td + 2 * sizeof(void*));
            if (mangled_name == name) {
                // The COL sits one pointer before the vtable.
                return p + sizeof(void*);
            }
        }
    }
    return nullptr;
}

Called with ".?AVCBaseEntity@@" against a shipping Source 2 build, that returns the CBaseEntity vftable in a millisecond or two. Combined with a small vtable-index table, it survives most patches without any hardcoded offset at all, which is exactly the resilience property you want on a game that updates weekly.

How KyTech handles this

KyTech was founded in 2025 by two engineers who spent the previous decade doing this work, and the reverse engineering pipeline described above is the same one that feeds our production cheats for Apex Legends, Counter-Strike 2, Overwatch 2, Call of Duty Black Ops 7, Forza Horizon 6, and Roblox. We maintain an internal offset database per game, per branch, per platform, and it is regenerated automatically on every patch via the Diaphora-driven diffing pipeline described in this article. Manual review still catches what the diff misses, but the majority of updates ship within hours of the game's own patch going live.

Our shared kernel-mode driver consumes those offsets to do the actual memory read and write work, which keeps every product on a consistent low-level base while the per-game logic stays small and auditable. The HWID spoofer is currently in Apex-only beta while we validate its behavior against the current EAC and Vanguard telemetry surfaces. The RTTI walker, global-singleton discovery scripts, and Diaphora diff pipeline in this post are the reference reverse engineering workflow behind KyTech Apex, which is where new vtable indices and offset revisions ship first before propagating to the rest of the catalog. You can see the full product lineup and current build status on our purchase page.

None of this is magic. It is IDA, Ghidra, WinDbg, a lot of Python, and the patience to run the same RTTI walker against a new build every Tuesday.

Signed by KyTech Research

We still play these games and we still push every build in production. If something in here is wrong, and eventually something will be, ping us in Discord and we will fix it.

Enough theory. get in.

The loader is one click away. Ring-0 kernel driver, polymorphic per download, memory-only injection. Six games across VAC, EAC, Ricochet, Byfron, and Warden.

Get in ›