ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Cheat Engine内存修改全解析:从数值扫描到指针与代码注入

Cheat Engine内存修改全解析:从数值扫描到指针与代码注入 内存修改这件事很多人第一次接触都是被某个单机游戏卡住——血条见底、金币不够、材料刷到吐。Cheat Engine下称CE就是在这个场景里被反复提起的工具。它本质上是一个内存扫描与调试工具能读取进程的虚拟内存、定位关键数值、追踪访问路径最终让你理解一个程序在运行时到底把数据放在了哪里。这篇内容面向的是想系统搞懂CE工作流程的人从最基础的数值扫描到多级指针的追踪再到把一段自定义逻辑写回目标进程。全程以单机环境下的自我研究与逆向学习为前提不涉及任何线上服务或多人环境。我接触CE断断续续有几年最开始也是照着教程改个金币数字就完事后来发现真正有价值的部分根本不是改数值而是理解程序如何组织内存。数值会变、地址会失效但指针链和数据结构是稳定的。把这条链路吃透你才算真正会用CE而不是只会按首次扫描。1. 先搞清楚CE到底在扫描什么很多人打开CE第一件事就是输入数值点扫描但如果你不知道它在扫什么后面所有操作都是碰运气。这一节把底层概念讲清楚后面才不会迷路。1.1 虚拟内存与进程地址空间一个32位进程启动后操作系统会给它分配一块独立的虚拟地址空间范围通常是0x00000000到0x7FFFFFFF也就是4GB。64位进程则是更大的空间。这块空间不是真实物理内存而是操作系统通过页表映射出来的假地址程序里看到的每一个地址都是虚拟地址。CE扫描的就是这块虚拟地址空间。它通过操作系统提供的调试接口Windows下主要是OpenProcess、ReadProcessMemory、WriteProcessMemory这一组API来读取和写入目标进程的内存。理解这一点很关键CE本身不侵入程序它只是借助系统提供的合法调试通道去读写。所以如果目标进程有反调试保护CE可能连进程都附加不上这是第一道门槛。虚拟内存被划分成一个个页常见页大小是4KB。页有权限属性可读、可写、可执行。CE扫描时默认只扫可读的页因为不可读的页读出来会报错。你在扫描设置里看到的可写可执行过滤选项就是在按页权限筛选。1.2 数值类型决定了扫描方式CE支持非常多的数据类型选错类型是新手最常见的失败原因。下面这张表是我实际用下来最需要记住的几类类型字节数典型场景扫描注意点Byte1等级、状态标志范围0-255超过就溢出2 Bytes2小数值属性范围-32768到327674 Bytes4金币、血量、经验最常用整数默认选它8 Bytes8大数值、时间戳数值很大时优先试Float4带小数的属性精度问题需用范围扫描Double8高精度小数同上String变长名字、文本注意编码ASCII/UTF-16举个实际例子游戏里显示金币是100你选4 Bytes扫100可能扫出几十万个结果。但如果这个数值实际是Float存储的你扫4 Bytes整数就永远找不到。判断方法很简单——看这个数值会不会出现小数。会就优先试Float/Double不会先试4 Bytes。提示Float和Double不要用精确值扫描因为浮点存储有精度误差。100.0在内存里可能是99.99999或100.00001。正确做法是勾选值介于...之间范围设成99.9到100.1。1.3 扫描策略精确值、未知初始值、变动/未变动CE的扫描类型不止精确数值一种这是很多人忽略的强大功能精确数值知道当前值直接扫。适合金币、血量这种明确数字。未知初始值不知道具体值但知道它在变化。先扫一次全内存然后让数值变化再用变动了/未变动逐步缩小范围。血条这种没有数字显示的进度条就靠这个。数值增加了/减少了比变动了更精确适合你知道数值在涨还是在跌的场景。我个人的经验是能拿到精确值就绝不用未知扫描。未知扫描第一次会扫出几百万甚至上千万个地址后续要反复操作游戏让数值变化再一轮轮过滤非常耗时。只有在数值完全不可见时才用它。2. 从一次成功的扫描到定位真实地址扫描出结果只是开始真正的难点在于几万个结果里哪个才是你要的这一节讲怎么一步步收敛。2.1 首次扫描与结果收敛假设目标是一个单机游戏的金币当前显示500。操作流程在CE里点左上角打开进程选中目标进程。数值类型选4 Bytes扫描类型选精确数值输入500点首次扫描。结果可能是几万到几十万个地址。接下来是关键动作回到游戏里让金币发生变化比如花掉一部分变成320。回到CE输入320点再次扫描。结果会大幅缩小可能只剩几个到几十个。重复这个过程两三次通常就能锁定到1-3个地址。这里有个细节很多人不知道再次扫描时不要改数值类型否则CE会重新全量扫描。另外如果数值变化后一个结果都不剩说明你第一次的数值类型选错了或者这个数值根本不是直接存储的可能是加密的、或者是通过计算得出的显示值。2.2 验证地址改一下看反应锁定候选地址后双击把它加到下面的地址列表。然后右键该地址选更改记录→数值改成一个大数比如99999。切回游戏看金币有没有变。变了恭喜这就是真实地址。没变说明这是个显示副本或者无关地址删掉继续试下一个。我踩过的一个坑有些游戏会把数值存两份一份是逻辑值一份是渲染值。你改渲染值画面变了但实际逻辑没变一操作就还原。判断方法是改完之后触发一次游戏内的数值计算比如再花一次钱如果数值立刻跳回原值说明你改的是副本。2.3 找出谁在写这个地址找到地址只是第一步因为这个地址下次启动游戏大概率会变。原因是现代程序普遍使用动态内存分配每次运行分配的地址都不同。要让修改持久化必须找到稳定的访问路径。CE提供了一个核心功能找出是什么改写了这个地址Find out what writes to this address。操作右键地址选找出是什么改写了这个地址。CE会附加一个调试器到目标进程并把这个地址设为写入断点。回到游戏触发一次数值变化比如再花一次钱。CE会捕获到改写指令显示类似mov [eax0x1C], ecx这样的汇编。这条指令信息量极大eax是基址寄存器0x1C是偏移ecx是写入的值。它告诉你程序是通过基址偏移的方式访问这个数值的。这就是指针链的起点。注意附加调试器可能触发目标程序的反调试机制导致程序崩溃或退出。如果出现这种情况说明该程序有保护需要换思路比如用CE的Ultimap或者VEH调试器模式。3. 指针扫描让地址在重启后依然有效这是CE里技术含量最高、也最容易劝退的一环。但只要你理解了原理它其实是有章可循的。3.1 为什么地址会变指针为什么稳定程序里的局部变量存在栈上栈地址每次运行都可能不同。但全局变量和堆上的对象通常通过一个固定的模块基址固定偏移来访问。模块基址是exe或dll加载到内存的起始地址虽然每次运行也可能变ASLR但CE能自动处理这个偏移。指针链的本质是一条多级索引路径[[[基址偏移1]偏移2]偏移3] 目标值。每一级方括号代表一次解引用。只要这条路径上的偏移是固定的无论程序重启多少次你都能顺着它找到目标值。3.2 指针扫描的完整操作假设你已经通过找出是什么改写了这个地址拿到了一条指令比如mov [eax0x1C], ecx并且知道eax的值。接下来在CE地址列表里右键目标地址选生成指针映射Pointer scan for this address。弹出的窗口里最大偏移层级Max level先设3最大偏移值Max offset设2048左右。层级越高、偏移越大扫描越慢但覆盖越全。点确定CE会扫描整个内存找出所有能指向目标地址的指针路径。这个过程可能几分钟到十几分钟。扫完后会得到成千上万条候选路径。关键来了重启目标程序重新找到那个数值的地址这次地址肯定变了然后用指针扫描器里的重新扫描内存功能把新的地址填进去。CE会用新地址去验证之前那批候选路径只保留仍然有效的。重复重启-重扫两到三次候选路径会收敛到几条甚至一条。那条就是稳定的多级指针。3.3 手动验证指针链拿到候选指针后不要直接信。手动验证一遍记下路径比如game.exe0x0012A3B0 → 0x10 → 0x1C。在CE里手动添加地址勾选指针填入基址和偏移。看它指向的值是不是你要的数值。重启游戏再看一次。如果还是对的这条链就成立了。我遇到过一个典型问题指针扫描扫不到东西。原因通常有三个——目标地址本身是动态分配的临时地址比如栈上的最大偏移层级设得太低或者扫描时没有勾选正确的模块。解决办法是提高层级到4-5扩大偏移范围并确保只扫描主模块和必要的dll。4. 代码注入从改数值到改逻辑改数值只是入门CE真正强大的地方是能让你往目标进程里注入自己的代码。这一节讲清楚注入的原理和实操。4.1 代码注入解决什么问题有些数值不是简单存储的而是每帧计算出来的。你改一次下一帧就被覆盖。这时候改数值没用得改产生数值的那段逻辑。比如一个游戏每帧执行health maxHealth - damage你想让health永远满直接改health会被覆盖但如果你把这条指令改成health maxHealth就一劳永逸。CE的自动汇编Auto Assemble功能就是干这个的。它能让你在目标进程里分配一块内存写入自定义汇编代码然后把原来的指令跳转到你的代码执行完再跳回去。4.2 用代码注入实现一个锁血操作流程用找出是什么改写了这个地址定位到写血量的指令比如mov [ebx0x20], eax。右键这条指令选显示反汇编程序会打开反汇编窗口。在反汇编窗口里选中这条指令点工具→自动汇编。CE会自动生成一个模板包含alloc、label、code等段。一个典型的注入脚本长这样[ENABLE] alloc(newmem, 2048, game.exe0x0012A3B0) label(returnhere) label(originalcode) label(exit) newmem: // 这里写你的逻辑 mov eax, [ebx0x24] // 把maxHealth读进eax mov [ebx0x20], eax // 写回health jmp exit originalcode: mov [ebx0x20], eax exit: jmp returnhere game.exe0x0012A3B0: jmp newmem nop returnhere: [DISABLE] game.exe0x0012A3B0: db 89 43 20 // 恢复原始指令字节这段脚本的逻辑是在目标进程里分配一块内存newmem把原本写血量的指令替换成跳转到newmem。在newmem里我们改成从maxHealth读取值写进health实现锁血。执行完跳回原指令的下一条。4.3 注入时的几个致命细节代码注入看着简单但有几个坑必须注意第一指令长度必须对齐。你替换的原始指令如果是5字节比如mov [ebx0x20], eax是3字节但jmp是5字节那替换后必须用nop填充剩余字节否则会破坏后面的指令。CE的自动汇编模板会自动处理但手动写的时候要小心。第二寄存器必须保护。你的注入代码如果修改了原逻辑依赖的寄存器会导致程序崩溃。比如原指令后面还要用eax你在注入代码里把eax改了却没恢复就会出问题。稳妥做法是在注入代码开头pushad结尾popad保存和恢复所有通用寄存器。第三alloc的地址要选对。分配的内存最好靠近目标模块这样jmp能用短跳转2字节减少指令长度问题。CE的alloc支持指定near参数。第四禁用时要能干净还原。[DISABLE]段必须把原始字节写回去否则你关掉脚本后程序会执行到残留的jmp直接崩溃。提示写注入脚本前先在反汇编窗口看清楚原始指令的完整字节。CE的反汇编窗口会显示每条指令对应的机器码这是你还原时的依据。5. 汇编与指针的进阶配合到这一步你已经能改数值、能锁数值、能注入代码了。但真正让修改稳定的是汇编层面的理解和指针链的灵活运用结合。5.1 读懂常见的访问指令CE捕获到的写入指令常见的有这么几类mov [地址], 寄存器直接写入。地址可能是绝对地址也可能是[基址偏移]。mov [寄存器偏移], 寄存器通过寄存器间接写入这是指针链的典型形式。add/sub [地址], 值增量修改常见于扣血、扣钱。movss [地址], xmm0浮点数写入xmm寄存器是SSE指令集用的。看懂这些指令你就能判断出这个数值是怎么被访问的。如果是[寄存器偏移]形式那这个寄存器里存的就是某个对象的基址顺着它就能找到对象结构。5.2 从单条指令推导数据结构假设你捕获到mov [esi0x14], eax并且知道esi指向某个对象。那么0x14就是这个对象里血量字段的偏移。如果你再捕获到mov [esi0x18], ecx那0x18可能是魔法值。慢慢地你就能还原出整个对象的内存布局。这个能力非常有用。比如你想一次性修改多个属性只要找到对象基址就能通过不同偏移访问所有字段。CE的 dissect data structures分析数据/结构功能就是干这个的它能让你定义一个结构体把各个偏移命名方便管理。5.3 多级指针的实际构建回到指针扫描。假设你扫出来的路径是game.exe0x0012A3B0 → 0x10 → 0x1C → 0x20。这条链的意思是从game.exe基址加0x0012A3B0读出一个地址A。A加0x10读出地址B。B加0x1C读出地址C。C加0x20就是目标数值。在CE里添加这个指针时基址填game.exe0x0012A3B0偏移列表依次填0x10、0x1C、0x20。CE会自动逐级解引用。我个人的经验是指针层级不要盲目追求多。层级越多越脆弱中间任何一级对象被释放都会导致链断掉。通常3级以内就能覆盖绝大多数场景。如果扫出来5级以上的链先怀疑是不是扫错了。6. 实战中那些教程不会告诉你的坑前面讲的都是正确流程但实际操作中你会遇到各种意外。这一节专门讲踩坑。6.1 扫描不到数值的几种真实原因原因一数值被加密存储。有些游戏会把金币乘以2再加1存储或者做异或运算。你扫显示值永远扫不到。解决办法是扫未知初始值然后通过变动/未变动逐步逼近最后看捕获到的指令里有没有解密逻辑。原因二数值是浮点但显示为整数。比如显示100实际存的是100.0的Float。这时候用4 Bytes扫不到得用Float扫并且用范围。原因三数值是计算出来的。显示的金币 基础金币 加成金币。你扫显示值内存里根本没有这个数。得分别扫基础值和加成值。原因四进程有保护。某些程序会检测调试器CE附加后直接拒绝访问。这时候可以试试CE的VEH调试器模式或者用内核模式需要额外驱动操作复杂。6.2 指针扫描失败的排查链路指针扫描扫不到东西按这个顺序排查确认目标地址是否稳定。重启程序重新找一次地址如果新旧地址差异巨大且无规律说明这个地址是动态分配的指针扫描可能无效。检查最大偏移层级。默认3级可能不够试4-5级。检查最大偏移值。默认4096可能太小试8192或更大。确认扫描模块。只勾选主exe和必要的dll不要全选。确认目标地址在扫描时是有效的。如果扫描时数值已经变了扫出来的路径是错的。我遇到最坑的一次是目标地址在一个临时对象里每次操作都会重新分配。这种情况下指针扫描根本无解只能通过注入代码在对象创建时hook记录下稳定的访问点。6.3 注入代码导致崩溃的常见原因崩溃基本逃不出这几个指令长度不对齐替换的jmp是5字节原指令只有3字节剩下2字节没处理后面的指令错位。寄存器没保护注入代码改了原逻辑依赖的寄存器。栈不平衡注入代码里push了但没pop。跳转地址错误returnhere标签指向的地址不对跳回去执行了错误的指令。分配的内存被回收alloc的内存如果没被正确引用可能被程序的其他分配覆盖。排查方法CE的反汇编窗口能单步执行你可以一步步看注入代码的执行流程找到崩溃点。7. 把整套流程串起来一个完整的修改案例前面分模块讲了这里用一个完整案例把流程串一遍。目标让某个单机游戏的金币在重启后依然保持修改状态。第一步定位数值。打开CE附加进程4 Bytes精确扫描当前金币值通过消费金币触发变化再次扫描收敛到1个地址。第二步验证地址。改数值看游戏反应确认是真实地址。第三步找写入指令。右键找出是什么改写了这个地址触发一次金币变化捕获到mov [eax0x1C], edx。第四步指针扫描。对目标地址做指针扫描层级设3偏移设4096。扫完后重启游戏重新定位金币地址用重新扫描内存过滤候选路径。重复两次得到稳定路径game.exe0x0012A3B0 → 0x10 → 0x1C。第五步验证指针。手动添加指针地址重启游戏确认有效。第六步持久化。在CE里保存这个指针地址到CT表Cheat Table下次直接加载即可。如果想让修改自动生效可以写一个自动汇编脚本在游戏启动时自动应用。第七步可选注入。如果金币会被游戏逻辑覆盖就注入代码锁定。用自动汇编把写金币的指令改成写入固定值。整个流程走下来你对这个游戏的金币系统就有了完整的理解它存在哪、怎么被访问、怎么被修改、怎么持久化。这套方法论可以迁移到任何单机程序的数值研究上。8. 关于工具边界与学习路径的几点体会CE是个工具工具本身没有对错关键看用在什么场景。我的原则很简单只在自己有权限的环境里做研究比如单机游戏、自己写的测试程序、CTF逆向题。任何涉及线上服务、多人环境、他人系统的操作都不在讨论范围内。从学习路径来说我建议的顺序是先用CE改几个单机游戏的简单数值熟悉扫描流程。然后学找出是什么改写了这个地址理解汇编层面的访问。接着练指针扫描这是分水岭过了这关才算入门。最后学代码注入理解程序逻辑的修改。每一步都不要跳。我见过太多人直接跳到代码注入结果连寄存器是什么都没搞明白写出来的脚本全是崩溃。另外CE自带一个教程程序Tutorial里面有从基础扫描到指针到注入的完整练习。把这个教程从头到尾做一遍比看十篇教程都管用。它每一步都有验证机制你做对了它才让你进下一步非常适合打基础。最后说一个心态问题内存修改这件事失败是常态成功是偶然。一个数值扫不到可能是类型错了、可能是加密了、可能是计算出来的。你需要有耐心去一个个排除。我改一个复杂数值花过整整一个下午但当你最终找到那条稳定的指针链时那种看透程序的感觉是值得的。
返回列表