在实际游戏开发、逆向分析或自动化测试领域,经常会遇到需要理解程序内部数据结构和运行机制的场景。Cheat Engine(简称CE)作为一款经典的内存扫描与调试工具,是许多开发者、安全研究员和自动化脚本作者入门逆向工程的“第一课”。它不局限于游戏修改,更是一种理解进程内存布局、动态数据追踪和外部程序交互的实践窗口。结合Python这类强大的脚本语言,我们可以将CE的手动分析过程转化为可重复、可扩展的自动化脚本,从而构建出具有一定智能(AI)特性的辅助工具,用于数据分析、行为模拟或测试验证。
本文面向有一定Python基础,希望了解如何将CE的手动操作流程自动化,并初步探索“AI外挂脚本”开发思路的开发者。我们将从CE的核心概念讲起,逐步搭建一个能够与目标进程交互的Python环境,并完成一个简单的内存数据读取与修改的自动化案例。学习完成后,你将能够理解CE工具的基本原理,并掌握使用Python控制CE或直接操作进程内存的基础方法,为后续更复杂的自动化逻辑开发打下基础。
1. 理解 Cheat Engine 的核心工作机制
在开始编写任何自动化脚本之前,必须清楚CE在背后做了什么。这并非鼓励违规行为,而是理解Windows程序如何与内存交互的关键知识,广泛应用于软件调试、性能分析和自动化测试。
1.1 进程、内存与指针:数据存在的形式
一个运行中的程序(进程)在操作系统中拥有自己独立的虚拟内存空间。游戏中的金币、血量、坐标等数据,都以特定值(整数、浮点数、字符串等)的形式存储在这个内存空间的某个地址上。这个地址类似于门牌号,而值就是房间里的物品。
然而,每次程序重启,由于动态链接库加载、堆分配等因素,数据存放的“绝对地址”很可能发生变化。为了解决这个问题,程序普遍采用“基地址+偏移”的多级指针寻址方式。基地址通常指向一个模块(如.exe或.dll)的起始位置,这个位置相对固定。通过基地址找到一块动态内存区域,再经过一层或多层偏移,最终定位到存储目标数据的“动态地址”。CE的核心功能之一,就是帮我们找到这个稳定的寻址路径。
1.2 Cheat Engine 的工作流程:扫描、锁定与注入
CE的典型使用流程揭示了与进程交互的基本步骤:
- 附加进程:CE需要获得目标进程的调试或读取权限,才能访问其内存空间。
- 首次扫描:根据已知数值(如100点血量),在进程内存中搜索所有匹配该值的内存地址。结果可能成千上万。
- 过滤扫描:改变游戏内数值(如血量变为95),然后在首次扫描的结果中,搜索数值发生相应变化的地址。反复几次,就能精确定位到存储目标数据的唯一或少数几个地址。
- 分析访问/改写代码:找到地址后,CE可以查看是哪些汇编指令读取或改写了这个地址,这有助于理解游戏逻辑。
- 生成指针:CE可以帮我们分析出指向这个动态地址的稳定指针路径(基地址与偏移)。
- 修改与锁定:可以手动修改该地址的值,或者开启“锁定”功能,让CE定时将其恢复为指定值,实现“无限血量”等效果。
自动化脚本的目标,就是用Python代码复现步骤1、3、5、6,并加入决策逻辑。
1.3 从手动到自动:Python 的角色
Python可以通过ctypes、pymem等库直接调用Windows API(如ReadProcessMemory,WriteProcessMemory)来读写进程内存,实现CE的核心读写功能。更高级的自动化可能涉及:
- 模式扫描:替代CE的扫描界面,用算法在内存中搜索特定字节序列。
- 行为决策:基于读取到的内存值(如敌人坐标、自身状态),由脚本逻辑决定执行什么操作(如移动、攻击)。
- 模拟输入:通过
pyautogui、pydirectinput等库模拟键盘鼠标操作,影响游戏。 - 图像识别:作为内存读取的补充,使用
opencv识别屏幕内容,应对无法直接从内存获取信息的情况。
这种基于状态感知(读内存/识图)和动作执行(写内存/模拟操作)的循环,构成了所谓“AI外挂脚本”的雏形。
2. 环境准备与依赖配置
工欲善其事,必先利其器。我们需要配置一个能够进行进程内存操作的Python开发环境。请注意,以下操作仅用于学习与研究,请在合法授权的程序上进行实践。
2.1 Python 环境与编辑器
确保你已安装Python(推荐3.8及以上版本)。可以使用Anaconda或直接安装官方Python。通过命令行验证:
python --version选择一个顺手的代码编辑器,如VSCode。在VSCode中安装Python扩展后,可以方便地运行和调试脚本。
2.2 核心 Python 库安装
我们将使用几个关键的库。通过pip在命令行中安装:
pip install pymem pip install pywin32 pip install keyboard pip install pyautogui- pymem:一个封装了Windows内存操作API的友好库,比直接使用
ctypes更简单。 - pywin32:提供对Windows API更广泛的访问,某些高级操作会用到。
- keyboard/pyautogui:用于模拟用户输入。注意,
pyautogui的模拟可能被某些游戏屏蔽,此时可考虑pydirectinput(需额外安装)。
2.3 目标程序与 Cheat Engine
准备一个用于学习的目标程序。强烈建议使用专门为内存修改学习设计的单机小游戏,例如“植物大战僵尸”、“经典扫雷”或“Cheat Engine自带教程程序”。切勿在在线多人游戏或受保护商业软件上尝试,这可能导致法律纠纷或封号。
同时,下载并安装Cheat Engine。我们将用它进行手动分析,获取我们自动化脚本所需的关键地址和偏移信息。
3. 实战:从 CE 分析到 Python 自动化脚本
我们以一个假设的、简单的单机游戏为例,目标是自动化修改游戏中的“金币”数量。假设游戏进程名为MyTestGame.exe。
3.1 第一步:使用 CE 手动定位金币地址与指针
这是自动化之前不可或缺的“侦察”阶段。
- 打开游戏和CE:运行
MyTestGame.exe,记下当前金币数量(例如500)。 - CE附加进程:在CE中点击左上角电脑图标,选择
MyTestGame.exe进程。 - 首次扫描:数值类型选择“4字节”(许多游戏整数用4字节存储),在数值框输入“500”,点击“首次扫描”。
- 改变数值并过滤:回到游戏,通过正常操作让金币发生变化(如消费变成450)。回到CE,在数值框输入新值“450”,点击“再次扫描”。
- 定位地址:重复步骤4,直到左侧地址列表只剩下少数几个地址。尝试逐个修改这些地址的值,并在游戏中确认金币是否变化,从而找到真正的金币地址。假设找到的动态地址是
0x1A2B3C4D。 - 找出指针:右键点击找到的地址,选择“找出是什么改写了这个地址”。然后在游戏中触发一次金币变化(如再消费),CE会记录下访问该地址的汇编指令。更重要的是,我们可以点击“找出访问该地址的指针”。经过分析,CE可能会给出一个指针路径,例如:
这里的MyTestGame.exe+002A34D0 -> 偏移 18 -> 偏移 10 -> 偏移 5CMyTestGame.exe+002A34D0就是模块基址+静态偏移,后面的18,10,5C是逐级偏移。
3.2 第二步:编写 Python 脚本读取进程内存
现在,我们使用Python和pymem来复现CE的读取功能。
首先,创建一个Python脚本文件game_helper.py。
import pymem import pymem.process import sys def find_game_process(process_name): """查找目标游戏进程""" try: pm = pymem.Pymem(process_name) print(f"[+] 成功附加进程: {process_name} (PID: {pm.process_id})") return pm except pymem.exception.ProcessNotFound: print(f"[-] 未找到进程: {process_name},请确保游戏已运行。") sys.exit(1) except Exception as e: print(f"[-] 附加进程时发生未知错误: {e}") sys.exit(1) def read_pointer_chain(pm, module_name, base_offset, offsets): """ 通过多级指针链读取最终值 :param pm: Pymem对象 :param module_name: 模块名,如 'MyTestGame.exe' :param base_offset: 模块基址的静态偏移 :param offsets: 偏移量列表,如 [0x18, 0x10, 0x5C] :return: 读取到的整数 """ try: # 获取模块基地址 module_base = pymem.process.module_from_name(pm.process_handle, module_name).lpBaseOfDll print(f"[*] 模块 {module_name} 基地址: {hex(module_base)}") # 计算第一级地址 addr = module_base + base_offset print(f"[*] 基址+静态偏移: {hex(addr)}") # 逐级遍历指针链 for i, offset in enumerate(offsets): # 读取当前地址存储的值,这个值就是下一级的地址 addr = pm.read_int(addr) if addr == 0: raise ValueError(f"在偏移 {i} 处读到空指针(0)。") addr += offset print(f"[*] 第{i+1}级 -> 地址: {hex(addr - offset)}, 加偏移{hex(offset)} -> {hex(addr)}") # 读取最终地址的值 value = pm.read_int(addr) print(f"[+] 读取成功!地址 {hex(addr)} 的值为: {value}") return value, addr except Exception as e: print(f"[-] 读取指针链失败: {e}") return None, None if __name__ == "__main__": PROCESS_NAME = "MyTestGame.exe" MODULE_NAME = "MyTestGame.exe" BASE_OFFSET = 0x002A34D0 # 从CE获取的静态偏移 OFFSETS = [0x18, 0x10, 0x5C] # 从CE获取的多级偏移 # 1. 附加进程 pm = find_game_process(PROCESS_NAME) # 2. 尝试读取金币 gold_value, gold_address = read_pointer_chain(pm, MODULE_NAME, BASE_OFFSET, OFFSETS) if gold_value is not None: print(f"当前金币数量: {gold_value}") # 这里可以加入更多逻辑,比如判断金币少于某个值时自动修改关键代码解释:
pymem.Pymem(process_name):核心对象,用于连接和操作目标进程。pm.read_int(address):从指定内存地址读取一个4字节整数。根据数据类型,还有read_float,read_string等方法。- 指针链遍历:这是最关键的逻辑。我们从一个“基地址+静态偏移”的地址开始,读出的值是一个指针,加上下一个偏移得到新地址,再读,再加偏移……直到最后一级偏移,读出的值才是我们想要的数据。
- 错误处理:内存操作极易因地址无效、权限不足、进程退出而失败,必须用try-except包裹。
3.3 第三步:实现自动化修改与简单逻辑
现在,我们为脚本增加写入内存和基于条件的自动化逻辑。
def write_memory_int(pm, address, value): """向指定地址写入一个整数""" try: old_value = pm.read_int(address) pm.write_int(address, value) print(f"[+] 写入成功!地址 {hex(address)} 的值从 {old_value} 改为 {value}") return True except Exception as e: print(f"[-] 写入内存失败: {e}") return False def auto_gold_maintain(pm, module_name, base_offset, offsets, threshold=1000, set_value=5000): """ 自动维护金币:当金币低于阈值时,自动设置为指定值 :param threshold: 金币阈值 :param set_value: 要设置的金币值 """ print(f"[*] 启动金币自动维护,阈值: {threshold}, 设置值: {set_value}") try: while True: gold_value, gold_address = read_pointer_chain(pm, module_name, base_offset, offsets) if gold_value is None: print("[-] 读取金币失败,循环终止。") break if gold_value < threshold: print(f"[!] 金币 {gold_value} 低于阈值 {threshold},尝试修改...") if write_memory_int(pm, gold_address, set_value): print("[+] 金币修改完成!") else: print("[-] 金币修改失败。") else: print(f"[*] 金币充足: {gold_value}, 无需操作。") # 等待一段时间再检查,避免循环过密 import time time.sleep(2) # 每2秒检查一次 except KeyboardInterrupt: print("\n[!] 用户中断,退出自动维护。") except Exception as e: print(f"[-] 自动维护循环发生错误: {e}") # 在主函数中调用自动化逻辑 if __name__ == "__main__": PROCESS_NAME = "MyTestGame.exe" MODULE_NAME = "MyTestGame.exe" BASE_OFFSET = 0x002A34D0 OFFSETS = [0x18, 0x10, 0x5C] pm = find_game_process(PROCESS_NAME) # 选择模式 print("选择模式:1. 单次读取 2. 自动维护金币") choice = input("请输入选项 (1 或 2): ") if choice == '1': gold_value, _ = read_pointer_chain(pm, MODULE_NAME, BASE_OFFSET, OFFSETS) if gold_value is not None: print(f"=== 当前金币: {gold_value} ===") elif choice == '2': # 启动自动维护循环 auto_gold_maintain(pm, MODULE_NAME, BASE_OFFSET, OFFSETS, threshold=1000, set_value=5000) else: print("无效选择。")这个脚本实现了一个简单的“AI决策”:持续监控金币数值,当低于1000时,自动将其修改为5000。这就是自动化脚本从“读取”到“决策”再到“写入”的基本闭环。
4. 运行验证、问题排查与进阶思考
4.1 如何运行与验证脚本
- 首先运行目标游戏
MyTestGame.exe。 - 在命令行或VSCode中运行你的Python脚本:
python game_helper.py。 - 根据提示选择模式。如果选择模式2,脚本会开始循环打印日志。此时你可以在游戏中消费金币,观察脚本日志是否会检测到并执行修改,同时查看游戏内金币是否真的被改回5000。
注意:首次运行时很可能失败,因为从CE获取的
BASE_OFFSET和OFFSETS是基于你当时游戏进程的。如果游戏重启,基地址可能变化,但“模块名+静态偏移”通常不变。如果指针链失效,需要重新用CE分析。
4.2 常见问题排查表
在开发此类脚本时,你会遇到各种问题。下表列出了常见现象、原因及解决思路:
| 问题现象 | 可能原因 | 检查与解决思路 |
|---|---|---|
ProcessNotFound错误 | 1. 进程名错误。 2. 游戏未启动或已关闭。 3. 权限不足。 | 1. 使用任务管理器确认精确的进程名(不带.exe)。2. 确保游戏已运行。 3. 尝试以管理员身份运行Python脚本。 |
| 读取到的值始终是0或垃圾数字 | 1. 指针链地址错误或已失效。 2. 数据类型不匹配(如该读4字节却读了8字节)。 3. 偏移量错误(十进制/十六进制混淆)。 | 1.最重要:重新用CE分析指针,确认游戏未重启。确保使用“找出指针”功能并勾选“指针”。 2. 在CE中查看目标地址的数据类型,在Python中使用对应的 read_*函数。3. 确认偏移量是十进制还是十六进制。代码中 0x开头表示十六进制。 |
| 写入成功但游戏内无变化 | 1. 写入的地址并非真实数据地址(可能是显示值,真实值在别处)。 2. 游戏有服务器校验或内存保护(如反作弊)。 3. 写入的值超出合理范围被游戏纠正。 | 1. CE中找到地址后,尝试手动修改并冻结,看游戏是否生效。如果不生效,说明找错了。 2.对于在线游戏,此路不通且违规。仅限学习单机程序。 3. 尝试写入一个合理范围内的值。 |
| 脚本运行一次后崩溃 | 1. 游戏进程退出或被结束。 2. 内存访问违规(地址无效)。 3. 代码逻辑错误(如除零)。 | 1. 增加异常捕获,在read_pointer_chain和主循环中做好异常处理。2. 在每次读取前,可以尝试用 pm.is_process_alive()检查进程是否存活。3. 使用调试器或打印语句逐步定位崩溃行。 |
| 基地址每次重启都变 | 这是正常现象。基地址是模块加载到内存的起始地址,每次启动可能不同。 | 我们使用的“模块名+静态偏移”(如MyTestGame.exe+002A34D0)中的002A34D0就是相对于模块基址的偏移,是固定的。pymem会自动帮我们计算当前运行的模块基址。 |
4.3 从基础修改到“AI”决策的扩展
当前的脚本只是一个条件判断。真正的“AI”或自动化逻辑可以更复杂:
状态机驱动:定义游戏角色的不同状态(如“寻找敌人”、“攻击”、“拾取物品”、“回血”),根据内存读取的坐标、血量、敌人信息等,在不同状态间切换。
class BotState: IDLE = 0 MOVING = 1 ATTACKING = 2 PICKING = 3 current_state = BotState.IDLE while True: hp = read_hp(pm) enemy_nearby = check_enemy(pm) if hp < danger_threshold: current_state = BotState.IDLE # 触发回城或使用血瓶逻辑 elif enemy_nearby: current_state = BotState.ATTACKING # ... 根据状态执行不同操作图像识别辅助:当某些信息无法从内存获取时(如任务提示、特定图标),使用
opencv进行模板匹配或OCR识别,作为决策的输入。路径与行为规划:结合游戏地图坐标,实现自动寻路、打怪、完成任务序列的自动化。
机器学习应用(高级):收集游戏状态数据(内存值、屏幕截图)和成功操作记录,训练一个简单的模型来预测最佳操作。这属于更前沿的研究领域。
4.4 最佳实践与重要警告
- 仅用于授权环境与学习目的:所有技术应应用于自己拥有完全控制权的程序(如自己开发的程序、明确允许修改的单机游戏、CE教程程序)。在在线游戏、商业软件上使用可能导致账号封禁、法律诉讼。
- 注重代码健壮性:内存操作极不稳定。务必进行全面的异常处理(try-except),检查指针有效性,并加入适当的延迟(
time.sleep),避免因循环过快导致游戏卡顿或脚本崩溃。 - 模块化与配置化:将指针偏移、扫描模式、关键地址等易变参数写在配置文件(如JSON)中,与代码分离。这样当游戏更新导致地址变化时,只需更新配置,无需修改代码。
- 理解原理而非复制代码:本文示例代码是高度简化的。真实游戏中的数据结构可能复杂得多(数组、链表、结构体)。深入理解CE的“分析内存结构”功能,学习汇编基础,才能应对复杂情况。
- 反作弊对抗是另一个维度:现代游戏普遍采用强大的反作弊系统(如 BattlEye, EasyAntiCheat)。它们会检测内存修改、调试器附加和异常API调用。绕过它们属于安全攻防范畴,复杂度极高,且法律风险巨大,绝非初学者应涉足的领域。
通过本课的学习,你应该已经掌握了使用Python配合CE进行基础内存扫描与修改自动化的全流程。这项技术的核心价值在于理解软件运行机制和实现自动化测试,请务必在合法合规的范围内运用这些知识。下一步,你可以尝试分析更复杂的数据类型(浮点数、字符串、数组),或者结合图像识别库,打造一个混合型的信息获取与自动化框架。