Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Source2Forge for IDA Pro

Console variable and command recovery plugin for IDA Pro 9.x with complete cross-platform support.

Finds every ConVar and ConCommand a Source 2 binary registers, names the globals after them, decodes their flags, and lists every place the engine touches them.


Support My Work

I build free, open-source tools for the community. If they save you time, you can support the work through GitHub Sponsors. One-time or monthly, any amount, no tiers and nothing locked behind it.

GitHub Sponsors

Or support me for free by starring this repository!


Features

Core Capabilities

  • Registration constructor discovery, nothing hardcoded to a game build
  • ConVar and ConCommand separation from how the objects are used
  • Automatic renaming of every registered global (cvar_sv_cheats, ccmd_bot_add)
  • Command handler recovery, the function behind a command gets named cmd_bot_add
  • The constructors and the value accessor get their hl2sdk names, but only where the argument shape proves which one they are
  • Descriptions written back as repeatable comments
  • FCVAR flag names read out of the binary, with an hl2sdk reference table as backup
  • Use site listing: every place a variable's object is referenced, and what each site calls
  • Prefix learning, the seeds find one constructor and its call sites teach the rest
  • Engine gate, the scan only runs on binaries that actually look like Source 2

Platform Support

  • IDA Pro 9.x (modern SDK)
  • Windows x64
  • Linux x64/ARM64
  • macOS Intel/Apple Silicon

Integration

  • Native IDA chooser interface
  • Nested Source2Forge menu with separate ConVar and Command views
  • Rename offer on first analysis, no scan is started before autoanalysis finishes
  • JSON and CSV export

Installation

Download binary for your platform from releases

Copy to IDA plugins directory:

Windows:  C:\Program Files\IDA Pro 9.x\plugins\source2forge64-windows-x64.dll
Linux:    /opt/ida-pro/plugins/source2forge64-linux-{x64,arm64}.so
macOS:    /Applications/IDA Pro 9.x.app/Contents/MacOS/plugins/source2forge64-macos-{arm64,x64}.dylib

Restart IDA Pro


Quick Start

Open a Source 2 binary (server.dll, client.dll, engine2.dll, or their Linux counterparts). When autoanalysis finishes the plugin offers to name every ConVar object it found. Accept and the globals stop being qword_25ABF00.

Menu

Right-click anywhere in the disassembly or pseudocode view:

Context menu

Source2Forge
    ConVars  → List
             → Export
    Commands → List
             → Export
    Name all ConVar objects

In the list

Type to filter by name, flag or description. Enter opens the use sites of the selected entry.

Rows carrying FCVAR_CHEAT are tinted red. Rows that are never touched outside their registration are greyed out, those are the variables the binary registers but never reads.

ConVar list
Every ConVar with its object address, use count, decoded flags and description

Command list
The same for commands, cheat protected ones tinted red

Use sites

Selecting a variable and pressing Enter opens every place its object is referenced, with the function it sits in and the accessor that consumes it. Source 2 hands back a pointer to the value and lets the caller decide what to do with it, so the sites are listed rather than guessed at.

Use sites

After naming

Annotated disassembly
The handler, the objects and their descriptions, all recovered from the registration


Use Cases

Server plugin development

// Source2Forge shows: mp_friendlyfire | FCVAR_NOTIFY | FCVAR_REPLICATED | 0x181FDF398
auto mp_friendlyfire = (ConVar*)(base + 0x1FDF398);   // address straight from the list

Reverse engineering

  • Find which convars a build added, removed or re-flagged between versions via the JSON export
  • Locate the accessor a game uses to read values, then work backwards from there
  • Spot cheat-protected and replicated variables before touching them
  • Recover command names for a binary that ships without symbols

Documentation

  • Export the full convar table with flags and descriptions to CSV for a wiki
  • Diff two exports to build a changelog for a game update

How It Works

Constructor discovery

Every ConVar registration passes the variable name as a string. The plugin walks the string list, keeps the ones shaped like a convar name, and follows each reference to the first call after it. Whatever function collects the most of those calls is a registration constructor candidate.

Argument profiling

A candidate's call sites are then read back to learn what each argument slot carries:

0x1815172F0   rcx = object   rdx = name   r8 = flags     r9 = description
0x1815187F0   rcx = object   rdx = name   r8 = callback  r9 = description   [rsp+20h] = flags
0x180182B50   rcx = object   rdx = name   r8 = type      r9 = flags         [rsp+20h] = description

Nothing about this is tied to a game build. Addresses move every patch, the shape does not. MS x64 spills the fifth argument onto the stack, which is why command flags are read from [rsp+20h] and not from a register.

Validation

A string helper takes a this pointer and a char* too, so shape alone is not enough. What separates them is that a helper pours many different names into one buffer while a registration gives every object exactly one name. Counting distinct objects instead would punish a reference looked up from several places, which is normal. Entity factories survive that test as well, so they are separated by whether their objects are ever handed to a value accessor.

