不寻常的手游反调试——反hook分析与绕过

移动安全 1年前 (2023) admin
230 0 0

前两天群友遇到了一个游戏样本:


不寻常的手游反调试——反hook分析与绕过

我点开一看,符号都没去?什么逆天保护:

不寻常的手游反调试——反hook分析与绕过

所谓“浅藏诲盗,冶容诲淫”,保护做成这样不就是自找破解吗?于是我想当然的说“适合新手入门!”
不寻常的手游反调试——反hook分析与绕过

深入研究了之后,发现比想象中的要复杂,而且检测手段都挺别致,不常见,并且是一款叫做nhnent appguard的日韩商业保护。挺有意思,于是出一篇文章总结一下。




加固分析


打开最核心的libloader.so,映入眼帘的是ida的一堆warning,提示section header不合法。

无所谓,直接强行进去:

不寻常的手游反调试——反hook分析与绕过

看到代码都是灰色的,一些无意义的二进制,应该是被加密了,看看init array:

不寻常的手游反调试——反hook分析与绕过

第二个是红色的什么鬼?试图jump过去看看,但是ida显示:Command “JumpAsk” failed

说明这个地址目前还不在可见范围?

010editor打开看看他的段信息:

不寻常的手游反调试——反hook分析与绕过

代码段的文件长度是0x25c848,内存长度也是0x25c848,padding是0x10000,没什么问题。

数据段的文件偏移是0x25ea50,但是内存中的偏移却是0x35ea50,这中间活生生多出来了0x100000字节(100K),根据代码段的padding 0x10000,数据段的内存偏移应该是0x26ea50才对,这显然不正常。

init array 的第二个函数地址是0x29c1ec,既没有在代码段里,也没有在数据段里,而在中间空出来的padding里?神奇!

所以第一个init 函数肯定要把这部分处理好,否则linker会报错,我们先看第一个init函数:

不寻常的手游反调试——反hook分析与绕过

通过fopen打开/proc/self/maps文件,然后通过strtok和strtoul找到链接过程中多映射的100k的内存空间,获取首地址。

不寻常的手游反调试——反hook分析与绕过

然后通过mmap将这些地址先设置为rwx权限,再通过memcpy把藏在数据段的代码复制过去,然后使用mprotect把这块内存恢复为rx权限:

不寻常的手游反调试——反hook分析与绕过

其中memcpy分别根据源地址,目标地址,长度,分5次copy。

不寻常的手游反调试——反hook分析与绕过

复制完后,第二个init函数就已经在内存中准备好了,可以开始执行。

第二个init函数就是作解密用的:

不寻常的手游反调试——反hook分析与绕过

可以看到先解密了字符串,简单了异或了几个字符串就结束了,然后解密section。

字符串解密比较简单,我们看看如何解密section:

给linker的callarray函数下断点,在执行到第二个init函数时跟进去:

不寻常的手游反调试——反hook分析与绕过

首先根据X0定位到so里面的加密数据,起始地址是:0xab710。

不寻常的手游反调试——反hook分析与绕过

刚好是.text的开始。

然后将.text开始偏移0x4和0xc的字节拿来异或,得到的结果是加密数据的长度:

不寻常的手游反调试——反hook分析与绕过

然后根据X1(解密后数据大小长度)malloc一段内存,再将真正的加密数据内容(.text偏移0x10开始),按照计算的长度x22,memcpy过来。

不寻常的手游反调试——反hook分析与绕过

然后将.text偏移0x8和0xc的数据异或,计算出第一个key(w11),用于后面解密:

不寻常的手游反调试——反hook分析与绕过

第二个key 在加密数据的末尾,通过x22找到加密数据的末尾,下一个字节即是第二个秘钥(w25)。

不寻常的手游反调试——反hook分析与绕过

然后开始解密工作,每字节异或一次,同时根据当前的偏移,奇数字节用key1,偶数字节用key2:

不寻常的手游反调试——反hook分析与绕过

