其他/辅助/植物大战僵尸阳光修改器.e,2016-04-13,1,755,148 字节,扫描产出 75,198 条串。

我第一个"改别人进程内存"的东西。方法在旗舰篇,这一篇的两个看点是:怎么量出"多少是别人的",以及我在这一个文件上读错过一次。


一、我自己写的部分:1,520 字节就到头了

窗体区的全部产出:

 480  _启动窗口
 493  在程序启动后自动调入本窗口
 770  PlantsVSzombies阳光修改器      ← 窗口标题
 827  读当前阳光
 944  读取当前阳光
 984  当前阳光标签
1155  当前阳光:
1201  阳光设置
1316  确认设置阳光
1356  阳光编辑框
1519  100000                        ← 编辑框的默认内容

就这些。两组功能:

  • 读:读取当前阳光 按钮 → 结果显示在 当前阳光标签(标题文案 当前阳光:)
  • 写:阳光编辑框(默认值 100000)→ 确认设置阳光 按钮 → 写进游戏进程

界面分成 读当前阳光 和 阳光设置 两个区。从偏移 1520 往后,这个文件里再没有一件东西是我写的。

1,520 / 1,755,148 = 0.09%。

这个数不是猜的 —— 上一条有效串在 1519,下一条是 3851 的模块文档,中间那 2.3 KB 是图片资源。窗体记录的结构边界就是模块区的起点,这个界限在容器里是看得见的。


二、剩下的 1.75 MB 是什么:一整套内存/驱动模块

类清单

1206355  驱动操作类
1206986  驱动双开类
1208568  驱动模拟类
1208736  内存_多条件搜索类      ← 作者署名见下
1208872  内存_驱动读写1类
1211034  驱动保护1类 / 驱动级保护进程
1211103  驱动打开进程1类 / 驱动级打开进程
1212304  内存_驱动读写2类

内存_多条件搜索类 那条的说明文本是完整的:

本类建立于内存类功能之上,通过支持几个数据的偏移位置而找到动态地址.作者:sttacckz

sttacckz —— 这个模块作者的署名我原样留在仓库和这篇里。引用了别人的东西就堂堂正正写出来。

旁边两条类的说明更有时代感:

驱动模拟类     采用驱动进行模拟按键,与WINIO模拟相比,无DLL文件,效率更好,编译后测试效果
内存_驱动读写1类 编译后测试效果

编译后测试效果 反复出现 —— 意思是在 IDE 里直接运行是不生效的,必须编译成 exe 再测。因为要装驱动。与WINIO模拟相比,无DLL文件 说明这个模块是 WINIO 那条路线的替代品(WINIO 是当年最常被拿来做驱动级键鼠/内存读写的那个老驱动)。

搜索:完全是 Cheat Engine 的路子

111220  内存_不变的数值
111252  内存_变大的数值
111284  内存_变小的数值
111316  内存_改变的数值
111348  内存_偏移量10
111378  内存_偏移量16
1410248 搜索基址6

四种过滤条件:值不变 / 变大 / 变小 / 改变。这就是 CE 的多轮搜索 —— 先扫一遍全部内存,然后"采阳光 → 筛变大的 → 花阳光 → 筛变小的",几轮下来剩不到十个候选地址。

而全文扫不到任何形如游戏内存地址的硬编码常量(我把 6–8 位十六进制样式的全列了一遍,出现的都是 4294967295、2147483647、16777216 这种数值边界,没有 0x00XXX 形态的游戏地址)。

所以这个工具不是"内置 PvZ 阳光地址表",是"现场搜出来再写回去"。这也解释了界面上为什么是 读取当前阳光 + 确认设置阳光 这两个动作而不是一个"锁定"开关 —— 地址得先找到,找到了才能读写。

顺带一个好处:没有硬编码地址,就不会因为游戏小版本更新而失效。这也是为什么这类工具活得比地址表长。

读写:一套完整的带类型 API

读内存字节型 / 读内存短整数型 / 读内存整数型 / 读内存长整数型
读内存小数型 / 读内存双精度小数型 / 读内存日期时间型
读内存子程序指针 / 读内存文本型 / 读内存字节集 / 读内存代码
写内存字节型 / 写内存整数型 / 写内存文本型 / 写内存字节集 / 写内存代码
(以及 读内存整数型2 / 读内存代码2 / 写内存字节集2 这批变体)

取进程名 / 取进程路径 / 取进程模块 / 取进程ID / 取进程命令行
取进程用户名 / 取进程窗口信息 / _关闭对象

两条文档串把用法写清了:

读内存整数型,如果表达式无效或地址无效返回-1
写内存整数型,如果表达式无效或地址无效返回假

“表达式"这个词说明它的地址参数不是一个数,而是能写 基址+偏移1+偏移2 这种式子(同模块里就有 基址 / 基址加偏移 和 偏移1 偏移2 偏移3 偏移4 四个参数位)。所以它处理的是指针链,而 PvZ 的阳光正好在一条链的末端。

还有这个:

默认为:假,只搜索可读内存 真:无视内存保护属性!

以及 内存属性.当前属性 0=未分配,不可访问=1,2=可读,读写=4,可写与拷贝=8,可运行=16,…guard=256,非物理内存=512 —— 一整张页保护属性表,和 Win32 PAGE_* 常量的对应关系。

说句实话:一个改单机游戏阳光的工具,用上了驱动级内存读写、页保护属性表、指针链表达式。这是典型的"为了拧螺丝造了台机床”。 但这也是我那时候唯一的学习途径 —— 我现在还记得为了搞明白"为什么我的 ReadProcessMemory 会失败"而把这类模块的参数文档一条一条读完。


