Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Source engine reverse-engineering tutorial

Learn how Counter-Strike: Source fits together by building a small x86/x64 internal experiment DLL. This is a personal reverse-engineering and systems exercise, not a ready-to-use product.

Safety note: Use this only for source study and permitted, offline/local testing. There is no VAC bypass, anti-cheat defeat, injector, or online-testing workflow here. Do not load it into protected multiplayer or someone else's process.

Why this exists

I enjoy Source games and programming. C++ is not my favorite language, but it is the right tool for exploring client state, input, entities, and rendering at this level.

The practical goal is defensive understanding: learn what a client-side tool can read and change so I can better protect a large Counter-Strike: Source community. The code is intentionally small enough to follow while learning.

Why CS:S?

  • It is still played and interesting to study.
  • It is old enough that updates are relatively infrequent, so signatures and offsets remain useful for longer.
  • Its architecture is close enough to other Source-era games for research notes to transfer.
  • There are enough public write-ups and dumps to cross-check findings.

The first guide I used was Guided Hacking's beginner guide.

What works today

This is a teaching codebase tied to one Counter-Strike: Source client build. It builds architecture-specific DLLs with CMake and MSVC and contains these experiments:

Experiment Hook / input Implementation
Bunny hop CreateMove / hold Space features/bhop.cpp writes the client's force-jump command.
Aimbot CreateMove / hold LMB features/aimbot.cpp checks bone 14 and IEngineTrace visibility, then chooses the closest visible enemy; the candidate set is rebuilt every tick so dead or unreadable targets fall through to the next candidate.
Triggerbot CreateMove / hold Shift features/triggerbot.cpp uses the client-only crosshair target (m_iIDEntIndex); the x64 field is player + 0x1B20, but the x64 profile keeps the feature off until its complete input/target path is validated.
Command no-recoil CreateMove features/perfect_nospread.cpp composes the configured punch policy with aim and spread in fire space; features/norecoil.cpp supplies checked punch state.
Perfect no-spread CreateMove Replays the verified x64 CS polar cone and applies inverse command-angle compensation; default off because the feature remains build-specific and exploratory.
Silent angles CreateMove return value Prevents compensated command angles from being copied into the render camera.
Visual no-recoil ClientMode::OverrideView Subtracts punch from the completed camera view without changing player state.
ImGui menu EndScene + window procedure + VGUI_Surface030::LockCursor features/menu.cpp exposes runtime toggles and is opened with Insert; the cursor hook prevents the engine from re-locking the mouse while it is open.
Debug dump CreateMove / F1 Prints resolved state plus command, recoil, spread, and one-shot client fire-time diagnostics.

The x86 profile retains the legacy triggerbot experiment, while the x64 profile keeps triggerbot disabled and enables the aimbot after validating IClientNetworkable::IsDormant. Both profiles keep perfect no-spread off by default. The x64 C_BaseAnimating::SetupBones cache fields are reached through the renderable subobject: Ghidra shows [this + 0xB40]/[this + 0xB50], which translate to a matrix pointer at entity + 0xB48 and a count at entity + 0xB58 from the entity-list pointer. The corrected path has been runtime-validated on the current x64 build (count=50, readable and usable); the F1 dump continues to report the raw values and readability flags so future updates can be checked. The menu itself starts closed. MinHook installs CreateMove, ClientMode::OverrideView, VGUI_Surface030::LockCursor, and D3D9 EndScene. The window procedure is replaced separately so Insert works even when CreateMove is not running.

In-game controls

Key Action
INSERT Toggle the ImGui menu.
F1 Print the debug dump to the allocated console.
END Disable hooks, restore the window procedure, and unload the DLL.
Space Hold for bunny hop when enabled.
Shift Hold for triggerbot when enabled.
LMB Hold for the aimbot when enabled.

The menu is initialized on the first successful D3D9 EndScene call. If it is closed, the hook skips ImGui's NewFrame and render work; Insert is still handled by the window procedure. While the menu is open, the verified VGUI_Surface030::LockCursor hook substitutes UnlockCursor and an arrow cursor. The engine then observes the unlocked surface and deactivates first-person mouse recentering through its normal input path. This path has been runtime-validated on x64: the pointer moves freely as soon as the menu opens, without first opening the console or settings.

Runtime flow

The DLL keeps the entry point small and does the work on a worker thread:

