第一章

静态修改文件

游戏文件被静态修改并重新打包,签名

静态修改游戏资源

修改游戏的资源文件,达到改变游戏逻辑或者塞入一些不健康信息的目的。

静态修改游戏代码

一般手游都是用c++编写,通过编译后的游戏主逻辑的代码一般都用动态链接库保存。修改较多的是代码段和只读数据段,只读数据段的话存的一般是固定的一系列数据,一般是某些固定的参数。代码段自不用说。

修改配置

和修改资源差不多,一般配置文件打包的时候都是加密的,但是一些游戏还是不加密,这样很容易达到篡改的目的。

动态篡改逻辑

常规的动态篡改逻辑都会伴随着注入的操作,注入就是将动态链接库加载至目标进程,让目标进程执行动态链接库的代码,实现篡改逻辑

android平台的注入一般分为两种,一种是通过Zygote进程(xposed)渗透到目标进程之中,另一种是直接注入到目标进程之中。

IOS平台一般用MobileSubStrate组件将动态链接库注入游戏进程。

修改数据

安卓可以通过”/proc/<pid>/maps”文件遍历有r标识的模块,然后读写”/proc/<pid>/mem”文件达到和注入式修改差不多的效果

游戏协议

游戏协议修改可以有两种因素,重发游戏协议或者篡改游戏协议。一般通过给发至客户端的游戏协议加密或者哈希校验来解决篡改游戏协议的问题,通过给协议加序号字段的方式防止对方重发游戏协议。

服务器不能轻易相信客户端传来的数据,才能保护游戏的安全性

重发游戏协议

一般这种危害造成的原因不是游戏开发对协议的逻辑检测有问题,而是协议字段少,协议之间耦合性不好。要是每个协议加上时间等字段,就不至于会被轻易的复制粘贴而攻破。

第二章 外挂的定义,分类和实现原理

外挂分类

辅助版外挂

内存修改器

修改内存,和cheat engine差不多

变速器

加快或减慢游戏速度。

按键精灵

记录手机的按键,实现自动化刷金币等操作。

模拟器

用电脑模拟,打枪啊格斗啊都比手机好用很多。

抓包工具

主要是作用于协议没加密的那种类型。抓到协议的包,然后篡改或者重发实现外挂功能

破解版外挂

本质上是一个非法客户端。可分为脱机挂和修改原客户端两类。

脱机挂是外挂作者基于对协议的分析,自行开发的一个非法客户端。工作室对这个需求巨大

辅助版外挂实现原理

专用插件

定制外挂,每个外挂对应一个游戏。用注入的技术将功能模块注入内存之中,并执行功能模块的入口函数。

android一般是Zygote注入或者ptrace注入,ios一般是利用Cydia框架注入dylib模块。

外挂模块被注入内存后会通过hook的方式去hook一些游戏的相关函数,实现外挂的功能。这个需要外挂人员提前逆向分析得到对应操作的代码位置和逻辑,然后通过功能模块hook这些函数,来改变参数或者进行其他操作。

通用工具

内存修改器

实现方式有两种:一类实现和专用插件类似,外挂的功能模块注入游戏进程中然后用本地的Socket接收操作,直接遍历内存的方式实现。

另一类就是利用平台加载机制取巧实现。例如在Android平台下通过/proc/[pid]/maps可读写游戏的内存镜像。

变速器

影响了游戏的时间度量,游戏一般是以帧为时间计量单位,通过调用c函数来计算每帧的更新。加速器一般就是hook libc.so文件中的关于时间的函数,比如gettimeofday,clock_gettime。修改方式就是上面的hook实现。针对libc的导出函数可以利用导入表hook的方式。

按键精灵

调用系统api来发送按键序列,一般和系统息息相关.Android可通过Instrumentation接口的sendPointerSync函数实现,也可以通过有root权限的驱动程序去调用Runtime.getRuntime().exec()去执行sendevent等命令

模拟器

主要实现逻辑就是用VirtualBox模拟Android系统,可直接模拟x86的Android系统。

抓包工具

本质是一个网络数据包编辑器,一类实现是用硬件的方法,让网卡变成混乱模式,即可拦截数据包,另一类实现是hook recv,send等函数,获得数据包。

破解版外挂实现原理

破解版外挂是预先静态修改过的第三方游戏客户端。脱机挂是外挂作者在逆向分析协议后编写的第三方客户端,对技术要求大,因为要完全分析协议。

另一类是对游戏客户端进行静态修改的破解版,分为修改逻辑代码和数据两种

逻辑代码

Android下的逻辑代码修改,不同游戏引擎方法也不同。

Cocos2D游戏,逻辑代码保存在so文件之中,一般用ida分析

C#保存在Assembly-CSharp.dll中。

ios的文件就直接分析bin文件即可。

改动的话一般是改跳转或者改参数。