解密完后看到数据的开头是0x78,0x9c,显然是zlib的压缩数据:

不寻常的手游反调试——反hook分析与绕过

接下来初始化zlib为1.2.7版本:

不寻常的手游反调试——反hook分析与绕过

然后循环解压,再将解压后的代码复制回.text段:

不寻常的手游反调试——反hook分析与绕过

解压复制完代码也就解密完成了:

不寻常的手游反调试——反hook分析与绕过

所以我们可以根据他的解密方式,自己写一套加密方式,把解压后的代码修改后压缩,加密回去,这样就可以对原代码进行修改了。加密代码如下:

import zlib
def encode(inName,outName):
with open(inName,"rb") as f:
data = f.read()
#压缩
o1 = list(zlib.compress(data))
#加密
index = 0
step = 0
key1 = 0xEAB54A3C
key0 = 0x5AB9C6D5
lenraw = len(o1)
out = []
while index < lenraw:
if lenraw - index >= 4:
raw = getInt32(o1,index)
if step == 0:
addInt32(out,0,raw ^ key0)
step = 1
else:
addInt32(out,0,raw ^ key1)
step = 0
index = index + 4
else:
while index<lenraw:
out.append(data[index])
index = index + 1
addInt32(out,0,key0)
with open(outName,"wb") as f:
f.write(bytes(out))

注意由于修改后的代码经过压缩长度可能会变化,所以需要针对性的修改.text开始的0x4,0x8和0xc处的字节,以符合解密算法。同时需要在加密数据的末尾按照小端法重新添上key2。

至此,该加固的解密方式已经全部清楚,并且我们也可以定制化的修改代码,加密回去了。




反调试


2.1 Tracerid反调试


加固研究完了,我们直接F9起飞,但是ida崩了。显然后反调试存在,但是so中没搜索到什么有用的字符串信息,应该是被加密了。

研究各种安全保护的第一步应该是:找到字符串解密函数。找到这个之后对方做的各种骚操作就一目了然了。

寻找字符串解密函数通常可以给fopen下断点,然后回溯。以反调试为例,通常先打开/proc/self/status文件,然后读tracerid。而打开这个文件会用到fopen。so里没有明文/proc/self/status,那肯定是解密后传给fopen的,即解密函数离fopen不远。

通过简单的调试观察发现,sub_C4A50是字符串解密函数:

不寻常的手游反调试——反hook分析与绕过

可以看到也是简单的异或。

我们给sub_C4A50下断点,看看能发现什么:

发现解密出了很多常用的libc函数,感觉不妙。

不寻常的手游反调试——反hook分析与绕过
不寻常的手游反调试——反hook分析与绕过
不寻常的手游反调试——反hook分析与绕过
不寻常的手游反调试——反hook分析与绕过
不寻常的手游反调试——反hook分析与绕过

然后出现了熟悉的 /proc/self/status

不寻常的手游反调试——反hook分析与绕过

单步跟下去,发现又加密了Tracerid,这下图穷匕首现了:

不寻常的手游反调试——反hook分析与绕过

接下来就是常规的操作:

fopen打开/proc/self/status:

不寻常的手游反调试——反hook分析与绕过

fgets循环读取,然后strstr检查是否读到了tracerid:

不寻常的手游反调试——反hook分析与绕过

如果读到了通过atoi获取Tracerid,如果不是0就进入退出流程:

不寻常的手游反调试——反hook分析与绕过

退出流程比较复杂:

不寻常的手游反调试——反hook分析与绕过

最终会根据传入的参数不同,定制一种退出方式,有exit,有造crash,也有svc exitgroup,不再赘述。

我们只需要将atoi的返回结果改成0,就可以绕过了。

但是,要真的这么简单也就不会有这篇文章了。

2.2 断点指令检测反调试


一开始并不知道atoi后走的是什么退出流程,所以给exit下了断点:

不寻常的手游反调试——反hook分析与绕过