三、我在这个文件上读错过一次,现在改回来

做前面那批归档时,我看到 SouGou 相关的字样,当时的判断是:

这些 SouGou 只是易语言支持库的参数说明文档(“输入法名称默认「SouGou」"、RunDllHostCallBack),不是内嵌的搜狗二进制;文件里也没有游戏文件。

前半句是对的,结论是错的。

这次我换了个做法:不找字符串,直接扫 PE 头 —— 找 MZ,读 e_lfanew,验证那个位置是不是 PE\0\0,然后解出 Machine / 节数 / Magic / SizeOfImage / 节表。结果:

found 2 PE headers

@ 3899   machine=0x14c  nsec=6  magic=0x10b  SizeOfImage=57344
         .text 17178 | .rdata 3632 | .data 12640 | mysechx 822 | .rsrc 1864 | .reloc 3190

@ 57337  machine=0x14c  nsec=6  magic=0x10b  SizeOfImage=57344
         .text 17178 | .rdata 3632 | .data 12608 | mysechx 822 | .rsrc 3448 | .reloc 3190

容器里编进了两个真的 32 位 PE(0x14C = i386,0x10B = PE32),每个 56 KB,.text 段大小完全一致(17,178),只有 .data 差 32 字节、.rsrc 差 1,584 字节 —— 也就是同一个东西的两次构建,后一份资源多了一点(图标或版本信息)。

而这两个头的位置,正好紧跟在两条说明串后面:

3851  输入法文件SouGou
3868  输入法文件(SouGou)搜狗拼音
3899  └─ MZ…  ← 二进制从这里开始

后面那一大段 !This program cannot be run in DOS mode.(3976)、.rdata(4418)、.data(4458)、.reloc(4578),以及 28739 之后一整块 MSVC CRT 的默认字符串:

__GLOBAL_HEAP_SELECTED / __MSVCRT_HEAP_SELECT / runtime error / TLOSS error
SING error / DOMAIN error / Microsoft Visual C++ Runtime Library / Runtime Error!
GetLastActivePopup / GetActiveWindow / MessageBoxA / user32.dll
H:mm:ss / dddd, MMMM dd, yyyy / M/d/yy / January…December / Monday…Sunday

这些是任何 MSVC 编译出来的 PE 都带的东西,它们出现在这里就是那个内嵌二进制的运行时字符串表,不是易语言容器的内容。

还有一个细节:这两个 PE 都有一个非标准段 mysechx(822 字节) —— 不是 .text/.rdata/.data/.rsrc/.reloc 里的任何一个。名字看起来是手工加的(my sec hx)。

那么这两个 DLL 是什么?根据紧邻的说明串,它们是输入法注入用的那个 DLL:这类模块的原理是伪装成一个第三方拼音输入法(这里伪装的是搜狗),让宿主进程通过 IME 的合成串接口把文本"送"进另一个窗口 —— 也就是当年易语言圈子里很有名的"后台打字/后台送字"手法。imehost.dll 那份导入表印证了这个机制:

CandWndProc / CompWndProc / IMEClearPubString / IMESetPubString / StatusWndProc

IMESetPubString 就是往目标窗口的输入法候选区里塞文本的那个 API。

所以:

  • 不是搜狗的文件,是一个"以搜狗的名义被写出来"的第三方输入法注入 DLL;
  • 不是我写的,是模块自带;
  • 我上一次的判断(“只是参数文档,没二进制”)是错的,错在只扫字符串、没扫结构。

方法上的收获:找字符串只能证明"有文本”,要找二进制必须按结构的规则去扫(MZ → e_lfanew → PE\0\0 → 校验段名表)。而且校验要做得够严 —— 1.75 MB 随机数据里 MZ 会撞出很多次,只有 e_lfanew 落在 0x40–0x400 且那个位置真的是 PE\0\0 的才算,这样才把 2 个真货从噪声里挑出来。


四、边界

  • 目标游戏 Plants vs. Zombies 是 PopCap / EA 的商业软件。这个仓里没有任何游戏文件 —— 没有 exe、没有数据文件、没有存档。只有我这边的工具容器。
  • 单机改自己存档里的阳光数值,不联机、不涉及他人。
  • 不保证还能用。 一是 PvZ 版本对不上就搜不到那条指针链;二是这类"驱动级读写"依赖的驱动在 64 位系统上早就签不了名,装不上。
  • 容器里那两个内嵌的第三方输入法 DLL 我原样保留了 —— 它们是模块的一部分,把它们抠出来会让这个 .e 的长度和偏移全部错位,等于毁掉样本。它们不是搜狗的产物,也没有任何凭据在里面。

五、这一篇真正想留下的东西

一句话:“专有容器有多大"和"你写了多少"之间没有任何关系。

1.75 MB 里,我写的部分是一个标题、七个控件、一个默认值 100000。剩下的是别人写的内存搜索类、驱动读写类、输入法注入 DLL,以及 MSVC 运行时和位图资源。

如果我当时按文件体积估工作量,会估出 1000 倍的误差。这条读法反过来更有用:先在容器里找到"我的东西"的边界(窗体记录之后紧跟的第一个模块文档串就是分界),再决定剩下的部分值不值得读。 这次答案是值得 —— 因为那一整套"不变/变大/变小/改变 + 基址加偏移链"的搜索设计,比我那个阳光修改器有意思得多。


仓库

https://cnb.cool/t1mmoc/tspace/eyuyan/lp-pvz-trainer