首页 / 资讯中心 / 文章详情

Cheat Engine 6.8.1源码编译:从内存扫描到DBK驱动的完全解析

Cheat Engine 6.8.1源码编译:从内存扫描到DBK驱动的完全解析 ★ FEATURED ARTICLE
简介Cheat Engine 6.8.1源码是一套面向逆向工程、游戏修改与内存调试学习者的完整工程资料可帮助深入理解CE从扫描到修改的底层实现。压缩包共1523个文件体积约8.45MB主要文件类型包括pas、h、c、cpp、lfm、lua等对应Delphi核心逻辑、C/C头文件、界面窗体定义、脚本接口与工程配置目录结构清晰适合按模块研读。源码重点展现了精确扫描、模糊扫描、宽范围扫描等不同查找策略的算法实现也覆盖了内存读写、指针链追踪、Lua自定义脚本、反调试对抗等关键主题同时附带了GUI设计代码与多工程文件方便还原构建。对于想从零搭建调试器、分析游戏保护机制或基于CE二次开发的读者这份源码提供了直接可用的参考案例。目前已有501人学习下载是游戏安全研究和工具开发领域值得反复查阅的原始素材。1. 拿到 cheat-engine 6.8.1 源码之后先别急着编译如果你是因为某个游戏内存数据搞不定、或者想找一个能反复修改内存布局的“调试杠杆”而盯上 cheat-engine-master 这套 6.8.1 源码那恭喜你踩对了一半。Cheat Engine 6.8.1 是最后一个在 master 分支里还能整套编译、带 DBK 驱动源码、并且社区资料最全的版本后面 7.0 以上更多是面向 64 位和反作弊对抗的重构源码复杂度高不少。这套源码适合三类人想搞懂 Windows 内存扫描算法怎么设计的、想给 CE 做二次开发加定制功能的、以及需要用驱动级读写绕过游戏保护做内存分析的。它不是一个开箱即用的“成品”而是你往下游走的起点。我这篇文章就从目录结构、编译链路、核心模块和一堆血泪坑讲起争取让你拿到手之后 24 小时内看到主界面。需要先说清楚CE 是正经的调试辅助工具在逆向工程、软件调试、内存取证里都有合法用途。如果你是冲着破坏公平竞技环境去的那这篇文章帮不了你也请你别这么干。2. 先读懂 cheat-engine-master 的目录6.8.1 的模块划分与选型理由2.1 顶层目录到底放了什么把压缩包解压之后你会看到十几个顶层文件夹。我第一次打开这堆目录时也有点懵但按功能归一下类就很清晰了。核心的源码在Cheat Engine目录下这是 Lazarus 工程的主目录DBK目录是内核驱动工程负责 Ring0 读写Cheat Engine\C\下面是一些 C 写的辅助模块比如lua5.1嵌入引擎的 C 源码、winsock相关组件Cheat Engine\extrafiles放的是发布时打 zip 包的脚本和说明Cheat Engine\Languages是多语言翻译文件。注意没有 bin 目录也就是说你拿到的确实只是源码需要自己编。主程序六个最关键的目录我列一下Cheat Engine\FindUnit是地址扫描的 Pascal 实现Cheat Engine\CFindwhat是核心扫描对话框逻辑Cheat Engine\KernelUtils封装了对 DBK 驱动的事件调用Cheat Engine\ProcessList负责枚举进程、附加进程Cheat Engine\MonoLib和MonoDisAsm负责解析 Mono/.NET 模块和反汇编显示Cheat Engine\ImageLoader管理 PE 镜像加载。如果你只打算改按键脚本或者 UI碰前两个目录就够想理解内存扫描原理CFindwhat和FindUnit是主线想做驱动级读写那得去DBK目录。目录职责修改频率Cheat EngineLazarus 主程序源码、窗体、脚本逻辑高DBK内核驱动DKMain.c、IOCTL 处理低但一改就翻车Cheat Engine\CFindwhat扫描类型精确值/未知初始值、过滤流程中Cheat Engine\KernelUtils用户态到 Ring0 的调用封装中Cheat Engine\ProcessList进程枚举与附加逻辑低Cheat Engine\MonoDisAsmMono 方法反汇编、IL 解析低2.2 为什么偏偏是 6.8.1 这个版本你可以用 git 直接切 taggit tag能看到ce680、ce681这种标记master 分支在 6.8.1 时代还算稳定不像 7.x 主干开发期那样每天变 API。我实际对比过 6.8.1 和 6.8.2 的差异两者的扫描核心几乎一样但 6.8.1 的 Lua 脚本接口更老很多网上流传的脚本是基于这个版本写的兼容性好。另外 6.8.1 自带一个可用的 DBK 驱动在 Windows 7 到 Windows 10 的早期版本上可以直接加载需要关闭驱动签名强制或者进测试模式而 6.8.2 的驱动没有太大变化所以没必要追新。这里的选型理由很简单你以后想搜问题、抄脚本、问别人踩坑经验搜“CE 6.8.1 源码”得到的结果比搜“CE 7.0 源码”多一个量级。2.3 源码工程的构建链先说清楚6.8.1 用 Lazarus 做 IDE编译器是 Free PascalFPC不是 Delphi。很多人第一步就卡在安装版本上Lazarus 2.0 以上打开这个工程会报一堆控件不兼容建议用 Lazarus 1.6.4 FPC 3.0.0老版本里最匹配这个工程的就是这个组合。主工程文件在Cheat Engine\CheatEngine.lpi不是.dproj也不是.sln。驱动工程在DBK\DBK.dproj不对它不是 Delphi 工程你打开DBK\DBKD.dpr会看到一部分 Borland 风格的 Pascal但实际编译它建议走批处理脚本而不是 IDE因为要处理驱动签名和ntoskrnl.lib导入。这里我直接说结论用户态主程序用 Lazarus 打开 lpi 编译驱动用DBK\build.bat配合 WDK 编。编译之前你还需要确认系统里有没有 Lua 的开发库CE 把 Lua 5.1 的 C 源码直接放在Cheat Engine\C\lua5.1里编译时 Lazarus 会自动把lua51.dll编出来但前提是你的 FPC 配置里包含了 C 源码路径。如果缺这一步链接阶段会报cant find lua51.dll我后面在避坑章再展开。3. 从零编译 cheat-engine 6.8.1最小命令集与四步跑通3.1 准备环境时最容易忽视的三个细节不要直接在 Windows 命令行里git clone完就双击 lpi你要先做三件事第一把 Lazarus 1.6.4 装到默认路径装的时候勾选“FPC source directory”这样编译器自带的 RTL 源码路径才有效第二确保项目路径没有中文和空格C:\ce-src可以D:\我的源码\CE大概率连 IDE 都打不开第三如果打算编驱动提前安装 WDK 7或者用 VitualKD/Driver Loader 这类工具而不是事后补。常见做法是先在纯用户态编译一次主程序暂时忽略驱动。也就是说你先用 Lazarus 打开 lpi按 CtrlF9 编译看到CheatEngine.exe生成再去折腾 DBK。很多新手一上来想全编结果驱动部分报错就以为整个工程编不了其实是两个互相独立的工程。3.2 克隆源码并切换到 6.8.1 标记如果你拿到的是一个名为cheat-engine-master的文件夹那就已经省了 clone 这一步。如果是从 GitHub 拉的我用的是这条命令git clone --depth 1 --branch master https://github.com/cheat-engine/cheat-engine.git cd cheat-engine git tag -l git checkout ce681参数说明--depth 1只拉取最新快照不加历史体积小很多CE 仓库里有大量二进制依赖全量克隆可能到 1GB 以上--branch master明确拉 master因为有些镜像默认分支是maingit checkout ce681切到 6.8.1 对应的 tag有的仓库 tag 命名是CE681全大写先git tag -l看一眼再切如果你拿到的压缩包本身就是 master直接解压到纯英文路径就往下走。3.3 用 Lazarus 编译主程序的最小操作打开 Lazarus点File - Open选中Cheat Engine\CheatEngine.lpi。首次打开会弹出一个Packages对话框提示缺少依赖不要慌等它自动加载完。然后做三个配置到Project Options - Compiler Options - Paths把Other unit files里补上..\C\lua5.1;..\..\DBK;相对路径以 lpi 所在目录为基准到Config - Global Options - Files确认 FPC source directory 指向你安装的 FPC 3.0.0 的源码目录最后点Project - Project Inspector检查有没有叫CheatEngine的主程序目标。配置完之后直接按 CtrlF9 编译。正常情况要等两三分钟期间能看到控制台刷一堆Compiling ...。第一次编最容易翻车的地方是模式选择Lazarus 默认是Debug模式Debug模式会产生大量断言代码某些窗口初始化顺序和 Release 不同会出现编译通过但启动即崩的情况。我一般直接切到Release在Project Options - Compiler Options - Debugging里取消Debug info两个勾选再把优化级别设为-O2。编译成功后Cheat Engine\cheatengine.exe或者Cheat Engine\bin\i386-win32\cheatengine.exe就在那里了。我实际在 Windows 10 1709 上跑过双击第一次会弹 Windows Defender 提示选择“仍要运行”就行因为 CE 自己会用调试权限附加进程杀软很容易拦。3.4 编译 DBK 驱动的最小流程驱动不编你只能做普通用户态读写要跨进程读内存、受保护区域读写必须把DBK编出来。DBK目录下有一个build.bat但你直接双击大概率报错原因在于批处理里写死了 WDK 路径。我一般这样改echo off set WDK_ROOTC:\WinDDK\7600.16385.1 set BIN_DIR%WDK_ROOT%\bin\x86 set INCLUDE%WDK_ROOT%\inc\ddk;%WDK_ROOT%\inc\api set LIB%WDK_ROOT%\lib\wxp\i386 cl /DDBK /DKERNEL_MODE /GS- /W3 /O1 -I%INCLUDE% -c DKMain.c link /DRIVER /BASE:0x10000 /SUBSYSTEM:NATIVE -LIBPATH:%LIB% DKMain.obj ntoskrnl.lib参数说明WDK_ROOT要改成你实际安装的 WDK 路径版本 7600 对应 Windows 7 SDK 那代这是和 CE 6.8.1 驱动配合最稳的/DDBK是编译宏决定DKMain.c走NTSTATUS还是用户态兼容路径不定义会编出一堆 Win32 代码link 时必错/GS-关闭缓冲区安全检查内核驱动里栈布局更可控CE 源码就是这么写的/O1体积优先驱动小一点加载更容易过link 成功后得到DKMain.sys这就是你要的驱动文件。加载它需要签名或者测试模式Win7 下直接bcdedit /set testsigning on重启后就可以用sc create和sc start加载。Win10 及以上建议用 VirtualKD 或者双机调试来跑否则驱动签名够你折腾一天。4. CE 6.8.1 核心机制拆解地址扫描、指针追踪、DBK 驱动的设计思路4.1 精确值扫描为什么“从上一次结果中过滤”是灵魂CE 最经典的用法是先搜一个已知血量初值比如 100再让游戏里血量变了搜 80反复几次定位到那个变量地址。这个流程在源码里由FindUnit的FindDialog控制核心数据结构是ScanState它保存了所有候选地址的列表。第一次扫描时CE 会把整个进程的 Virtual Memory 按页读回来用ReadProcessMemory逐页比较匹配的值存入数组后续过滤不是重扫全进程而是只遍历上一次的结果数组让IsAddressMatch再判断一次当前值是否等于你输入的新值。procedure TFindDialog.ScanFilterAgain(GuessValue: Int64; ScanType: Integer); var i, matchCount: Integer; currentValue: QWord; begin matchCount : 0; for i : 0 to LastScanResult.Count - 1 do begin currentValue : ReadProcessMemory(LastScanResult.Addresses[i], ValueSize); if CompareValue(currentValue, GuessValue, ScanType, ValueSize) then begin NewScanResult.Addresses[matchCount] : LastScanResult.Addresses[i]; Inc(matchCount); end; end; ShowResultCount(matchCount); end;逻辑说明LastScanResult是上一次的候选地址数组每次过滤都不重新枚举全进程只精确地读取当前候选地址的值做比较CompareValue根据ScanType决定是“等于”“大于”“小于”还是“变化值”。这里的ValueSize很重要扫描类型选 Byte1字节、2字节、4字节、8字节对应不同的内存读取宽度选错就搜不到。参数说明GuessValue是你输入的目标值必须是十进制的整数浮点扫描另有Single类型ScanType在 6.8.1 里是0精确 1大于 2小于 3变化其中“变化”类型配合未知初始值扫描时不需要输入值直接比较当前值和上次是否不同ValueSize由扫描选项里的“Byte”“Word”“DWord”“QWord”决定注意 CE 的 Word 是 2 字节DWord 是 4 字节和 Windows 的类型定义一致这个设计最值得学习的一点是它把“扫描全进程”和“过滤候选集”分开前者慢但只做一次后者快所以可以反复操作。你以后写自己的内存分析工具照这个思路来性能和体验都会有质的提升。4.2 指针扫描与指针树为什么找到地址还不够很多人扫描拿到一个静态地址后重开游戏又变了这是因为游戏对象都是堆上动态分配的地址每次运行都不同。CE 6.8.1 的指针扫描是解决这个问题的常用手段。源码里PointerScan模块会先把当前地址的“潜在指针链”抓出来从目标地址出发向上层遍历所有被指向该地址的指针记录偏移量和指针的指针生成一棵树。这份源码里PointerScan.pas的TLevelInfo结构保存了每一级节点的偏移范围、过滤条件、最大深度。默认最大深度 5 级、偏移范围从 0 到 0x1000意思是你允许最多 5 层指针跳转每层指针加上任意偏移落在目标地址上。搜完之后你拿到的是类似[[[base0x30]0x50]]的表达式这就是可在重开游戏后重新定位的指针链。注意一个原理性边界指针扫描不是万能的。如果游戏对象是被 array 索引管理的指针跳转深度会非常深默认 5 级就不够用你需要调大MaxDepth但结果数量会指数上涨一秒算出几百万条都有可能扫描会卡死。我一般先 3 级浅扫看能不能锁定到全局句柄再逐步加深而不是一次性 5 级。4.3 DBK 驱动CE 如何在用户态和 Ring0 之间穿梭DBK 是 CE 6.8.1 源码里最“硬”的部分它不是一个完整驱动框架而是一个极简内核扩展注册一个设备\Device\DBK暴露一组 IOCTL 给用户态调用。核心文件DKMain.c里最关键的函数是DKReadProcessMemory和DKWriteProcessMemory它们用KeStackAttachProcess切换到目标进程的上下文然后直接MmCopyVirtualMemory读写。NTSTATUS DKReadProcessMemory(HANDLE pid, PVOID address, PVOID buffer, SIZE_T size) { PEPROCESS targetProcess; PVOID oldProcess; NTSTATUS status; status PsLookupProcessByProcessId((ULONG)pid, targetProcess); if (NT_SUCCESS(status)) { oldProcess KeStackAttachProcess(targetProcess); status MmCopyVirtualMemory(targetProcess, address, CurrentProcess, buffer, size, KernelMode, size); KeStackDetachProcess(oldProcess); ObDereferenceObject(targetProcess); } return status; }逻辑说明PsLookupProcessByProcessId从 PID 找 EPROCESSKeStackAttachProcess让当前线程挂到目标进程的地址空间MmCopyVirtualMemory是微软未公开但稳定的内核 API能够跨进程复制内存最后KeStackDetachProcess切回原进程。这套操作是驱动读写的标准姿势比起暴力改 CR3 寄存器要安全得多。注意KernelMode参数表示这是在内核态发起的复制不经过用户态缓冲区检查所以速度很快也有一定风险。用户态那边KernelUtils.pas里CDBKCommunicator负责打开\\.\DBK设备句柄并通过DeviceIoControl发IOCTL_DBK_READ_MEMORY之类的控制码。整个设计很简洁没有用 WDF纯粹的传统 WDM 思路这也是为什么它在 Win7 上最稳定到 Win10 上容易被 PatchGuard 盯上。4.4 Mono 模块CE 6.8.1 如何对付 .NET 游戏Unity 游戏很多是 Mono 架构CE 6.8.1 专门做了MonoDisAsm来解析 Mono 运行时里的类型和方法表。它扫描进程内存里 Mono 模块的MiniModule结构解析出所有加载的类、字段、方法在Mono Inspector窗口呈现。这个模块的价值在于你不需要像搜普通内存那样盲搜可以直接按类名找字段。源码实现上MonoLib\mono.pas里定义了大量与 Mono 内部结构对齐的 Pascal 记录比如MonoImage、MonoClass、MonoMethod。注意这些结构在不同 Mono 版本里有偏移差异CE 6.8.1 只保证它发布时那几版 Mono 能解析遇到新版 Unity 2019 很容易字段错位。我踩过这个坑在找 Unity 游戏的角色坐标时Mono Inspector 显示类型名乱码字段偏移全错后来直接用步进扫描定位浮点数组解决了所以别神话 Mono 模块。5. 源码使用者的常见翻车点编译、加载、扫描三个阶段的排查记录5.1 编译阶段Lazarus 版本过新导致 LCL 类型定义冲突现象用 Lazarus 2.0 打开 6.8.1 工程编译到一半报TBitmap 未声明或Identifier not found: HTREEITEM。 原因FPC 3.2 的 Win32 头文件里TreeView常量名和 CE 源码里的自定义类型重名新版 LCL 又改了消息处理参数老代码直接炸。 解决装 Lazarus 1.6.4不要用新版强行编译如果一定要用新版可以在Cheat Engine\extrafiles里找官方为新版准备的补丁文件夹里面有Lazarus_2_fixes把那些*.patch打到源码上再编但我试过打补丁后仍有小概率启动崩溃不如直接老版本省心。5.2 编译阶段提示找不到 lua51.dll现象编译通过但运行时弹窗lua51.dll 缺失或者链接阶段报cant find -llua51。 原因Lazarus 工程的库搜索路径没包含C\lua5.1目录FPC 链接器找不到导入库。 解决在Project Options - Compiler Options - Paths - Libraries里加一行..\C\lua5.1然后在Other unit files里也要加同一个路径注意分号分隔不要替换原有内容。改完重新编译Lazarus 会自动编译C\lua5.1下的 C 文件生成lua51.dll如果没自动编就手动打开C\lua5.1\lua5.1.dpr单独编译一次。失败时看编译窗口的Hint如果提示LUA_API没定义说明缺少LUAI_FUNC等宏需要把C\lua5.1\src也加入 include 路径。5.3 运行阶段主程序能起来但附加进程后所有值显示 ??现象打开 CE选中一个进程附加地址列表里的数值全部是问号无法读取。 原因用户态ReadProcessMemory被目标进程的权限限制挡了。CE 的openProcess用的是OpenProcess(PROCESS_ALL_ACCESS)如果你的进程以管理员运行目标进程是更高权限的系统服务或者反作弊驱动保护的进程就会失败。 解决用右键“以管理员身份运行”CE如果还不行加载 DBK 驱动切到内核读写模式。驱动加载后 CE 会自动使用CDBKCommunicator走 Ring0 读写不再依赖用户态的权限检查。我在 Win7 上碰过一次附加一个 32 位游戏什么都没显示加载驱动之后马上能扫了。所以如果只是辅助学习建议一开始就把驱动编好装上。5.4 运行阶段驱动加载失败sc start DBK返回 577现象手动创建驱动服务后启动提示错误码 577 或 1275。 原因577 表示驱动签名验证失败1275 是驱动被阻止加载。CE 6.8.1 的驱动没有签名Win10 x64 默认禁止未签名驱动。 解决正式环境用有签名的版本开发环境进测试模式管理员命令行执行bcdedit /set testsigning on重启后加载即可。如果不想重启用 VirtualKD 的vmsys方式双机调试也能绕过签名。还有一个小坑很多教程让你关Secure Boot但即使关了测试模式也可能被安全启动策略拦建议在 BIOS 里同时关闭 Secure Boot。5.5 扫描阶段结果越来越少到最后只剩 0 条现象用未知初始值扫描一个变量第一次全内存扫描有几十万条结果改了几次值后结果变 0重新搜也搜不到。 原因最常见是扫描类型选错。比如你实际想搜 4 字节浮点却用了默认的 Byte再比如输入新值时值大小不匹配游戏里血量是float你按4 bytes搜能出来但按Byte搜就废了。另一种情况是开启了“写时拷贝”或者内存页被反作弊打了补丁扫描被误导。 解决先确认Scan Type是Exact Value还是Changed ValueValue Type选Float或Double再重扫。对于 0 条切换到 CE 的内存查看器手动跳到目标地址附近看看实际字节长什么样用真实数据反推类型。还有一个取巧办法把扫描结果窗口最下方的Hexadecimal复选框勾上搜十六进制更直观。6. 进阶给 CE 6.8.1 扫描器加一个“连续变化值”过滤并验证自己的改动现在你已经能编译、运行、基本扫描了最后的进阶是把你的定制想法落到源码里。我以“过滤连续值变化”为例教你改一条逻辑并重新编译验证这是能否二次开发 CE 的分水岭。在CFindwhat\FindUnit.pas里找到TFindDialog.ScanFilterAgain它只负责“当前值和目标值比较”。我要加一个 数组值连续自增的过滤规律当目标值是后一次比前一次大 1 时自动匹配。思路不复杂在CompareValue调用前加一个分支判断if ScanType ctIncreasedByOne then begin currentValue : ReadProcessMemory(LastScanResult.Addresses[i], ValueSize); if currentValue LastScanResult.OldValues[i] 1 then begin NewScanResult.Addresses[matchCount] : LastScanResult.Addresses[i]; NewScanResult.OldValues[matchCount] : currentValue; Inc(matchCount); end; Continue; end;逻辑说明LastScanResult.OldValues保存了上一次扫描时每个候选地址的旧值你只需要把当前值拿出来和“旧值 1”比较精确匹配。这种“递增型”过滤在找怪物 ID 递增、导弹编号连续变化这类常见场景里非常实用。参数说明ctIncreasedByOne是你自己新增的枚举类型要在FindUnit.pas顶部的TScanType枚举里加上一项不然编译不过OldValues[i]数组长度必须和Addresses[i]一致这要求每次扫描写入NewScanResult.OldValues时同步记录值CE 原版里只有少数扫描类型会记录旧值要自己补Continue跳过后面的原有比较逻辑减少一次无意义调用改完编译Android 模拟器里跑一个内存值每次 1 的小测试程序确认新过滤规则生效。如果你发现OldValues全为 0那说明上一次扫描时没存旧值要去FindDialog的常规扫描分支里补一句OldValues[i] : currentValue这里是最容易改崩的地方改完记得做回归普通精确扫描还要能正常工作。最后一句话是我最想提醒你的CE 6.8.1 源码的二次开发断点永远比脑补好用。在ScanFilterAgain里打一个断点看LastScanResult.OldValues的填充过程比反复读代码快得多。每次改完不要只验证新功能老功能全扫一遍否则就是给自己埋定时炸弹。这个习惯救过我很多次希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站