简介:Cheat Engine 6.8.1 完整源码包,面向游戏逆向、内存调试与安全分析方向的中高级开发者,以及希望深入理解动态内存扫描原理的技术人员。源码覆盖精确扫描、模糊扫描、内存读写、指针链追踪、Lua 脚本接口、反调试对抗及图形界面实现等核心模块,可用于二次开发或定制专属调试工具。压缩包共 1523 个文件,约 8.45MB,以 426 个 pas 与 181 个 h 文件构成主体逻辑,辅以 152 个 c、34 个 cpp 及 18 个 asm 文件呈现底层实现,另有 144 个 lfm、127 个 lrt 等界面资源,以及 21 个 lua 脚本、24 个 dll 与多套工程文件,目录结构完整。已有 501 人学习下载。通过研读可掌握扫描算法、虚拟地址转换、指针解析与脚本集成等关键技术,为逆向工程与软件调试提供扎实参考。
1. 拿到 cheat-engine-master 这份 CE6.8.1 源码,先搞清楚它能干什么
很多人第一次接触 Cheat Engine,都是从改一个单机游戏的金币数值开始的。但当你把cheat-engine-master这份 6.8.1 的源码拉下来,事情的性质就变了——你面对的不再是一个改数值的小工具,而是一套完整的 Windows 进程内存读写、调试器、反汇编引擎和脚本运行时的工程实现。这份源码能解决的核心问题是:让你看清一个内存扫描与修改工具从进程附加、内存区域枚举、数值比对到代码注入的完整链路,而不是停留在“搜数值、改数值”的黑盒操作上。
它适合谁?适合已经会用 CE 改单机游戏、想进一步理解扫描算法和调试器集成原理的逆向入门者;也适合做安全工具开发、需要在自己的程序里嵌入内存读写与断点能力的工程师。但要说清楚:这份源码是 Delphi/Pascal 写的,不是 C++,编译链和现代工程习惯差别很大,指望它像普通 GitHub 项目那样clone完就能跑是不现实的。下面按“先立住原理、再动手编译、最后避坑”的顺序拆开讲。
2. 从进程附加到内存扫描:CE6.8.1 源码里的核心链路
2.1 进程枚举与 OpenProcess 的权限选择
CE 能改内存的第一步是拿到目标进程的句柄。在源码里,进程列表来自CreateToolhelp32Snapshot配合Process32First/Next的遍历,这部分逻辑集中在进程列表单元里。拿到 PID 之后,真正决定你能不能读写的是OpenProcess的权限参数。CE 默认会尝试PROCESS_ALL_ACCESS,失败后回退到一组更保守的组合,这个回退逻辑是很多人自己写工具时忽略的。
// 进程附加时的权限回退逻辑(示意,对应源码中进程打开部分) function OpenTargetProcess(pid: DWORD): THandle; var h: THandle; begin // 先尝试全权限,调试场景下最省事 h := OpenProcess(PROCESS_ALL_ACCESS, False, pid); if h = 0 then // 失败后回退:查询信息 + 虚拟机读写 + 虚拟机操作 h := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ or PROCESS_VM_WRITE or PROCESS_VM_OPERATION, False, pid); Result := h; end;逻辑说明:PROCESS_VM_READ/WRITE是读写内存的必需权限,PROCESS_VM_OPERATION决定你能不能改内存保护属性(比如把只读页改成可写再写回),PROCESS_QUERY_INFORMATION用于查询模块和内存区域。参数上,False表示句柄不被子进程继承,调试工具一般不需要继承。失败时先看GetLastError,返回 5 基本就是权限不够,需要提权或换目标。
2.2 内存区域枚举:VirtualQueryEx 与可扫描页的筛选
扫描不是对整个地址空间盲扫,而是先用VirtualQueryEx枚举内存区域,筛掉不可访问和已保留的页。源码里这一步决定了扫描速度和会不会触发目标进程崩溃。关键字段是State、Protect和Type。
| 字段 | 取值 | 是否纳入扫描 | 原因 |
|---|---|---|---|
| State | MEM_COMMIT | 是 | 已提交的页才有实际数据 |
| State | MEM_RESERVE / MEM_FREE | 否 | 保留或空闲页读了没意义 |
| Protect | PAGE_READWRITE | 是 | 最常见的数据区 |
| Protect | PAGE_READONLY | 是 | 只读数据也可能命中 |
| Protect | PAGE_NOACCESS / PAGE_GUARD | 否 | 访问会抛异常 |
| Type | MEM_IMAGE | 视情况 | 代码段,扫数值时通常跳过 |
常见做法是只扫MEM_COMMIT且保护属性不是PAGE_NOACCESS、PAGE_GUARD的区域。PAGE_GUARD是栈增长用的保护页,碰它直接触发异常,这是新手自己写扫描器最容易翻车的地方。
2.3 数值扫描的比对策略:从精确值到未知初值
CE 的扫描分两类:已知数值扫描和未知初值扫描。已知数值扫描直接按数据类型(byte/word/dword/float/double/string)逐字节比对;未知初值扫描先把整个可扫描区域快照下来,之后每轮用“变大/变小/未变/变了”来缩小候选集。源码里这两条路径共用同一套内存快照结构,区别只在比对函数。
// 精确值扫描的核心比对(示意) function CompareExact(buf: PByte; offset: NativeUInt; value: Int64; vtype: TValueType): Boolean; begin case vtype of vtByte: Result := PByte(buf + offset)^ = Byte(value); vtWord: Result := PWord(buf + offset)^ = Word(value); vtDWord: Result := PDWord(buf + offset)^ = DWord(value); vtSingle: Result := PSingle(buf + offset)^ = Single(value); vtDouble: Result := PDouble(buf + offset)^ = Double(value); end; end;逻辑说明:offset是当前扫描位置在缓冲区里的偏移,vtype决定按几个字节解释。参数上要注意浮点比较不能用=直接判等,实际源码里对 float/double 会做一次范围容差判断,否则你搜 100.0 永远搜不到,因为内存里存的是 100.000001 这种近似值。这是浮点扫描最典型的坑。
2.4 首次扫描与下次扫描的状态维护
CE 的“首次扫描 / 下次扫描”不是每次都重新扫全内存,而是维护一个候选地址列表。首次扫描把命中的地址存进列表,下次扫描只在这个列表上重新读值比对。源码里这个列表用动态数组加容量倍增来管理,避免频繁重分配。
// 候选地址列表的追加(示意) procedure TScanResult.Add(addr: NativeUInt); begin if FCount = FCapacity then begin // 容量翻倍,减少重分配次数 FCapacity := FCapacity * 2; SetLength(FAddresses, FCapacity); end; FAddresses[FCount] := addr; Inc(FCount); end;逻辑说明:FCapacity初始给一个合理值(比如 1024),满了就翻倍。参数上,候选列表越大,下次扫描越慢但越不容易漏;列表越小越快但可能因为一次误判丢掉正确地址。实操里如果首次扫描命中几十万条,说明数值类型选错了,比如把 dword 当 byte 搜。
3. 把 CE6.8.1 源码编译起来:环境、依赖与最小验证
3.1 编译环境与 Delphi 版本选择
这份源码是 Delphi 工程,不是 Visual Studio 能直接开的。常见做法是用 Delphi 7 到 Delphi 2010 之间的版本,因为 6.8.1 时代的代码大量使用了老式字符串和 VCL 习惯。用太新的 Delphi(比如 10.x 之后)打开,Unicode 字符串和部分 API 声明会报一堆错,改起来很痛苦。
| 组件 | 推荐选择 | 说明 |
|---|---|---|
| IDE | Delphi 7 / 2007 | 兼容性最好,改动最少 |
| 目标平台 | Win32 | 源码以 32 位为主 |
| 依赖库 | 源码自带 | 部分第三方单元需一并编译 |
| 调试器组件 | 源码内置 | 依赖 Windows 调试 API |
如果手头只有新版 Delphi,也不是不能编,但要预期处理字符串类型和部分已废弃 API 的替换。我的建议是先用 Delphi 7 跑通,再考虑迁移。
3.2 工程文件结构与编译顺序
源码目录里通常有多个.dpr工程文件,主工程是 CE 本体,还有一些辅助工具是独立工程。编译顺序上,先编公共单元,再编主工程。常见做法是直接在 IDE 里打开主.dpr,让 IDE 自动解析依赖,缺哪个单元补哪个。
# 命令行编译思路(需先配置 dcc32 到 PATH) # 进入源码目录后,对主工程执行编译 dcc32 -B CheatEngine.dpr # -B 表示全量重建,避免增量编译残留旧目标文件逻辑说明:-B强制重建所有单元,第一次编译或改了公共单元后必须加,否则可能链接到旧的.dcu。参数上,如果报找不到某个.pas,检查搜索路径里有没有把源码子目录加进去。命令行编译只是备选,多数人还是在 IDE 里点编译更省事。
3.3 编译后最小验证:附加一个记事本进程
编出来之后别急着上游戏,先用记事本验证进程附加和内存读取是否正常。打开记事本,输入一段固定文字,用 CE 附加它,扫描这段文字对应的字符串。能扫到就说明进程枚举、内存读取、扫描链路都通了。
验证步骤: 1. 启动 notepad.exe,输入 "CE681TEST" 并保持窗口打开 2. 启动编译出的 CE,点进程列表,选中 notepad 3. 数值类型选 String,搜索 "CE681TEST" 4. 命中地址应指向记事本进程的堆区逻辑说明:记事本进程结构简单、权限正常,是验证工具链的最小目标。如果这一步扫不到,问题一定在进程附加或内存读取环节,不用怀疑扫描算法。参数上,字符串扫描要注意编码,记事本默认可能是 UTF-16,搜的时候选对编码类型。
3.4 调试器功能的启用条件
CE 的调试器依赖 Windows 调试 API,部分功能需要工具本身有足够权限。如果你在普通用户下跑,附加某些进程会失败。常见做法是以管理员身份运行编译出的 CE,再附加目标。注意这里说的是调试权限,不是让你去搞什么系统级操作,纯粹是SeDebugPrivilege的常规使用。
4. 避坑与排查:编译和扫描阶段最容易翻车的几件事
4.1 编译报“找不到单元”或大量字符串类型错误
现象:打开工程后一编译就报一堆Undeclared identifier或字符串赋值类型不匹配。原因:Delphi 版本不对,新版默认 Unicode 字符串,老代码按 AnsiString 写的。解决:换 Delphi 7/2007 编译,或者逐个把string显式改成AnsiString,但工作量大,不推荐新手硬扛。
4.2 扫描时目标进程直接崩溃
现象:一扫描游戏就闪退。原因:扫到了PAGE_GUARD或PAGE_NOACCESS的页,读取触发访问异常,异常没被正确捕获就带崩了目标。解决:在VirtualQueryEx枚举阶段严格过滤保护属性,只保留可读页;读取用ReadProcessMemory并检查返回值,失败就跳过而不是硬读。
4.3 浮点数怎么搜都搜不到
现象:明明游戏里显示 100,搜 100 搜不到。原因:浮点在内存里是近似存储,且可能是 float 也可能是 double,甚至被乘了系数。解决:先用未知初值扫描,改变数值后用“变大/变小”缩小范围;或者用范围搜索,比如搜 99.9 到 100.1 之间。
4.4 下次扫描结果越来越少最后为空
现象:首次扫描命中很多,几次下次扫描后一条不剩。原因:数值类型选错,或者目标数值在两次扫描之间被程序重写到了新地址(动态分配)。解决:确认数据类型;如果是动态地址,改用指针扫描或找基址加偏移的稳定路径,而不是死盯绝对地址。
4.5 编译出的 CE 附加进程提示权限不足
现象:附加时报错,GetLastError返回 5。原因:目标进程权限更高,当前工具没有调试权限。解决:以管理员身份运行工具;确认目标进程没有反调试保护在拦截OpenProcess。这一步排查顺序是先提权,再怀疑保护。
5. 进阶:用源码里的扫描结构写自己的内存搜索小工具
把 CE 源码读透之后,最有价值的产出不是改 CE 本身,而是把它的扫描结构抽出来,写一个轻量的、只做内存搜索的小工具。我一般会保留三块:进程枚举、VirtualQueryEx区域筛选、候选列表维护,把 UI 和调试器全部砍掉。这样得到的工具几十 KB,启动快,适合嵌到自己的分析流程里。
具体做法是:先照搬区域枚举逻辑,写一个EnumScanRegions(pid)返回可扫描区域数组;再实现一个FirstScan(value, vtype)返回候选地址列表;最后实现NextScan(condition)在候选列表上做增量比对。验证方法是拿记事本做字符串搜索,再拿一个单机游戏做数值搜索,两步都过就说明结构对了。
| 模块 | 保留 | 砍掉 | 理由 |
|---|---|---|---|
| 进程枚举 | 是 | 无 | 基础能力 |
| 区域筛选 | 是 | 无 | 决定稳定性 |
| 候选列表 | 是 | 无 | 扫描核心 |
| 调试器 | 否 | 全部 | 体积大,非必需 |
| 脚本引擎 | 否 | 全部 | 依赖重 |
| UI | 精简 | 复杂面板 | 够用就行 |
一个我踩过的坑:早期我直接把 CE 的候选列表结构照抄,但没注意它内部对地址做了排序去重,导致我的版本在下次扫描时重复读同一地址,速度慢了一倍。后来加上排序和去重才正常。所以抄结构可以,但要想清楚每一步为什么这么设计,不然性能问题会以很隐蔽的方式出现。
另一个习惯是:每次改完扫描逻辑,先用记事本验证字符串搜索,再用一个已知数值的单机游戏验证数值搜索,两步都过才继续往下做。这个习惯帮我省了很多“以为是算法问题、其实是权限问题”的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取