数据资源

包括除源代码以外的所有资源,包括图片资源,配置资源,音乐资源等。方式一般有两种,无脑替换和分析调试

无脑替换就是直接一个个删除,看游戏的变化。比如有些游戏安全性不够,就会被如此打击。比如射击游戏删除子弹资源,跑酷游戏覆盖关卡信息等。

分析调试就是通过逆向分析的方式去识别加密并解密,得到逻辑后再替换或patch

第三章 手游外挂技术汇总

arm汇编

所有ios和绝大多数Android都是用ARM架构的CPU,做游戏安全分析很多时候要修改汇编代码,所以学习arm汇编是很必要的。

c/c++

虽然Android平台主要用java编写应用,IOS平台主要用Objective-C来编写,但是考虑到游戏的跨平台性和执行效率,一般主要还是考虑用c或c++,用OpenGL ES来绘制界面。而cocos2d-x可以用多种语言编写游戏逻辑,不过为了效率大部分还是用c++,且大部分外挂都是c++编写。

Android开发

绝大部分逻辑代码在native层实现,所以Android开发的最低要求是会Android程序的生命周期和native程序的开发。

IOS开发

和Android差不多,要了解IOS的生命周期和Xcode,Objective-C。

常用的游戏引擎

unity3D用c#开发,Cocos2d-x用c++或lua

静态修改

对于java代码,一般用apktools反编译后直接修改smali代码,然后重打包和签名

c#一般是修改IL指令然后编译成dll文件替换原来的。

第七章 手游开发基础

游戏玩法与分类

MMORPG

和魔兽世界差不多,意思是大型多人在线角色扮演游戏。服务器一直延续,打怪的时候别人能参与,不会进入一个特定的地方单独进行。优点是实现了一个世界,很自由。缺点是不爽,打击感不强,因为首先这是强服务器游戏,要保证服务器的正常运作,所以每个人特效不会很离谱,而且一般是服务器端判断你的击中或者伤害或者什么再下发给你,所以会有网络延迟问题,没实时的那么爽。但是安全性会很高。

FPS

第一人称射击游戏,特别注重实时性,所以不可避免的要把逻辑放在本地。所以挂多,嗯

ARPG

动作角色扮演游戏,相比MMORPG更加注重实时性,所以本地也有很多逻辑

手游开发语言

手机游戏开发分为服务端开发和客户端开发

服务端开发一般都是用c/c++,不过java和python也有。但是对于MMO这种对服务器实时性要求高的游戏,就必须用c++

客户端开发一般都是用公开的引擎,unity3D,Cocos2d-x等。而这些引擎都有固定的使用语言,所以其实安全性在定下用什么引擎的时候就已经大致定下来了,和语言没关系

手游网络模式

一般分为两种,强联网和弱联网模式

强联网对应的一般是FPS等对于实时性要求很高的游戏,这个是不能断网的。

弱联网就像天天酷跑等游戏,中间断网没事,只要结算的时候有网就行。

优点和缺点很容易看出,强联网逻辑大部分在服务器,安全性高,弱联网本地多,所以安全性差

第八章 游戏引擎的基本概念和常见介绍

游戏引擎是什么

游戏的核心部件的功能代码

游戏的运行实现可以分为4层,从下往上依次是硬件层,第三方组件层,引擎层,游戏逻辑层

游戏引擎子系统

渲染系统

是引擎所有子系统中最复杂最强大的一块。渲染可以看成把二进制文件变成图像输出到屏幕的一个过程

游戏引擎的渲染系统一般是用图像API完成的,例如d3d,OpenGL等。

音频系统

有类似渲染系统的API:Windows Multimedia,OpenAL等。

物理系统

游戏中比较常用的场景是碰撞检测,因为接口是公开的,写外挂的人很容易可以定位对应代码并且修改。会实现穿墙或者无敌等功能。

人工智能

第九章 游戏漏洞简述

基本概念

游戏逻辑漏洞

通过修改客户端的游戏来实现外挂功能。和游戏开发时的网络架构有关,不是所有游戏都有,在游戏客户端实现。

游戏逻辑漏洞和网络协议关系密切,如果强联网游戏漏洞较少,反之较多。所以要找游戏逻辑漏洞要先看网络类型。

游戏协议稳定性漏洞

指构造畸形协议使协议处理方因无法处理协议而崩溃的漏洞。和服务器类型和网络架构没关系,所有游戏都可能出现。挖掘漏洞的过程实际上就是畸形协议的构造过程,一般用fuzz的思想去挖掘畸形协议字段

游戏服务端校验疏忽型漏洞

就和篡改协议那种差不多,主要是服务端校验不严,依赖于开发人员的严谨性。比如协议耦合性不好,字段少等。

第十章 静态分析

arm速成

体系简介

