- Status
- Offline
- Joined
- Mar 3, 2026
- Messages
- 690
- Reaction score
- 457
Still hardcoding your interface strings like a paste-monkey in Counter-Strike: Source? If you are building an external (RPM/WPM based) tool, relying on static offsets for things like VEngineClient013 or VClient017 is a surefire way to have your build break after a minor engine update.
This logic allows you to walk the PE export directory to find CreateInterface and then traverse the s_pInterfaceRegs linked list entirely from the outside.
Manual PE Header Parsing
Instead of relying on GetProcAddress (which requires an internal handle or hijacked thread), this implementation manually walks the export directory of the target module. This is the clean way to handle things when you're sitting in a separate process.
The Interface Reg Loop
Once you have the address of the CreateInterface export, you need to resolve the relative jump to find the global list head. The structure TInterfaceReg is standard for the Source Engine (CSS, HL2, etc.). It contains the callback, the interface name, and a pointer to the next node in the list.
Drop your crash logs below if you're having trouble resolving the list head on specific engine builds.
This logic allows you to walk the PE export directory to find CreateInterface and then traverse the s_pInterfaceRegs linked list entirely from the outside.
Manual PE Header Parsing
Instead of relying on GetProcAddress (which requires an internal handle or hijacked thread), this implementation manually walks the export directory of the target module. This is the clean way to handle things when you're sitting in a separate process.
Code:
inline uintptr_t GetModuleExportAddress(uintptr_t Base, const char* funcName)
{
auto peHeadersOffset = read<int32_t>(Base + 0x3c);
auto peHeaders = read<IMAGE_NT_HEADERS64>(Base + peHeadersOffset);
auto exportDirRVA = peHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
auto exports = read<IMAGE_EXPORT_DIRECTORY>(Base + exportDirRVA);
for (DWORD i = 0; i < exports.NumberOfNames; i++)
{
DWORD nameRVA = read<DWORD>(Base + exports.AddressOfNames + i * 4);
auto name = read_string(Base + nameRVA);
if (strcmp(name.chars, funcName) == 0)
{
WORD ordinal = read<WORD>(Base + exports.AddressOfNameOrdinals + i * 2);
DWORD funcRVA = read<DWORD>(Base + exports.AddressOfFunctions + ordinal * 4);
return Base + funcRVA;
}
}
return 0;
}
The Interface Reg Loop
Once you have the address of the CreateInterface export, you need to resolve the relative jump to find the global list head. The structure TInterfaceReg is standard for the Source Engine (CSS, HL2, etc.). It contains the callback, the interface name, and a pointer to the next node in the list.
Code:
inline void DumpInterfaces(const char* moduleName, uintptr_t ModuleBase)
{
struct TInterfaceReg {
uintptr_t callback; // Pointer to the \"Create\" function
uintptr_t namePtr; // Pointer to the name string
uintptr_t next; // Pointer to next node
};
uintptr_t exportAddr = GetModuleExportAddress(ModuleBase, \"CreateInterface\");
if (!exportAddr) {
printf(\"Failed to find CreateInterface export in %s\\n\", moduleName);
return;
}
int32_t relativeOffset = read<int32_t>(exportAddr + 3);
uintptr_t listHeadGlobal = exportAddr + 7 + relativeOffset;
uintptr_t currentInterfaceAddr = read<uintptr_t>(listHeadGlobal);
printf(\"--- [%s] Interfaces (Base: 0x%llX) ---\\n\", moduleName, ModuleBase);
while (currentInterfaceAddr != 0)
{
TInterfaceReg reg = read<TInterfaceReg>(currentInterfaceAddr);
auto interfaceName = read_string(reg.namePtr);
uintptr_t regOffset = currentInterfaceAddr - ModuleBase;
uintptr_t funcOffset = reg.callback - ModuleBase;
printf(\" > %-32s | RegOffset: 0x%llX | FuncOffset: 0x%llX\\n\",
interfaceName.chars,
regOffset,
funcOffset);
currentInterfaceAddr = reg.next;
}
}
- The relativeOffset calculation (exportAddr + 3) assumes the standard jmp or mov instruction found in the Source Engine's CreateInterface export. If you are targeting a different engine version, verify the opcode bytes.
- Ensure your read and read_string wrappers are robust. If the engine is x64, use IMAGE_NT_HEADERS64; for the older CSS builds, you might need to swap to IMAGE_NT_HEADERS32.
- This method is completely external and doesn't require any shellcode execution.
Drop your crash logs below if you're having trouble resolving the list head on specific engine builds.