但是发现下断点之后,还没走到检查Tracerid一步,exit就被调用了,说明有别的检查。

通过反复的调试发现,只要给exit下断点,目标进程还没到检查Tracerid就会提前走退出流程。如果不给exit下断点,就会去检查Tracerid,然后根据atoi的结果决定是否走退出流程。

我们记得在调试中看到了解密出了exit,_exit等字符串,跟下去看看:

不寻常的手游反调试——反hook分析与绕过
不寻常的手游反调试——反hook分析与绕过

发现不光解密了exit,_exit,还解密了__exit,kill,tgkill。

不寻常的手游反调试——反hook分析与绕过
不寻常的手游反调试——反hook分析与绕过
不寻常的手游反调试——反hook分析与绕过

而且每解密一个函数名,都通过dlsym获取函数的地址:

不寻常的手游反调试——反hook分析与绕过

神奇,接着跟下去,发现调用dlclose关闭了之前dlopen打开的libc。

不寻常的手游反调试——反hook分析与绕过

接着进入了另一个BLR X8。跟到某一步发现把exit的地址当做参数,调用了一个函数:

不寻常的手游反调试——反hook分析与绕过

发现将exit函数的前8个字节复制给了另一个地址:

不寻常的手游反调试——反hook分析与绕过

可以看到,复制的结果正好是exit的前八个字节:

不寻常的手游反调试——反hook分析与绕过

exit函数:

不寻常的手游反调试——反hook分析与绕过

提取前八个字节干什么,有点意思。

复制完后进入了另一个函数。先给w8赋予0xd4200000:

不寻常的手游反调试——反hook分析与绕过

然后把W8存入了var14,再把var14作为参数,exit的地址作为另一个参数传入sub_71be98e690:

不寻常的手游反调试——反hook分析与绕过

在sub_71be98e690中,会依次比较exit函数还是的4个字节是否是传入的00 00 20 d4。

如果不是的话返回非0值,如果是的话返回0:

不寻常的手游反调试——反hook分析与绕过

为什么比较 00 00 20 d4?

不寻常的手游反调试——反hook分析与绕过

原来 00 00 20 d4是arm64 下的断点指令的机器码!

如果检测到前8个字节中有 00 00 20 d4,该加固就会走退出流程,最终调用exit退出。

该加固依次检查了exit,_exit,__exit,kill,和tgkill的前八个字节中是否有断点指令,我们以_exit为例再看看:

_exit的前八个字节本来是c8 0b 80 d2 01 00 00 d4:

不寻常的手游反调试——反hook分析与绕过

下了断点之后,内存中获取到的是00 00 20 d4 01 00 00 d4:

不寻常的手游反调试——反hook分析与绕过

所以一下断点就会被检测到,然后退出。

这种反调试方法之前听说过,不过这是第一次在真实环境中看到,感觉挺牛批的。

除过exit,_exit这些函数,该加固还检查了open,fopen,popen,read,pread,memcpy,memcmp,fgets,strcmp,strncmp,strstr,strlen,strcpy,sleep,mmap,munmap,__ptrace,ptrace,fork这些函数是否被下断点,如果被下断也会直接走退出流程。方法也都是先解密出函数名,然后dlsym获取函数地址,然后检查前八个字节。




反hook


断点检查,Tracerid绕过之后,发现水友说还有反frida检测。

直接用frida启动目标app,确实一启动就闪退了。经过分析闪退也是走的之前的退出流程。但是给字符串解密函数下断点并没有看到任何和frida检测相关的字符串,很奇怪。

使用frida启动目标app,在启动时先把app挂起10s,在这期间用ida attach上去,看看发生了什么。

发现在检查完exit的断点之后就走退出流程了?可是我们并没有给exit下断点啊!

看看frida启动的app的exit函数有什么变化。

不寻常的手游反调试——反hook分析与绕过

我们并没有使用frida hook 任何函数,只是启动,但是发现frida自己hook了exit函数。

