欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

昨天还能用的地址今天全是问号?一次内存寻址的破案记录

昨天还能用的地址今天全是问号?一次内存寻址的"破案"记录

这篇我想换个写法,不讲概念,直接复盘我上个月一次真实的抓狂。如果你也写过内存读取脚本,大概率撞过一模一样的墙。

案发

我写了个读血量的小程序,前天晚上跑得稳稳的,0x7FF6A2C31 这个地址读出来就是当前血量,丝毫不差。关了游戏,第二天再开,同样的地址读出来变成了一串乱码,有时甚至直接读不了。我第一反应是"卧槽游戏更新了",去查更新日志——啥也没有。那问题出在哪?

侦查:地址本身就不该被记住

查了一圈才反应过来:现代程序几乎都开了 ASLR(地址空间随机化),加上堆内存每次启动重新分配,你看到的那个"地址"只是这次运行临时分配的门牌号。重启一次,整片小区重新发牌,你记的旧门牌当然指向陌生人。所以——记地址是错的,记"怎么找到这个地址的路径"才是对的。

线索一:模块基址 + 固定偏移

游戏主模块(那个 .exe)虽然加载基址随机,但它相对自己开头的偏移是固定的。真正的"家"不是 0x7FF6...,而是 模块基址 + 某个常量偏移。模块基址每次启动现取,偏移永远不变。

线索二:指针链(多级偏移)

很多数据不是"基址+一跳"就到的。比如血量藏在玩家对象里,玩家对象地址又存在另一个结构里……这就形成一条链:基址 → +偏移1 → 读出的值当作新地址 → +偏移2 → …… 像一串线索,每一环都指向下一环。Cheat Engine 里那条绿色的"基址"加上一串黑色偏移,就是这个链。

收网:怎么把链还原成代码

在 CE 里对着血量地址点"找出是什么访问了这个地址",一路反推到那个绿色基址,把中间的偏移全记下来,得到一串数组。然后代码里顺着链往下跳就行:

import pymem

pm = pymem.Pymem("game.exe")
base = pm.process_base          # 模块基址:每次启动现取,不写死

offsets = [0x3C, 0x18, 0x00, 0x54]   # 偏移链:从 CE 里抄来的
addr = base
for off in offsets:
    addr = pm.read_int(addr + off)   # 顺着链一级级往下跳

hp = pm.read_float(addr)             # 跳到头,这里才是血量
print("当前血量:", hp)

# 对比当年"写死地址"的写法(已废弃):
# hp = pm.read_float(0x7FF6A2C31)   # 重启即失效
为什么这套能扛重启? 因为代码里再没有写死的具体地址——基址是运行时现取的,偏移链是程序编译后就固定的。不论 ASLR 怎么打乱门牌号,只要"从基址出发按这条链走"的路径不变,你永远能摸到血量。这也是为什么我后来把所有内存脚本的"记地址"全部改成了"记偏移链 + 实时取基址"。

复盘:这案子教我的三件事

  • 看到"地址重启就变",先别怀疑游戏更新,先怀疑 ASLR——十次有九次是它。
  • CE 里绿色地址才是基址(模块相关),黑色是动态分配的,别记黑色。
  • 偏移链要存配置文件,别硬编码进逻辑;游戏大版本更新时只改偏移,不动框架。

内存读写最反直觉的地方,就是"你以为拿到了东西,其实只拿到了它这辈子的一张临时工牌"。从记地址切换到记路径,是写内存脚本必须跨的一道坎。跨过去之后,脚本就不再怕重启、不怕重开,稳定得像长在游戏里一样。这套"破案"思路,比背任何 API 都管用。

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功