Variable or command

A variable has to be read by something, so its object shows up again after registration. A command object is written once and never touched again, the dispatch goes through the callback it was given. Measured on a cs2 server.dll the split is 0 of 356 against 86-100%, so the vote is taken per constructor and never per object.

Command handlers

A ConCommand's callback never reaches the constructor directly. It is loaded with a lea, dropped into a stack delegate, and only a pointer to that delegate is passed. The plugin reads the load back instead:

lea  rax, sub_1801BBEE0            ; the handler
mov  [rsp+58h+var_28], rax
lea  r9, aLoadsAFileCont           ; description
lea  r8, [rsp+58h+var_28]          ; only points at the delegate
lea  rdx, aSvLoadForcedCl          ; name
lea  rcx, unk_181DA9C08            ; the object
mov  [rsp+58h+var_38], 80004h      ; flags, the 5th argument
call sub_1815187F0

Two different functions in the window means it cannot be told which one it is, and those are skipped rather than guessed. On a cs2 server.dll that recovers 339 of 356 handlers.

Engine functions

CConVar's constructor forwards to ConVarRefAbstract::Register( name, flags, help_string, value_info ), and both take the same registers, so the argument shape cannot tell them apart. What can is the call graph: the per type wrappers call the core and the core calls none of them. The core gets the name, the wrappers keep their address name because there is no way to know which T each was built for.

Commands are easier. A name, a help string and a callback struct is a shape nothing else passes, so that one is named outright. The accessor most variables are read through becomes ConVarData::Value.

The typed getters sit on top of that accessor. ConVarRefAbstract::GetFloat and friends are tiny wrappers whose only call is Value(slot), and the load out of the returned pointer says which type each was built for: movss is a float, a byte load is a bool, a dword an int, a qword a string. A game helper written around one convar looks identical from the outside, so only the wrapper the whole binary shares is named, and a close runner up means neither is.

UnRegisterConVar is named the same way. Every convar's destructor is a stub that hands its object over and does nothing else, so the function all those stubs call is the free void UnRegisterConVar( ConVarRef *cvar ).

The compiler emits these small functions once per translation unit, so the binary carries several copies of each at different addresses. A copy is not byte equal because every branch inside it is relative, but it has the same opcodes and every reference that differs is a jump that moved with the function. Copies get the name their original got, and IDA hangs a _0 on them.

Nothing else is named. A real SDK name sitting on the wrong function is worse than sub_180182B50, and ConVar_Register in particular is a genuine tier1 function that has nothing to do with any constructor.

Flags or value type

A constructor that takes a name but no description is a reference to a variable defined elsewhere, and its small integer argument is the value type, not the flag word. Those are listed without flags rather than with wrong ones.

Prefix learning

The seed list only has to find one constructor. Its call sites are then scanned for the prefixes this binary actually uses, and those find the constructors the seeds never covered. Two or three rounds and it stops growing.


Scripting

Three IDC functions expose the same data the lists show, callable from IDAPython:

import idc, json

convars = json.loads(idc.eval_idc('Source2Forge_Scan()'))['convars']

cheats = [c for c in convars if 'FCVAR_CHEAT' in c['flags_text']]
print(len(cheats), 'cheat protected')

one = json.loads(idc.eval_idc('Source2Forge_Uses(0x181FDF398)'))
print(one['convars'][0]['uses'])

print(idc.eval_idc('Source2Forge_Rename()'), 'objects named')
Function Returns
Source2Forge_Scan() Every entry as JSON, scanning first if needed
Source2Forge_Uses(ea) The single entry registered at that object address
Source2Forge_Rename() Number of objects it named
Source2Forge_Rescan() Drops the cache and scans again, logging every constructor candidate and why it was taken or dropped. Run this when a binary comes back with fewer entries than expected

tests/test_idc_export.py runs against these. Open a Source 2 binary, let the analysis finish, then File → Script file... and pick it. Every check is about the shape of the data rather than a specific build, so it stays valid across game patches.


Building from Source

Prerequisites

  • IDA SDK extracted to sdk/ directory
  • Docker (for Linux/macOS builds)
  • LLVM/Clang and xwin (for the Windows cross-compile)

Build Commands

# Linux + macOS (via Docker)
make build

# Windows x64 (cross-compile with clang-cl + lld-link)
make build-windows

# everything, all five binaries
make build-all

Native Windows builds go through CMake instead:

cmake -B build -DIDASDK_DIR=C:/idasdk90
cmake --build build --config Release

License

MIT, see LICENSE.

About

Automatic ConVar and ConCommand recovery, object naming, flag decoding and use site tracking for reverse engineering Source 2 binaries. Supports IDA Pro 9+ on any OS

Topics

Resources

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages