Platform: Windows x86-64 only. Linux, macOS, and 32-bit Windows builds are intentionally unsupported.
Counter-Strike 2 外部 offset / schema / SDK dumper。单 exe、默认全量输出。
本仓库是若干公开 GitHub 项目的融合:分析管线来自 a2x/cs2-dumper,消费端 include-tree SDK 来自 scros22/cs2-universal-offsets,离线 LoadLibrary、注入注册 schema、syscall 读内存等来自下面列出的其它 dumper。借鉴的东西都可以在 GitHub 找到,完整出处见 Credits。
默认走 memflow 外部读内存。它只 dump,不包含作弊功能。
需要 Rust 1.88+(edition 2024;slice::as_chunks 等 API 从 1.88 稳定)。仓库根目录:
cargo build --release
产物只有一个 target/release/cs2-dumper.exe。-c shade 用的 payload 在编译时嵌进这个 exe,不需要再带 DLL。
微基准打的是 dump 真正会走的公开函数(小端整数加载、模块名 intern、上一轮 patterns.json 缓存查找、IDA matcher、标识符 slugify),不是另写一套。不 attach cs2.exe。
cargo bench --features bench --bench hot_paths
FakeMemory 只在 cfg(test) 和 --features bench 下编译,默认 exe 不含它。--features bench 是必须的:[[bench]] 标了 required-features,不加这个 feature 时 cargo 会跳过该 bench。
cs2-dumper.exe
不传参数就会跑完全部阶段,写出扁平多语言文件、C++ include-tree,以及全局命名空间 sdk/。
如不希望回退扫描所有盘符,可设置环境变量 CS2_DUMPER_SKIP_DRIVE_SCAN=1;这不会影响注册表、Steam VDF 和常见路径探测。
- 本机有
cs2.exe:attach 后 dump 活进程。 - 没有游戏进程、且未指定 memflow 插件(
-c pcileech/kvm/winio):从注册表 /libraryfolders.vdf/ 本地盘自动找steamapps/common/Counter-Strike*,把 schema DLLLoadLibrary进 dumper 自己的进程 再 dump(不是注入cs2.exe)。插件连接器上找不到cs2.exe会直接失败,不会误 dump 本机 LoadLibrary 镜像。
可选参数:
| 参数 | 说明 |
|---|---|
-o, --output <DIR> |
输出目录,默认 output |
-v / -vv |
更详细的日志 |
-c, --connector <name> |
内存后端,见下表 |
--guess-structs |
额外写出 structs.hpp:按字段间距猜测未知类型大小。偏移会显式 pad;sizeof 仍可能错。默认关闭 |
--no-sound |
关闭 Windows Beep() 进度提示音 |
-h / -V |
帮助 / 版本 |
日志默认写到 <output>/cs2-dumper.log。终端默认 Warn(sanitize 丢弃、写失败等会显示);-v 提到 Info,-vv Debug。文件始终记 Info 及以上。输出目录一开始就会创建;日志文件写失败不会中断 dump。
| 名字 | 何时用 |
|---|---|
(默认 / native) |
memflow-native attach。游戏没开就自动走 LoadLibrary |
syscall |
活 cs2.exe,本机 Hell's Gate 风格 NtReadVirtualMemory stub(Windows) |
shade |
把内嵌 payload 注入活 cs2.exe,对已加载模块调用 InstallSchemaBindings,再 dump(Windows) |
pcileech / kvm / winio |
memflow 插件。例如 cs2-dumper.exe -c pcileech -a :device=FPGA |
syscall、shade 和 memflow 插件都要求目标上有活的 cs2.exe。DMA 连接器通常需要管理员 / root。
manifest.json 的 backend 字段会记下这次跑的是 native / syscall / shade / loadlib。LoadLibrary dump 不会把未初始化的 dwBuildNumber(常见 0 / 0xFFFFFFFF)写成引擎版本;game/csgo/steam.inf 的 PatchVersion / ClientVersion 写进 steam_inf。
每次运行默认全量写出三套消费端产物,互不替代:
- 扁平多语言文件(和 a2x/cs2-dumper 兼容):
offsets.*、buttons.*、interfaces.*、各<module>_dll.* - C++ include-tree(scros22/cs2-universal-offsets 那一套):
cs2.hpp单头 amalgamation +macros.hpp+schemas/+impl/entity_system.hpp - 全局命名空间 SDK:
sdk/sdk.hpp+sdk/sdk_types.hpp+sdk/modules/+sdk/classes/
写出 sdk/ 时控制台会打印生成的头文件名。根目录还会再写一份 entity_system.hpp(和 impl/entity_system.hpp 并存)。
<output>/
├── manifest.json
├── info.json
├── cs2.hpp # C++ 单头 amalgamation
├── macros.hpp # SCHEMA_FIELD + engine types + auto forwards
├── entity_system.hpp # 根目录 entity helper(消费 offsets_merged.hpp)
├── offsets.{cs,hpp,json,rs,zig} # canonical dwXxx
├── offsets_merged.{hpp,json} # canonical + pattern + interface RVA
├── buttons.* interfaces.*
├── <module>_dll.{cs,hpp,json,rs,zig}
├── patterns.{json,hpp,md,...}
├── vtables.{json,hpp,cs} # hpp 还会拷到 offsets/vtables.hpp
├── schema_index.json
├── schema_index.diff.json
├── sdk/
│ ├── sdk.hpp # 全局命名空间 umbrella
│ ├── sdk_types.hpp
│ ├── sdk_enums.hpp
│ ├── sdk_classes.hpp
│ ├── modules.hpp
│ ├── modules/<module>.hpp
│ └── classes/<module>/<Class>.hpp
├── schemas/
│ ├── schemas.json
│ └── <module>_dll.hpp # 带命名空间的 typed class
├── offsets/offsets.hpp
├── offsets/vtables.hpp
├── patterns/patterns.hpp
├── interfaces/interfaces.hpp # 可调用 vtable 结构 + ASLR-safe getter
├── impl/entity_system.hpp
├── engine/
│ ├── engine_structs.json
│ ├── ccsgoinput.h
│ ├── cusercmd.h
│ └── cviewsetup.h
├── netvars/
├── convars/
├── protobufs/
├── entities/
├── weapons/
├── gameevents/
└── verified_features.json
活进程还能写出 convars、entities、weapons、game events、netvars、protobufs。LoadLibrary 模式没有这些运行时对象,schema / pattern / interface 仍然有。
schema_index.json 里 enum 的 size 是 schema 存储宽度(字节),成员个数是 members。
| 你要干什么 | 用哪个文件 |
|---|---|
| C++ 一个 include 全要 | cs2.hpp |
| C++ 全局命名空间 SDK | sdk/sdk.hpp |
| C++ typed schema class | macros.hpp + schemas/client_dll.hpp |
| C++ 走 entity list | impl/entity_system.hpp 或根目录 entity_system.hpp |
| 任意语言只要常量 | offsets.hpp / <module>_dll.json |
| 更新后对 diff | manifest.json + *.diff.json + patterns.repair.patch.json |
| 按 vtable index hook | vtables.hpp / vtables.json |
| schema 标志(vtable / abstract / scope) | schema_index.json 的 flags,以及 schemas/*.hpp 注释 |
| dump 时没加载的模块 | manifest.json missing_schema_modules |
C++ 把 dump 目录加进 include path,然后:
my-project/
├── src/
└── vendor/cs2-dumper/ # 拷一份 <output>/
├── cs2.hpp
├── macros.hpp
├── sdk/
├── schemas/
├── impl/
└── ...
include-tree(推荐单头):
#include <cs2.hpp>
static_assert(CS2_BUILD != 0, "dump must record a live build number");
int health(void* pawn_ptr) {
auto* pawn = reinterpret_cast<client::C_CSPlayerPawn*>(pawn_ptr);
return pawn->m_iHealth();
}全局命名空间 SDK:
#include <sdk/sdk.hpp>cs2.hpp 会拉上 macros.hpp、各模块 schema(编辑器模块会跳过)、merged offsets、typed interfaces、buttons、protobufs、patterns、entity helper、engine 结构。缺的可选报告用 __has_include 挡住,照样能编。sdk/sdk.hpp 是另一套独立头,给要 sdk:: 全局命名空间的人用,默认每次 dump 都会写出。
Entity helper 在 impl/entity_system.hpp(根目录 entity_system.hpp 也有一份),走 offsets:: 常量,不写死数字:
auto* local = CGameEntitySystem::GetLocalPlayer();
auto* identity = CGameEntitySystem::GetIdentityByIndex(1);Interface 单例是 ASLR-safe 的,自己把模块基址传进去:
auto* input = ifc::inputsystem::get_InputSystemVersion001(module_base);
input->SetRelativeMouseMode(false);
void* slot = ifc::detail::vtable_slot(input, 76);C# / Rust / Zig / JSON 用扁平多语言文件(offsets.json、client_dll.cs、patterns.rs …)。include-tree 和 sdk/ 只给 C++。
游戏更新后:对着新 build 再跑一遍 dumper,看 manifest.json(build_number、pattern_summary),再看 patterns.diff.json / schema_index.diff.json / interfaces.diff.json。特征漂了会写出 patterns.repair.json 和可直接喂回去的 patterns.repair.patch.json。然后用新的 cs2.hpp 重编消费端。
Source 2 自带运行时元数据,不用 PDB 也能还原 class / enum / netvar / metadata。
引擎启动时建 CSchemaSystem 单例。每个用 schema 的模块注册一个 CSchemaSystemTypeScope(一 DLL 一 scope),scope 里是 CUtlTSHash,装着该模块的 CSchemaClassBinding / CSchemaEnumBinding。
每个 class 有:名字、可选父类、字段(名字 / 类型字符串 / 实例内偏移)、编译期 metadata。最常用的两条是 MNetworkVarNames(name, type) 和 MNetworkChangeCallback(name)。enum 同样是名字 + 对齐 + (name, value) 列表。
Walker 在 src/source2/:
- pelite 扫 PE,用特征定位 schema 系统(特征在
src/analysis/offsets.rs)。 - memflow 顺着
CUtlTSHash枚举每个 scope 的 class / enum。 src/analysis/schemas.rs拍扁成 emitter 用的Class/Enum。
三套写出:
// 扁平常量(任意语言)
namespace cs2_dumper::schemas::client_dll::C_CSPlayerPawn {
constexpr std::ptrdiff_t m_iHealth = 0x344;
}
// include-tree typed class(cs2.hpp / schemas/)
namespace cs2::sdk::client_dll {
class C_CSPlayerPawn : public C_BasePlayerPawn {
public:
SCHEMA_FIELD(std::int32_t, m_iHealth, 0x344)
};
}
// 全局命名空间 SDK(sdk/sdk.hpp)
#include <sdk/sdk.hpp>SCHEMA_FIELD 在 macros.hpp,展开成 this + offset 的 typed 访问器。带 MNetworkVarNames 的字段还会单独写到 netvars/。
相关代码:src/source2/、src/analysis/schemas.rs、src/output/schemas.rs、src/output/sdk.rs、src/output/sdk_classes.rs、src/output/netvars.rs。
特征是 IDA 风格字节串,空格分隔,? / ?? 是通配,也支持半字节通配(4?、?A)。扫描器基于 pelite,只扫指定模块的指定 PE 节(代码走 .text,字符串走 .rdata)。活进程读失败时会回退到磁盘 PE,先按 SizeOfImage 映射成 RVA 布局再扫,不会把 PointerToRawData 当成 VirtualAddress。
匹配地址是 pattern 第一个字节 的 RVA。真正要的值往往在后面的 lea / call / mov 里,所以每条特征带一个 resolver:
| resolve | 干什么 |
|---|---|
raw / None |
匹配地址就是结果(函数头,直接 hook) |
rel32 |
E8/E9 的 signed disp32。rel_off 是 disp 相对 pattern 起点的字节下标,通常是 1 |
riprel |
48 8D 0D disp32 这类 RIP-relative。48 8D 0D 时 rel_off 是 3 |
stringref |
先在 .rdata 找字符串,再合成 lea rXX, [rip+str] 去 .text 里做 riprel |
rel32 / riprel 算法一样:
disp = i32::from_le_bytes(text[match_rva + rel_off ..][..4])
RVA = match_rva + rel_off + 4 + disp
内置库在 src/patterns/database.rs。每条扫完记 match 次数;一条挂了不会把整次 dump 带崩。上次的 patterns.json 会先在缓存的 match_rva 上复验,没漂就跳过全模块扫描。
写出 patterns.{json,hpp,md,cs,rs,zig}。漂了的特征会尝试放宽通配,结果进 patterns.repair.json;其中唯一命中的子集写成 patterns.repair.patch.json。
- 在 IDA / Ghidra / Binary Ninja 里找到函数。
- 抽 12–20 个跨更新还在的字节:函数序言、唯一常量加载、有名字的 import。别用大段
mov reg, reg,也别把每补丁都变的立即数写死。 - 会变的位移 / 寄存器编码改成
??或半字节通配。 - 选 resolver:序言用
None;E8/E9用Rel32 { rel_off: 1 };48 8D 0D用RipRel { rel_off: 3 }。rel_off差 1 会得到看起来像那么回事但错的 RVA。 - 追加到
src/patterns/database.rs的CS2_PATTERNS:
Pattern {
name: "MyNewThing",
module: "client.dll",
needle: "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC ?",
resolve: NONE,
extra_off: 0,
prototype: "",
},cargo build --release,再跑一次 dumper。看patterns.json:found、matches(必须是 1)、rva。拿 RVA 回反汇编器对一下。
对每个解析到的 interface,跟实例指针的第一个 qword 拿到 vftable,再当连续函数指针数组往下走,直到槽位不落在任何已加载模块镜像里。写出 vtables.{json,hpp,cs},hpp 再拷到 include-tree 的 offsets/vtables.hpp。
没有 PDB。多数槽是 method_<N>,直接用下标。额外会拿槽的 RVA 去对 pattern 库,对上了就用特征名(例如 update_global_vars)。MSVC /GR 下还能顺着 vtable [-1] 的 RTTICompleteObjectLocator 解出 C++ 类名。
vtable 布局比函数体字节稳得多:槽位下标只有 Valve 给虚函数增删改序才会变。
#include <cs2.hpp>
using CreateMove_t = bool(__thiscall*)(void* self, int slot, float ft, bool active);
void hook_create_move(void* iface) {
auto** vt = *reinterpret_cast<void***>(iface);
auto* fn = vt[cs2::vtables::client_dll::Source2Client002::method_25];
install_hook(fn, &your_create_move_hook);
}vtables.json 的 vtable_module 是 vftable 字节所在的 DLL(实现有时在兄弟模块里)。Walker 在 src/analysis/vtables.rs,写出在 src/output/vtables.rs。
| 路径 | 做什么 |
|---|---|
src/main.rs |
CLI、日志、选后端、attach |
src/dump.rs |
分析 → 特征 → 对象级恢复 → 写出全部产物 |
src/analysis/ |
schema / offset / interface / button / vtable / entity / convar |
src/analysis/module_data.rs |
模块镜像读取;一次 dump 内缓存,避免反复读 client.dll |
src/analysis/read.rs |
远程 C 字符串读取,walker 共用 |
src/patterns/ |
内置特征库、扫描、repair |
src/output/ |
扁平文件、include-tree、sdk/、manifest |
src/output/cpp_types.rs |
schema → C++ 原语 / 存储宽度,两套 SDK 共用 |
src/memory/ |
native / syscall / shade / LoadLibrary 自附加 |
src/loadlib.rs |
游戏没开时对本进程 LoadLibrary |
shade-payload/ |
-c shade 注入 DLL,编译期嵌进主 exe |
cargo test --workspace
需要活 cs2.exe 的检查:
cargo test -- --ignored --nocapture
下面这些都是公开 GitHub 仓库。本项目只取各自强项,拼进一条管线。
| 仓库 | 拿了什么 |
|---|---|
| a2x/cs2-dumper | 主体:memflow attach、schema / interface / button / offset 扫描、多语言输出、pattern 修复与动态恢复 |
| scros22/cs2-universal-offsets | 带命名空间的 C++ include-tree、SCHEMA_FIELD、自动前向声明、typed entity helper、engine 结构布局 |
| bt629414/cs2-best-dumper | 额外 dwCreateMove / dwParticleManager / dwClientMode / dwPVSManager / dwVPhys2World、磁盘 PE 回退、可选 syscall RPM(-c syscall)、--guess-structs |
| arisuwine/shade-dumper | 完整 SCHEMA_CF1_* / SCHEMA_EF_* 标签、vtable slot dump、可选注入后调用 InstallSchemaBindings(-c shade) |
| xsip/cs2-schema-dumper-no-process | 游戏没开时:找 Steam 安装、对本进程 LoadLibrary schema DLL 再 dump |
| 仓库 | 角色 |
|---|---|
| memflow/memflow | 跨平台内存读写框架(默认后端) |
| memflow/memflow-native | Windows 本机 attach |
| CasualX/pelite | PE 解析与特征扫描 |
| am0nsec/HellsGate | -c syscall:从 ntdll stub 抽 SSN 再调 NtReadVirtualMemory |
| neverlosecc/source2gen | Source 2 schema → C++ SDK 的常见写法(SCHEMA_FIELD 这一路) |
| GAMMACASE/Source2SchemaDumper | 服务端 schema dump(CS2 / Dota 2 / Deadlock) |
| sneakyevil/CS2-SchemaDumper | 早期外部 schema walker |
| ValveResourceFormat/DumpSource2 | 离线 schema / convar dump(GameTracking 在用) |
| a2x/cs2-analyzer | a2x 的离线分析器(WIP) |
MIT(LICENSE)。上游项目保留各自许可证;本仓库的分析与输出代码按 MIT 发布。