有15个通用寄存器,r13是sp,r14是LR,保存的是函数的返回地址,r15是pc,指向当前指令地址+8处。

指令样例

B/BL指令

作用是跳到某个地址。

offset

二进制解释的话,0到23位是offset,是目标地址和当前指令的相对偏移,要左移两位,因为偏移是4字节对齐的。

L

24位为L,L位为一的话代表存储下一位的地址到LR寄存器中。

标识码

25到27位是标识码,代表当前的指令是什么

cond

28到31位是cond位。代表指令的条件,标识码101对应的是B指令,然后cond如果为0000代表是BEQ指令

LDR/STR

假设一个指令如下

LDR R2, [R12]

二进制含义如下

1110 01 011001 1100 0010 0000 0000 0000

Cond op IPUBWL Rn Rd Offset

Cond

表示啥都行

op

标识码

I

I是0代表offset是立即数

B

B是0代表传输的是一个字

L

L是1代表读内存

Rn

0xc,代表基址寄存器是R12

Rd

是2,表示把内存值读到R2

Offset

和上面一样

实际的指令含义为:读取R12寄存器指向的内存的值到R2寄存器中,读取4个字节。

Thumb指令简述

看CPSR寄存器的第6个bit是否为1,如果是1就是Thumb,否则arm

函数传参

比较常见的是把前四个参数放到R0~3中,然后剩下的放到栈里面。返回值放到r0之中。

第十一章 动态调试

加载安卓原生动态链接库

远程附加调试的时候,如果用户相对init_array段或JNI_Onload进行动态调试,则用attach的方法是不行的,因为他们已经执行了。

调试JNI_Onload和init_array

普通的附加是不行的,我们得用am来用调试模式打开程序。

1
am start -D -n com.example.xxx/.MainActivity

然后使用ida附加到被调试进程,不过调试选项还得勾选Suspend on library load/unload

在ida附加到程序后,还需要用jdb命令来让程序恢复执行。

1
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700

第十三章 注入技术的实现原理

Android下ptrace注入技术的实现

ptrace函数介绍

ptrace原型如下

1
long ptrace(enum __ptrace_request request,pid_t pid,void *addr,void *data);

request

request参数是一个union,该参数决定了ptrace的行为

不同的request含义不一样。

request含义比较多,挑几个重要的参数信息及解释如下

1
2
3
4
5
6
7
PTRACE_ATTACH		附加到指定远程进程
PTRACE_DETACH 从指定远程进程中分离
PTRACE_GETREGS 读取远程进程当前的寄存器环境
PTRACE_SETREGS 设置远程进程的寄存器环境
PTRACE_CONT 使远程进程继续运行
PTRACE_PEEKTEXT 从远程进程指定地址读取一个word大小的数据
PTRACE_POKETEXT 往远程进程指定内存地址出写入一个word大小的数据

pid

是远程进程的id

addr/data

ptrace注入进程流程

ptrace注入的目的是将将外部的模块注入到对应的游戏进程之中,然后执行模块相应的代码。一般有两种方法

  • ptrace将shellcode注入到远程进程的内存空间中,然后去执行该代码。
  • 远程调用dlopen,dlsym模块去加载被注入的模块并执行对应的代码。

整体流程

  1. attach到远程进程
  2. 保存寄存器环境
  3. 利用mmap函数分配内存
  4. 向远程的内存空间写入模块名和注入函数
  5. 用dlopen去打开注入函数
  6. 用dlsym去获得注入函数的地址
  7. 调用该函数
  8. 恢复寄存器环境
  9. detach

ptrace注入的实现

附加到远程进程

用ptrace,第一个参数设置为attach,addr和data参数都为null

1
ptrace(PTRACE_ATTACH,pid,NULL,NULL)

附加到远程进程后,远程进程执行会中断。父进程可以调用waitpid函数查看子进程是否处于暂停状态。

1
Pid_t waitpid(pid_t pid,int * status,int options)

其中要是option参数为WUNTRACED的话,表示如果pid对应的进程是处于暂停状态,立马返回。通过这个函数等待子进程执行到暂停。

读取和写入寄存器值

在通过ptrace改变对应进程的状态之前,需要先读取和保存对应进程的所有寄存器状态。detach的时候将保存好的寄存器值写入回目标进程,不保存的话目标进程恢复执行的时候会崩溃

实现的话,用ptrace的request中的setreg和getreg即可实现。对应调用代码如下

1
2
ptrace(PTRACE_GETREGS,pid,NULL,regs);
ptrace(PTRACE_SETREGS,pid,NULL,regs);

在ARM处理器下,ptrace函数中data参数的regs是pt_regs结构的指针,从远程进程获取的寄存器值将存储到该结构中。pt_regs结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct pt_regs
{
long uregs[18];
};
#define ARM_cpsr uregs[16]
#define ARM_pc uregs[15]
#define ARM_lr uregs[14]
#define ARM_sp uregs[13]
#define ARM_ip uregs[12]
#define ARM_fp uregs[11]
#define ARM_r10 uregs[10]
#define ARM_r9 uregs[9]
#define ARM_r8 uregs[8]
#define ARM_r7 uregs[7]
#define ARM_r6 uregs[6]
#define ARM_r5 uregs[5]
#define ARM_r4 uregs[4]
#define ARM_r3 uregs[3]
#define ARM_r2 uregs[2]
#define ARM_r1 uregs[1]
#define ARM_r0 uregs[0]
#define ARM_ORIG_r0 uregs[17]

远程进程的内存读取和写入数据

request参数为PTRACE_PEEKTEXT时,可以从对应的远程进程的内存中读入一个word回来。

调用实现如下

1
2
ptrace(PTRACE_PEEKTEXT,pid,pCurSrcBuf,0);
ptrace(PTRACE_POKETEXT,pid,pCurDestBuf,ITmpBuf);

addr参数为需要读取的远程内存的地址,返回值为读取的数据。

POKETEXT是写入远程的内存,也是一次一个word

写入数据时要注意,如果写入的数据长度不是word的倍数,则在写入最后一个不是word的数据时,要保存高位地址的值。

1
2
3
4
5
ITmpBuf = ptrace(PTRACE_PEEKTEXT,pid,pCurDestBuf,0);
memcpy((void *)(&ITmpBuf),pCurSrcBuf,nRemainCount);
if(ptrace(PTRACE_POKETEXT,pid,pCurDstBuf,ITmpBuf) < 0){
return -1;
}

远程调用函数

arm处理器的函数调用的前4个参数是由r0~r3传递。剩余的参数按由右向左的顺序压入栈中进行传递。

1
2
3
4
5
6
7
8
9
for(int i=0;i<num_params && i<4;i++){
regs->uregs[i] = parameters[i];
}
if(i<num_params){
regs->ARM_sp -= (num_params-i)*sizeof(long);
if(ptrace_writedata(pid,(void*)regs->ARM_sp,(uint8_t*)&parameters[i],(num_params-i)*sizeof(long))==-1){
return -1;
}
}

远程调用函数前要先检查一下调用参数的个数,如果多于四个就先调整sp寄存器在栈中分配的空间大小,再通过ptrace将剩下的参数写入栈中。

写入函数参数后,将pc改为要执行函数的地址。因为arm架构下有arm和thumb两种指令,所以要先看看是哪种。

可以通过地址最低位是否为1判断调用地址指令是哪个。

如果是thumb,要把最低位设置为0,并将CPSR的T标志位置位。如果是arm,就把CPSR的标志位复位

远程进程恢复运行前,要设置远程进程的LR寄存器为0,并在本地调用waitpid函数。

远程进程的函数调用结束后,会跳到LR对应的寄存器之中,而LR设置为0,所以会报错并停止程序,随后waitpid返回并接管子进程。通过读取远程进程的r0可以知道函数返回结果。

ptrace 要调用很多函数,比如mmap,dlopen等。

mmap函数位置在/system/lib/libc.so模块,dlopen,dlsym,dlclose都在/system/bin/linker模块中

读取/proc/pid/maps可以得到系统模块在本地进程和远程进程的加载的基地址,要获取远程进程的mmap等函数的地址,可以通过计算本地进程的mmap和模块的偏移,然后看看远程进程的模块基址,加上偏移就是对应的函数

恢复寄存器值

不恢复就奔溃

detach进程

用ptrace的PTRACE_DETACH即可。

Android下Zygote注入技术的实现

Zygote是Android操作系统的孵化器进程

注入原理

绝大部分的游戏进程都是由Zygote fork出来的,而我们知道fork出来的子进程和父进程的模块是完全一样,所以如果我们能在Zygote中注入恶意进程,他fork出来的进程也会有

注入实现流程

  • 注入器注入模块a到Zygote进程
  • 手动fork出进程b
  • 模块a夺取进程b的控制权
  • 执行模块a的代码
  • 归还控制权给进程b

此处注意两个点

  • 目标进程需要在注入Zygote后启动才能被注入
  • 成功注入Zygote之后所有生成的新进程都会有已注入Zygote的模块信息,需要在新启动的进程执行之前获得执行权,然后判断当前进程是否为目标进程,如果是,执行相关代码,否则返回执行权

注入器的实现方式

是整个流程的开始,也是最重要的一环。目的是将模块注入到Zygote进程之中。

image-20221106132655666

注入器一共实现了三次跨进程调用

  1. 调用mmap申请对应的进程地址空间,保存我们的shellcode
  2. 远程执行shellcode
  3. munmap释放之前的内存

shellcode实现的功能为:将指定的模块加载到目标进程之中,然后执行模块的入口代码