DllMain (process attach)
  -> hooks::MainThread
       -> load signatures.ini beside the DLL and apply runtime settings
       -> resolve client, engine, trace, VGUI, RecvTable, and D3D9 objects
       -> install MinHook hooks
       -> run feature code from the appropriate callback

CreateMove          -> buttons, aim, command recoil/spread composition, F1 debug
OverrideView        -> read-only visual punch removal from the camera view
LockCursor          -> preserve an unlocked OS cursor while the menu is open
EndScene            -> ImGui frame/render when the menu is open
Window procedure     -> Insert toggle and ImGui input
END                 -> disable hooks, shut down ImGui, unload the DLL

Project layout

Nikooo777/
  dllmain.cpp              # DllMain only — starts the main thread
  core/                    # constants, padding macros, module bases
  config/                  # runtime INI loader
  memory/                  # pattern scanner (ScanModCombo, module size, …)
  math/                    # Vector3 (POD so it works in overlay unions)
  netvars/                 # runtime ClientClass/RecvTable/RecvProp resolver
  sdk/                     # Source-like types only (no feature logic)
    entity/                # CLocal, CBasePlayer, CCSPlayer
    user_cmd.h, client_*.h, engine_*.h, vgui_surface.h, view_setup.h, create_interface.*
  game/                    # live game access
    entity_list.*          # local player + IClientEntityList access
    interfaces.*           # interfaces, EngineClient / EngineTrace / ClientMode / ClientState resolve
    player.*               # IsAlive, IsEnemy, IsValidTarget, EyePosition
  hooks/                   # MinHook lifecycle + individual hooks + dummy D3D device
  features/                # gameplay logic + menu + config flags
imgui/                     # Dear ImGui + DX9 / Win32 backends
minhook/                   # vendored source, headers, license, and legacy x86 libs
config/                    # architecture-specific signatures, settings, provenance

Layer rules (keep the tutorial readable)

  1. sdk/ — memory layouts and interface stubs. No hooks, no features.
  2. game/ — how we find and read live objects (signatures, entity list).
  3. features/ — what we do with that data. Prefer game:: helpers over copy-pasted field checks.
  4. hooks/ — only place that installs MinHook / D3D and calls into features.
  5. dllmain.cpp — attach / detach only.

Networked entity members use runtime RecvTable accessors (DEFINE_NETVAR) so their displacements come from the loaded client metadata. Client-only fields are kept explicit only after they are verified for the selected architecture; the x64 crosshair target is the C_CSPlayer::GetIDTarget field at player + 0x1B20. View angles are read through the named VEngineClient interface at vtable slot 19 instead of a copied x64 ClientState overlay. Entity lookup and input buttons use the named interface/command sources documented in aidocs/003.

Where to add something new

Goal Place
New cheat feature features/foo.* → call from hooks/create_move.cpp (logic) or hooks/end_scene.cpp (draw) → add .cpp to CMakeLists.txt → optional toggle in features/config.h + menu
Networked player / entity field Resolve and add its RecvTable path in sdk/entity/ with DEFINE_NETVAR; document the discovery in aidocs/002_netvars-and-entity-offsets.md.
Client-only entity field sdk/entity/ with DEFINE_MEMBER, after verifying that it is not in a receive table.
Global address or input slot Prefer a named interface or CUserCmd; document a true signature in aidocs/003_global-addresses-and-inputs.md when no semantic source exists.
New interface / signature Add the pattern, operand rule, and provenance to the matching architecture profile (config/signatures.ini for x86 or config/signatures-x64.ini for x64); keep resolution logic in game/interfaces.cpp and explain discovery in aidocs/.
Shared target / eye helpers game/player.*

Building

Requirements

  • Windows, with an MSVC profile matching the target game process: x86 for the legacy client or x64 for the updated client
  • CMake ≥ 3.19
  • MSVC with a toolset matching the selected architecture (VS 2019 Build Tools work)
  • DirectX SDK (June 2010)d3d9 only (ImGui’s DX9 backend does not need D3DX). Default path in CMake; override with -DDXSDK_DIR=...
  • C++17

Dear ImGui and the MinHook source/headers are included in the repository, so no package manager is required. The CMake file links only d3d9; ImGui’s DX9 backend does not require D3DX.

MinHook is built from the vendored source for the selected pointer size. The old prebuilt x86 libraries remain in minhook/lib/ as historical reference, but the CMake target no longer depends on them. VS 2019 Build Tools are the baseline used here; a compatible newer MSVC toolset should also work.

