本篇为 Windows 篇,Android 篇将和 @正己 梦幻联动,敬请期待
索引
通杀爆改 Unity FPS 游戏系列-序章:介绍及游戏下载
通杀爆改 Unity FPS 游戏系列-第二章:HOOK实现全屏改血+秒杀
通杀爆改 Unity FPS 游戏系列-第三章:il2cpp mono 差异
环境准备
作为第一章,需要的准备并不多,只需要修改神器:Cheat Engine 即可
在论坛中可以直接下载:https://down.52pojie.cn/Tools/Debuggers/CheatEngine_v7.5.exe
除此之外就是拿来开刀的 DEMO 程序,可以在序章篇获取
本章内容
本章将先以 windows mono 游戏包为例实现以下功能:
先介绍常规修改思路,然后再介绍通杀方法
windows il2cpp 游戏包的修改逻辑类似,主要在原理上会有区别,限于篇幅这篇不展开
功能展示
锁血
高跳
移速
无限子弹
基本思路
在不考虑 CE 修改器的 mono 功能下,基本的修改思路是:
- 搜索数值
- 修改数值
- 查看数值引用情况(可选)
- 修改赋值代码(可选)
- 分析数据结构(可选)
锁血
这里以锁血这个功能点为例,演示基本思路的改法
搜索数值
玩家的血量数值就在左下角,直接搜索即可
为照顾部分萌新,这里从打开修改器开始演示,熟悉的小伙伴可以跳过这部分
查看所有进程
打开游戏进程
打开后:
修改搜索类型
将搜索类型改成 Float 浮点数,像玩家血量这种数值通常都是浮点数
在计算机中,浮点数采用的编码格式是 IEEE 754
简单地理解就是在内存中,100.00 被编码为 0x42c80000 (十六进制)
数据本身是不变的,核心在于以什么方式去解读这个数据
打个比方,当你看到 10 这个数据时,如果以十六进制去解读则 0x10 = 16 ,如果以二进制去解读则 binary(10)=2
这里给一个浮点数和十六进制互相转换的网址:https://gregstoll.com/~gregstoll/floattohex/
关于数据类型的内容这里不再展开,有兴趣的可以回顾我以前的帖子:逆向基础笔记十一 汇编C语言基本类型
搜索目标数值
浮点数搜索
修改完搜索类型为 Float 后,填入目标为玩家当前血量即可
十六进制搜索
这里也可以选择类型为 默认的 4 Bytes,然后直接搜索 100 对应的十六进制: 0x42c80000,之后每次搜索也是以十六进制去搜
会发现找到的数据量很明显比直接搜 Float 要少,因为如果是浮点数还有精度问题,这里用十六进制去搜,就相当于锁定了精度
筛选出目标数值
然后等玩家血量变化后,再次搜索数值,最终得到的唯一值就是玩家的血量
浮点数筛选
十六进制筛选
浮点数 90 对应的十六进制为:0x42b40000,在前面提到的:https://gregstoll.com/~gregstoll/floattohex/ 可以转换得到
可以发现通过常规搜浮点数和直接搜十六进制得到的结果的 Address 内存地址是一样的
这里都是 1AFDAAC0,佐证了前面说的,在内存中浮点数是按 IEEE 754 标准存储的
锁定数值
锁定:
锁定这个值就完成锁血功能了
修改数值:
修改类型:
可以看到,将类型修改为 4 Bytes 和 Hexadecimal(十六进制)后,得到的结果和用十六进制搜索是一致的
爆杀思路
- 打开 mono 功能
- 分析对应程序集
- 分析对应类
- 分析对应函数
- 精准定位,一击必杀
锁血
依旧是以锁血为例子,演示 CE 修改器的 mono 功能
打开 mono 功能
在前面附加完游戏以后,会发现 CE 修改器多了个 mono 选项
点击激活该功能
分析对应程序集
点击 .Net Info 查看程序集信息
点击以后会展示当前游戏的所有程序集,这里可以看到连 unity 引擎提供的程序集都包含在这里面
过滤掉一些默认的程序集:Mono.Security、SystemXXX、UnityXXX 等
可以很轻松地找到游戏里相关逻辑的程序集在:
Assembly-CSharp、fps.XX 中
和源代码的关系(扩展内容)
简单说明一下这里的数据的由来,算是扩展内容,可跳过
Assembly-CSharp
可以看到只有一个类:ProduceEnemy,这个类是我新加的逻辑
为了演示,并没有走原有项目的路径格式,以此来说明源代码和这里的对应关系
ProduceEnemy 默认放在Assets目录下且没有自定义操作,所以最后是走到了 Assembly-CSharp 中
详细的对应关系可参考:Unity关于程序集(Assembly )的那些事
这里我只是稍微提一下,不作为重点
相关的游戏源码为:
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
照样给出相关的游戏源码:
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
找到了关键点,接下来要做的就是拿到这个类对应的内存地址,然后再以这个类的结构去解析
不难发现 CE 修改器提供了个很棒的功能:Lookup instances 查找实例,通过这个方法查找到的结果就包含了我们想要的内存地址
因为是 demo 小游戏,所以找出了的结果很少(结果很多时可以用后面找血量的方法),此次演示时只有 2 个:
在结果比较少的下,还是比较好筛选的,就是查看对应的值是否合理
比较好鉴别的就是 System.Single (C# 里的 Float),查看它的数值是否合理
这里以 GravityDownForce 为基准,发现第一个地址解析出来的数值相对合理:20.0
再看下一个地址就是一个乱七八糟的内容,排除:
于是回到第一个地址,找到 m_Health,并点击这个 + 号,展开结构
很快就能锁定 2 个关键数值:
- MaxHealth 最大生命值
- CurrentHealth 当前生命值 (这里之所以有个 BackingField 只是因为源代码中将它的作用域设置为 private 私有)
终于到了心心念念的修改环节:
右键→ Browse memory region (查看内存区域)
接下来会弹出一个新的界面:
选中上面选中的部分的开头,右键 Add this address to the list (将地址加入到列表中)
出现了新的窗口:
将类型改成对应的类型,这里是 Float :
然后回到 CE 修改器的主界面:
到这里就和前面的锁定数值是一样的了,不再赘述
找血量
找血量,关键词:Health,不难定位到:fps.Game 下的 Unity.FPS.Game.Health
对于血量,很明显不大好像前面找自己一样直接通过 Lookup instances 来定位
原因很简单:敌人的血量也包含在内,导致其实例会比较多,当然也可以根据玩家和敌人的不同点来筛选,但相对费时费力,不推荐
于是需要新的方法:当自己血量发生变化时,断点,其上下文环境一定包含我们需要的地址
先找到扣血的函数:TakeDamage
双击函数以后,跳转到对应的汇编代码段:
然后选中开头,按快捷键 F5 下断点,第一次下断点会询问需要附加进程,是否继续,点 Yes (是)
或者通过选项卡,Debug → Toggle breakpoint
成功下断点以后,选中的那一行会变绿色
然后回到游戏里,被敌人射一枪 `(>﹏<)′ ,触发断点
这个时候会发现,右边的寄存器全都变红了,变红表示和先前比有了变化
通常来说,我们需要的地址就在寄存器或者堆栈中
寄存器就是这里的 EAX EBX....
而堆栈则需要用 ESP 去查看
这里的 ESP 指向堆栈顶
可以选中下面的第一格,然后快捷键 Ctrl+G 或者右键 Goto address
然后将地址改成 esp 跳转得到:
再修改一下显示类型(Ctrl+5 或者 右键 Display Type > 4 Byte hex):
除开第一个地址是调用完这个函数的返回地址之外,剩下的头几个地址都是函数参数
具体原理可以回顾以前的:逆向基础笔记七 堆栈图(重点)
只想改游戏的萌新可以不用管这个
记住这个规则:在函数开头断点后,关键的数据在 EAX EBX ... EDI 或者 ESP 指向的地址从第二个开始 之中
这个时候已经其实已经可以一个个试哪个地址是我们需要的了
但也可以利用寄存器变化会变红的特性做下筛选,同一把游戏,我们受到伤害时,生命值实例的内存地址是不会变的
因此再让游戏跑起来后,让敌人再攻击我们一次( •̀ ω •́ )✧
点击 Run 按钮让游戏跑起来即可
再次断下来以后得到:
可以看到 只有 EBX ECX EDX 还有堆栈里的某些数据没有变
接下里就可以一个个试了,当然对照函数的参数,其实是可以推测出堆栈中内存地址的含义的
函数原型是 :
一个就是伤害值浮点数,还有一个是伤害来源,但在汇编之中其实还会多出一个参数:这个类实例本身的内存地址
因此对照堆栈里的信息可以得出对应地址的含义:
|
内容 |
含义 |
ESP |
0071D3C4 |
完成调用后要返回的内存地址 |
ESP+4 |
1A940410 |
我们想要的指向 Health 的内存地址 |
ESP+8 |
41200000 |
damage 伤害,这里把这个十六进制按 IEEE 754 去解读,其值就是:10,也就是敌人对我们造成了 10 点伤害 |
ESP+C |
04170640 |
damageSource 伤害来源 |
接下来就是验证这个地址是否为我们想要的,可以把地址丢回原本的 .Net Info 里
很明显这个地址就是我们想要的,之后的步骤就同上面找自己一样,这里不再赘述
但丢回 .Net Info 有个问题,就是如果数据对不上不能解析时,可能会报错,不是很方便
再介绍另一个验证的途径:
回到先前的 Memory Viewer 界面,Tools --> Dissect data/structures 或者快捷键 Ctrl + D
在新出来的界面把要解析的内存地址丢进去,然后 Structures --> Define new structure 或者快捷键 Ctrl + N
这一步建议取消断点,然后让游戏跑起来以后再操作,不然容易失败
然后点击后,CE 修改器会自动根据你的内存地址推导出它的结构,因此可以验证数据是否为我们想要的
得以验证确实是该地址就是我们所需的指向我们角色自身血量的
作业
你已经学完 1+1=2 了,是时候来点高等数学了(〃 ̄︶ ̄)( ̄︶ ̄〃)
高跳、移速和无限子弹都是类似的原理,就当作是课后作业了
总结
- 基本思路中介绍了搜索浮点数数值可以使用十六进制搜索法更精准锁定数值
- 数值本身不重要,重要的是我们如何去解析这个数值
- 要善于结合关键词来对游戏逻辑进行分析,关键词可以起到很好的推进作用
- CE 修改器的 mono 功能主要是帮助我们更方便去解析游戏的结构
- CE 修改器的 Lookup instances 查找实例更适用于实例少的情况,实例多时则需要动态断点分析
这一期只介绍了 CE 修改器 mono 功能最基础的玩法,还有动态调用游戏函数等内容限于篇幅放在后面
关于 CE 修改器的 mono 功能原理限于篇幅也暂未展开,也放到后面
使用 Visual Studio 编写外挂部分也留到后面
有能力的小伙伴可以试试做出全屏秒杀功能,刚进入游戏就完成击杀并胜利:
最后的最后 @正己 大佬 ,快去催更他出 Android 版
放 2 张 @正己 大佬的图