注入器各功能实现

关闭SeLinux

Selinux是linux的一个安全子系统,在Android4.4后的版本默认打开此功能,Selinux会影响注入的实现,所以要事先关闭此功能。

关闭有三个操作

  • 获取SeLinux的配置目录。可以通过查看/sys/fs/selinux目录的文件系统状态,如果等于SELINUX_MAGIC的话就是SeLinux的配置目录。另一种是通过/proc/mounts文件查看是否存在SeLinux的路径
  • 获取配置文件的SeLinux的开关状态
  • 关闭SeLinux

附加到Zygote,保存进程现场

Android系统中进程同步的信号机制和Linux相似。

当注入器附加到Zygote上后,Zygote会接到一个SIGSTOP信号,然后他处理这个信号,这时父进程用waitPid函数进行等待,等待子进程也就是Zygote进程进行操作,结束后才返回。

waitpid很多作用,一般的作用是阻塞当前进程,等待指定进程状态发生变化。

第三个参数为0的时候,使用默认的阻塞式等待,知道Zygote处理完SIGSTOP信息。Zygote停止时,进程状态发生变化,waitpid返回Zygote的进程状态,注入器被唤醒。

下方为WaitPid的代码

1
2
3
4
5
6
7
8
9
10
11
int WaitPid(pid_t pid,int *status,int option){
while(waitpid(pid,status,option) == -1){
if(error == EINTR){
continue;
}
else{
return -1;
}
}
return 0;
}

如果注入器收到的是让waitpid中断的信号就不管,否则就直接返回。

获取Zygote进程关键函数的地址

和pwn差不多,首先在注入器加载libc.so,然后获得注入器的函数地址,相减求得偏移。随后在Zygote里找到libc.so的基址然后加上这个偏移即可。

调用mmap函数

mmap的定义如下

1
void *mmap(void* start, size_t length,int prot,int flags,int fd,off_t offset)

根据arm编译器的规则,要把前四个参数放r0到r3中,然后其余参数放到堆栈中。所以我们要调用mmap,要布置一下堆栈。

解决堆栈布局的思路是,将pc设置为mmap的首地址,同时LR设置为0.给Zygote发送PTRACE_CONT信号,让Zygote从mmap的首地址开始执行,执行结束后跳到LR对应的地址,也就是0,然后出现异常停止,waitpid返回,回到父进程继续执行。

配置shellcode

shellcode代码主要做了三个事

  • 用dlopen加载指定模块
  • 用dlsym加载指定模块的地址
  • 调用指定模块

最后同样设置ir为0,让父进程接管之后的操作。

远程调用shellcode

和调用mmap差不多,只是mmap将ir赋值为0是在调用之前的操作,而shellcode是在shellcode执行的过程中将其赋值为0

调用munmap释放内存

恢复进程到初始状态

将寄存器和堆栈还原后,直接detach

注入Zygote模块的功能

除了特定的一系列功能外,模块基本上都有这两个功能

  • 劫持新启动的进程并获得控制权,hook Zygote的一些系统函数来感知新进程的创造
  • 查看当前进程是否是目标进程

Android感染elf文件的注入技术实现

可执行文件感染意思是,通过修改可执行文件的二进制文件,让该可执行文件在执行的时候先执行我们写入的代码再执行原逻辑

感染elf文件实现原理

通过修改Program Header Table中的依赖库信息,添加自定义的库文件,当游戏进程加载主逻辑模块的时候,也加载了我们所写的库文件

Program Header Table的表项结构如下

image-20221106184045441

其中p_type的取值定义如下

image-20221106184144895

当p_type取值为PT_LOAD时,描述程序加载时的内存映射信息

当p_type取值为PT_DYNAMIC时,p_offset(偏移)和p_filesz(大小)会指向dynamic段。dynamic段描述的是链接和加载时的库信息,是一个结构体数组。

image-20221106184417221

需要关注的是d_tag的四种取值情况

image-20221106184534299

感染elf文件的注入实现过程

  • 在DT_STRTAB指向的字符串表中加入一个新的元素,就是我们的so模块的名称,由于凭空添加一项,插在中间的话后面的所有元素偏移都将改变,所以一般将字符串表移至文件末尾。
  • 由于字符串表会被映射到内存之中,所以我们得在Program Header Table处添加一个PT_LOAD表项,指向我们的船新字符串表,同时将Program Header Table添加到文件末尾。
  • 修改DT_STRTAB,DT_STRSZ,指向新的字符串表,同时在dynamic array的结尾加上DT_NEEDED,指向我们的so模块
  • 修改ELF Header的Program Header Table信息,指向我们的船新表

image-20221106190247989

感染elf文件的实例分析

image-20221106190627481

