unity3D是目前主流的游戏开发引擎,比如《闪耀暖暖》、《纪念碑谷》、《剑网3:指尖江湖》、《原神》等手游都是基于unity引擎创作的。
Android平台的APK可以直接解压,看是否有/assets/bin/Data/Managed
目录,也可以查看lib文件夹下面包含的一些So,如果有libmono.so
、libunity.so
等模块,基本可以确定是unity游戏了。
1. 注入技术
1.1 Zygote注入
Zygote注入原理:Zygote fork全局 父子进程
优点:隐蔽,强大
Zygote注入目的:将指定的模块注入到目标进程。
注入器作用:将指定的模块注入到Zygote进程。
So模块:外挂功能、控制权交还(/system/lib/libdvm.so
中的dvmLoadNativeCode()
函数,判断当前启动的进程是否是目标游戏进程,获取当前进程信息,匹配目标游戏进程字符串)。
注入实现流程:和ptrace注入很相似,但由于Zygote注入使用了shellcode方式远程调用函数,并且Zygote属于系统级进程,实现更复杂。
注入器实现:
关闭seLinux:seLinux是Linux的一个安全子系统
获取seLinux的配置目录
/sys/fs/selinux
、/proc/mounts
,获取配置文件中seLinux功能的开关状态,关闭seLinux。附加到zygote进程,阻塞进程,保存寄存器现场
ptrace_attach附加、waitpid、获取寄存器值(ptrace_getregs)、ptrace_setregs、保存Zygote进程注入前的寄存器环境(memcpy)。
获取Zygote进程中关键函数的地址(dlopen/dlsym/mmap/munmap/mprotect都属于系统函数)
该函数在So模块中的偏移地址 = 本地函数地址 - 本地So模块基址
远程函数地址 = 远程So模块基址 + 该函数在So模块中的偏移地址
注意:本地进程和远程进程都加载的So模块
远程调用mmap函数(r0-r3)分配内存空间
设置堆栈布局配置shellcode
调用
dlopen
函数加载指定的模块调用
dlsym
函数获取模块中关键函数地址调用已获取的关键函数
远程调用shellcode
LR置0,PC置0
远程调用
munmap
函数释放内存恢复进程、恢复寄存器现场、释放附加
关键点:
- 目标进程需要在注入Zygote完成后再启动
- 注入的模块需要获取控制权
- 判断当前进程是否是目标进程
1.2 ptrace注入
ptrace注入与Zygote注入的区别:
1.2.1 案例
以一款基于unity3D开发的跑酷类游戏为例,进行ptrace注入。
ptraceInject工程(点击下载:PtraceInject.zip)内的inject原生程序是注入程序,InjectModule工程中的libInjectModule.so
就是被注入到游戏进程的模块,模块内仅有一个Inject_entry()
函数,打印出”Inject_entry Func is called”。
修改.\PtraceInject\PtraceInject\jni\InjectModule.c
中目标进程包名以及杂七杂八编译起来会出错的代码,重新使用NDK编译inject程序。
在jni目录下使用ndk-build编译成ELF文件,生成inject程序。
1 | D:\Java\gametools\PtraceInject\PtraceInject\jni>D:\Java\Android\sdk\ndk\21.4.7075529\build\ndk-build |
将inject程序和libInjectModule.so
文件都push到Android设备的/data/v5/
目录下,并且添加可读可写可执行的权限。
启动目标游戏进程,执行inject程序,使用logcat查看打印出来的日志,可以看到一些重要的系统函数在目标游戏进程的地址被打印出来了。
正常来说从日志中可看到libInjectModule.so
已经成功注入到目标游戏进程内,并且模块中的Inject_entry()
函数已经执行。使用以下命令查看游戏进程的模块列表,可看到libInjectModule.so
文件已经在游戏进程的模块列表中,成功注入模块到远程进程。
1 | ps -A |grep hitrun |
但是现在并没有看到啊?
1.3 ELF文件静态感染注入
节是静态的,段是动态的。
当进行静态感染注入时,ELF文件头中段的起始偏移地址会被修改,段(程序头表)的个数、程序头表也会被修改。
最先加载到内存中的段:LOAD(PT_LOAD)
PT_LOAD:描述程序加载时的内存映射信息。
动态节区.dynamic
(PT_DYNAMIC),包含了程序动态链接和依赖库信息。感染即将恶意模块注入到依赖库中。
ELF文件注入流程:
- 在dt_strtab指向的字符串中添加自定义so模块名称,将字符串表移到文件末尾
- 添加一个pt_load表,用于能够内存映射我们添加的字符串,phdr移到文件末尾
- 修改dt_strtab、dt_strsz,添加dt_needed
- 修改ELF Header关于段头表的信息字段,偏移、大小
2. hook技术
hook技术主要分为系统消息hook技术与API hook技术。
系统消息hook技术一般应用于Windows平台,比如SetWindwsHookEx()
函数。
API hook技术一般应用于Android平台,有基于汇编代码指令的hook(异常hook、inline hook);基于函数地址的hook(导入表hook、导出表hook)。
2.1 异常hook
非法指令集:
微处理器 | 寄存器位数 | 地址 | 字节数 |
---|---|---|---|
ARM | 32 | 0xe7***f* | 4 |
Thumb | 16 | 0xde** | 2 |
Thumb2 | 32 | 0xf7f*a*** | 4 |
实现流程:
注册异常处理函数
判断hook处是ARM指令还是Thumb指令
如果是Thumb指令,进一步判断该指令是Thumb指令还是Thumb2指令
若PC位置为目标地址,那么就写入异常、恢复异常;PC位置为目标指令的下一条指令,则往目标指令处写入异常
异常hook实现的难点:异常处理函数、实现多次hook。
2.2 inline hook
inline hook是基于二进制汇编代码替换方式而实现的hook。它的功能非常强大,执行效率高。
注意:
- 由于基于底层指令,认平台
- 原指令的覆盖顺序,先要构造桩函数,再覆盖原指令
- 指令修复
2.3 导入表hook
实现原理:导入表hook通常以So文件的导入函数作为目标进行函数指针替换。
导入表hook流程:打开So文件,解析ELF文件结构,找到导入表位置,读取函数地址,匹配函数地址,判断匹配函数地址是否是目标导入函数地址,如果是就替换为新函数地址,实现导入表hook。从ELF Header结构入手,找到SectionHeader首地址、SectionTableEntry的size、shstringtableindex的索引值。
.shstrtab的header地址 = SectionHeader首地址 + SectionTableEntry的size * shstringtableindex的索引值
根据.shstrtab的sectionheader就找到了.shstrtab的位置和内容,遍历,找到.got节的位置,指针p指向.got节首地址,遍历,获得目标函数的地址。
3. 反调试技术
3.1 self-debugging反调试
3.1.1 原理及特点
原理:父进程创建一个子进程,通过子进程调试父进程。是一个非常实用、高效的实时反调试技术。
优点:
- 消耗的系统资源比较少
- 几乎不影响受保护进程性能
- 可以轻易地组织其它进程调试受保护的进程
缺点:实现比较复杂
实现:核心ptrace函数、进程的信号机制
注意:进程暂停状态比较多
3.1.2 暂停状态
signal-delivery-stop状态
调试器和被调试进程之间的关系
group-stop状态 sigcont信号
需要同时满足两个条件:进程/线程处于被调试状态、被调试进程/线程收到暂停信号,重置为0
sigstop
sigtstp
sigttin
sigttou
syscall-stop状态
ptrace-event-stop状态
3.1.3 反-反调试
- 让父进程不fork
- 把while函数循环去掉
- 调试子进程从而达到调试父进程的目的
3.1.4 self-debugging流程
3.2 轮询检测反调试
3.2.1 原理及特点
轮询检测反调试技术基于循环检测进程的状态,用于判断当前进程是否正在被调试。
原理:读取进程的/proc/{PID}/status
文件,通过该文件得到调试当前进程的调试器(检测调试器的PID,TracerPid的值)。
优点:实现比较简单。
缺点:系统资源消耗大。
3.2.2 status文件信息
字段 | 含义 |
---|---|
Name | 进程名称 |
State | 进程的状态 |
Tgid | 一般指进程的名称 |
Pid | 一般指进程ID,它的值与getpid()函数的返回值相等 |
PPid | 父进程的ID |
TracerPid | 实现调试功能的进程ID,值为0表示当前进程未被调试 |
3.2.3 反-反调试
对于self-debugging:
- 让父进程不fork出子进程
- 调试子进程
- 把while函数循环nop掉(mov R3,R3)
- 配合双IDA调试
- 挂起一个子进程
对于poll-debug:
- 动态调试时修改TracerPid字段值为0
- 修改系统内核文件,让TracerPid的值为负值(?)
3.3 Java层反调试及IDA反调试
4. DLL主流保护
4.1 DLL加密
特征:mono关键函数处代码明显出现加密解密算法。
解决方案:抠出DLL后,修改libmono.so
中解密DLL的名称,进行文件欺骗,直接替换官方原版So。根据程序的libmono.so
大小替换相应大小的原版So。
1 | libmono.so 3.58MB |
4.2 DLL压缩
特征:lib目录下明显出现如libzlib.so
压缩算法的So。
解决方案:抓住libmono.so
和原版有无发生变化,尤其是So依赖库中有没有libzlib.so
。如果有,就下手libmono.so
;否则就下手libzlib.so
。完全抹去压缩So的运行基本不可能,只能找到解压位置,分析解压DLL逻辑。
4.3 MDB备份
特征:bin及其子文件夹明显出现.mdb
文件。
解决方案:尝试将MDB文件删除,看是否能正常运行。如果可以正常运行,就修改DLL,运行是否正常,如果出现不一致,就是验证;如果MDB文件不允许删除,必须找到MDB文件使用逻辑,弄清是何原因造成游戏必须有MDB文件。一般来说,在DLL完整情况下,MDB文件是不需要的,当然还需要一些U3D开发使用MDB文件的知识。
5. 篡改游戏内容
篡改游戏内容本质上是对内存文件的读取和修改,可通过/proc
虚拟文件系统获取进程的状态信息。
过程:得到目标游戏的地址或者位置信息 -> 修改读写权限 -> 实现读写操作。
游戏内容读写方式:
注入式:需要注入到游戏进程空间(ptrace注入、Zygote注入)
适合读写动态数据
非注入式:不需要注入游戏进程空间
适合读写静态代码、资源信息、数据等,也可以获取动态数据
5.1 非注入式篡改
修改游戏安装包(APK)
修改跳转指令为nop:mov R3,R3 0xe1a03003
APK文件结构,WinHex修改DEX文件和So的hex操作数
修改寄存器,常见于函数头
抹除明文字符串
修改游戏的安装目录文件(/data/data/包名,用户私有文件数据)
篡改
/proc/
目录文件/proc
是一种文件系统,存在一个mem文件,它不能被cat,但可以被程序读取并且修改,非注入式跨进程修改就是修改该文件
5.2 注入式篡改
特点:注入式的读写,目的比较强。
过程:通过静态分析或者动态调试找到要篡改的数据位置信息,然后在游戏动态运行的过程中读写相应地址的内容。
有两种实现方式,一种是篡改内存数据,另一种是篡改逻辑代码。
5.2.1 篡改内存数据
- 通过汇编指令直接修改游戏内存数据(ARM汇编指令:STR)
- 通过系统的API函数实现对游戏内存数据的篡改(memset、memcpy、strcpy等函数)
总结:这两种方式适合于修改全局变量相关的数据,如果是局部变量(对象数据、堆栈数据),使用hook技术。
5.2.2 篡改逻辑代码
暴力修改:静态分析、修改内存代码段属性、修改关键代码
一般都会将跳转指令改为nop(mov R3,R3)
hook技术:基于函数地址的hook(导入表hook、导出表hook)、基于汇编指令的hook(异常hook、inline hook)
6. 游戏分类
MMORPG、FPS、卡牌、ARPG、MOBA、消除、RTS为当前市面上最常见的六种游戏类型,除此之外还有跑酷类,飞行射击类等。
6.1 MMORPG
大型多人在线角色扮演游戏(Massively Multiplayer Online Role-Playing Game,MMORPG)具有一个持续运行的虚拟世界,玩家离开游戏之后,这个虚拟世界仍在网络游戏运营商提供的主机服务器里继续存在,并且不断演进,直至游戏停运(即游戏终止运作)。
在手机MMORPG游戏中可以细分为回合制的《梦幻西游》、《大话西游》以及实时战斗的《六龙争霸》、《全民奇迹》等。其中实时战斗的MMORPG与典型的ARPG类游戏最大的区别有两点,一是实时MMORPG拥有“野外”的概念,在野外中玩家可以和怪物直接进行战斗,同时其他玩家也可以参与其中,而ARPG的战斗基本都在“关卡”(即主线副本)或者其他副本中进行;二是ARPG比实时MMORPG更强调实时性,更注重打击感,会尽可能地给玩家制造一种酣畅淋漓的感觉,而MMORPG则没有这么强的“爽快”感,往往会让人感觉更“迟钝”。
造成这种感觉差异的原因就是MMORPG游戏的强服务器逻辑,这也是MMORPG游戏一大特点。MMORPG游戏由于要营造一个虚拟现实的世界,对于玩家作弊的容忍度非常低,因为一点平衡性被破坏就很可能会产生连锁影响造成整个游戏世界的崩溃。为了把游戏做到尽可能安全,MMORPG通常采用的架构就倾向于强服务器逻辑的架构,本地客户端尽可能只上报玩家的操作,由服务器校验操作的合法性,最终把计算结果下发到本地,由本地做结果表现。
以使用技能为例,本地客户端上报玩家使用技能的ID,服务器校验玩家是否可以使用该技能,校验技能CD是否在冷却中,计算技能结果(命中哪个怪物,打掉多少血量,造成什么其他效果),最后把结果返回给客户端,客户端显示结果。
这样做之后直接把外挂的对抗从本地对抗上升到服务器对抗,安全性有了质的提升。当然,也要付出巨大的代价,由于网络延迟,打击感基本没有;游戏对网络质量要求很高;服务器运算压力会变大;开发难度也随之提升。
6.2 FPS
第一人称射击游戏(First person shooter,FPS)是以主视角进行的射击游戏。玩家从显示设备模拟出主角的视点中观察存在的物体并进行射击、运动、跳跃、对话等等活动。
一款游戏的安全性关键还是在它的本地逻辑,游戏的本地逻辑越多,越致命就越容易导致游戏出问题。而FPS类游戏又是实时性很强的游戏,不可避免地要把许多逻辑放到本地。
手机端FPS游戏面临的问题本质上跟PC端并没有太大的区别,主要有,透视、自动瞄准、无后坐力、无限子弹等。
6.3 APRG
动作角色扮演游戏(Action Role-Playing Game,APRG)是电子游戏类型的其中一种。意指将动作游戏、角色扮演游戏(RPG)和冒险游戏的要素合并的作品。
ARPG相对于MMORPG而言更强调实时性,玩法上最大的的差别在于ARPG的战斗基本上都在副本中完成,不存在多名玩家一起战斗的场景(即“野外”)。
由于对实时性要求更高,ARPG游戏难免会把碰撞检测、血量、伤害等重要逻辑放到本地,以达到“即时”、“爽快”的效果,这样就容易引起一系列的问题。
6.4 卡牌
手机卡牌游戏主要指运行在智能手机平台的以卡牌形式发行的游戏,其中卡牌RPG养成游戏为热门游戏,此类游戏一般将游戏主角以卡牌的形式展示,然后玩家通过收集、养成自己的卡牌闯过一个又一个关卡完成游戏。现在的卡牌RPG养成游戏又陆续加入了PVP等多样玩法吸引玩家。
卡牌游戏较之其他类型的游戏逻辑要简单的多,服务器可以很方便地进行复盘,玩家在本地也不会有太多的操作,因此安全性较高。但由于游戏简单,不排除制作团队安全意识较差,把逻辑放到本地的情况。
6.5 RTS
即时战略(Real-time Strategy,RTS)游戏是战略游戏的一种。顾名思义,游戏的过程是即时进行而不是采用回合制。通常,标准的即时战略游戏会有资源采集、基地建造、科技发展等元素。在玩家指挥方面,即时战略游戏通常可以独立控制各个单位,而不限于群组式的控制。
RTS类游戏对实时性要求也不高,因此大部分逻辑都可以放到服务器,但仍然不排除部分制作团队由于各种原因做成强本地逻辑的架构,特别是一些由单机移植成网络的RTS游戏,由于本身就是本地逻辑,和服务器的交互可能只是一种“远程存档”的形式。
6.6 消除
消除类游戏是益智游戏的一种,玩家游戏过程中主要是将一定量相同的游戏元素,如水果、宝石、动物头像、积木麻将牌等,使它们彼此相邻配对消除来获胜。通常是将三个同样的元素配对消除,所以此类又称为“三消游戏”。
消除类游戏和卡牌类游戏都属于游戏逻辑相对简单的游戏,因此服务器都可以比较低成本地做到复盘校验。但往往是这类游戏过于简单,游戏制作方会忽略游戏的安全性问题,把许多逻辑放在本地。
6.7 MOBA
多人在线竞技游戏(Multiplayer Online Battle Arena,MOBA),是即时战略游戏的一个子类。玩家被分为两队,单个玩家只能控制其中一队的一个角色。
MOBA游戏对实时性要求很高,但更重要的是公平性,根据这样的要求,游戏务必要做成强服务器逻辑,否则无论本地保护做得多好,都有被攻破的风险,一旦被攻破,后续的运营需要付出的人力财力都会增大许多。
6.8 跑酷
跑酷类游戏是动作游戏的一种,主要玩法是由玩家控制游戏人物不断地前行,同时需要躲避时不时出现的障碍物。跑酷类游戏的收益一方面是跑动距离,另一方面是游戏中获取和金币等道具。
跑酷类游戏一大重点在于角色与障碍物的碰撞,碰撞是一件对实时性要求很高的事件,碰撞逻辑一般就只能放在本地,因此对碰撞的检测就需要做一些服务器校验来保证逻辑安全。
6. unity3D游戏分析
6.1 雷电星海战歌(unity3D)
7. cocos2dx游戏分析
游戏引擎:cocos2dx、unity3D
Unity3D游戏大多使用C#开发,游戏主逻辑大多打包在Assembly-Csharp.dll
中, 而cocos2dx游戏一般采用了C++或者lua语言开发。
7.2 雷霆战机(C++)
射击类游戏,关卡类
7.3 2048(cocos2dx)
关键So:libcocos2dcpp.so
Java层只是进行了简单的界面实现。
libcocos2dcpp.so
直接在String窗口搜关键字符串,关键函数并没有被加密。
7.4 lua游戏
关键So:libcocos2dlua.so
、libhellolua.so