lyl610abc 发表于 2023-9-6 12:27

通杀爆改 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:03

本帖最后由 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)

乔帮主 发表于 2023-9-7 10:18

记住这个规则:在函数开头断点后,关键的数据在 EAX EBX ... EDI 或者 ESP 指向的地址从第二个开始 之中

这一句没有看懂,能说说吗

lyl610abc 发表于 2023-9-7 11:45

乔帮主 发表于 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 ,然后去分析它的上下文环境(寄存器+堆栈),再深入的话就要去分析反汇编代码了

正己 发表于 2023-9-6 12:33

这个系列给个精华,别画饼了,《安卓逆向这档事》都还有好多没更新呢{:301_973:}

Ming520Ying 发表于 2023-9-6 12:48

确实挺细的,但我还是不太懂,害

fa22 发表于 2023-9-6 13:24

有点意思感谢了!

yaodm2019 发表于 2023-9-6 13:53

我挺期待全屏秒杀的。
平时自己也改游戏,最烦的就是找到关键特征值

gamingnow 发表于 2023-9-6 14:26

特地过来支持了

艾莉希雅 发表于 2023-9-6 14:36

好好好,等后续更新
这章是mono,下一章估计是il2cpp了吧

BonnieRan 发表于 2023-9-6 15:12

最近比较感兴趣il2cpp,能看懂一点点,期待Android的

wystudio 发表于 2023-9-6 15:25

请问楼主这篇文章是由markdown语法写的吗?

lyl610abc 发表于 2023-9-6 15:34

wystudio 发表于 2023-9-6 15:25
请问楼主这篇文章是由markdown语法写的吗?

是,用markdown 配合 oss 图床
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 通杀爆改 Unity FPS 游戏系列-第一章:常规搜索+通杀结构解析