从MOV指令到稳定基址:Cheat Engine逆向寻址实战解析
1. 项目概述从一条指令到稳定基址的完整旅程在逆向分析特别是游戏修改或软件调试的领域里找到一个稳定的“基址”是后续所有操作如制作修改器、分析数据流的基石。很多新手朋友拿到一个地址发现重启程序就变了这就是典型的动态地址其背后往往隐藏着一个由多层指针构成的寻址链。今天我就以一个实战案例带大家走一遍从游戏内存里一条最基础的MOV指令开始如何一步步抽丝剥茧最终定位到那个“重启不变”的稳定基址的全过程。这个过程会涉及到对指令流的分析、对寄存器值的追踪、偏移量的计算以及最考验耐心的多层指针定位。我们使用的核心工具是Cheat Engine (CE)它不仅是内存扫描利器更是动态分析的神器。无论你是想了解游戏内存结构还是想深入学习逆向中的地址寻址逻辑这篇手把手的解析都能给你提供一条清晰的路径。2. 核心思路与工具准备2.1 为什么是“MOV指令”在x86/x64汇编中MOV指令数据传送是最常见、最核心的指令之一。当我们锁定一个感兴趣的数据比如角色的生命值、金钱数量时在内存中访问或修改这个数据的代码几乎必然包含MOV指令。例如可能是MOV [RCX28], EAX将EAX寄存器中的值可能是计算出的新生命值写入到以RCX值为基址、偏移0x28的内存地址中。我们的目标就是找到访问我们目标数据的这条关键MOV指令然后逆向推导出这个内存地址RCX28是怎么来的。RCX里的值很可能又是一个从更底层地址“读”出来的指针。这样一层层回溯直到找到一个模块如Game.exe的基址加上一个固定偏移的地址这个地址就是稳定的。2.2 工具链与前期准备工欲善其事必先利其器。除了主角Cheat Engine (CE 7.5或更高版本)我们还需要一些辅助目标程序一个简单的、带有明确数值显示的单机游戏或演示程序例如“植物大战僵尸”的向日葵阳光值或一个自定义的测试程序。选择单机是因为网络游戏通常有更复杂的反作弊机制不适合初学者。CE的基本功你需要已经会用CE进行精确值/未知初始值扫描找到动态地址。这是我们的起点。“什么访问了这个地址”功能这是CE最强大的动态分析功能之一也是本次实战的核心。计算器系统自带的计算器程序员模式即可用于十六进制Hex和十进制Dec的转换与计算。耐心与笔记寻址链可能很长每一步的地址、偏移、寄存器值都需要记录下来逻辑清晰是关键。注意所有分析请仅在合法授权的、自己拥有版权的程序或明确用于学习研究的演示程序上进行。尊重知识产权和法律法规是技术人的底线。3. 实战第一步定位动态地址与关键指令假设我们的目标是修改一个单机游戏中的“金币”数量。我们首先通过CE的常规扫描找到了存储当前金币数量的动态地址例如0x12345678。这个地址每次启动游戏都会变化。锁定动态地址在CE的地址列表中找到这个金币地址并将其添加到下方列表。启用访问监视右键点击地址列表中的这个地址选择“找出是什么访问了这个地址”。CE会弹出一个空窗口。触发访问切回游戏进行一个会读取或更新金币数量的操作比如捡起一枚金币、打开商店界面等。此时CE的监视窗口会立即捕获到一条或多条汇编指令。识别关键MOV指令在捕获的指令中寻找MOV指令。通常我们关注的是将内存数据读入寄存器的指令因为我们要找数据的来源。例如MOV EAX, [ESI000005F8]— 这条指令将地址ESI5F8处的值读入EAX寄存器。如果[ESI5F8]正好是我们的金币地址那么这条指令就是关键。也可能看到MOV [EDI10], ECX这样的写入指令这表示该指令在向某个地址写入数据。我们需要根据上下文判断哪条指令是直接操作我们目标数据的。假设我们找到了这条关键指令MOV EAX, [RCX28]并且确认[RCX28]就是我们的金币动态地址0x12345678。那么RCX28 0x12345678。由此可知RCX寄存器中存储的值是0x12345678 - 0x28 0x12345650。这个0x12345650是一个新的、上一层的动态地址。我们的任务变成了RCX里的这个值0x12345650又是从哪里来的4. 指令流分析与寄存器追踪现在我们需要知道在执行到MOV EAX, [RCX28]这条指令时RCX寄存器里的值0x12345650是如何被赋予的。这需要分析该指令之前的代码。查看指令上下文在CE的“什么访问了…”窗口中通常可以双击那条MOV指令或者CE提供了“显示反汇编程序”的选项这会打开CE内置的反汇编窗口并定位到该指令处。向上回溯分析在反汇编窗口中向上滚动查看前面的指令。我们寻找给RCX赋值的指令。常见的有MOV RCX, [RBP-30]— 从栈RBP-30中取值到RCX。LEA RCX, [RDXRAX*4]— 计算一个有效地址并加载到RCX。MOV RCX, [0xDEADBEEF]— 从一个绝对地址0xDEADBEEF取值到RCX。如果是这样0xDEADBEEF可能就是我们要找的上一层指针地址。CALL或函数返回后RCX可能被用作第一个参数在x64调用约定中其值由调用者传入。假设我们在上方不远处看到了MOV RCX, [R1510]。这意味着当前RCX的值是从地址[R1510]中读取出来的。那么[R1510] 0x12345650。追踪R15问题又转移了R15的值是多少我们需要知道R15的值才能算出R1510这个地址。此时我们可以利用CE的另一个强大功能“找出指令访问的地址”。但更直接的方法是在反汇编器中断点处查看寄存器上下文。在MOV RCX, [R1510]这条指令上设置一个断点在CE反汇编器中通常可以点击地址左侧或按F5。让游戏继续运行并再次触发金币访问比如再捡一次金币。游戏会在断点处暂停。此时CE的寄存器窗口会显示当前所有寄存器的值。我们记下R15的值假设是0x37000000。那么[R1510] [0x37000000 0x10] [0x37000010]。我们之前知道[0x37000010]里存的值是0x12345650。所以地址0x37000010是一个指针它指向了0x12345650。而0x1234565028才是最终的金币地址。现在我们有了一个两层的指针链Level 1: 0x37000010- (存放的值是)0x12345650Level 2: 0x12345650 0x28- (存放的值是)金币数值但是0x37000010很可能还是一个动态地址。我们需要继续追问0x37000010这个地址本身是怎么来的5. 偏移计算与多层指针定位继续向上回溯分析MOV RCX, [R1510]之前的指令看R15是如何被赋值的。假设我们发现MOV R15, [游戏模块.exe0x123456]。这是一条非常关键的指令游戏模块.exe是主程序模块比如Game.exe。它的基址在每次程序加载时由操作系统分配所以是变化的。0x123456是一个固定的偏移量Offset。[游戏模块.exe0x123456]这个表达式计算出一个绝对地址从这个地址中读取出的值存入了R15。我们可以在CE中验证在CE主界面打开“内存查看窗口”Memory Viewer。在地址栏输入“游戏模块.exe”123456CE支持这种表达式。这会计算出当前的绝对地址例如0x50001234。查看0x50001234地址处存储的值应该就是我们之前记录的R15的值0x37000000。那么完整的寻址链就清晰了从固定的模块基址加上固定的偏移A(0x123456)得到第一层指针的地址P1 [[Game.exe0x123456]]。从P1指向的地址加上偏移B(0x10)得到第二层指针的地址P2 [[P10x10]]。从P2指向的地址加上偏移C(0x28)得到最终的数据地址FinalAddr [[P20x28]]这里存储着金币数值。写成CE能理解的指针扫描格式就是[[[“Game.exe”0x123456]0x10]0x28]5.1 使用指针扫描器验证CE的“指针扫描器”功能可以自动化地为我们寻找多层指针链。回到最初的金币动态地址0x12345678这次启动游戏后的地址。右键该地址选择“指针扫描器” - “生成指针映射图”这需要一点时间。生成后打开指针扫描器点击“指针扫描”输入当前金币地址。在结果中我们可以设置过滤条件比如“最终偏移”为0x28因为我们知道最后一层偏移是0x28并寻找基址是Game.exe的指针链。指针扫描器可能会列出多条可能的链。我们需要结合之前手动分析的信息来判断寻找一条链其偏移序列与我们分析的一致例如123456 - 10 - 28并且“静态地址”基址偏移在游戏重启后保持不变。找到正确的链后我们可以将其添加到地址列表。即使游戏重启只要这个指针链是正确的CE就能自动解析出新的动态地址并定位到金币数据。这就是“稳定基址”的意义——它不依赖于某次运行时的内存布局。6. 常见问题与排查技巧实录在实际操作中绝不会总是一帆风顺。下面是我踩过的一些坑和总结的技巧6.1 指令流复杂难以追踪问题反汇编出来的代码跳转JMP、CALL很多逻辑复杂寄存器被频繁覆盖难以追踪值的来源。技巧关注函数开头给疑似函数起始的地址下断点。函数开头通常有PUSH RBP, MOV RBP, RSP等序言参数如RCX, RDX, R8, R9在此时是有效的。使用“找出是什么改写了这个地址”如果你追踪的中间指针地址如0x37000010本身也是被写入的可以用这个功能找到是谁在修改它从而逆向找到它的来源。结合数据断点对中间指针地址设置“内存写入断点”当该地址被写入时中断直接看到写入的指令和来源。6.2 指针扫描结果太多无法判断问题指针扫描器返回成千上万条结果不知道哪条是正确的。技巧重启游戏这是最有效的验证方法。记录下当前找到的疑似稳定指针链基址和偏移序列。关闭游戏重新启动再次扫描金币的新地址。然后在指针扫描器中使用“重新扫描指针映射”功能并指定新的目标地址。如果之前那条指针链是正确的它应该能再次匹配上并且其“静态地址”部分Game.exeOffset不会改变。利用手动分析信息将手动分析得到的偏移如10,28作为过滤条件输入指针扫描器能极大缩小范围。观察“指针计数”和“模块”通常正确的指针链的“指针”数量链的长度不会特别离谱比如超过5层就值得怀疑并且基址模块通常是主程序或核心DLL。6.3 偏移量计算错误问题在手动计算寄存器偏移时搞错了十六进制加减法或者看错了指令是MOV EAX, [ECXEDX]这种带索引寄存器的复杂情况。技巧善用CE的计算器在内存查看窗口的地址栏可以直接输入RCX28这样的表达式CE会帮你计算并跳转。这是最可靠的验证方法。理解LEA指令LEA加载有效地址指令是计算地址而不是读内存。LEA RAX, [RBXRCX*410]计算出的地址是RBXRCX*410这个结果被存入RAX但并没有从该地址读取数据。它常用于数组索引计算。逐字节查看内存在内存查看窗口中对照汇编指令一个字节一个字节地核对地址和数据。对于[BASEINDEX*SCALEDISPLACEMENT]这种复杂寻址耐心分步计算。6.4 基址本身是ASLR的问题即使找到了Game.exe0x123456但Game.exe的基址每次启动也可能因为操作系统的地址空间布局随机化ASLR而不同。不过CE的“模块基址”概念已经处理了这个问题。“Game.exe”在CE中代表的是该模块加载的当前基址它是一个符号。只要模块不变0x123456这个偏移相对于模块基址就是固定的。ASLR影响的是绝对地址但模块内的相对偏移是稳定的。6.5 实战心得保持清晰的记录这是我个人觉得最重要的一点。准备一个文本文件或笔记按以下格式记录每一步[时间] 游戏启动 目标数据金币 首次找到动态地址0x12345678 关键指令MOV EAX, [RCX28] 0xGameFunctionABC RCX 0x12345678 - 0x28 0x12345650 上一条指令MOV RCX, [R1510] 0xGameFunctionAA0 断点处 R15 0x37000000 [0x37000010] 0x12345650 寻找R15来源... 发现MOV R15, [Game.exe0x123456] 验证[[Game.exe0x123456]] 0x37000000 (正确) 最终指针链[[[Game.exe0x123456]0x10]0x28]这样的记录在排查问题、重启验证时无比有用。寻址链的挖掘就像侦探破案从结果数据被改变反推原因谁、从哪里、以何种方式改变了它。这个过程需要对汇编指令有基本的理解更需要耐心和细致的观察。CE工具提供了无与伦比的动态分析能力将这个过程从纯粹的静态反汇编中解放出来让我们能够实时地观察程序的运行状态。当你第一次通过自己手动分析找到一条长达三层的指针链并成功在CE中用[[[...]offset]offset]的形式稳定锁定目标数据时那种成就感是巨大的。这不仅仅是学会了一个技巧更是对程序内存模型和运行时数据结构的一次深刻理解。