For CLion, select an MSVC x86 or x64 CMake profile matching the game. Its bundled MinGW toolchain is not the baseline for this Windows DLL. With Visual Studio, choose the platform that matches the loaded game.

MSVC x86 command line

Open an x86 Native Tools Command Prompt for Visual Studio. If you start from a regular cmd.exe, initialize the x86 environment first:

call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" x86

Configure and build a Debug DLL:

cmake -S . -B build-msvc-x86 -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Debug
cmake --build build-msvc-x86 --target nikooo777

If the DirectX SDK is installed elsewhere, point DXSDK_DIR at its root directory—the directory containing Include and Lib:

cmake -S . -B build-msvc-x86 -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Debug -DDXSDK_DIR="D:/SDKs/DirectX SDK (June 2010)"

Debug output: build-msvc-x86/nikooo777.dll.

The build also copies config/signatures.ini to build-msvc-x86/signatures.ini, beside the DLL. Edit the checked-in file, then rebuild before testing a new binary.

For a single-config NMake Release build:

cmake -S . -B build-msvc-x86-release -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release
cmake --build build-msvc-x86-release --target nikooo777

MinHook is compiled in the selected configuration. For a multi-configuration Visual Studio generator, use cmake --build <build-dir> --config Debug or --config Release. Use a fresh build directory when switching architectures.

MSVC x64 command line

The updated game uses the x64 module set. Open an x64 Native Tools Command Prompt or initialize it from a regular prompt:

call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" x64
cmake -S . -B build-msvc-x64 -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Debug
cmake --build build-msvc-x64 --target nikooo777

This selects Lib/x64/d3d9.lib, builds the x64 MinHook sources, and copies config/signatures-x64.ini to build-msvc-x64/signatures.ini. Do not reuse an x86 build directory when changing pointer size.

Offline deterministic tests

The seed derivation, cone replay, command-layout checks, and inverse-cone math are also built as a small architecture-specific test executable. They do not load the game, resolve signatures, or call game-owned interfaces:

cmake --build build-msvc-x64 --target weapon_math_tests
ctest --test-dir build-msvc-x64 --output-on-failure

Run the same two commands with build-msvc-x86 after configuring the x86 profile. With a multi-configuration Visual Studio generator, append --config Release to the build and -C Release to ctest. These tests catch deterministic math and ABI regressions; they do not replace a permitted in-game smoke test for signatures, vtables, timing, or object lifetimes.

Permitted offline smoke test

This repository does not provide a standalone executable, injector, or anti-cheat workaround. For a local test environment you control:

  1. Build the DLL for the same architecture as the target game process.
  2. Start a permitted offline or local Counter-Strike: Source session.
  3. Load the DLL using an injector you already trust and are authorized to use.
  4. Check the console for BaseClient, Netvars initialized, ClientEntityList, ClientMode, OverrideView, CreateMove, LockCursor, and EndScene addresses.
  5. Press F1 to print the module/interface/netvar dump, then Insert and confirm the pointer moves freely without another UI being open.
  6. Press End to restore hooks and unload cleanly.
  7. Confirm the console shows the loaded config path and the signature provenance/match offsets before treating a resolution as valid.

The addresses, offsets, and signatures are build-specific. A successful DLL build does not mean that it is safe to load into a different game binary.

Offsets, signatures, and vtables

These are the files to revisit when the client build changes:

