游戏安全--手游安全技术入门笔记

留坑:里面涉及了太多linux库函数和汇编的东西,坑挖给复习算法的时候一起来


内存修改器:用来搜索、修改游戏的内存数据,玩家一般根据游戏面板中的精确数据利用修改器搜索相应数值,再根据数值变化的规律多次搜索、排除并定位到相应属性所在的内存位置,直接修改即可(模糊搜索、加密搜索、反简单加密搜索)。
变速器:可加快游戏节奏,节省玩家时间;也可减慢游戏时间影响帧更新的频率。
按键精灵:可以直接录制一段固定按键序列,然后循环模拟该按键序列,后续发展成可识别图像来触发特定按键。可用于刷重复性操作较多的游戏。
模拟器:端游到PC
抓包工具:用于拦截游戏的上下行数据包,可篡改、重发、丢弃。拦截后还可以使用复杂规则编辑数据包并模拟和发送数据包。因此协议加密很重要,还有序列的逻辑加密。
脱机挂:外挂作者基于对游戏协议的分析自行开发了一个客户端。这类客户端可以直接刷副本。
外挂功能模块在被注入进游戏进程后,会执行hook操作实现外挂功能。外挂作者需要先逆向分析游戏代码逻辑,找到一些。功能函数的地址,然后用hook来挂钩相应函数,改写参数或者调用逻辑。注入一个通用模块到游戏进程中,根据本地socket接收操作直接遍历内存。以及平台加载机制读写内存镜像。
加速器一般都是调用C库函数获取系统时间进行每帧的更新,Libc.so相关函数使用hook修改。
按键精灵是调用系统API来发送特定操作序列模拟全局按键,如sendPointSync函数,具有Root权限的驱动程序调用Runtime.getRuntime().exec()执行sendevent等命令。
抓包可以通过硬件让网卡处于混乱模式拦截,或者通过hook,针对send和recv类函数进行拦截。

Cocos2D逻辑代码保存在so文件中,可使用IDA等工具读取和修改ARM、Thumb汇编指令;Unity3D游戏的C#脚本在Assembly-CSharp.dll中,可通过ildasm等工具转换为IL代码进行修改操作。在ios中在App的bin文件中,在安卓中是so文件。
ARM汇编:了解常用的赋值、跳转、算术运算、位移运算、堆栈操作、内存读写指令和函数调用约定。
游戏引擎的主逻辑和外挂一般是通过C/C++,重点关注指针和虚函数。
安卓的native层开发以及程序生命周期还有ios的生命周期以及Objective-C语言的特性和Xcode的使用方法,开发外挂需要MobileSubStrate越狱。
如果了解游戏引擎是如何实现碰撞检测的话就可以轻松穿透。
如果是C/C++和Objective-C一般使用IDA静态分析,Android需要ILSpy来反编译。
如果主逻辑加了反静态分析的技巧,一般使用IDA来动态调试
针对C/C++和Objective-C编写的逻辑使用IDA来直接修改二进制文件。
针对Java代码通过APKTool反编译后直接修改smali代码,然后用APKTool重新打包签名。
针对C#会再用ildasm反编译后修改IL指令,然后通过ilasm编译成DLL文件,替换之前的DLL文件。
ARPG由于对实时性要求比较高,因此可能会把逻辑放至客户端。
登录系统、成长系统(战斗系统的基础)、战斗系统(闯关、挑战副本、PVP战、攻城战)、经济系统、邮件系统、聊天系统。
PVE模式弱联网、PVP模式强联网。
服务如果有和客户端同步的要求,包括状态同步、帧同步,强联网模式安全性比较高,因为服务器处理了绝大部分的游戏逻辑,客户端只显示和处理少量游戏逻辑。
弱联网指客户端和服务器端之间的交互不频繁,或者没有实时的交互把游戏逻辑放在客户端中计算,游戏在玩家结算的时候上报结算和校验信息即可。客户端强逻辑意味着客户端被改的东西有很多,一旦服务器做的校验不够,安全问题就会凸显。

游戏的运行分四层:从下往上依次是硬件层、三方功能组件层、游戏引擎层、游戏逻辑层。
游戏引擎子系统:渲染系统,基于成熟的图像API实现,比如DirectX或者OpenGL,这些API封装了GPU和显卡的部分功能,实现涉及浓淡处理、纹理映射、距离模糊、阴影、反射、透明等较多复杂技术的算法。
透视外挂:这个外挂的核心是关闭引擎的z-buffer,导致墙壁等阻碍物后面的任务也能被渲染出来。
音频系统,每个能发出声音的物体都代表了一个音源,音频处理技术有混响、反射、闭塞、障碍物、远方传来的声音等等。multimedia、OpenAL。
物理系统:常见碰撞检测。有PhysX、Box2D等三方物理引擎。
人工智能:A*寻路算法、NPC行为的自动有限状态机。

游戏漏洞:
1.游戏逻辑漏洞
与开发时网络架构有关,游戏逻辑漏洞在游戏客户端实现,通过修改代码、修改数据和调用游戏函数等方式实现。
2.游戏协议稳定型漏洞
畸形协议字段,与架构无关,所有游戏都会出现类似问题,畸形协议自己构建或者修改数据让游戏自动构建。
3.服务端校验疏忽漏洞
此类型漏洞与架构无关,而与开发者逻辑相关。

逆向篇
静态分析,ARM反汇编
ARM架构是一个精简指令集处理器架构,是目前使用最为普遍的手机处理器架构。
有15个通用寄存器为R0~R15。R13也叫作SP,用来保存栈顶地址。R14是LR。用来保存函数的返回地址。R15也叫作PC,用于ARM架构的指令预读。在实际运行时PC寄存器并不指向当前执行的指令,而是当前指令地址+8处。
B/BL指令,作用是跳转到指定的位置。有BNE、BEQ、BGT。
CMP R2,R0
BEQ loc_1C04
Cond B(101) L offset
LDR/STR指令用于向内存中读写数据。
LDR R2,[R12]意义为读取R12寄存器指向的内存到R2寄存器中,一共读取4个字节。
根据state bit来区分Thumb指令集和ARM指令集。
函数传参:BL是指带结果跳转。通常用作子程序传参调用。
void *gameFunc=old_compile(a1,a2,a3,a4,a5,a6);
if(!hasHooked(int)gameFunc) MSHookFunction;
汇编为:
BLX R6//R6中存放了函数old_compile
MOV R6,R0//把返回值作为参数直接调用hasHooked
BL hasHooked//(原因:把前4个参数放到R0~R3寄存器中,把剩下的参数放至栈中,把函数的返回值放入R0中)

ELF文件属于Linux平台下的可执行文件,与windows下的PE格式相似.
PE结构:掌握磁盘文件和它在内存里的映射关系。PE文件被加载到内存运行中的过程以及在内存中如何被管理,加载的地址等。PE内存地址是在PE文件中被设置好的,它会告诉系统将自己加载到哪个位置里,基址为0x00400000。拿到虚拟地址后求虚拟页面偏移量,虚拟页面偏移量知道后再后再去虚拟页面和物理页面转化一下成物理偏移量即可找到实际的语句地址,这个地址就是我们要的。
hook:windows下每个线程都有自己的地址空间,并且进程只能调用自己地址空间内的函数。要做的就是使API的执行流程转向我们指定的代码段。HOOK API需要设法将自己的代码注入到目标进程中,就是说有实现HOOK API的dll文件,和启动注入的主调程序。
IDC脚本用于Dump内存数据。
动态逆向分析:NOP
NOP的作用:
1.padding填充
2.I/O缓存区清空
3.破解,使用NOP占位
4.避免程序内存地址突变
5.清除flag位
6.时钟延迟
lldb和GDB都可以memory read、memory write、register read、register write,遍历数据。

定制化外挂
1.注入游戏进程(动态或者是通过静态的方法感染ELF或ios上通过mobileSubStrate组件将dylib模块注入游戏进程中)
2.枚举游戏进程模块
首先要根据模块名称定位到指定的模块加载基地址
3.执行hook
对游戏的关键函数进行hook操作,在游戏调用关键函数时获取执行权限。
4.游戏内存数据修改
定制化外挂通常采用注入式篡改游戏代码和数据。
5.反调试功能

注入技术:ptrace函数为一个进程提供了监视和控制其他进程的方法,在注入进程后,父进程还可以读取和修改子进程的内存空间数据和寄存器的值。
long ptrace(enum __ptrace_request request,pid_t pid,void *addr,void *data);
dlopen函数以制定的模式打开指定的动态连接库文件,并返回一个句柄给调用进程。
dlclose可以卸载打开的库。
dlsym函数:根据动态链接库操作句柄与符号,返回符号对应的地址。使用这个函数不但可以获取函数地址,也可以获取变量地址。句柄是由dlopen打开动态链接库后返回的指针,符号是要求获取得函数或全局变量的名称。
Android的Linux系统的注入过程细节:
1.attach到远程进程
ptrace(PTRACE_ATTACH,pid,NULL(addr),NULL(data));
在附加到远程进程后,远程进程的执行将会中断,父进程可以调用Pid_t waitpid(pid_t pid,int *status,int options);
2.读取和写入寄存器的值
在通过ptrace改变远程进程的执行流程之前需要先读取和保存远程进程的所有寄存器的值,当detach操作发生时,可将远程进程写入已保存的原寄存器的值,用于恢复远程进程原有的执行环境。
ptrace(PTRACE_GETREGS,pid,NULL,regs);
ptrace(PTRACE_SETREGS,pid,NULL,regs);
这里的regs是一个结构为pt_regs的指针,从远程进程获取的寄存器的值将存储到该结构中。
struct pt_regs{long uregs[18];};
#define ARM_lr uregs[14]
3.远程进程的内存读取和写入数据
从远程进程中读取数据,一次读取一word大小的数据。
ptrace(PTRACE_PEEKTEXT,pid,pCurSrcBuf,0);
ptrace(PTRACE_POKETEXT,pid,pCurDestBuf,lTmpBuf);//addr为需要写入数据的远程进程的内存地址,data为需要写入的内容。
然后memcpy
不足1个word的数据需要先保存原地址的高位数据。
lTmpBuf=ptrace(PTRACE_PEEKTEXT,pid,pCurDestBuf,NULL);
memcpy((void *)(&lTmpBuf),pCurSrcBuf,nRemainCount);
4.远程调用函数
a.写入函数的参数,参数小于4个时,直接按顺序写入R0~R3寄存器,若大于4个,则首先调整SP寄存器在栈中分配的空间大小,然后通过ptrace函数将剩余的参数写入栈中。
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset);//先使用mmap为函数分配空间
void *dlopen(const char*pathname,int mode);
void *dlsym(void *handle,const char *symbol);
int dlclose(void *handle);
b.修改PC寄存器为需要执行的函数的地址,要辨别ARM和Thumb两种指令

for(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;
}

if(regs->ARM_pc&1)
{/*thumb*/
regs->ARM_pc&=(~lu);
regs->ARM_cpsr|=CPSR_T_MASK;
}
else
{/*arm*/
regs->ARM_cpsr&=~CPSR_T_MASK;
}

扫描二维码关注公众号,回复: 2478744 查看本文章

5.恢复寄存器的值
6.detach进程
ptrace(PTRACE_DETACH,pid,NULL,0);

ARM寄存器:
R13也就是sp,是栈指针
R14也就是lr,是连接区某一个存结果的地方,适用于执行A函数的时候要调用函数B,用于记录返回值。
R15也就是pc,是CPU在每一个空间切换的时候的计数器
(A/C)PSR是程序状态寄存器。A是应用程序,C是当前程序。

Hook技术:Hook是Windows消息处理机制的一个平台,应用程序可以在上面设置回调例程以监视指定窗口的某种消息,而且所监控的窗口可以是其他例程建立的;消息到达后在目标窗口函数处理前获得执行时机。
1.基于异常机制:
为SIGILL信号设置ExceptionHookHandler
需要对目标地址写入异常指令,同时注册异常处理函数来获取执行时机。
当目标地址获得执行时,需要恢复目标地址的异常指令,同时设置目标地址
2.Inline hook
备份hook点信息(hook点类型,原指令等)
构建跳转桩(寄存器保存+跳转功能)
构建hook点原指令(指令修复+跳转)//LDR、BLX
重构hook指令(跳转到桩函数中)
LDR PC,_shellcode_stub_start
_shellcode_stub_start:
LDR R3,_hookstub_function_addr
BLX R3
LDR PC,_old_fuction_addr
3.导入表hook

游戏进程的模块信息获取:
模块信息一般包括:动态加载的链接库信息和可执行文件的信息。通过遍历模块信息可获取到:模块基地址、模块名和模块路径。
可以通过proc虚拟文件系统获取进程的状态信息。
proc文件系统是一种伪文件系统,不占用磁盘空间,而由内核挂载到内存中。proc文件系统提供了内核配置、进程状态输出等功能。
进程内存模块信息存放在proc文件系统下以pid为目录名称的maps文件中,可以通过cat /proc/21385/maps命令打印21385进程的相关信息。