重要信息如下

  • elfheader.e_phoff, elfheader.e_phentsize ,elfheader.e_phnum分别保存了program header table的位置,大小和表项个数信息
  • program_header_table.program_table_element[0]保存了program_header_table的位置和内存映射的信息
  • program_header_table.program_table_element[1]将文件相对偏移0到2238752的数据映射到了内存之中
  • program_header_table.program_table_element[3]保存了dynamic array结构数组的起始位置和大小信息

代码分析

看D:\书籍\游戏安全手游安全技术入门\代码\elfinfector\jni\elfinfector.c

它会告诉你一切

第十四章 hook技术的实现原理

简介

hook技术的目的是,在目标模块执行之前,监视寄存器和数据,改变模块功能

基于异常的hook实现

用SIGILL异常机制可以实现hook操作。对想要监控的地址设置一个非法指令,当执行到此指令时触发异常,调用事先设定好的异常回调函数,恢复监控地址的原指令,打印寄存器,获得上下文。不过这样只能一次hook,下次再执行到该地址的时候就不会再触发异常了。于是我们可以通过在要触发异常的地址的下一个地址处设置一个非法指令,判断当前的指令和下一个指令是否为非法指令,如果是的话证明是我们要处理的那个指令,如果不是的话证明是我们的恢复指令,再把对应的地址的指令设置为非法指令,从而在下一次运行到该指令的时候再次触发。

image-20221108185510732

系统通过sigaction API注册SIGILL信号的异常处理回调函数

arm之间的地址差值为4,Thumb2指令集的画有2有4,所以需要解析当前指令的长度

image-20221108185743767

可以从表中得出,只有在opcode[15:13]为111且[12:11]不为00时,才对应32位

Android平台基于异常hook的实现流程

image-20221108190001784

注意的点主要是两个

  • 注册异常处理函数,对目标地址写入异常指令
  • 执行完后将目标地址恢复,在地址后设置异常,然后下一个异常将原指令地址设置为异常

基于异常hook的实现代码

代码自己看,逻辑主要就是DoExceptionHook中将sigaction结构体数据赋值对应的异常和自己写的异常处理函数,再传入sigaction函数之中。然后就和上面的差不多了。

Android平台的Inline Hook实现

实现原理

改汇编跳到其他位置执行自己的代码和原汇编指令后跳回来

image-20221110110628461

注意此处第③步执行原指令2的时候,如果指令2是相对寻址的指令,要进行指令修复

Android平台导入表hook的实现

和windows下的IAT Hook相似,原理就是替换要hook的导入函数地址,然后导入函数调用的时候首先获得执行时机

实现原理

假设模块TargetLibrary模块内部调用了gettimeofday函数 ,我们想要hook该函数修改某些信息。

打开so文件并解析elf文件格式。先找到静态的got表的位置,读取gettimeofday的函数地址,然后在got表里面寻找对应的函数,看那个函数的入口地址是gettimeofday。遍历一圈,如果遍历到的名字相匹配,就将该函数的地址替换为新的函数地址。

注意的点如下

  • 可以在/proc/[pid]/maps找到对应so文件的绝对路径和so文件的基址
  • 需要将对应需要改写的函数地址的页改为可写的权限

导入表hook实现流程

image-20221110144529916

第十六章 游戏进程的模块信息获取

游戏内容读写方式分类

可以分为两大类

  • 注入式:注入游戏进程空间,比如ptrace,Zygote式注入
  • 非注入式:通过安卓系统读写游戏内容,不需要注入游戏空间

非注入式篡改

主流的非注入实现方式有三类

  • 修改APK安装包
  • 修改Android系统data目录下的文件
  • 修改proc文件夹相关信息

篡改apk安装包

常用修改方式

  • 改跳转为nop
  • 修改寄存器 在函数头,只需要修改一个byte就可以实现。改变寄存器,让本来减去的很小的值变成另一个寄存器的很大的值,从而实现秒杀
  • 抹除明文字符串 这些字符串想key-value的形式,如果抹去key字符串的话,如果验证不严谨,可能导致出现默认值或错乱值,导致外挂功能实现

注入式篡改

目的性比较强,首先先静态分析so文件,找到要修改的部分,然后动态运行途中注入,通过改变读写权限然后修改或hook的形式来对程序进行篡改

需要一定代码开发能力和逆向分析能力

篡改数据

两种方法

  • 通过汇编指令修改,比如STR指令可以直接对内存数据进行修改
  • 通过API修改,比如memcpy,memset等

篡改逻辑代码

有两种方式

  • 暴力篡改,这种是注入式里面最简单的,用一个mprotect改变权限,然后嗯修改就完事了
  • hook篡改

其中hook篡改一般也分为两种,一种是函数地址的hook,比如hook导入表和虚表。还有一种是汇编语言的hook,比如异常hook,inline hook。

  • 函数地址的hook,操作较为简单,但是缺点是只能获得返回值和参数,局限性比较强
  • 汇编语言的hook,虽然比较麻烦,但是优点很明显,可以获得函数执行过程的参数和寄存器数据