File What it contains
netvars/netvars.* Runtime ClientClass/RecvTable traversal and entity-relative offsets.
sdk/client_entity_list.h / game/entity_list.cpp Named VClientEntityList003 interface and player-slot translation.
sdk/entity/*.h Netvar-backed entity accessors plus explicitly client-only fields and the remaining padded layout.
config/signatures.ini / config/signatures-x64.ini Architecture-specific patterns, named interfaces, operand decoders, pointer indirections, validation settings, feature defaults, and discovery links.
game/interfaces.cpp CreateInterface lookup plus configured entity-list, EngineClient, EngineTrace, ClientState, and ClientMode resolution logic.
hooks/hooks.cpp Vtable slots for OverrideView, CreateMove, VGUI_Surface030::LockCursor, and EndScene.
core/constants.h Entity stride, player limits, team values, and movement flags.

The debug dump reports module bases, runtime interface/client-only offsets, resolved netvar offsets, the resolved ClientState address, view angles from VEngineClient, the local bone-cache pointer/count, aimbot valid/readable/visible target counts, and the local player position when one is available. If a signature or required netvar is not found, or a read produces null/garbage data, treat the binary and the offsets as mismatched and re-dump them rather than guessing.

Tutorial notes

The architecture-selected signature profile is the source of truth for the runtime signatures and named client/engine interfaces. It intentionally records how each pattern or interface was found, not just the bytes: update the provenance fields whenever a new build is reverse-engineered.

Troubleshooting

Symptom Likely cause / next check
CMake selects the wrong architecture Check CMAKE_SIZEOF_VOID_P, initialize the matching MSVC environment, and configure a fresh build directory.
d3d9.h or d3d9.lib is missing Set DXSDK_DIR to the DirectX SDK root and verify Include/d3d9.h plus Lib/x86/d3d9.lib or Lib/x64/d3d9.lib exist.
A MinHook library cannot be opened MinHook is built from source; reconfigure after adding the vendored minhook/src files and inspect the selected C/C++ toolchain.
signatures.ini cannot be loaded Build from the repository so CMake copies the architecture-selected profile beside the DLL; do not launch with a stale or missing adjacent config.
ClientState signature not found or ClientMode signature not found The byte pattern is for another client build. Confirm the executable/module version and update the pattern.
BaseClient is null VClient017 was not exposed by the loaded client module, or the DLL was loaded at the wrong time/process.
ClientEntityList interface not found VClientEntityList003 is unavailable or the configured client module is wrong. Re-check the interface name and the selected ABI.
EngineTrace interface not found or all targets are invisible EngineTraceClient003, TraceRay slot 4, the Ray_t/CGameTrace layout, or the entity-skip filter does not match the loaded engine. Re-check the x64 evidence in aidocs/004.
Failed to initialize netvars GetAllClasses, a required RecvTable, or a required property does not match this client build. Stop and re-check the table path and pointer-width assertions.
D3D9 capture fails or the menu never appears The code needs a visible, suitably sized game window and a D3D9 device. Wait until the game window is initialized and verify that the target is using D3D9.
The menu opens but the cursor stays pinned to the center Confirm VGUI_Surface030 resolves and LockCursor is logged. Re-check surface slots 61/62 and the engine CalculateMouseVisible/IsCursorLocked path documented in aidocs/004; a Win32-only cursor change cannot stop Source input recentering.
A feature crashes or reads implausible values Stop testing: an entity/global offset is stale or the target is not the expected architecture/build. Disable the feature and return to Ghidra.

Known limitations

  • Signatures, interface versions, vtable assumptions, and remaining client-only fields are tied to the client build this project was developed against.
  • Netvars remove several hardcoded entity displacements, but the ClientClass/RecvTable ABI, table names, property names, and type assumptions are still build-family dependencies.
  • Memory access is direct and lightly validated; this is experimental code, not a hardened runtime.
  • The aimbot remains intentionally basic: closest visible target, bone 14, and an immediate angle change. It does not implement smoothing, weapon handling, or movement correction; visibility is a client trace approximation and is not a server-side visibility guarantee.
  • The triggerbot is deliberately narrow: it uses the client-only crosshair target ID and requires the local player to be on the ground.
  • Rendering support is D3D9-specific and depends on finding the game's visible top-level window.
  • The deterministic seed/spread math has an offline CTest target; signatures, interfaces, timing, and object lifetimes still require an architecture-matched build followed by a permitted local smoke test.
  • Perfect no-spread is validated only for the documented x64 sample path and remains off by default; the x86 cone path and other weapon branches require their own runtime evidence.
  • Nothing here is intended to bypass VAC, FaceIT, or any other anti-cheat system.

Videos / notes while reversing

Topic Link
Finding ClientState Odysee · YouTube
Finding view angles Odysee · YouTube
Finding bone matrix Odysee · YouTube

Remaining client-only offsets in the entity headers are for the client build this project was developed against. The x64 bone-cache offsets documented in aidocs/004 came from the current SetupBones implementation, but they still need to be re-dumped if the binary changes.

The signature records point back to this section and the numbered tutorial so the pattern bytes, operand offsets, and pointer-chain assumptions can be re-derived instead of copied blindly.

Status / honesty

  • Code quality is tutorial and experimental, not production.
  • There is no support for protected multiplayer or anti-cheat bypass.
  • If you need a maintained end-user package, this repository is intentionally not that.

End goal

Understand the client well enough to defend a large CS:S community: know what cheats can see and do, and how to reason about them when they show up on the server.

About

For the sole purpose of re-learning C++ and enjoying game hacking

Topics

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages