通杀爆改 Unity FPS 游戏系列-第一章:常规搜索+通杀结构解析
本帖最后由 lyl610abc 于 2023-9-18 20:09 编辑本篇为 Windows 篇,Android 篇将和 @正己梦幻联动,敬请期待
# 索引
[通杀爆改 Unity FPS 游戏系列-序章:介绍及游戏下载](https://www.52pojie.cn/thread-1829474-1-1.html)
[通杀爆改 Unity FPS 游戏系列-第二章:HOOK实现全屏改血+秒杀](https://www.52pojie.cn/thread-1832964-1-1.html)
[通杀爆改 Unity FPS 游戏系列-第三章:il2cpp mono 差异](https://www.52pojie.cn/thread-1835443-1-1.html)
# 环境准备
作为第一章,需要的准备并不多,只需要修改神器:Cheat Engine 即可
在论坛中可以直接下载:https://down.52pojie.cn/Tools/Debuggers/CheatEngine_v7.5.exe
除此之外就是拿来开刀的 DEMO 程序,可以在序章篇获取
# 本章内容
本章将先以 windows mono 游戏包为例实现以下功能:
- 锁血
- 高跳
- 移速
- 无限子弹
先介绍常规修改思路,然后再介绍通杀方法
windows il2cpp 游戏包的修改逻辑类似,主要在原理上会有区别,限于篇幅这篇不展开
------
# 功能展示
## 锁血
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230904182756122.png)
## 高跳
![](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230904191937343.png)
------
## 移速
![移速](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/%E7%A7%BB%E9%80%9F.gif)
------
## 无限子弹
![无限子弹](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/%E6%97%A0%E9%99%90%E5%AD%90%E5%BC%B9.gif)
------
# 基本思路
在不考虑 CE 修改器的 mono 功能下,基本的修改思路是:
- 搜索数值
- 修改数值
- 查看数值引用情况(可选)
- 修改赋值代码(可选)
- 分析数据结构(可选)
## 锁血
这里以锁血这个功能点为例,演示基本思路的改法
### 搜索数值
玩家的血量数值就在左下角,直接搜索即可
为照顾部分萌新,这里从打开修改器开始演示,熟悉的小伙伴可以跳过这部分
#### 查看所有进程
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905100856921.png)
#### 打开游戏进程
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905101035543.png)
------
打开后:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905101134015.png)
------
#### 修改搜索类型
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905102644818.png)
将搜索类型改成 Float 浮点数,像玩家血量这种数值通常都是浮点数
在计算机中,浮点数采用的编码格式是 IEEE 754
简单地理解就是在内存中,100.00 被编码为 0x42c80000 (十六进制)
数据本身是不变的,核心在于**以什么方式去解读这个数据**
打个比方,当你看到 10 这个数据时,如果以十六进制去解读则 0x10 = 16 ,如果以二进制去解读则 binary(10)=2
这里给一个浮点数和十六进制互相转换的网址:https://gregstoll.com/~gregstoll/floattohex/
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905103256130.png)
关于数据类型的内容这里不再展开,有兴趣的可以回顾我以前的帖子:[逆向基础笔记十一 汇编C语言基本类型](https://www.52pojie.cn/thread-1381512-1-1.html#37158433_%E6%B5%AE%E7%82%B9%E7%B1%BB%E5%9E%8B)
------
#### 搜索目标数值
##### 浮点数搜索
修改完搜索类型为 Float 后,填入目标为玩家当前血量即可
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905104830702.png)
------
##### 十六进制搜索
这里也可以选择类型为 默认的 4 Bytes,然后直接搜索 100 对应的十六进制: 0x42c80000,之后每次搜索也是以十六进制去搜
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905104934405.png)
会发现找到的数据量很明显比直接搜 Float 要少,因为如果是浮点数还有**精度**问题,这里用十六进制去搜,就相当于锁定了精度
------
#### 筛选出目标数值
然后等玩家血量变化后,再次搜索数值,最终得到的唯一值就是玩家的血量
##### 浮点数筛选
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905105034449.png)
------
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905105217346.png)
------
##### 十六进制筛选
浮点数 90 对应的十六进制为:0x42b40000,在前面提到的:https://gregstoll.com/~gregstoll/floattohex/ 可以转换得到
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905105348987.png)
------
可以发现通过常规搜浮点数和直接搜十六进制得到的结果的 Address 内存地址是一样的
这里都是 1AFDAAC0,佐证了前面说的,在内存中浮点数是按 IEEE 754 标准存储的
------
#### 锁定数值
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905110142685.png)
------
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905110721456.png)
------
锁定:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905110451564.png)
锁定这个值就完成锁血功能了
------
修改数值:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905110428203.png)
------
修改类型:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905110745720.png)
------
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905110813091.png)
------
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905110827226.png)
------
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905110841744.png)
可以看到,将类型修改为 4 Bytes 和 Hexadecimal(十六进制)后,得到的结果和用十六进制搜索是一致的
------
# 爆杀思路
1. 打开 mono 功能
2. 分析对应程序集
3. 分析对应类
4. 分析对应函数
5. 精准定位,一击必杀
## 锁血
依旧是以锁血为例子,演示 CE 修改器的 mono 功能
### 打开 mono 功能
在前面附加完游戏以后,会发现 CE 修改器多了个 mono 选项
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905133718769.png)
------
点击激活该功能
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905133825663.png)
------
### 分析对应程序集
点击 .Net Info 查看程序集信息
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905133944293.png)
------
点击以后会展示当前游戏的所有程序集,这里可以看到连 unity 引擎提供的程序集都包含在这里面
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905134355278.png)
------
过滤掉一些**默认**的程序集:Mono.Security、SystemXXX、UnityXXX 等
可以很轻松地找到游戏里相关逻辑的程序集在:
Assembly-CSharp、fps.XX 中
#### 和源代码的关系(扩展内容)
简单说明一下这里的数据的由来,算是扩展内容,可跳过
##### Assembly-CSharp
可以看到只有一个类:ProduceEnemy,这个类是我新加的逻辑
为了演示,并没有走原有项目的路径格式,以此来说明源代码和这里的对应关系
ProduceEnemy 默认放在Assets目录下且没有自定义操作,所以最后是走到了 Assembly-CSharp 中
详细的对应关系可参考:(https://blog.csdn.net/weixin_43405845/article/details/105174096?ydreferer=aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS8%3D)
这里我只是稍微提一下,不作为重点
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905135218485.png)
相关的游戏源码为:
```c#
using System.Collections;
using System.Collections.Generic;
using Unity.FPS.Game;
using Unity.FPS.Gameplay;
using UnityEngine;
//注意这里并没有声明任何的命名空间,所以在类中直接就是 ProduceEnemy 而不是 XXX.XXX.ProduceEnemy
public class ProduceEnemy : MonoBehaviour
{
//这里主要说明命名空间对应到 CE 修改器中的程序集,具体逻辑不在这里展示...
}
```
------
##### fps.AI
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905140101845.png)
照样给出相关的游戏源码:
```c#
using System.Linq;
using Unity.FPS.Game;
using UnityEngine;
using UnityEngine.Events;
//注意这里的这里的命名空间影响的是类的信息,程序集信息则跟路径和自定义有关系
namespace Unity.FPS.AI
{
public class DetectionModule : MonoBehaviour
{
//省略
}
}
```
------
#### 找寻血量对应程序集
先说思路:
我们的目标是修改我们**自己**的血量
因此有 2 种找法:
- 找自己,自己底下肯定有一个血量
- 找血量,当自己收到伤害时,一定会触发到血量的变动,这个时候就能够定位到自己的血量
------
因为这个游戏是我自己搞出来的 demo,所以对这些程序集、类都比较熟悉,但是”假装“不知情的情况下
可以通过检索**关键词**来过滤,通常来说程序员是有一套**命名规范**的,不会随便给类或方法瞎起名,不利于后期维护
举个例子,一个函数的功能是受到伤害,通常情况下就是对应含义的英文组合:TakeDamage 之类的
如果瞎起名,起个 Abcd,怕不是分分钟被暴打 φ(* ̄0 ̄)
下面给一组比较常用的关键词:
| 关键词 | 相关点 |
| --------- | ------ |
| Heath | 生命值 |
| Damage | 伤害 |
| Speed | 速度 |
| Character | 角色 |
| Player | 玩家 |
| Weapon | 武器 |
| Pickup | 拾取 |
------
##### 找自己
找自己,很明显就是找角色,正好可以使用我上面提供的关键词:Character 或 Player
不难通过关键词锁定目标:fps.GamePlay 下的 Unity.FPS.GamePlay.PlayerCharacterController
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905145819634.png)
------
找到了关键点,接下来要做的就是拿到这个类对应的内存地址,然后再以这个类的结构去解析
不难发现 CE 修改器提供了个很棒的功能:Lookup instances 查找实例,通过这个方法查找到的结果就包含了我们想要的内存地址
因为是 demo 小游戏,所以找出了的结果很少(结果很多时可以用后面找血量的方法),此次演示时只有 2 个:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905150702227.png)
------
在结果比较少的下,还是比较好筛选的,就是查看对应的值是否合理
比较好鉴别的就是 System.Single (C# 里的 Float),查看它的数值是否合理
这里以 GravityDownForce 为基准,发现第一个地址解析出来的数值相对合理:20.0
再看下一个地址就是一个乱七八糟的内容,排除:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905151003157.png)
------
于是回到第一个地址,找到 m_Health,并点击这个 + 号,展开结构
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905151125969.png)
------
很快就能锁定 2 个关键数值:
- MaxHealth 最大生命值
- CurrentHealth 当前生命值 (这里之所以有个 BackingField 只是因为源代码中将它的作用域设置为 private 私有)
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905151248679.png)
------
终于到了心心念念的修改环节:
右键→ Browse memory region (查看内存区域)
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905151949896.png)
------
接下来会弹出一个新的界面:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905152142127.png)
选中上面选中的部分的开头,右键 Add this address to the list (将地址加入到列表中)
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905152357279.png)
------
出现了新的窗口:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905152518197.png)
------
将类型改成对应的类型,这里是 Float :
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905152559091.png)
------
然后回到 CE 修改器的主界面:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905152631627.png)
到这里就和前面的锁定数值是一样的了,不再赘述
------
##### 找血量
找血量,关键词:Health,不难定位到:fps.Game 下的 Unity.FPS.Game.Health
对于血量,很明显不大好像前面找自己一样直接通过 Lookup instances 来定位
原因很简单:敌人的血量也包含在内,导致其实例会比较多,当然也可以根据玩家和敌人的不同点来筛选,但相对费时费力,不推荐
于是需要新的方法:当自己血量发生变化时,断点,其上下文环境一定包含我们需要的地址
先找到扣血的函数:TakeDamage
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905155511757.png)
------
双击函数以后,跳转到对应的汇编代码段:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905155556082.png)
------
然后选中开头,按快捷键 F5 下断点,第一次下断点会询问需要附加进程,是否继续,点 Yes (是)
或者通过选项卡,Debug → Toggle breakpoint
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905155932556.png)
------
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905155700834.png)
------
成功下断点以后,选中的那一行会变绿色
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905160126028.png)
------
然后回到游戏里,被敌人射一枪 `(*>﹏<*)′ ,触发断点
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905160320968.png)
------
这个时候会发现,右边的寄存器全都变红了,变红表示和先前比有了变化
通常来说,我们需要的地址就在寄存器或者堆栈中
寄存器就是这里的 EAX EBX....
而堆栈则需要用 ESP 去查看
这里的 ESP 指向堆栈顶
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905160608321.png)
------
可以选中下面的第一格,然后快捷键 Ctrl+G 或者右键 Goto address
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905161005359.png)
------
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905161035410.png)
------
然后将地址改成 esp 跳转得到:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905161214462.png)
------
再修改一下显示类型(Ctrl+5 或者 右键 Display Type > 4 Byte hex):
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905161311889.png)
------
除开第一个地址是调用完这个函数的返回地址之外,剩下的头几个地址都是函数参数
具体原理可以回顾以前的:[逆向基础笔记七 堆栈图(重点)](https://www.52pojie.cn/thread-1379952-1-1.html)
只想改游戏的萌新可以不用管这个
记住这个规则:在函数开头断点后,关键的数据在 EAX EBX ... EDI 或者 ESP 指向的地址从第二个开始 之中
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905161515263.png)
------
这个时候已经其实已经可以一个个试哪个地址是我们需要的了
但也可以利用寄存器变化会变红的特性做下筛选,同一把游戏,我们受到伤害时,生命值实例的内存地址是不会变的
因此再让游戏跑起来后,让敌人再攻击我们一次( •̀ ω •́ )✧
点击 Run 按钮让游戏跑起来即可
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905162336310.png)
------
再次断下来以后得到:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905162418754.png)
可以看到 只有 EBX ECX EDX 还有堆栈里的某些数据没有变
------
接下里就可以一个个试了,当然对照函数的参数,其实是可以推测出堆栈中内存地址的含义的
函数原型是 :
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905162646117.png)
一个就是伤害值浮点数,还有一个是伤害来源,但在汇编之中**其实还会多出一个参数:这个类实例本身的内存地址**
因此对照堆栈里的信息可以得出对应地址的含义:
| | 内容 | 含义 |
| ----- | -------- | ------------------------------------------------------------ |
| ESP | 0071D3C4 | 完成调用后要返回的内存地址 |
| ESP+4 | 1A940410 | 我们想要的指向 Health 的内存地址 |
| ESP+8 | 41200000 | damage 伤害,这里把这个十六进制按 IEEE 754 去解读,其值就是:10,也就是敌人对我们造成了 10 点伤害 |
| ESP+C | 04170640 | damageSource 伤害来源 |
------
接下来就是验证这个地址是否为我们想要的,可以把地址丢回原本的 .Net Info 里
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905163535687.png)
很明显这个地址就是我们想要的,之后的步骤就同上面找自己一样,这里不再赘述
------
但丢回 .Net Info 有个问题,就是如果数据对不上不能解析时,可能会报错,不是很方便
再介绍另一个验证的途径:
回到先前的 Memory Viewer 界面,Tools --> Dissect data/structures 或者快捷键 Ctrl + D
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905163852685.png)
------
在新出来的界面把要解析的内存地址丢进去,然后 Structures --> Define new structure 或者快捷键 Ctrl + N
这一步建议取消断点,然后让游戏跑起来以后再操作,不然容易失败
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905164154612.png)
------
然后点击后,CE 修改器会自动根据你的内存地址推导出它的结构,因此可以验证数据是否为我们想要的
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905164413336.png)
------
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905164523847.png)
得以验证确实是该地址就是我们所需的指向我们角色自身血量的
------
# 作业
你已经学完 1+1=2 了,是时候来点高等数学了(〃 ̄︶ ̄)( ̄︶ ̄〃)
高跳、移速和无限子弹都是类似的原理,就当作是课后作业了
------
# 总结
- 基本思路中介绍了搜索浮点数数值可以使用**十六进制搜索法**更精准锁定数值
- 数值本身不重要,重要的是我们**如何去解析**这个数值
- 要善于结合**关键词**来对游戏逻辑进行分析,关键词可以起到很好的推进作用
- CE 修改器的 mono 功能主要是帮助我们更方便去解析游戏的**结构**
- CE 修改器的 Lookup instances 查找实例更适用于实例少的情况,实例多时则需要动态断点分析
------
这一期只介绍了 CE 修改器 mono 功能**最基础**的玩法,还有动态调用游戏函数等内容限于篇幅放在后面
关于 CE 修改器的 mono 功能原理限于篇幅也暂未展开,也放到后面
使用 Visual Studio 编写外挂部分也留到后面
有能力的小伙伴可以试试做出全屏秒杀功能,刚进入游戏就完成击杀并胜利:
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/5ede2a4e0a9d04760e8cf5512de1320b.png)
------
最后的最后 @正己大佬 ,快去催更他出 Android 版
放 2 张 @正己 大佬的图
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230905171305365.png)
!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20230906134358778.png)
------
本帖最后由 lyl610abc 于 2023-9-7 11:08 编辑
乔帮主 发表于 2023-9-7 10:18
记住这个规则:在函数开头断点后,关键的数据在 EAX EBX ... EDI 或者 ESP 指向的地址从第二个开始 之中
...
在这里的例子中,我们下断点的函数是 Health.TakeDamage
断下来的时机是我们受到伤害的时候
我们想要在这个时机拿到的数据是:我们自己的 Health
我们自己的 Health 肯定是在断下来这一刻的上下文环境里:也就是 寄存器(EAX EBX ... EDI) 或 堆栈(ESP) 里
好比:正己老师在路上走路的时候,突然被人给了一拳,在他被打的这一刻
肯定是能知道是谁打的他(伤害来源 damageSource)
他受了多少伤害(有多痛 damage)
是他自己被打了(指向自己的Health) 记住这个规则:在函数开头断点后,关键的数据在 EAX EBX ... EDI 或者 ESP 指向的地址从第二个开始 之中
这一句没有看懂,能说说吗 乔帮主 发表于 2023-9-7 11:31
感谢解释,是我没有描述清楚, EAX EBX EDI因为断点的时候变红了,说明有变换,你说的伤害 和来源还有收 ...
关键值可能通过寄存器(EAX EBX .. EDI)传递,也可能通过堆栈(esp)来传递
对于初学者来说,可以粗浅地理解从 EAX EBX .. EDI 和 ESP 里找就行
但实际情况可能更复杂,因为不同函数可能会有不同的调用协定,如:stdcall cdecl fastcall
关于调用协定可以参考我以前的帖子:https://www.52pojie.cn/thread-1380788-1-1.html#37141712_%E8%B0%83%E7%94%A8%E5%8D%8F%E5%AE%9A
现在一般都是以 stdcall 居多,本次的 demo 也是 stdcall
具体情况还需要具体分析,我这里想要表达的点是:
在合适的时机做 HOOK ,然后去分析它的上下文环境(寄存器+堆栈),再深入的话就要去分析反汇编代码了 这个系列给个精华,别画饼了,《安卓逆向这档事》都还有好多没更新呢{:301_973:} 确实挺细的,但我还是不太懂,害 有点意思感谢了! 我挺期待全屏秒杀的。
平时自己也改游戏,最烦的就是找到关键特征值 特地过来支持了 好好好,等后续更新
这章是mono,下一章估计是il2cpp了吧 最近比较感兴趣il2cpp,能看懂一点点,期待Android的 请问楼主这篇文章是由markdown语法写的吗? wystudio 发表于 2023-9-6 15:25
请问楼主这篇文章是由markdown语法写的吗?
是,用markdown 配合 oss 图床