前言
最近无意间接触到UE4Dumper这个话题,看了几篇github开源项目后着手撸了个iOS平台虚幻4引擎的手游。
游戏名:B.o.T
bundleID:com.rbuttongames.battlemechs
iTunes Store链接:itms-apps://itunes.apple.com/app/id1231732980
虚幻引擎版本:4.20
B.o.T 是一款动作射击类3D游戏,玩家可以驾驶轻型,中型和重型泰坦的机器人作战。玩家可随机匹配或三俩好友组队游戏。
也许是为了物理效果逼真,身披重甲的机器人行动迟缓,本次逆向的主要目的就是让其“灵活”起来。
准备工作 & 工具
-
Hopper Disassembler & IDA
-
iOS-UE4Dumper工具
-
一定的arm汇编基础
-
对虚幻4引擎有一定的了解
-
C++ 基础(虚表)
-
frida-ios-dump
1. 初识虚幻4
要了解虚幻4引擎,首先要了解的就是“蓝图”。以下是官方文档原文:
虚幻引擎中的 "蓝图可视化脚本" 系统是一类完整的游戏性脚本系统,此系统的基础概念是使用基于节点的界面在虚幻编辑器中创建游戏性元素。与诸多常用脚本语言相同,其用于定于引擎中的对象驱动(OO)类或对象。使用UE4后便知,使用蓝图所定义的对象通常被直接称为"蓝图"。
以及一些非常重要的蓝图类:
Actor 一个可以在世界中摆放,或者生成的Actor。
#所有可以放入关卡的对象都是 Actor,比如摄像机、静态网格体、玩家起始位置。
#Actor支持三维变换,例如平移、旋转和缩放。你可以通过游戏逻辑代码(C++或蓝图)创建(生成)或销毁Actor。
#在C++中,AActor是所有Actor的基类。
---------------------------------
Pawn Pawn是一个可以从控制器获得输入信息处理的Actor。
#Pawn 是Actor的子类,它可以充当游戏中的化身或人物(例如游戏中的角色)。
#Pawn可以由玩家控制,也可以由游戏AI控制并以非玩家角色(NPC)的形式存在于游戏中。
#当Pawn被人类玩家或AI玩家控制时,它被视为 已被控制(Possessed)。相反,当Pawn未被人类玩家或AI玩家控制时,它被视为 未被控制(Unpossessed)。
---------------------------------
Character 角色是一个包含了行走,跑步,跳跃以及更多动作的Pawn。
#角色(Character) 是Pawn Actor的子类,旨在用作玩家角色。角色子类包括碰撞设置、双足运动的输入绑定,以及由玩家控制的运动附加代码。
---------------------------------
PlayerController PlayerController一种的Actor,负责控制玩家所使用的Pawn。
#玩家控制器(Player Controller) 会获取游戏中玩家的输入信息,然后转换为交互效果,每个游戏中至少有一个玩家控制器。
#玩家控制器通常会控制一个Pawn或角色,将其作为玩家在游戏中的化身。
#玩家控制器还是多人游戏中的主要网络交互节点。在多人游戏中,服务器会为游戏中的每个玩家生成一个玩家控制器实例,因为它必须对每个玩家进行网络函数调用。
#每个客户端只拥有与其玩家相对应的玩家控制器,并且只能使用其玩家控制器与服务器通信。
---------------------------------
Game Mode 一个Game Mode 定义了游戏是如何被执行的,游戏规则,如何的分以及其他方面的内容。
#游戏模式(GameMode) 类负责设置当前游戏的规则。规则包括:
#1.玩家如何加入游戏。
#2.是否可以暂停游戏。
#3.任何与游戏相关的行为,例如获胜条件等等。
---------------------------------
World 容器(很多脚本、辅助都是先找指向它实例的指针)
#世界场景(World) 是一个容器,包含了游戏中的所有关卡。它可以处理关卡流送,还能生成(创建)动态Actor。
2. 砸壳 & IDA、hopper分析
AppStore发布的所有App,都是经过加密与签名的,如果要逆向、分析,首先要砸壳。iOS上的砸壳工具有很多,推荐 frida-ios-dump ,简单便捷、一键砸壳 + dump(这个dump不是虚幻4引擎的dump,是Maco-O格式文件dump,砸壳之后得到的是一份解密了的Mach-O格式文件,游戏逻辑和虚幻引擎都包括在其中)。
拿到砸壳后的解密文件,就可以丢到 IDA、hopper分析了,两大神器都可以,功能上IDA更强大。hopper的话对arm汇编指令转OC的支持更加友好,对字符串的搜索也方便很多。个人习惯两个一起用,除了电脑有点烫手以外鱼与熊掌兼得。
3. 寻找基址
上面说到,World是一个大容器,包含了游戏中的所有关卡,如果内存中找到这个地址,那么Actor、Pawn等都能遍历到。那么如何找这个基址呢?起初我也是费了老些劲,后来了解了一些UE4关键术语意思(Actor、Pawn、Character等),加上各大论坛到处逛,最后总结到经验:通过一定的特征,找到大概的位置,然后参考github虚幻4源代码和IDA中汇编逻辑回溯找到World基址。
那这一定的特征是什么呢?字符串!!! 在源代码中找到引用GWorld变量的附近的字符串。
在源码4.20/Engine/Source/Runtime/Engine/Private/World.cpp
中SeamlessTravel FlushLevelStreaming
字符串与GWorld引用非常近,可以作为特征
经过几个if条件的判断,最终无论如何也会走到SeamlessTravel FlushLevelStreaming
字符串代码处(代码从上往下执行):
对比IDA、hopper的反汇编逻辑图:
接着再往上挪挪:
bWorldChanged = true;
代码上面就一个if条件判断,对应的逻辑图节点也就俩箭头。要么不进if分支直接到bWorldChanged = true;
,要么进了if以后再到bWorldChanged = true;
,至此GWorld = LoadedWorld;
离我们越来越接近了。再往上挪一挪就能找到GWorld:
验证是否正确,切换到伪代码:
源码中GWorld = LoadedWorld;
这个赋值操作在众多if分支之前,转换成伪代码后答案非常明显*0x10430ec68 = r0;
中的0x10430ec68
就是GWorld的基址。加上ASLR之后就是World实例的动态地址。当然其他基址可能没这么明显,如果没这么明显,那就需要指令级慢慢回溯了,回溯的时候多多留意:
adrp 0xXXXXXX
add 0xXXX
这种类似的组合。
因为本身adrp
就是小范围地址读取指令。能找到字符串做特征当然整挺好,源码如果没有字符串特征可找,条件分支的判断条件也是个不错的特征,以及其他更多。取决你对arm汇编的了解程度!有了World实例地址之后,遍历AActors就能得到每一个玩家,进而便可开发ESP(俗称导盲功能)。
除了GWorld基址,还有GObjectsArray、GNamesArray两个基址也非常重要,是制作dumpSDK文件的必要条件,回溯过程参考GWorld。
SDKs文件片段:
4.修改数值 & 制作插件
有了GWorld基址,就能获取到游戏的实例(GameInstance),有了游戏的实例就能遍历到AActor、Pawn等等。有了AActor就能拿到UActorComponent(也包括其他如:碰撞组件,射击组件,移动组件等更多)。
以下是源码片段:
3. 最后
1.正向开发的过程中,有些属性的偏移是在虚表中的,所以需要一定的 C++基础
,包括有些功能的实现需要用到 函数指针
。
2.逆向的过程往往就像找寻制作“饼干”的原理,但前提是得先要去品尝、了解这块饼干。