1. 项目概述为什么QT程序的基地址定位是个技术活如果你刚接触逆向工程面对一个用QT框架写的桌面程序想用Cheat EngineCE去改点东西第一步“找基地址”可能就让你卡住了。这感觉就像拿到一把万能钥匙却找不到锁眼在哪。我刚开始玩CE那会儿也在这步上栽过跟头尤其是面对QT这种自带“魔法”的框架程序。网上很多教程讲CE找基地址大多拿简单的C控制台程序举例指针路径简单一步到位。但QT程序不一样它内部有一套复杂的对象模型和事件循环内存布局经常“飘忽不定”直接用常规的“查找访问该地址的代码”可能找到一堆无关的QT内部函数调用让人一头雾水。这个项目的核心就是帮你绕过这些坑快速、精准地定位到QT程序里你关心的那个数据的“老家”——也就是基地址。为什么非要找基地址因为现代程序尤其是QT这种带GUI的几乎都启用了地址空间布局随机化ASLR每次启动模块加载的基地址都不同。你今天找到的数据地址明天程序一重启就变了。基地址是一个相对固定的参考点通常是程序主模块exe的加载地址通过“基地址 偏移”的方式我们才能写出每次重启都有效的修改脚本或外挂。5分钟不是夸张一旦掌握了针对QT程序的特定技巧和避坑思路整个过程确实可以非常高效。接下来我会把整个流程掰开揉碎从原理到实操特别是自动汇编脚本里那些容易让新手崩溃的“坑”都给你讲明白。2. 核心思路与工具准备理解QT程序的内存特征在动手之前我们必须先理解对手。QT不是一个简单的GUI库它是一个完整的应用程序框架。这意味着你用QT写的程序内存里不仅有你的业务逻辑数据还有大量QT运行时库Qt5Core.dll, Qt5Gui.dll, Qt5Widgets.dll等的对象、字符串内部缓存、信号槽连接表等等。这些都会干扰我们的扫描。2.1 核心思路从动态数据回溯到静态指针链我们的目标是从一个动态变化的游戏数据比如血量、金币出发找到一条指向程序主模块exe内部某个固定地址的指针链。这个固定地址就是基地址。QT程序由于大量使用堆分配new来创建界面元素和业务对象这条指针链往往比传统程序更长、更曲折。核心思路可以概括为精确值扫描用CE的精确数值扫描定位到数据在内存中的当前地址。找出是什么改写了这个地址这是关键一步目的是找到直接操作该数据的汇编指令。分析指令上下文查看这条指令访问了哪些寄存器或内存地址这些地址很可能就是指向我们数据的指针。逐级回溯对找到的指针地址再次执行“找出是什么访问了该地址”层层向上直到找到偏移量不再变化、且指向主模块exe内部的地址为止。2.2 工具准备与关键设置工欲善其事必先利其器。除了准备好Cheat Engine建议使用较新的6.8.2版本对64位程序支持更好针对QT程序有几个设置至关重要。Cheat Engine 设置要点扫描设置在CE主界面点击扫描设置-通用扫描设置。务必勾选“可写”、“可读”、“可执行”。QT程序的数据可能位于各种内存页限制过多会导致扫描不到。调试器设置设置-调试器选项。确保调试器已正确安装CE会提示。对于QT程序特别是使用了QML的建议暂时关闭“使用VEH调试器”而尝试使用“使用Windows调试器”有时稳定性更好能更好地处理QT的内部异常。附加进程附加目标QT程序时如果CE列表里没有尝试以管理员身份运行CE。附加后注意观察CE窗口标题栏是否显示“已附加到 [程序名]”。目标程序准备为了演示我建议你自己用QT写一个最简单的测试程序。比如一个窗口里有个标签QLabel显示一个整数一个按钮QPushButton每点击一次就让这个整数加1。这样你就有了一个明确的可修改目标。自己写的程序你清楚数据的来龙去脉验证起来最直观。3. 实战五步法定位QT程序基地址现在我们以那个自制的QT测试程序为例假设我们要修改那个点击增加的整数值。3.1 第一步首次扫描与精确锁定运行你的QT测试程序记下标签上显示的初始值比如100。在CE中打开进程选择你的测试程序。在“数值”框输入“100”扫描类型选择“精确数值”数值类型选择“4字节”对于int类型。点击“首次扫描”。点击测试程序的按钮让数值变成101。回到CE在数值框输入“101”点击“再次扫描”。重复步骤4-5几次直到左侧的地址列表只剩下少数几个理想情况是1个地址。这个地址就是你的数据当前在内存中的位置。注意如果扫描结果太多可能是数值类型选错了。QT中int通常是4字节但如果是qint64或double用于进度等就需要选8字节或浮点数。也可以尝试“未知的初始值”扫描但耗时更长。3.2 第二步下访问断点揪出修改指令这是最关键的一步目的是找到“凶手”——是哪条汇编指令修改了我们的数据。在CE左侧的地址列表中右键点击你找到的地址选择“找出是什么改写了这个地址”。会弹出一个新窗口。回到你的测试程序再次点击按钮改变数值。此时CE的断点窗口会立刻捕获到一条或多条汇编指令。通常最核心的那条指令是mov [寄存器], 数值这种格式。比如mov [rcx04], eax。记录下这条指令。重点来了在QT程序中由于信号槽机制修改数据的代码可能不在主线程或者被内联优化。你看到的指令地址可能位于Qt5Core.dll内部。别慌这很正常。我们的目标不是它而是它操作的数据来源。3.3 第三步分析指令定位上一级指针现在看这条改写指令mov [rcx04], eax。[rcx04]这是我们数据的最终目的地。rcx是一个寄存器里面存放的是一个指针04是这个指针的偏移。所以rcx寄存器里的值就是指向我们数据所在结构的上一级指针。eax是将要写入的新值。我们的任务是找到rcx的值从哪里来。在CE的汇编窗口就是显示mov [rcx04], eax的那一行右键点击该指令选择“分析Analyze”→“分析此命令访问的数据结构”。CE会尝试解析。但更直接的方法是在指令上右键选择“找出指令访问的地址”。或者更常用的是记下rcx的值在指令执行时寄存器窗口可以看到。假设rcx的值是0x1A2B3C4D。在CE的主地址列表区域手动添加一个地址地址就填0x1A2B3C4D。然后对这个新地址再次执行“找出是什么访问了该地址”。3.4 第四步逐级回溯逼近静态区域对0x1A2B3C4D这个地址下“访问断点”后继续在测试程序里点击按钮。这次CE会断在另一条指令上这条指令负责设置rcx的值。它可能长这样mov rcx, [rbx10h]。这说明rcx的值是从内存地址[rbx10h]里加载来的。那么rbx10h这个地址里存放的就是上上级指针。同样我们记下rbx的值比如0x2B3C4D5E然后计算0x2B3C4D5E 10h0x2B3C4D6E。对这个地址再次进行“找出是什么访问了该地址”。重复这个过程就像侦探顺藤摸瓜。你会看到指针链数据地址-[指针A偏移1]-[指针B偏移2]- ... 每次回溯都注意观察CE中“地址”栏旁边的“模块”信息。我们的目标是当“模块”从Qt5Core.dll、Qt5Widgets.dll等QT库最终变成你的程序主模块名.exe时就快成功了。3.5 第五步验证与计算最终基地址与偏移假设经过N层回溯后你找到一条指令mov rdx, [YourProgram.exe0x123456]。这里的YourProgram.exe0x123456就是一个静态地址因为YourProgram.exe的基地址虽然每次启动会变但0x123456这个偏移是固定的。基地址就是YourProgram.exe模块的加载地址。你可以在CE主界面的“内存查看器”窗口或者附加进程时弹出的模块列表中看到它。偏移就是0x123456。最终的指针链可以表示为[[[...[[[YourProgram.exe 0x123456] 偏移1] 偏移2] ... ] 偏移N] 最后偏移] 数据当前地址。为了验证你可以重启你的QT测试程序然后在CE中添加一个新地址。地址类型勾选“指针”。在指针地址框里填入YourProgram.exe0x123456CE会自动计算当前模块基地址偏移。然后在下方的偏移列表中按照你回溯的顺序从最外层到最内层依次添加偏移偏移1 偏移2 ... 最后偏移。如果一切正确这个指针地址应该能正确指向当前的数据值并且重启程序后依然有效需要重新附加进程CE会自动更新基地址。4. 自动汇编Auto Assembler避坑指南从脚本到稳定修改找到基地址和偏移只是拿到了地图。自动汇编AA脚本才是我们实现自动化修改的交通工具。但这里坑最多尤其是对QT程序。4.1 基础AA脚本结构与注入点选择一个最简单的AA脚本目的是锁定我们的数值不变[ENABLE] // 分配内存用于我们的代码 alloc(newmem, 2048) label(returnhere) label(originalcode) label(exit) newmem: // 我们的新代码把要写入的值固定为100 mov eax, 100 // 无论原逻辑想写什么我们都强制写100 // 原始被替换的指令假设是 mov [rcx04], eax originalcode: mov [rcx04], eax exit: jmp returnhere // 目标地址我们之前找到的那条 mov [rcx04], eax 指令地址 目标地址: jmp newmem nop // 如果原始指令字节大于5字节可能需要多个nop填充 returnhere: [DISABLE] // 禁用时恢复原状 目标地址: db 90 90 90 90 90 // 原始指令的机器码需要从CE中复制 // 释放分配的内存 dealloc(newmem)关键避坑点1注入点选择不要想当然地在找到的第一条指令上注入。对于QT程序尤其是通过信号槽触发的修改避免在QT库内部如Qt5Core.dll注入这些代码可能被多个地方共享注入会导致程序其他部分异常。应尽可能回溯到属于你自己程序模块exe的代码段再进行注入。寻找稳定的、每次修改必执行的路径通过多次触发修改观察断点命中情况选择那条100%会执行的指令。有时QT事件分发可能导致代码路径不固定。4.2 寄存器与线程安全QT多线程的陷阱QT程序大量使用多线程GUI主线程与工作线程分离。如果你的数据是在工作线程中被修改的而你的AA脚本注入到了主线程的代码里那永远不会生效。避坑点2线程上下文判断在AA脚本中可以通过判断线程ID来确保代码在正确的线程执行。CE的AA脚本支持GetCurrentThreadId函数。newmem: push rbx mov rbx, GetCurrentThreadId call rbx cmp eax, 目标线程ID // 你需要先查到修改数据时的线程ID jne originalcode // 如果不是目标线程跳回去执行原代码 // 是你的线程执行修改逻辑 mov eax, 100 pop rbx jmp originalcode如何获取“目标线程ID”在CE调试器断在修改指令时查看CE窗口底部的状态栏或寄存器窗口附近通常会显示当前线程ID。4.3 调用约定与栈平衡崩溃的元凶这是新手写AA脚本崩溃的最常见原因。如果你在注入的代码中call了其他函数或者修改了栈指针rsp必须严格遵守调用约定通常是__fastcall或__stdcall并保持栈平衡。避坑点3保存与恢复寄存器环境在你自己编写的代码块开头保存所有你会修改的寄存器通常用push结尾再恢复pop。newmem: push rax push rcx push rdx push r8 push r9 push r10 push r11 // ... 你的代码 ... pop r11 pop r10 pop r9 pop r8 pop rdx pop rcx pop rax jmp originalcode对于x64位程序前四个整数参数通过rcx, rdx, r8, r9传递你需要确保你的操作不会破坏这些寄存器原本的值除非你故意要修改参数。4.4 指针链的动态获取与AOBArray of Bytes注入最理想的AA脚本不应该硬编码基地址偏移而应该动态获取。因为你的程序更新后代码地址可能会变。这时需要使用AOB注入。避坑点4使用AOB扫描定位代码不要记录YourProgram.exe0x123456这样的绝对地址。而是记录目标指令处的一串独特的字节序列机器码。在CE汇编窗口右键目标指令 - “将选定的区域复制到剪贴板” - “字节”。你会得到一串像48 89 41 04 C3这样的十六进制码。这就是AOB。在AA脚本的[ENABLE]部分使用AOBScan函数动态查找地址[ENABLE] aobscanmodule(INJECT, YourProgram.exe, 48 89 41 04 C3) // 扫描特征码 registersymbol(INJECT) // 注册符号方便引用 alloc(newmem, 2048) ... INJECT: // 这里使用符号而不是固定地址 jmp newmem nop ... [DISABLE] INJECT: db 90 90 90 90 90 dealloc(newmem) unregistersymbol(INJECT)这样即使程序更新导致指令位置移动只要特征码没变脚本依然能自动找到正确位置注入。4.5 处理QT内部对象与虚函数表vTableQT对象都是C对象第一个成员通常是指向虚函数表vTable的指针。当你回溯指针链时可能会遇到“指针指向的内容看起来是另一个地址”。这很可能就是vTable指针。避坑点5识别并跳过vTable假设你回溯到一层指针P 查看P指向的内存发现第一个8字节64位程序的值V看起来像一个代码段的地址通常在YourProgram.exe或Qt5Core.dll范围内。那么P很可能是一个对象指针V是它的vTable。此时对象的数据成员通常从P8开始。所以你的下一级偏移应该是8 而不是0。在CE中你可以使用“指针扫描”功能来辅助验证复杂的多级指针链它能帮你找出所有指向目标地址的指针路径对于梳理QT对象网络特别有用。5. 常见问题排查与实战心得即使按照步骤操作你也一定会遇到各种问题。这里记录几个我踩过的坑和解决方法。5.1 问题一CE附加QT程序后程序立刻崩溃或无响应可能原因1反调试检测。一些打包或保护过的QT程序会检测调试器。尝试使用CE的“隐藏调试器”选项设置-调试器选项-使用VEH调试器并勾选尝试隐藏调试器。也可以尝试在程序启动完成后再附加。可能原因2权限不足。以管理员身份运行CE和/或目标程序。可能原因3调试器冲突。如果你开了其他调试工具如OllyDbg, x64dbg先关闭它们。确保Windows的“驱动程序强制签名”等设置不会冲突某些CE版本需要安装驱动。5.2 问题二找到了修改指令但回溯时断点无法命中或命中在无关代码上可能原因1指针链中存在临时指针或缓存。QT内部可能使用临时变量或缓存指针。尝试对找到的指针地址进行“指针扫描”然后从扫描结果中寻找那些看起来“更稳定”的指针例如来自主模块.data或.rdata段的指针。可能原因2多级指针的偏移计算错误。仔细检查汇编指令。是mov reg, [regoffset]还是lea reg, [regoffset]LEA是取地址并不访问内存所以对lea指令的目标寄存器下访问断点是没用的需要对lea指令中[]内的计算出的地址下访问断点。可能原因3代码被优化。编译器优化可能导致指针计算被展开或合并。如果回溯困难可以尝试在编译你的测试程序时关闭编译器优化如GCC的-O0 MSVC的/Od让生成的代码更“直白”便于分析。5.3 问题三AA脚本注入成功但游戏数值没变化或游戏崩溃检查线程99%的QT程序问题出在线程上。确认你的注入点是否在执行修改的同一线程。用GetCurrentThreadId验证。检查栈平衡和寄存器严格按照前面“避坑点3”保存恢复寄存器。特别是RSP和RBP。检查指令覆盖确保你用jmp和nop覆盖的原始指令字节数完全正确。多一个或少一个字节都会导致后续指令错位必然崩溃。使用CE的“自动汇编”模板功能中的“完整注入”有时比手动写更安全。检查内存保护你alloc的新内存页是否有可执行权限CE默认会分配具有执行权限的内存但极少数情况下系统策略可能禁止。可以在alloc后使用VirtualProtect显式设置但CE通常帮你做了。5.4 问题四重启程序后指针链失效原因你的指针链中间某一级可能不是指向主模块的静态地址而是指向了堆Heap上的某个动态分配的对象。这个对象每次启动的地址都不同。解决你需要继续回溯直到找到从静态数据区如全局变量或代码段中直接派生的指针。使用“指针扫描”并保存结果重启程序后再进行“指针扫描”并对比两次结果寻找那些重启后仍指向有效地址的“静态指针”。这通常意味着你需要回溯到更深的层级。5.5 个人实战心得耐心与验证逆向QT程序尤其是商业软件是对耐心和细心的极大考验。我最大的心得有两点分而治之层层验证不要试图一口气找完7、8层指针。每找到一层就手动添加地址并用“手动添加地址”的指针功能验证一下看它是否能正确指向最终数据。验证通过后再以此为起点找下一层。这样能及时发现问题在哪一层。善用CE高级功能结构体分析对疑似对象指针的地址右键“浏览相关内存区域”然后“分析Analyze”→“分析数据结构”CE会尝试解析出结构体成员对于理解QT对象布局有帮助。调用栈当断点命中时查看CE的调用栈窗口。它能告诉你当前函数是被谁调用的帮你理解代码在QT框架中的执行路径有时能直接发现上层调用者持有的对象指针。内存查看器中的反汇编在内存查看器中对数据地址右键“浏览相关内存区域”然后切换到“反汇编”标签可以看到访问这块内存的代码引用这是另一种寻找相关代码的途径。最后记住逆向工程的本质是理解和探索。面对QT程序多一份对框架本身的理解比如信号槽如何在内存在表示QObject的内存布局往往比盲目扫描更能事半功倍。从自己写的小程序开始练手逐步挑战更复杂的目标这个过程积累的经验才是最有价值的。