篡改游戏内容
注入式与非注入式:
非注入式有:修改APK安装包、修改Android系统data目录下的文件内容和修改proc文件夹的相关信息。
一般而言AndroidManifest.xml中包含大量敏感信息,例如native代码(so文件)、签名(META-INF)、资源信息等。关于修改需要涉及ELF文件格式以及ARM/Thumb等汇编指令。常见的方式是通过IDA工具静态分析so文件。
常见修改方式:
1.改跳转指令为NOP
2.修改寄存器,常见于函数头,只需要修改一个Byte数据即可实现。
3.抹除明文字符串
篡改APK安装包
篡改游戏的安装目录文件
篡改/proc/目录文件
注入式篡改:
篡改内存数据:
全局数据的修改
通过汇编指令直接修改游戏的内存数据(如ARM的STR指令)
通过系统API函数实现对内存的篡改,例如memset、memcpy、strcpy等函数可实现对内存数据的修改
如果是局部数据例如对象数据和堆栈数据,则需要借助Hook技术获得修改的时机点。
篡改逻辑代码:
暴力修改法(简单的外挂功能)
首先静态分析代码并定位到关键代码的地址信息
然后修改对应地址的页属性,在安卓平台下通常调用mprotect函数改写属性
修改关键代码
Hook技术(复杂的外挂功能)
函数地址替换hook方式(导入表hook,虚表hook),简单,但是局限性较强,不能获取到函数执行过程中的相关数据,仅限于对参数和返回值进行获取。
基于汇编代码替换的hook(Inline Hook、异常Hook),复杂,但是优势明显:可获取函数运行过程中的任意数据。
注入游戏进程后才可以修改相应代码段的页属性为可读写属性(通过调用mprotect函数实现)

反调试技术用于提高逆向调试软件的门槛。
反调试方案:
1.Self-Debugging反调试、轮询检测反调试、Java层反调试
Self-Debugging是指父进程创建一个子进程,由子进程来调试父进程的技术。
就相当于父进程被子进程保护了起来,其他想去调试父进程的进程将会失效。并且这种方法消耗的资源非常少,几乎不影响受保护进程的性能。
基于ptrace函数。
当一个进程被调试时,它只有运行和暂停两种状态。暂停状态可以被细分为signal-delivery-stop(当收到除SIGKILL之外的信号时,内核将会选择进程内的任意一个线程处理这个信号,假如被选择的线程正在被调试,则进入该状态)、group-stop(进程/线程处于被调试的状态,被调试的进程或线程收到了暂停信号,因此调试器应该避免被调试进程收到暂停信号:SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU)、syscall-stop和ptrace-event-stop。
2.基于轮询的,可能会耗资源,但有时候特别有用
3.Java层的JDWP协议的Java代码调试器

C++调试:
类指针:指针其实就是内存地址的意思,指针变量可以理解为内存地址变量。如函数指针就是一个变量,保存着一个地址,这个地址指向一个函数。类指针通常用来作为基址。
虚表:如果一个类里有虚函数,那么在它的内存结构中会有头部的4个字节来存储一个指针变量。这个指针变量叫虚表指针,它指向的位置是一个函数指针数组,这个数组叫做虚表,也就是一连串的函数地址。同一个类的所有对象共享一个虚表,如果两个类的虚函数不同,则会有不同的虚表。
类的成员函数与一般的全局函数和静态函数的区别在于它的第一个参数是一个“隐式”参数,这个参数所属的对象是this指针。隐式是指在语言层面不可见但汇编层面可见。
1.借助字符串信息
2.send函数回溯
一般send函数过于底层会被加密,拦截的数据通常都经过加密,因此需要向上回溯。向上回溯找到组包函数和它的上层调用函数。可以借助IDA过滤一些被频繁调用的函数如心跳函数和Log输出。
C++游戏的破解思路:
Hook修改传入参数
Hook修改返回值
Hook多次调用
Hook直接返回
Hook直接修改判断逻辑(在二进制层面直接修改汇编指令)
IDA查找交叉引用
可以采用红黑树保存加密信息。

Unity3D和Lua调试之后再看,以及一些修改的其他功能的方式。

猜你喜欢

转载自blog.csdn.net/parallel2333/article/details/81279109
今日推荐