如果你搜过“怎么修改单机游戏数值”“CE怎么用”,八成会看到一堆术语绕来绕去的教程,什么“附加进程”“指针扫描”“反汇编”,看着就头大。今天这篇我直接按自己的实操经验来写,把CheatEngine汉化版(以下简称CE)从打开到改内存的完整流程捋一遍,目标是让一个完全没碰过逆向的小白,也能在半小时内亲手改掉一个C++程序里的数值,并且搞清楚每一步到底做了什么、为什么要这么做。
先说明白:CE本质上是一个内存调试工具,它可以读写某个正在运行的程序的内存空间。你玩单机游戏时血量、金币、分数这些数字,最终都会以二进制形式存在内存的某个地址里。CE做的事就是帮你定位那个地址,然后让你自由改值、锁定数值、甚至通过指针分析找到数值的根源。这篇我用的示例是自写的一个C++演示程序,全程可复现,不涉及任何在线服务或他人系统,放心操作。
文章我会分成六个部分:先是概念和环境准备,再走一遍最简单的“精确数值扫描”,然后是“未知初始值扫描”的场景,接着是重头戏“指针与基址”,再往后是常见问题排查,最后聊聊这套技能的合法边界和进阶方向。
1. 动手之前的准备:拆开“逆向”这层窗户纸
1.1 先理解内存的“房号”概念
很多人一听到逆向就慌,其实不需要。程序跑起来之后,Windows会给它分配一块独立的虚拟内存空间,里面装着的就是一堆字节。这些字节有明确的地址,你可以把整个内存想象成一栋超大的公寓楼,地址就是房号,某个int数值就是某个房间里的物品。正常情况下,程序通过变量名或者对象引用来访问这些房间,编译后的机器码其实就是在用“房号”直接取东西。
C++里如果你写一个全局变量int money = 100;,它在编译后基本就是放在某个固定的数据段地址上,程序启动后这个地址就确定了。如果你写的是new出来的对象成员,那它就在堆上,地址是动态分配的——每次运行可能都不一样。这正是CE这两大核心功能“直接搜索地址”和“指针扫描”各自要解决的问题:前者对付固定地址,后者对付动态地址。
1.2 为什么选CheatEngine而不是别的工具
市面上做内存读写的工具不少,比如WinDbg、x64dbg、Process Explorer这些,但它们的定位偏调试与分析,对只想“快速找到那个数值并锁定它”的场景来说太笨重。CE的优势是自带了强大的扫描引擎和指针扫描器,界面友好,而且对新手极其宽容:搜错了可以直接重新搜,锁定失败可以看数值回跳过程,版本更新也快。
汉化版这边多说一句:CE的界面核心是“扫描类型”“数值类型”“十六进制”“锁定”这几个概念,汉化版只是把菜单翻译成中文,功能逻辑和原版完全一致。我下面写操作方法时,会同时标注中文和英文常用名,方便你拿到原版或汉化版都能跟上。
1.3 准备一个可控的C++练手程序
用真实游戏做演示有个麻烦:数值可能被保护、有随机性、不容易观察变化。所以我强烈建议你第一次练习用一个自己写的(或者某教程项目里的)简单C++程序。我这边就用一个模拟控制台小游戏来说明,核心代码如下:
#include <iostream> #include <conio.h> #include <windows.h> int g_hp = 100; // 全局变量,固定地址 int* g_ptrHp = &g_hp; // 指针,指向全局变量 class Player { public: int hp; Player() : hp(100) {} }; int main() { Player* p = new Player(); // 堆上的对象,动态地址 while (true) { system("cls"); std::cout << "=== 演示程序 ===" << std::endl; std::cout << "[Global HP] " << g_hp << std::endl; std::cout << "[Heap Player HP] " << p->hp << std::endl; std::cout << "按 A 减少全局HP,按 S 减少堆HP,按 Q 退出" << std::endl; int ch = _getch(); if (ch == 'a' || ch == 'A') g_hp -= 10; if (ch == 's' || ch == 'S') p->hp -= 10; if (ch == 'q' || ch == 'Q') break; Sleep(50); } delete p; return 0; }这个程序里有两种典型的变量:一个全局变量,地址相对稳定;一个堆上对象成员,地址动态分配。你在CE里改这两种数值的过程,正好覆盖了最核心的两种搜索思路。
注意:如果你没有自己编译程序的习惯,可以随便打开一个单机Demo游戏,只要它能显示数值且不让CE附加失败就行。但千万别拿在线游戏去试,一方面是很多反作弊系统会直接拦截调试器,另一方面是这种操作本身也跨了边界,后面我会专门说。
2. 基础实操:第一次用“精确数值”锁定一个变量
2.1 附加进程:让CE拿到目标程序的读写权限
打开CE,左上角有个小电脑图标,点一下会弹出进程列表。找到你的演示程序进程名(比如DemoApp.exe),选中它,点“打开”。这一步的本质是CE通过系统的调试接口与目标进程建立连接,相当于它获得了这个进程虚拟内存空间的访问权。
实际操作中有个细节容易卡住新手:如果CE提示“无法附加”或“权限不够”,先检查你是不是用管理员身份运行CE。Windows对跨进程读写内存有权限限制,非管理员状态下,很多进程你根本打不开。还有,如果目标程序是以管理员权限启动的,CE也必须是管理员权限,否则两个进程的完整性级别不一致,读写会直接失败。
经验:我习惯先把CE用管理员模式打开,再启动目标程序,再附加。顺序反了容易遇到奇奇怪怪的错误。
2.2 第一轮扫描:精确值匹配
进程附加成功后,CE主界面中间有个大输入框,这就是“值”输入区。默认情况下,扫描类型是“精确数值”,数值类型是“4字节”,这也是最常用的组合。
我们拿全局变量g_hp来试。此时程序界面上显示HP是100,在第一轮扫描的值输入框里填100,点“首次扫描”。CE会把目标进程整个内存空间里所有能被读取到的、4字节值恰好等于100的地址全部列出来。听起来很神奇,其实背后就是一个遍历过程:
- CE枚举这个进程所有已提交的内存区域(堆、栈、静态区等);
- 把每块区域按4字节切分成一个个“候选槽”;
- 用批量指令快速比较每个槽的当前值是否等于100;
- 把所有匹配的地址放入结果列表。
首次扫描的结果集通常非常大,一个普通程序可能瞬间出来几万几十万个候选地址,因为内存里到处都是碰巧值为100的数据。这很正常,别慌,接下来要用“变化”来收敛范围。
2.3 二次扫描:只留“值也变了”的地址
回到演示程序,按一下A,全局HP从100变成90。这时在CE的输入框里填90,点击“再次扫描”(不是首次扫描)。此时CE会拿这份新值,只在上一次的结果列表里筛,把那些当前值恰好是90的地址保留下来。其他的自动淘汰。
为什么这样能快速收敛?因为程序里所有“碰巧等于100”的地址中,只有真正跟HP变量挂钩的那个地址,会在你按A键时也变成90。剩下的地址值没变,不满足“从100变到90”这个条件,自然就被删掉了。你多按几次A、反复搜,候选地址会从几万变成几十、几个,最后就剩一条。
这个过程短则三轮、长则五轮。到结果列表只剩一两个地址时,基本可以确定那就是g_hp的内存位置。你可以双击这个地址,它会加到下方的地址列表里。
注意:二次扫描时,输入框填的是当前的最新值,不是差值。如果你按了两次A,HP变成80,就填80。CE的逻辑永远是对“新旧值本身”做匹配,不是做减法。
2.4 改值与锁定:让数值“钉”在指定大小
地址加入列表之后,双击“值”列下的数字可以直接改成任意int值。比如你把90改成9999,回到程序界面再看,全局HP直接就变成9999了。这说明你确实写进了目标进程的内存。
锁定功能就在这行地址前面,有个小方框,勾上之后CE会以极快的频率反复把该地址的值写成你指定的数值。这招在游戏里就是“锁血”“锁金币”的效果——程序自己再怎么扣数值,CE都会瞬间把它“拉回来”。
这里有个重要认知:CE修内存不是修改程序的机器码,它不碰程序文件,只改运行时的数据。程序本身并没有被改,只是数据变了。所以这个地址一旦关闭CE或程序重启,所有修改就没了——除非你配合指针和自启脚本,那是后话。
2.5 为什么有时会出现“搜到地址但改不动”
我遇到过的典型翻车场景:搜到了唯一地址,双击添加后却发现值列的数值改不了,或者改了马上回弹。原因排查顺序如下:
- 检查CE是否用管理员权限运行;
- 检查附加的是不是目标程序本体进程(有些程序有多个进程,数值在子进程里);
- 检查是否勾选了锁定且锁定的值本身又被程序回调覆盖;
- 检查目标程序的架构是32位还是64位,CE版本要匹配(64位程序用64位CE)。
如果以上都排除了还改不动,就要考虑程序是否故意做了内存校验,这种情况我会在第5章展开讲。
3. 进阶场景:不知道初始值时用“未知初始值”反向扫描
3.1 为什么不能总用“精确数值”
刚才的流程看起来丝滑,但有个前提:你知道当前的准确值。问题是很多场景下你并不知道。比如一个随时间增长的经验条、一个由很多系数叠加出来的伤害值、一个内部用浮点存储但只显示整数的数值——你根本猜不准它的底层数是多少。
这时候就得换个玩法:不告诉CE当前值是多少,而是告诉它“这个值变多了/变少了/没变”,让CE帮你筛选。这招在处理动态显示但又变化规律的数值时极其好用。
3.2 扫描类型对照与选择逻辑
CE的扫描类型面板里,除了“精确数值”,还有“未知的初始值”“变化的数值”“未变化的数值”“增加的数值”“减少的数值”等。我给一个对照表,照着选就行:
| 扫描类型 | 适用场景 | 操作说明 |
|---|---|---|
| 精确数值 | 知道当前确切值 | 每轮填值后点“再次扫描” |
| 未知的初始值 | 不知道当前值 | 首次扫描时选该项,直接扫全内存 |
| 增加的数值 | 值在增长 | 返回程序让值增长,然后选该项并再次扫描 |
| 减少的数值 | 值在减少 | 返回程序让值减少,然后选该项并再次扫描 |
| 未变化的数值 | 值没动 | 返回程序但不让它变,然后选该项并再次扫描 |
这里的核心循环是:回程序操作一下数值 => 回CE“告诉它刚才发生了什么” => 点再次扫描 => 重复。你告诉CE的信息越准确,候选地址收敛得越快。
3.3 用演示程序走一遍“未知初始值”全流程
拿堆上的Player* p举例。假设你不想关心对象地址,只想找p->hp。首次扫描时扫描类型选“未知的初始值”,值类型选“4字节”,直接点首次扫描。这一次的结果范围是整个进程里所有有可读权限的4字节区域,数量巨大,估计有几万甚至几十万。但无所谓,因为接下来你每按一次S,HP都会变小,这个过程就是“减少的数值”。
流程:
- 回程序,按一下S,HP减少10;
- 回CE,扫描类型切成“减少的数值”,点“再次扫描”;
- 重复上面的步骤四五次;
- 等候选数量降到几十个以内后,再用“精确数值”填入当前HP值(比如50)收尾;
- 剩下的唯一地址就是
p->hp在堆上的真实位置。
这个思路的本质,是在完全不知道内存布局的情况下,利用“值的走向”来缩小候选。你不需要知道对象在堆的哪个位置,也不需要知道成员偏移,跟着值的变动走就能定位到它。
3.4 新手最容易翻车的地方
第一个坑:选“减少的数值”之后,如果你返回程序多做了一步无关操作,导致数值没减甚至增加了,那么这一轮的过滤条件就不成立,稳定收敛会被打断。
第二个坑:不要每轮都在输入框填新值,尤其是“未知初始值”模式下,那个值和“精确数值”的语义完全不同。未知初始值模式下,CE根本不关心输入框,只看你选的“变化方向”。如果你一直填数字但不切扫描类型,等于跟CE说“我要精确匹配”,那结果自然不对。
第三个坑:数值类型别选错。C++里int默认就是4字节,但如果程序里用的是double或者float,你按4字节扫就永远扫不到。碰到这种情况,试试“浮点”或者“8字节”,甚至先扫“所有类型”再慢慢收。
4. 从“改一次”到“每次都能改”:指针、偏移与基址
4.1 为什么重启后地址就失效了
接着用我们的堆对象举例。如果你上一章成功找到了p->hp的地址,比如某一次是0x00A3F2B0,然后你把CE存档关掉,重新启动演示程序,再去搜一遍,会发现这次p->hp的地址变了,变成了0x00C52140。为什么?
因为堆地址是程序运行时动态分配的。每次启动,new Player()去操作系统申请内存,操作系统给的位置几乎不可能跟上一次一样。这意味着如果你只记住一个绝对地址,重启之后那个地址里根本不再是HP,可能已经是别的东西了。
那动态地址就没法修改了吗?当然不是。关键思路是:p这个指针变量本身也有它自己的存储位置,而且这个指针变量通常在栈上或数据段上,它的地址相对固定(栈地址每次也会变,但相对位置不变;全局指针的地址则完全固定)。你需要的不是硬编码对象地址,而是“找到指向对象地址的那个指针”,从指针出发按偏移导航。
4.2 指针扫描的第一种做法:手动追踪
在CE里,当你在地址列表里选中那个动态HP地址时,右键可以看到“查找访问此地址的指令”或者“找出改写此地址的指令”。这个功能会在目标进程里下硬件断点,拦下所有读写这个地址的指令,然后把指令显示出来。
比如它可能会告诉你:mov [ebp-0x04], eax或lea eax, [eax+0x08]之类。这些指令里的寄存器或偏移关系,就暗示了访问路径。最常见的形式是某个全局指针加上成员偏移:
[全局基址 + 0x10] -> 指向 Player 对象 Player 对象内 hp 成员的偏移是 0x00 或 0x04这种情况下,你要存的就不是对象地址,而是那个“全局基址”本身。
手动搞一层两层还行,对象结构深了(比如A对象里有B对象,B对象里有C对象,C里才有血量)就麻烦到骨子里。所以CE带了自动化的指针扫描器。
4.3 指针扫描的正确姿势
操作路径:在地址列表里选中那个动态HP地址,右键 => “指针扫描” => 弹窗里设置“偏移”和“深度”,保存一个指针扫描文件,然后重新启动目标程序。重启后重新搜索HP地址,再回到指针扫描器里选“重新扫描”,CE会基于新的目标地址在已保存的内存快照里寻找“能从某个全局地址一步步偏移到这个新地址”的路径。
这里的原理简单说:CE会在你第一次扫描时把目标进程内存里所有可能的指针值记录下来,然后一层层解引用,构建从某个已知静态地址到目标地址的“指针链”。你换了一个新目标地址后,它用同样的偏移链去匹配,最后你就能找到一条稳定的路径,比如:
"DemoApp.exe"+0x0032F0A0 -> [ +0x4 ] -> [ +0x1C ] -> HP其中"DemoApp.exe"+0x0032F0A0是一个模块基址偏移,它和程序文件本身挂钩,每次启动基本都不会变(除非程序升级改偏移)。你把这条链保存在CE的地址列表里,重启后也能正常定位到HP并修改。
注意:指针扫描的偏移深度和最大偏移量如果设置太小,可能漏掉正确路径;设置太大,结果会海量到难以筛选。我个人习惯第一轮用“偏移最大256、深度5”去扫,等候选中找不到稳定链再加大深度。
4.4 基址偏移的存档与“养成好习惯”
找到一条稳定指针链以后,建议把地址列表保存成一个.CT文件,下次启动程序直接加载,不用重新搜索。这是所有CE老手的工作方式:先分析出结构,再保存为可复用的存档。
这部分的实操体会是:搜地址人人都会,难的是“分析出稳定关系”。指针扫描不是万能的,它在目标程序用了虚拟函数表、引用计数、对象池复用的时候可能给出很多无效路径。我的习惯是筛选时多挑那些包含模块名的路径(比如DemoApp.exe+XXXX),因为模块基址比堆地址稳定太多;路径里都是纯堆地址的,基本可以直接淘汰。
5. 常见问题与排查技巧实录
5.1 附加失败、搜不到值、结果列表为空
这算是我被问得最多的三类问题。逐一排:
- 附加失败:进程权限不足或进程已退出,先确认目标程序还开着、CE是管理员运行、且附加的是正确进程;
- 首次扫描就0结果:检查数值类型,常见程序int是4字节,但如果是bool(1字节)、short(2字节)、double(8字节),你按4字节扫自然没有;
- 多次扫描后结果为0:大概率是某一步操作了程序导致值变化方向和你选的“扫描类型”不一致。比如你选了“增加的数值”,但程序把值减小了,那这轮筛选结果就会清空。
遇到结果为0,不要硬扛,直接点“新扫描”重来。新手老手都一样,这不丢人,CE扫描本身就是一个“试错—收敛”的过程。
5.2 锁定了还被改回去
勾选了锁定但数值依然回弹,通常有三种情况:
- 目标程序主动周期性写入该值(比如恢复血量的自然增长),CE的锁定频率没它快;
- 地址其实不是真实存储位置,只是某个临时副本或缓存,程序刷新时会从主存地址同步过来;
- 程序内部有完整性校验,发现数值被改后主动恢复到期望值。
对付前两种,可以通过“查找谁改写了这个地址”反查出写入指令所在位置,然后用debugger把那条指令“覆盖”掉(CE支持给机器码打补丁)。第三种比较折腾,大体思路是找出校验逻辑所在调用点,跳过校验。这一块已经不是入门,我后面单独写一篇。
5.3 汉化版专属的高频故障
汉化版CE在我这边跑过多个版本,最常见的坑有三个:
- 字体或显示乱码:多数是系统区域设置或字体问题,换用官方原版加汉化语言包可解决;
- 快捷键冲突:汉化版有时改了热键布局,导致“查找访问地址”的快捷键无效,去“设置=>热键”里自己重新绑定即可;
- 下载渠道不靠谱导致被杀软拦截:最好自己核对校验值和来源,甚至是原版加语言包的方式,少用打包好的“绿色版”。因为CE本身有驱动组件,很多杀软会误报,不放心就先加白名单再使用。
5.4 搜索速度慢或者内存爆掉
如果目标进程非常庞大(比如Unreal引擎开发的游戏),首次扫描可能耗时几十秒甚至更久,这是正常的。优化方法:
- 数值类型尽量精确,别用“所有类型”;
- 扫描设置里开启“仅扫描可读内存”,减少无效遍历;
- 用“快速扫描”或“并行扫描”选项;
- 如果你只关心某个模块的静态区,可以在“内存扫描选项”里限定内存范围,这需要你懂一点模块布局,新手可以先跳过。
6. 工具的边界意识和下一步怎么走
6.1 什么场景可以用,什么场景不要碰
CE这类内存修改器天然是双刃剑,它本身是中性的调试工具,但用途分出了高低界限。我可以明确给出建议:
- 单机游戏、自研程序、CTF比赛、教学实验:放心用,这是学习系统和调试技巧的好方式;
- 在线多人游戏、有反作弊的竞技游戏:绝对不要碰。这类游戏的服务端校验极其严格,本地修改内存不仅会封号,也破坏了其他玩家的体验,跨了安全和伦理边界;
- 商业软件的授权校验破解:同样不要碰。理由不用多说。
分享一个判断标准:如果你的操作目标只是“让自己运行在本地的一个程序行为发生改变”,且程序本身允许调试,那基本在合法范围内;如果你试图绕开某个在线服务或软件授权体系,那就越界了。
6.2 从“改数值”到“逆向思维”的进阶路线
到这里你其实已经掌握了几个核心概念:进程内存空间、地址与值、读写外部进程、指针与偏移、通过修改指令来改变行为。这些概念不会只属于CE,它们是整个二进制逆向的地基。
如果往后想继续深入,我建议按这个顺序扩展:
- 学会用Cheat Engine的汇编注入功能写脚本,把“改数值”升级为“改逻辑”;
- 开始接触x64dbg或OllyDbg,学会下断点、看调用栈、跟踪寄存器变化;
- 学一点x86/x64汇编,重点看mov、lea、call、jmp这几条指令的实际形态;
- 自己写一个带反调试、带动态计算的C++程序,然后用CE和调试器去拆解它;
- 了解PE文件结构,看看程序启动时模块基址是怎么装载的,这能让你理解为什么有的地址稳定、有的不稳定。
这个过程不需要一口气走完,但方向清楚。逆向本质是一个推理游戏:你看到一个数值,想办法找到它存在的形态,然后解释为什么它会变,最后控制它按你想要的方式变。CE只是第一根拐杖,重要的是你在这过程中建立起来的“内存视野”。
我自己最早练这个的时候,用了一个自己写的贪吃蛇程序,反反复复搜分数地址,学会指针扫描后又在真实游戏里琢磨队列和链表结构,前前后后折腾了一周才算真正理解。所以你要是卡在哪一步上别着急,关掉程序休息一下,回来再看一遍操作流程,往往就通了。如果这篇文章能在你卡壳的时候帮上一把,那就算没白写。