简介:这份资源是Cheat Engine 6.8.1完整源码包,面向游戏逆向、内存调试与安全分析方向的学习者和开发者,帮助其从源码层面理解动态内存扫描、读写与指针追踪的实现机制。压缩包共1523个文件,约8.45MB,以426个pas与181个h文件构成核心逻辑,152个c与34个cpp文件承载底层实现,另有144个lfm与127个lrt界面资源、21个lua脚本接口、18个asm汇编模块及多套vcproj、sln工程文件,覆盖扫描算法、内存访问、指针链解析、Lua脚本集成、反调试对抗与图形界面设计等模块。已有501人学习关注。通过研读可掌握精确扫描与模糊扫描的差异、虚拟地址转换与内存映射的写法,并借鉴其工程组织方式进行二次开发与功能定制,适合具备一定逆向基础、希望深入调试工具内核的读者。
1. 拿到一份 CE 6.8.1 源码,先别急着编译
Cheat Engine 这个名字在逆向圈子里几乎无人不晓,但大多数人只停留在“用”的层面——下载安装包、附加进程、搜数值、改内存。真正把cheat-engine-master这份 CE 6.8.1 源码拉下来翻过一遍的人,比例其实很低。我最初也是抱着“看看它怎么实现内存扫描”的心态解压的,结果发现这份源码的价值远不止“读一读”:它是一套完整的 Windows 底层工具链实战样本,涵盖进程附加、内存读写、调试器接口、Lua 脚本引擎、反汇编器集成,甚至自带内核驱动模块。如果你想搞清楚“内存修改器到底怎么工作的”,或者想基于它裁剪出一个自己的轻量工具,这份源码能省掉你大量从零摸索的时间。它适合有 C++ 基础、对 Windows API 和逆向工程有基本认知的从业者,新手直接啃会比较吃力,但跟着本文的步骤走,至少能把编译环境和核心模块跑通。
2. 编译环境搭建:从 Delphi 到 Visual Studio 的选型与配置
2.1 为什么 CE 6.8.1 的编译链和你想的不一样
很多人第一次打开cheat-engine-master目录时会愣住——里面大量.pas文件、.dpr工程文件,还有Lazarus和Delphi的工程配置。没错,Cheat Engine 的主体是用 Object Pascal 写的,具体来说是 Delphi 的方言,后期版本开始向 Lazarus/FPC 迁移。CE 6.8.1 这个版本正好处于过渡期,主工程cheatengine.dpr可以用 Delphi 7 到 Delphi 2007 之间的版本打开,但部分模块已经带了 Lazarus 的.lpi文件。
这就带来第一个选型问题:你到底是用 Delphi 还是 Lazarus?我的建议是,如果你只是想在 Windows 上快速编译出一个能跑的版本,用 Delphi 7 或 Delphi 2007 最省事,因为 CE 6.8.1 的代码里大量使用了 Delphi 特有的 VCL 组件和Windows单元的直接调用,Lazarus 虽然兼容大部分,但在dbk32驱动接口和TCEForm的某些消息处理上会有差异。如果你手头没有 Delphi 授权,Lazarus 2.0 以上版本配合 FPC 3.2 也能编译通过,但需要手动改几处条件编译指令。
常见做法是:先装 Lazarus,再装 Delphi 兼容包,然后打开cheatengine.lpi。但更稳的路径是直接用 Delphi,因为源码里的CEConfig.pas和CETools.pas引用了Registry和ShellAPI单元,Lazarus 下需要额外配置LCL的对应实现。
2.2 一步步把编译环境跑起来
下面是我实际走通的步骤,以 Lazarus 2.2 + FPC 3.2.2 为例,Delphi 用户可以直接跳到第 4 步。
# 1. 安装 Lazarus 2.2(自带 FPC 3.2.2) # 下载地址自行搜索,安装时勾选“Windows 32-bit”和“Windows 64-bit”目标 # 2. 把 cheat-engine-master 解压到不含中文和空格的路径 # 比如 D:\dev\cheat-engine-master # 3. 进入源码根目录,先检查是否有 Lazarus 工程文件 dir *.lpi # 应该能看到 cheatengine.lpi 和几个子工程的 .lpi # 4. 用 Lazarus 打开 cheatengine.lpi # 菜单 Project -> Project Options -> Compiler Options # 在 “Other” 里加上 -dLAZARUS 和 -dCE68 两个条件编译符号 # 5. 先编译 dbk32 驱动模块(如果不需要内核读写可以跳过) # 打开 dbk32\dbk32.lpi,编译生成 dbk32.sys # 6. 回到主工程,Build All # 如果报错 “Unit not found: Windows”,在项目选项里把 LCLWidgetType 设为 win32编译过程中最常见的报错是Identifier not found: "TCEForm"或Cannot find unit "dbk32"。前者通常是因为条件编译没开对,后者是因为dbk32的搜索路径没加到主工程里。在 Lazarus 的Project Options -> Compiler Options -> Paths里,把dbk32、luaclient、ceserver这几个子目录都加进Other Unit Files搜索路径。
参数说明:-dLAZARUS告诉源码当前是 Lazarus 环境,会启用{$IFDEF LAZARUS}分支里的替代实现;-dCE68是版本标记,影响CEVersion.pas里的常量定义。如果你用的是 Delphi,这两个符号不需要加,但要在Project -> Options -> Directories/Conditionals里确认Conditional defines为空。
提示:编译前先把
cheat-engine-master整个目录复制一份,因为 Lazarus 编译过程中会生成大量.o、.ppu和backup文件,污染原始目录后不好清理。
2.3 编译产物验证与首次运行
编译成功后,你会在cheat-engine-master\bin或工程输出目录下看到cheatengine.exe。双击运行,如果界面能正常弹出,说明主体编译通过。但这时候还别高兴太早——CE 6.8.1 的很多功能依赖dbk32.sys驱动和ceserver服务,如果只是编译了主程序,内存扫描能用,但内核级读写和调试器附加会失败。
验证方法:打开 CE,点击“打开进程”按钮,选择一个记事本进程,然后手动搜索一个已知数值(比如记事本里输入 12345,搜 12345)。如果能搜到地址并修改,说明用户态内存读写链路是通的。如果搜不到,检查dbk32是否加载——在 CE 的“设置”里看“内核模式”是否勾选,以及dbk32.sys是否在C:\Windows\System32\drivers下。
这一步的坑在于:Windows 10/11 默认开启驱动签名强制,自己编译的dbk32.sys没有签名,加载会失败。解决办法是临时关闭驱动签名强制(开机按 F8 选“禁用驱动程序签名强制”),或者用测试签名模式。但注意,这只是为了本地调试,不要在生产环境这么干。
3. 核心模块拆解:内存扫描、Lua 引擎与调试器接口
3.1 内存扫描模块的代码结构与关键参数
CE 6.8.1 的内存扫描核心在memscan.pas和MainUnit.pas里。memscan.pas定义了TMemScan类,封装了ReadProcessMemory、WriteProcessMemory和VirtualQueryEx的调用。扫描流程大致是:先用VirtualQueryEx遍历目标进程的所有可读内存区域,过滤掉PAGE_NOACCESS和PAGE_GUARD的页,然后在每个区域里按扫描类型(精确值、范围、未知初始值)逐字节或逐 DWORD 比对。
关键参数在TMemScan的ScanType和VarType属性里。ScanType决定扫描策略:stExact是精确匹配,stUnknown是未知初始值(先快照再对比变化),stIncreased/stDecreased是变化趋势扫描。VarType决定数据类型:vtByte、vtWord、vtDword、vtSingle、vtDouble、vtString等。这些类型直接影响扫描时的步长和对齐方式。
我一般会先看memscan.pas里的Scan方法,它接收一个TScanRegion数组,然后对每个区域调用ScanRegion。ScanRegion里有一个FastScan优化:如果扫描类型是vtDword且对齐方式是 4 字节,它会用CompareMem按块比对,而不是逐字节循环。这个优化在扫描大内存区域时能差出几倍速度。
如果你想改扫描精度或加自定义类型,改VarType的枚举和GetValueSize函数即可。但注意,CE 的扫描结果列表用的是TList存地址,内存占用会随结果数量线性增长,扫几百万个结果时容易爆内存。常见做法是加一个结果上限,比如MaxResults参数,超过就截断。
3.2 Lua 引擎的集成方式与脚本扩展点
CE 6.8.1 内置了 Lua 5.1 引擎,代码在luaclient和LuaHandler.pas里。Lua 引擎的作用是让用户写脚本自动化操作,比如“每 100ms 读取一次某地址的值,如果大于阈值就修改另一个地址”。集成方式是:CE 启动时初始化 Lua 状态机,然后把 CE 的内部函数注册成 Lua 的全局函数,比如readInteger、writeInteger、getAddress、createHotkey等。
注册过程在LuaHandler.pas的RegisterFunctions里,用lua_register或lua_pushcfunction把 C 函数指针绑到 Lua 名字上。比如readInteger对应LuaReadInteger,内部调用ReadProcessMemory并返回结果。如果你想加自己的函数,比如readFloat或scanPattern,就在这个文件里加一个LuaReadFloat函数,然后在RegisterFunctions里注册。
脚本扩展的另一个入口是autorun目录。CE 启动时会自动执行autorun下所有.lua文件,你可以把常用脚本放进去,省得每次手动加载。我一般会放一个init.lua,里面定义一些快捷函数和默认热键。
注意:Lua 引擎默认没有沙箱,脚本可以调用
os.execute和io.open,这意味着恶意脚本可以干任何事。如果你要把 CE 嵌入自己的工具,记得把os和io库去掉,或者用lua_setglobal覆盖掉。
3.3 调试器接口与反汇编器的对接逻辑
CE 6.8.1 的调试器部分在debugger.pas和vehdebugger.pas里。它支持两种调试模式:Windows 原生调试 API(DebugActiveProcess、WaitForDebugEvent、ContinueDebugEvent)和 VEH(Vectored Exception Handler)模式。原生模式兼容性好但容易被反调试检测,VEH 模式更隐蔽但需要手动处理异常分发。
反汇编器用的是BeaEngine的 Delphi 移植版,代码在disassembler.pas里。TDisassembler类封装了Disassemble方法,输入是一段机器码和起始地址,输出是汇编指令文本和指令长度。CE 的“反汇编”窗口就是调这个类,把目标进程的内存读出来,逐条反汇编显示。
对接逻辑是:调试器在断点命中时,先读当前EIP附近的 16 字节,传给TDisassembler.Disassemble,拿到指令文本后显示在 UI 上。如果你想加自己的反汇编后端,比如换成 Capstone,改TDisassembler的内部实现即可,接口不用动。
参数方面,Disassemble的MaxLen默认是 16,够覆盖 x86 最长指令(15 字节)。ShowEIP参数控制是否在指令前加地址前缀。这些在disassembler.pas的TDisassemblerOptions里可以调。
4. 避坑与排查:编译和运行 CE 6.8.1 源码时最容易翻车的五个点
4.1 现象:编译报 “Unit ‘Windows’ not found”
原因:Lazarus 默认的 LCL 单元里没有Windows单元,它属于 Delphi 的 RTL。CE 源码里大量uses Windows, Messages, SysUtils在 Lazarus 下需要换成LCLIntf, LCLType, LMessages。
解决:在Project Options -> Compiler Options -> Additions and Overrides里加一个Win32的Add unit映射,或者手动在报错的单元里加条件编译:{$IFDEF LAZARUS} uses LCLIntf, LCLType; {$ELSE} uses Windows; {$ENDIF}。更省事的办法是装一个Delphi Compat包,它提供了Windows单元的 Lazarus 兼容层。
4.2 现象:程序启动后闪退,无任何报错
原因:CE 启动时会加载dbk32.sys,如果驱动加载失败且没有异常处理,主程序会直接退出。另外,ceserver的初始化失败也会导致闪退。
解决:用 Debug 模式编译,在cheatengine.dpr的Application.Run前加断点,单步跟到LoadDriver或InitCEServer看是哪一步失败。如果是驱动签名问题,临时禁用签名强制;如果是ceserver端口被占用,改ceserver.ini里的端口号。
4.3 现象:内存扫描能搜到地址但修改无效
原因:目标进程有写保护,或者 CE 没有拿到PROCESS_VM_WRITE权限。CE 默认用OpenProcess拿PROCESS_ALL_ACCESS,但在某些系统上会被降权。
解决:检查OpenProcess的返回值,如果失败,用AdjustTokenPrivileges开启SeDebugPrivilege。代码在CETools.pas的EnableDebugPrivilege函数里,确保它在OpenProcess之前被调用。另外,如果目标进程是 64 位而 CE 是 32 位,写操作会失败,需要编译 64 位版本的 CE。
4.4 现象:Lua 脚本执行时报 “attempt to call a nil value”
原因:脚本里调用了 CE 未注册的函数,或者函数名拼写错误。CE 6.8.1 的 Lua 函数名是大小写敏感的,readInteger和readinteger不一样。
解决:在 Lua 控制台里输入for k,v in pairs(_G) do print(k) end,列出所有已注册的全局函数,确认你要调的函数在列表里。如果不在,检查LuaHandler.pas的RegisterFunctions是否编译进去了。
4.5 现象:反汇编窗口显示乱码或指令错位
原因:BeaEngine的版本和当前 CPU 架构不匹配,或者读取的内存区域跨越了页边界,导致指令截断。
解决:确认disassembler.pas里引用的BeaEngine版本是 4.x 以上,支持 x86-64。如果是 32 位 CE 调试 64 位进程,反汇编会错位,因为指针宽度不同。解决办法是编译 64 位版本,或者在 32 位下只调试 32 位进程。另外,读内存时加一个VirtualQueryEx检查,确保读取范围在同一个可执行页内。
5. 进阶用法:裁剪 CE 6.8.1 源码做一个自己的内存工具
5.1 最小化裁剪:只保留内存扫描和读写
如果你不需要调试器和 Lua 引擎,只想做一个轻量内存修改器,可以把 CE 源码裁剪到最小。核心保留memscan.pas、CETools.pas、MainUnit.pas里的进程列表和扫描结果 UI,去掉debugger.pas、vehdebugger.pas、LuaHandler.pas和luaclient目录。
裁剪步骤:
// 1. 在 cheatengine.dpr 的 uses 里删掉 Debugger, VEHDEBUGGER, LuaHandler // 2. 在 MainUnit.pas 里注释掉所有调用 Debugger 的代码 // 搜索 "Debugger" 关键字,把相关过程调用注释掉 // 3. 在 Project Options 里去掉 luaclient 和 dbk32 的搜索路径 // 4. 重新编译,生成的 exe 体积会从 20MB+ 降到 5MB 左右裁剪后,你得到一个只有进程附加、内存扫描、地址列表和手动修改功能的工具。适合嵌入到自己的辅助工具里,或者作为学习内存扫描的起点。
5.2 加一个自定义扫描类型:以 Float 范围扫描为例
CE 原生支持vtSingle和vtDouble,但范围扫描(比如“搜 1.0 到 2.0 之间的浮点数”)需要手动实现。在memscan.pas的TMemScan.ScanRegion里加一个分支:
// 在 ScanRegion 的 case VarType of 里加: vtSingleRange: begin PSingle := @Buffer[i]; if (PSingle^ >= FFloatMin) and (PSingle^ <= FFloatMax) then AddAddress(RegionStart + i); Inc(i, 4); // 单精度浮点占 4 字节 end;参数说明:FFloatMin和FFloatMax是新增的类字段,在TMemScan.Create里初始化为 0。AddAddress是已有的结果添加方法,内部会检查结果数量上限。注意浮点比较有精度问题,如果范围很窄,建议加一个Epsilon容差。
5.3 验证裁剪和扩展是否成功
验证方法分三步:第一步,编译后运行,确认主界面正常,进程列表能刷新。第二步,附加一个测试进程(比如自己写的一个简单 C 程序,里面有一个全局 float 变量),用自定义的 Float 范围扫描搜到地址并修改,看目标进程的值是否变化。第三步,用Cheat Engine原版对比扫描结果,确认地址一致。
我一般会写一个小的测试程序,里面放几个不同类型的变量(int、float、double、string),然后分别用 CE 原版和裁剪版扫描,对比结果数量和地址。如果地址一致但数量不同,通常是结果上限或对齐方式的问题。
从那以后我每次裁剪 CE 源码,都会先跑一遍这个对比测试,确认核心扫描逻辑没被改坏。希望帮到你。
本文还有配套的精品资源,点击获取