简介Cheat Engine 6.8.1 完整源码包面向游戏逆向、内存调试与安全分析方向的中高级开发者以及希望二次开发调试工具的工程师。源码覆盖精确扫描、模糊扫描、宽范围扫描等内存查找策略内存读写与虚拟地址转换动态指针链追踪解析Lua 脚本接口实现反调试与反作弊对抗思路以及图形界面控件与事件响应等模块可帮助读者理解 CE 核心算法与工程结构。压缩包共 1523 个文件约 8.45MB以 426 个 pas、181 个 h、152 个 c 等 Pascal/C 源码为主体辅以 144 个 lfm 窗体、127 个 lrt 资源、34 个 cpp、24 个 dll、21 个 lua 脚本及 18 个 asm 汇编文件另有 vcproj、lpi、sln 等多套工程配置便于按模块检索与编译研究。目前已有 500 人学习适合作为逆向工程与内存调试的实战参考。1. 拿到 cheat-engine-master 这份 CE6.8.1 源码先搞清楚它能干什么很多人第一次接触 Cheat Engine都是从修改单机游戏数值开始的搜血量、锁金币、改属性点。但当你把cheat-engine-master这份 CE6.8.1 源码拉下来之后会发现它远不止一个“内存修改器”。它本质上是一整套 Windows 平台下的进程调试、内存扫描、反汇编、脚本注入和插件扩展框架带完整的 Pascal/Delphi 工程、Lua 脚本引擎、DBK 驱动模块和 VCL 界面层。你手里拿到的不是一份“外挂成品”而是一个可以二次开发、可以研究内存扫描算法、可以学习调试器架构的工程源码。这份源码适合三类人一是想深入理解内存扫描与指针追踪原理的逆向工程学习者二是需要在合法授权环境下做软件调试、漏洞分析的安全从业者三是想基于 CE 框架做插件、做自动化脚本工具的开发人员。它解决的核心问题是让你从“只会用工具”变成“能看懂工具怎么跑、能改工具怎么跑”。接下来我会按“先跑通编译、再拆核心模块、最后落到二次开发”的路径把这份源码的落地方式讲清楚。2. 把 CE6.8.1 源码在本地编译跑起来环境、依赖与第一条命令2.1 为什么 CE 源码的编译环境比一般项目更挑Cheat Engine 的主工程是 Delphi 写的CE6.8.1 这个版本对应的是较老的 Delphi 编译链。你如果直接拿最新版 RAD Studio 去打开大概率会碰到一堆“找不到单元”“类型不兼容”“字符串类型不匹配”的报错。常见做法是使用 Delphi 7 到 Delphi 2010 之间的版本社区里用 Delphi 7 和 Delphi 2007 的居多。原因在于 CE 源码里大量使用了 AnsiString、PChar 和旧版 VCL 控件新版 Unicode 编译器会把这些当成错误。除了 Delphi 本体你还需要准备以下组件和依赖依赖项作用注意事项Delphi 7 / 2007 / 2010主工程编译优先选 2007兼容性折中Windows SDK驱动与系统 API 头文件版本不宜过新Lua 5.1 源码CE 内置脚本引擎源码包内通常自带DBK 驱动源码内核层内存访问需要 WD 编译环境SynEdit 控件反汇编与脚本编辑器源码包内一般已集成这里有个血泪经验不要一上来就试图编译整个工程组。CE 的工程文件里包含主程序、驱动、辅助工具、示例插件等多个目标你先把主程序CheatEngine.dpr单独编译通过再去碰驱动和插件。否则一个驱动编译失败会把你的排查方向带偏。2.2 从零编译主程序的最小步骤假设你已经装好了 Delphi 2007并且把cheat-engine-master解压到了D:\ce681\。按下面步骤走# 第一步确认目录结构找到主工程文件 cd /d D:\ce681 dir /s /b CheatEngine.dpr # 第二步检查源码包内是否自带 Lua 和 SynEdit dir /s /b lua*.pas dir /s /b SynEdit*.pas # 第三步用 Delphi 命令行编译如果装了命令行工具 dcc32.exe -B CheatEngine.dpr如果你用的是 IDE操作路径是打开 Delphi → File → Open Project → 选中CheatEngine.dpr→ Project → Options → 确认搜索路径包含lua、SynEdit、dbk等子目录 → Build All。编译过程中最常见的三类报错和处理方式第一类File not found: lua.pas。这说明搜索路径没配全。在 Project Options 的 Directories/Conditionals 里把Source\lua、Source\SynEdit、Source\dbk都加进 Search Path。第二类Incompatible types: AnsiString and UnicodeString。这是 Delphi 2009 及以上版本的 Unicode 问题。解决办法是换用 Delphi 2007 或更早版本或者在代码里显式做AnsiString()转换。我一般建议直接换编译器改代码是无底洞。第三类Unit CheatEngine was compiled with a different version of ...。这是 DCU 缓存不一致。删掉所有.dcu文件重新 Build All 即可。2.3 编译通过后先验证什么主程序编译成功后不要急着去改代码。先做三件事验证环境是通的运行生成的CheatEngine.exe确认界面能正常打开进程列表能加载。打开 CE 自带的 Lua 脚本引擎执行一行print(ce ok)确认脚本层可用。附加到一个你自己写的测试进程上做一次简单的数值扫描确认内存读写链路正常。这三步走完说明你的编译产物是完整可用的。接下来再去拆模块、改功能才有意义。很多人跳过验证直接改代码结果出了问题分不清是编译环境的问题还是自己改的问题这就是典型的“没有后悔药”操作。3. 拆开 CE6.8.1 的核心模块内存扫描、Lua 引擎与 DBK 驱动3.1 内存扫描模块的代码入口与关键参数CE 最核心的能力是内存扫描。在源码里扫描逻辑主要集中在memscan.pas、mainunit.pas和ProcessHandler.pas这几个文件中。你不需要一上来就通读全部先找到扫描线程的入口函数理解它的参数结构。常见做法是搜索TMemoryScanner或ScanThread关键字。你会看到一个扫描任务通常包含以下参数参数含义典型取值ScanType扫描类型精确值/范围/未知初值vtExact、vtUnknownVarType数据类型字节/字/双字/浮点/字符串vtDword、vtSingleStartAddress扫描起始地址0x00000000StopAddress扫描结束地址0x7FFFFFFFProtection内存页保护属性过滤PAGE_READWRITEAlignment地址对齐4 字节对齐这些参数直接决定了扫描速度和命中率。比如你把 StartAddress 设成 0、StopAddress 设成 0x7FFFFFFF又不做 Protection 过滤那扫描一遍 4GB 地址空间会非常慢。实际使用中我一般会先限定模块范围比如只扫主模块的.data段速度能提升一个数量级。扫描线程内部用的是分块读取加多线程比对的方式。源码里可以看到它把地址空间切成若干块每块交给一个工作线程去读、去比对最后汇总结果。这个设计思路值得学分块大小、线程数量、比对算法的选择直接影响扫描效率。3.2 Lua 引擎在 CE 源码里的集成方式CE6.8.1 内置了 Lua 5.1 引擎源码里对应的文件是luahandler.pas和luainterface.pas。它的作用是把 CE 的内部功能暴露成 Lua API让用户可以用脚本控制扫描、读写内存、注册快捷键、画界面。集成方式并不复杂CE 把 Lua 虚拟机初始化在TLuaHandler类里然后通过lua_register系列函数把 Pascal 函数注册进去。你在源码里搜lua_register或RegisterFunction就能看到所有对外暴露的 API 列表。如果你想自己加一个 Lua API步骤是// 在 luahandler.pas 里找到注册区域添加你的函数声明 function MyCustomFunc(L: Plua_State): Integer; cdecl; begin // 从栈里取参数 // 执行你的逻辑 // 把结果压回栈 Result : 1; end; // 在注册过程里加上 lua_register(L, myCustomFunc, MyCustomFunc);逻辑说明Lua 调用 Pascal 函数时参数通过虚拟栈传递。Plua_State就是栈指针你用lua_tointeger、lua_tostring取参数用lua_pushinteger、lua_pushstring压返回值。返回值个数由函数返回的 Integer 决定。参数说明cdecl调用约定不能省否则栈会乱。注册名就是 Lua 里调用的函数名建议用小写加下划线风格和 CE 原有 API 保持一致。3.3 DBK 驱动模块的作用与编译边界DBK 是 CE 的内核层驱动主要用来做跨进程内存读写和调试事件捕获。在源码里对应dbk目录和dbk32.pas、dbk64.pas等文件。它的存在让 CE 能绕过部分用户态限制直接在内核层操作目标进程内存。但这里必须说清楚边界DBK 驱动的编译需要 WDWindows Driver Kit环境而且驱动加载需要管理员权限和测试签名模式。如果你只是做用户态的内存扫描研究完全可以不编译驱动CE 主程序在用户态下也能完成大部分扫描和调试功能。我一般建议的编译顺序是先编译用户态主程序 → 验证功能 → 再单独编译驱动 → 在虚拟机里测试加载。不要在主力工作机上直接加载未签名的测试驱动蓝屏一次你半天的工作就没了。驱动源码里值得关注的是DBK_ReadProcessMemory和DBK_WriteProcessMemory这两个入口。它们处理了地址校验、权限检查和实际的内存拷贝。你如果想理解 CE 是怎么做到“附加到受保护进程”的这两个函数是起点。4. 基于 CE6.8.1 源码做二次开发插件、脚本与自动化4.1 插件系统的加载流程与最小插件示例CE 的插件系统允许你以 DLL 形式扩展功能。源码里插件相关的接口定义在plugin.pas和ceplugins.pas中。一个最小插件需要导出几个固定函数CE 在启动时会扫描插件目录并加载。最小插件的结构大致如下library MyPlugin; uses Windows, plugin; function GetPluginVersion: Integer; stdcall; begin Result : 1; end; function InitializePlugin: Boolean; stdcall; begin // 在这里注册菜单项、快捷键、事件回调 Result : True; end; exports GetPluginVersion, InitializePlugin; begin end.逻辑说明CE 加载插件时先调用GetPluginVersion确认接口版本匹配再调用InitializePlugin执行初始化。你在初始化函数里可以通过 CE 提供的插件 API 注册菜单、注册热键、挂接扫描事件。参数说明stdcall调用约定必须和 CE 的插件接口一致。导出函数名不能改否则 CE 找不到入口。插件编译成 32 位 DLL 还是 64 位 DLL取决于你的 CE 主程序版本两者不能混用。4.2 用 Lua 脚本做自动化扫描的实操如果你不想编译 DLL用 Lua 脚本做自动化是更轻量的路径。CE 的 Lua 引擎可以直接调用扫描 API写一个自动扫描加结果过滤的脚本几十行就能跑。-- 打开目标进程 local pid getProcessIDFromProcessName(test.exe) if pid nil then print(进程未找到) return end openProcess(pid) -- 执行精确值扫描 local scan createMemScan() scan.firstScan( soExactValue, -- 扫描类型精确值 vtDword, -- 数据类型4 字节整数 rtRounded, -- 舍入方式 1000, -- 目标值 , -- 起始地址空表示默认 0, -- 结束地址 0x00000000, -- 保护属性过滤 0x7FFFFFFF, true, -- 是否包含可写内存 false, -- 是否包含不可写内存 false -- 是否只扫对齐地址 ) scan.waitTillDone() -- 获取结果数量 local count scan.getResultCount() print(命中数量: .. count) -- 遍历前 10 个结果 for i 0, math.min(count - 1, 9) do local addr scan.getResultAddress(i) print(string.format(地址: %X, addr)) end scan.destroy()逻辑说明createMemScan创建一个扫描对象firstScan执行首次扫描waitTillDone等待扫描线程结束getResultCount和getResultAddress用来读取结果。这个流程和你在 CE 界面上手动操作是一一对应的。参数说明soExactValue是扫描类型枚举vtDword是数据类型枚举。起始地址和结束地址传 0 和 0x7FFFFFFF 表示全地址空间扫描实际使用中建议缩小范围。rtRounded影响浮点数的比对精度整数扫描时影响不大。4.3 把扫描逻辑封装成可复用的自动化流程单次扫描脚本只能解决一次性问题。如果你想做可复用的工具需要把“附加进程 → 扫描 → 过滤 → 读写 → 监控”串成流程。我一般会封装成几个函数function attachAndScan(processName, value) local pid getProcessIDFromProcessName(processName) if not pid then return nil end openProcess(pid) local scan createMemScan() scan.firstScan(soExactValue, vtDword, rtRounded, tostring(value), , 0, 0x00000000, 0x7FFFFFFF, true, false, false) scan.waitTillDone() return scan end function filterResults(scan, newValue) scan.nextScan(soExactValue, vtDword, rtRounded, tostring(newValue), , 0, 0x00000000, 0x7FFFFFFF, true, false, false) scan.waitTillDone() return scan.getResultCount() end逻辑说明attachAndScan完成首次扫描filterResults在已有结果上做二次过滤。这样你改数值后再过滤一次就能快速缩小候选地址范围。参数说明nextScan的参数和firstScan类似但它是在上一次结果集上做过滤不是重新全量扫描。所以速度更快适合做“改值后重新过滤”的操作。5. 避坑与排查CE6.8.1 源码编译和二次开发中的常见翻车点5.1 编译时报“找不到单元”但文件明明存在现象Delphi 报File not found: xxx.pas你去目录里看文件确实在。原因搜索路径没配全或者路径里有中文、空格导致解析失败。解决在 Project Options → Directories/Conditionals 里把所有子目录都加进 Search Path用相对路径。路径里不要有中文和空格。如果还不行把源码挪到D:\ce681\这种纯英文短路径下再试。5.2 编译通过但运行闪退没有任何报错现象CheatEngine.exe双击后窗口一闪就没了。原因通常是运行时依赖缺失比如 Lua DLL 没放到输出目录或者 DBK 驱动加载失败导致主程序退出。解决用 Debug 模式编译在 IDE 里按 F9 运行看它停在哪一行。常见的是LoadLibrary(lua5.1.dll)失败。把 Lua 的 DLL 复制到 exe 同目录即可。如果是驱动加载失败先在设置里关掉“使用内核模式”。5.3 Lua 脚本调用扫描 API 返回空结果现象脚本执行了firstScan但getResultCount返回 0。原因进程没打开成功或者扫描参数里的数据类型和目标值格式不匹配。解决先确认openProcess返回成功再检查vtDword和值字符串是否对应。比如目标是浮点数你用了vtDword那肯定扫不到。另外注意 CE 的扫描是大小写敏感的字符串扫描时尤其容易翻车。5.4 插件 DLL 加载后 CE 直接崩溃现象把编译好的插件放进插件目录CE 启动时崩溃。原因插件导出函数名不对或者调用约定不匹配或者插件是 64 位而 CE 是 32 位。解决用 Dependency Walker 检查 DLL 的导出表确认GetPluginVersion和InitializePlugin都在且是stdcall。确认位数一致。如果还崩在InitializePlugin里只写Result : True其他什么都不做先确认最小插件能加载再逐步加功能。5.5 修改源码后扫描速度明显变慢现象改了一些扫描相关的代码重新编译后扫描同一个进程速度慢了很多。原因可能是改了分块大小、线程数量或者加了额外的地址过滤逻辑。解决对比修改前后的扫描线程参数。重点看块大小是不是变小了、线程数是不是被限制了、有没有在扫描循环里加了同步锁。我一般会保留一份原始编译产物做基准改完代码后跑同一个扫描任务对比耗时超过 20% 的差异就要查原因。6. 从源码里挖出可复用的调试技巧断点追踪与指针路径验证CE6.8.1 源码里有一套完整的调试器实现集中在debugger.pas和debugthread.pas。很多人只用 CE 的界面功能没注意到源码里的调试事件处理逻辑其实可以单独抽出来用。比如你想在自己的工具里实现“找到访问某个地址的指令”就可以参考 CE 的硬件断点设置流程。核心思路是通过调试寄存器 DR0-DR3 设置硬件断点监听EXCEPTION_DEBUG_EVENT在异常回调里读取CONTEXT结构拿到触发断点的指令地址。源码里对应的函数是SetHardwareBreakpoint和DebugLoop。一个简化的验证流程是这样的// 设置硬件断点监听对目标地址的写入 function SetWriteBreakpoint(hProcess: THandle; ThreadId: DWORD; Address: Pointer): Boolean; var ctx: CONTEXT; begin ctx.ContextFlags : CONTEXT_DEBUG_REGISTERS; GetThreadContext(hProcess, ctx); ctx.Dr0 : DWORD(Address); // 断点地址放入 DR0 ctx.Dr7 : ctx.Dr7 or $00000001; // 启用 DR0 断点 ctx.Dr7 : ctx.Dr7 and not $000F0000; // 设置为写入触发 SetThreadContext(hProcess, ctx); Result : True; end;逻辑说明DR0 存放断点地址DR7 控制断点的启用和触发条件。$000F0000这几位控制断点类型清零后设置为“写入时触发”。设置完以后目标线程每次写入这个地址都会触发调试异常。参数说明hProcess需要有THREAD_GET_CONTEXT和THREAD_SET_CONTEXT权限。ThreadId是目标线程硬件断点是线程级的不是进程级的。DR0-DR3 最多支持四个断点DR7 里对应的控制位不要搞混。验证指针路径的时候我习惯用 CE 的“找出是什么访问了这个地址”功能先在界面上操作一遍然后去源码里对照它调用了哪些函数。这样你能把界面操作和底层实现对应起来比干读代码效率高得多。源码里的debugger.pas有完整的调用链从用户点击菜单到最终设置断点每一步都能跟。最后说一个我自己的习惯每次改 CE 源码之前先把原始版本完整编译一份放到单独的目录里做基准。改完代码后用同一个测试进程、同一个扫描任务、同一组参数对比原始版本和修改版本的耗时和结果。这样你才能确定自己的修改是优化还是负优化。逆向工程和工具开发最怕的就是“感觉快了”实际上一跑数据发现还不如原来。希望帮到你。本文还有配套的精品资源点击获取