第十七章 反调试技术

安卓平台常规反调试

self-Debugging反调试

指父进程创建一个子进程,并由这个子进程调试父进程的技术,ctf也遇到过几次了,就是双进程保护

  • 消耗的系统资源比较少
  • 几乎不影响保护进程的性能
  • 轻易的阻止其他进程调试受保护的进程

方案很简单,实现起来很麻烦。此种反调试的基石是ptrace

介绍被调试进程的状态:一个被调试进程只有两种状态,一个是运行一个是暂停。运行好理解,暂停比较麻烦。

有四种暂停的状态:signal-delivery-stop,group-stop,syscall-stop,ptrace-event-stop

signal-delivery-stop

当一个进程收到除了SIGKILL之外的信号的时候,内核会选择该进程任意一个线程来处理这个信号,如果当前线程处于被调试的状态,该线程就会处于signal-delivery-stop的状态,然后调试器通过Waitpid来等待该事件并收到信号。此时调试器来处理该信号,此时进程没有真正的收到信号。它可以让该信号被丢弃,让进程永远无法收到信号。

group-stop

出现于多线程的情况。如果调试器a调试进程b,而调试器a的进程崩溃了。但是进程b还在调试中,就会处于group-stop的状态,只有给进程b SIGCONT的信号才会让他继续进行,可是这种情况的发生对self debugging是很不利的

被调试进程处于group-stop的状态必须满足两种条件

  • 被调试进程处于被调试状态
  • 收到暂停信号

调试器可以通过避免被调试的进程收到暂停信号来阻止这种情况发生。

总共有四种信号

SIGSTOP SIGTSTP SIGTTIN SIGTTOU

会导致进程陷入group-stop状态

反调试方案的实现过程

image-20221111165606465

破解方法

因为父进程fork出一个子进程作为调试器来调试主进程嘛,我们直接ida attach子进程,然后间接调试父进程即可。

轮询检测反调试方案

轮询的原理

通过读取/proc/pid/status文件,判断当前进程有无在被调试器调试。

image-20221111171436139

各个字段的解释如下

name:进程名

State:状态

Tgid:线程组的id,一般只进程名

Pid:线程的id,和getpid函数返回值一样

PPid:父进程的进程id

TracerPid:实现调试功能的进程ID,如果值为0代表未被调试

此种方法消耗资源较大,因为它会一直查询对应的TracerPid。

实现

就是看TracerPid

java层的反调试

主要针对基于JDWP协议的Java代码调试器

安卓系统中,要想用JDWP来调试Java代码,需要在AndroidManifest.xml里面将android:debugger的属性改为true

所以实现的话,就判断这个属性是否为true咯,Android SDK中的android.os.Debug类里面提供了一个isDebuggerConnected的方法,就是用于判断这个

第十八章 游戏逆向分析实战篇

c++游戏分析实战

c++游戏识别

Cocos2d-x是一个移动游戏开发框架,可以单独用c++或lua进行开发,也可以混合使用

c++基础

虚表

如果一个类有虚函数,那么在它的内存结构中,头部首先会有一个指针,这个指针叫做虚表指针,指向的位置是一个函数指针数组,就叫做虚表,也就是一连串的函数地址。这个数组每一个元素都是这个类的虚函数的地址。同一个类的所有对象共享一个虚表。如果两个类的虚函数不同,会有两个虚表

逆向方法

看字符串

shift+f12

send函数回溯

游戏总会进行网络通讯,而Android的最底层的通信的函数是send,而如果直接看send的数据是看不到啥的,因为一般都会把数据进行加密。所以我们只需要查找每个用到send的地方,然后交叉引用往回回溯,找到对应的加密函数或者组包函数就行。组包函数的上层一般是功能函数,在组包函数中修改可以改变游戏与服务器通讯的底层通信逻辑

过程可以借助ida脚本,过滤一些用的多的函数,比如心跳函数,也可以输出log

输出log

在一些被频繁调用或者有明文信息的函数处,如果有必要,也可以输出log进行分析,方式可以是ida脚本也可以是注入进程并设置hook的方式

破解思路

相对较简单,注入然后hook,篡改游戏逻辑

  • hook关键函数,修改传入参数,比如如果有SetHp函数,直接传进去一个114514,让你成为臭血牛
  • hook关键函数,修改返回值。比如如果存在一个计算伤害的函数,直接改变返回值,实现一刀9999
  • hook关键函数,实现多次调用。比如hit函数调用114514次,直接臭死对手
  • hook关键函数,直接返回。比如dead
  • 直接修改逻辑。通过注入或者远程ptrace后,直接修改判断代码,嗯改

Unity 3D游戏分析实战