同时还发现frida自己hook 了_exit,fork等函数。

由于在检查完exit的断点之后退出了,所以我们有理由怀疑他还检查了其他的东西,再仔细看看。

检查完断点后进入了sub_71bd56debc这个函数,传入的参数是刚刚复制过来的前八个字节:

不寻常的手游反调试——反hook分析与绕过

不寻常的手游反调试——反hook分析与绕过

跟进去发现,该函数将复制来的前8个字节全部与0x26异或了:

不寻常的手游反调试——反hook分析与绕过

异或后的结果:

不寻常的手游反调试——反hook分析与绕过

异或完后分别取第7个字节与0x39比较,取第8个字节与0xf0比较:

不寻常的手游反调试——反hook分析与绕过

发现我们的加密后的第7个字节正好是0x39,第8个字节正好是0xf0。对比成功后发现程序走了退出流程,通过两次调用pthread_mutex_destory对同一个mutex进行销毁造crash退出了。

所以为什么要比较0x39和0xf0?我们知道这里的比较是加密后比较的,加密前,0x39 ^ 0x26 = 0x1f,0xfo ^ 0x26 = 0xd6。

所以他本质是检查原始的值是不是0x1f 和 0xd6。为什么检查这两个值?

我们看看arm64的指令手册,对于BR(寄存器跳转)指令:

不寻常的手游反调试——反hook分析与绕过

昭然若揭!

br指令的高16位是0xd61f!

所以该加固其实是在检查_exit函数的第二条指令是不是br指令。通常inline hook第一条指令是mov 常数到寄存器,然后第二条是一个br 寄存器指令。检查第二条指令高16位是不是0xd61f,就可以判断目标函数是否被inline hook了!

由于frida自己hook了exit和_exit函数,虽然断点指令检查通过了,但是hook检查没有通过,程序造crash退出了。不过该加固在检查前是先加密,然后比较加密后的结果,有点骚。

知道这个之后也好绕过了,只需要把比较的0x39和0xf0(加密后的)换成其他的值就可以绕过了。然后重新压缩,加密,回填到原so中即可,不在赘述。
至此,该加固的加固方式(多映射内存,然后解密解压填充),反调试(Tarcerid + 断点指令),反hook(br 指令检查)全部绕过了,可以随意调试,hook该游戏了。




后记


这次研究的是日本的游戏和加固,所以写的比较露骨。游戏名字叫 ブルーロック 咱也不知道是啥意思hh。看到了apk里面的nhnent appguard,不知道有没有朋友知道是什么。不过本质是一个加密的压缩壳,这在手游保护里不太常见。反调试的断点指令检测手法和反hook的br指令检测手法也很有新意,不是常规的路数。

希望这篇文章能对大家学习检测手法,反调试与绕过有一些帮助,也希望给国内手游保护厂商提供一些外面人的思路。



不寻常的手游反调试——反hook分析与绕过


看雪ID:乐子人

https://bbs.kanxue.com/user-home-872365.htm

*本文为看雪论坛优秀文章,由 乐子人 原创,转载请注明来自看雪社区

不寻常的手游反调试——反hook分析与绕过

# 往期推荐

1、Tcache安全机制及赛题详细解析

2、Amateurs CTF 2023 逆向分析题解

3、命令注入漏洞CVE-2022-34527复现

4、TTD调试与ttd-bindings逆向工程实践

5、某摄像头协议分析

6、GoJni 协议加解密分析


不寻常的手游反调试——反hook分析与绕过


不寻常的手游反调试——反hook分析与绕过

球分享

不寻常的手游反调试——反hook分析与绕过

球点赞

不寻常的手游反调试——反hook分析与绕过

球在看

原文始发于微信公众号(看雪学苑):不寻常的手游反调试——反hook分析与绕过

版权声明:admin 发表于 2023年8月3日 下午6:00。
转载请注明:不寻常的手游反调试——反hook分析与绕过 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...