Unity3D 破解方法

  • 修改Unity运行时编译生成的汇编代码
    • 修改传入的参数,寄存器,一般为set函数
    • 在汇编代码中尽量不修改内存和opcode,尽量修改寄存器
  • 反编译Assenbly-CSharp.dll,直接修改源代码
    • 修改函数的返回值
    • 直接删除函数体,留下ret指令
    • 对应的函数中添加新的函数调用,主动进行调用
  • 逆向分析Assenbly-CSharp.dll,修改IL指令
  • 在加载dll文件函数的位置动态保存原始的dll代码,可以绕过dll加密,并修改源代码
    • Hook mono_image_open_from_data_full函数动态保存dll文件。通过ida和jeb动态挂起进程,然后在mono_image_open_from_data_full函数位置设置断点,执行到断点处的时候动态保存即可

如果可以在函数头下断点,就用改变寄存器的方法进行测试,如果不行就用第二种方法。把修改后的Assenblyxxx文件注入游戏中,让游戏执行修改后的代码

分析涉及的工具

对Unity3D的分析实际上就是对源码层的分析

ida

不用多说

ILSpy

反编译和分析dll代码,也可以以源码形式保存反编译的代码

.NET Reflector和Reflexil工具

.NET Reflector也可以反编译和分析dll代码,也弥补了ILspy的功能性缺陷,可以分析出错误的CLR文件头。一些在ILSpy中不能加载的dll文件,如果只是改了文件头这些,可以放到relfector里面分析。

ildasm和ilasm工具

ildasm可以反编译dll文件,动态保存反编译后的IL指令,ilasm则可以用命令ilasm /dll *.il重打包IL码,二者可以用于静态修改c#代码的中间层IL指令

  • nop的机器码 0x00
  • ldc.i4.0 0x16
  • ldc.i4.1 0x17
  • ret 0x2a
  • ldc.r4 0x22,后面为四个字节数据

lua游戏分析实战

关键点是获取lua代码,和Unity3D类似是源码层的逆向分析

识别lua游戏

去Android解压后的lib目录下找so文件,看里面有没有lua的模块例如libcocos2dlua,libhellolua等。一般最大的so模块可能内置了。

也可以关注已解压的assets目录下是否有脚本信息。一般会加密,不过有的厂商也不会。

破解方法

主要分为两步:

  • 获取游戏的lua脚本
  • 替换

不同安全级别的手游,获取lua脚本的时机也不一样(本质是研制lua引擎加载lua脚本的整条加载链,不断分析并找到合适的时机点,然后动态保存和替换

asset资源中可获取lua脚本

在asset目录下获得lua或者luac源码

针对有lua源码的类型,直接APKtool重打包

针对有Luac源码的类型,可以用Unluac开源工具将其反编译为lua源码,然后修改并替换修改后的Lua源码文件(Luac是lua的字节码文件)

在luaL_loadbuffer函数处提取

这个是一个被频繁调用的加载点,Cocos2d-x引起的Lua加载器为cocos2dx_lua_loader,最终都是调用luaL_loadbuffer加载

一般而言,厂商会在该函数上方解密lua文件,然后传入这个函数的参数就是解密后的数据。我们只需要在调用该函数的地方前下断点,然后就能提取出解密后的文件了

image-20221112142200569

在底层的reader函数处获取

lua引擎加载Lua脚本的底层是lua_reader函数。负责底层的脚本遍历,因此在此处动态保存的脚本是lua明文脚本,所有加密都没了(除非改了lua opcode或者引擎逻辑)(感觉可以出题)

常用工具

ida

ChunkSpy

解析luac的文件结构,方便阅读

Unluac

Lua反编译开源项目,可以把luac反编译为lua代码

第二十章 外挂开发实战-《2048》手游快速通关

逆向分析

java代码一般只用于界面显示,如果没有lib的话证明这个游戏是很简单的游戏,逻辑不复杂。

具体逆向过程看书吧,这里直接得出结论,addBoxAtIndexWithLevel函数传入的第二个参数n是数值的2的n次方

功能实现

有几种思路

  • 用inline hook的方式。用Cydia Substrate组件libsubstrate.so模块的MSHookFunction函数对addBoxAtIndexWithLevel进行hook的操作,将传入的第二个参数改变即可实现外挂功能
  • 导入表hook 对导入函数arc4random(2048的随机数值的函数)进行hook,利用_Unwind_Backtrace, _Unwind_GetIP,dladdr函数得到调用者的信息,如果是addBoxAtIndex调用,就将返回值设置为0.这样就可以每次生成数值为2的box
  • 用异常hook和导入表hook的方式,和第二种差不多,在addBoxAtIndex中的arc4random调用之前设置异常hook并计数,然后对arc4random函数设置导入表hook,监控replace_arc4random的运行次数,然后把返回值都设置为0,就能和第二种一样