外挂运行逻辑
外挂进程先通过java层解密出sock001并运行,读取数据到/sdcard/1A.txt文件中,然后外挂进程native层不断读取/sdcard/1A.txt文件中的数据,同时调用java层的绘图函数,实现透视。
java层分析
在查看外挂apk给的so,发现里面并没有远程读写的代码,怀疑是新起了一个进程,在assert文件夹下发现了可疑的以sock开头的几个文件
用010打开,发现所有文件都是被加密过的,看不出来是什么东西
开始在java层上找解密的地方,java层被混淆的很厉害,最终在MainActivity中找到了解密的地方
这里是对sock1进行解密,跟进去,是非常明显的rc4加密,密钥为gamesec
本地用python对sock1进行解密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | DEFAULT_KEY = ""
def rc4(data, key = DEFAULT_KEY, skip = 1024 ):
x = 0
box = range ( 256 )
x = 0
for i in range ( 256 ):
x = (x + box[i] + ord (key[i % len (key)])) % 256
tmp = box[i]
tmp2 = box[x]
box[i] = box[x]
box[x] = tmp
x = 0
y = 0
out = []
if skip > 0 :
for i in range (skip):
x = (x + 1 ) % 256
y = (y + box[x]) % 256
box[x], box[y] = box[y], box[x]
for char in data:
x = (x + 1 ) % 256
y = (y + box[x]) % 256
box[x], box[y] = box[y], box[x]
k = box[(box[x] + box[y]) % 256 ]
out.append( chr ( ord (char) ^ k))
return out
if __name__ = = '__main__' :
import sys
tt = open ( "sock1" , "rb" )
ww = tt.read()
decrypt_files = rc4(ww, "gamesec" , 0 )
decttt = open ( "sock1dec" , "wb" )
decttt.write("".join(decrypt_files))
|
解密后,再用010打开,发现是一个安卓上的elf可执行文件
同时这里java层采用shell命令,启动了进程,可以用ps -ef查看启动进程的参数
发现是2236 1080,这两个是我测试手机屏幕的高宽,后面会用到
外挂的native层分析
从函数名都可以知道,外挂的native层主要是读取文件数据,然后调用java层绘图函数画图的,这里并不是重点。
解密后的elf文件分析
用ida打开后发现区段只有两个,明显是被加壳了
怀疑是upx的壳,用010查看upx的特征,发现被改动了
upx的字符串都被替换成了ue4,同时和正常upx加壳的对比,还删掉了声明,尝试手动修复,修复不了,转而采用动态调试方式找到oep入口,因为有反调试,调试器无法attach,老是报奇怪的错,后面才知道是信号的问题,由于调试过程中,老是会跑飞到linker里面,所以萌生了在底层函数上下断的想法,最直接是在libc_init这个函数下断,然后第三个参数就是main函数。
先单步到这,linker位置,此时libc已加载,再跟到libc中下断
成功断下,r2寄存器里面此时是main函数的地址,在main函数下断,f9,此时是程序真正的入口了,发现里面混淆的非常严重
主要混淆有字符串加密以及控制流平坦化,这里想办法脱壳并且去掉混淆先,方便调试
脱变种upx的壳
这里通过010editor发现里面的upx字符串都被改成ue4,然后用upx -d直接不识别为upx,应该是本地被patch修改过了,尝试修复
第一步 替换字符串
将所有的出现的ue4!字符串改成upx!,算是最基本的特征了
搜索UE4,发现有四个,其中小写的不用改,将其他改为UPX
第二步删除结尾的hack
这里主要是对比了其他upx加壳后的文件,发现基本都是3个0x00结尾
的,所以这里要删除
第三步修复p_info值
从文件尾部继续找倒数第一个和倒数第二个UPX!,距离倒数第一个UPX!的20个字节处,为正确的p_info值,取出并填入距离第一个UPX!字符串8个字节的位置。
取出的值0x0004f9ec
填入的位置
第四步 修复文件头填入.ELF
再次用upx -d尝试发现已经能识别出upx了,但是还是报错了
这里通过查看upx源码https://github.com/upx/upx,以及调试upx官方可执行程序,发现在一处文件头的对比中,我的数据是4个0x00,所以报错了,upx源码,同样找到了这个逻辑
说明需要去填入.ELF, 填入位置参考了其他upx加壳文件,在距离第一个UPX!字符串的第28个字节开始填入
第五步 修复b_info
再用upx -d,发现还是报错了,但是报错信息更新了
通过搜索upx源码的字符串,找到这里关键对比逻辑
通过调试upx官方程序,发现这里blocksize一直为0,所以报错
在这里下断,找到了位置了,同时我自己在本地用upx加壳后的程序进行对比,找规律,发现p_info的那个值要和blocksize相同。
第六步 最终脱壳
心情还是比较激动的,折腾挺久的了
去除混淆
去控制流平坦化混淆
可以看出是release版本的控制流平坦化,为了方便,这里用ida 插件https://github.com/obpo-project/obpo-plugin,来去掉混淆,主函数去除混淆前:
然后这里用这个插件来去掉,右键在函数主分发块上点击OBPO
点击mark and process function,再连续按两次f5,去除混淆成功
主函数去掉混淆后:
效果非常好,伪代码非常清晰
去除字符串加密
发现每个函数要用的字符串都被加密起来了,解密逻辑都在函数的开头.
而且执行过后,并没有再次加密,这里可以采用先让外挂正常运行一遍,然后dump下来,再把解密后的字符串patch到之前没有解密字符串的程序中。
类似shellcode部分
在调试过程中,发现sock1将sock002、sock003、sock004、sock005都进行了密钥为TencentGameSecurity的rc4算法解密,并且发现sock001文件中间有空缺,
空缺的位置的似乎就是那几个文件,本地重新分析一个安卓可执行elf文件,对照的分析,得出结论sock002解密后对应的是program table,并在调试中确定了这点
解析sock002(progame table),然后将sock001的要加载的代码加载进内存,然后下一步是通过解密后的sock005进行重定位,解密后的sock005为.rel.plt section
sock003解密后,本地查看发现很明显是动态链接的字符串表
sock004dec 经过对比,发现是动态符号表,几个文件的代表意义都知道了,继续分析,相当于自实现了一个linker,再继续调试时发现pc值跳到了sock001、sock002、sock003、sock004、sock005加载到内存中的区间,说明sock1进程相当于执行了一段这几个文件的shellcode,最终进入了这个外挂的关键函数
这里为了证实这个函数是那几个文件中的,用010去搜机器码的十六进制
是可以搜的,同时内存地址,也是在几个文件加载进内存中的地址区间。
反调试
- 打开/data/local/tmp文件夹将每个文件名与re.frida.server对比
- 用opendir函数打开了/proc/self/task,检测线程数是否为2
- 用popen函数调用 cat /proc/net/tcp | grep : 0x69A2,检测27042端口
- 用popen函数调用 cat /proc/net/tcp | grep : 0x5d8a ,检测23946端口
- 用popen函数调用ls /proc/self/fd -all,并将每一行内容拼接成一句字符串,判断是否包含frida和injector这两个字符串
- 通过signal函数设置了信号处理函数,并通过memove函数故意插入了断点指令
并设置pc值为此内存地址,触发异常,调试器会接受到此异常
如果没有调试器,则信号由先前设置的信号处理函数处理,跟进信号处理函数,会发现同样调用了momove函数,将之前的数据覆盖到断点指令上,程序回归正常。
外挂原理分析
透视
核心函数位于sock001偏移的0x8490
获取游戏进程pid和libUE4.so基地址
- 先获取游戏进程com.YourCompany.ThirdPerson的pid
- 通过读取/proc/pid/maps文件拿到libUE4.so的基地址base
获取Actors
- 通过Gworldptr+base=base+0x491E6F0读出Gworld
- 通过Gworld+0x20读出Level* PersistentLevel
- 通过PersistentLevel+0x70读出Actors(里面包含ptr和size)
获取gName
- 通过base+0x48711B4读出gNameptr
- 通过gNameptr读出gName
获取 gmarixptr
- 通过base+0x4907ea0读出ULocalPlayer::~ULocalPlayer()
- 通过ULocalPlayer::~ULocalPlayer() +0x20读出APlayerController::~APlayerController()
- 通过APlayerController::~APlayerController()+0x30c读出APlayerCameraManager::~APlayerCameraManager()
- APlayerCameraManager::~APlayerCameraManager()+0x320是APlayerCameraManager中CameraCacheEntry CameraCache对象的MinimalViewInfo POV的偏移,这里我称之为 POVPtr
遍历actors
- 初始循环变量yk为0,不断加1
- 不可以超过上面得到TArray<AActor*> Actors的size
- 通过Actors的ptr+循环变量yk*4拿到对应的actor[yk]
- 通过actor[yk]+0x10读出FNameIndex
- FNameIndex分别除以0x4000和取余0x4000得到page和idx
- 如果gname[page]等于0,将gname+page*4的值读出,赋值给gname[page]
- 如果不为0,则通过gname[page]+yk*4读出actornameptr
- 通过actornameptr读出actorname
- 判断是否等于ThirdPersonCharacter、ThirdPersonCharacter2、ThirdPersonCharacter3其中之一
- 如果不是,则继续循环
- 如果是,则通过actor[yk]+0x120拿到SceneComponent* RootComponent
- 再通过SceneComponent* RootComponent+0x100读出Vector RelativeLocation
- 拿到actor对应的三维坐标
计算屏幕坐标
- 接着上面循环所讲的,拿到actor对应的三维坐标
- 通过上面获取数据所说的POVPtr读出MinimalViewInfo POV
- worldtoscreen函数将MinimalViewInfo POV->Location.x、 MinimalViewInfo POV->Location.y、 MinimalViewInfo POV->Location.z 、 MinimalViewInfo POV->Rotation.Pitch、 MinimalViewInfo POV->Rotation.Yaw、MinimalViewInfo POV->Rotation.Roll以及actor对应的三维坐标作为参数进行运算,算出屏幕坐标
跟进去分析一下是如何做转换的
发现是将pitch yaw roll,传入函数并转化为matrix矩阵,为了后续的计算。
同时在github上找到类似的三维转换二维的代码https://github.com/kp7742/PUBGPatcher/blob/27628b3ff53e0766e7084a82b293bb58ee6c426b/Daemon/jni/Server/StructsCommon.h
不过这里计算并没有fov,其他倒是差不多
将计算后的屏幕坐标输出到/sdcard/1A.txt文件中
读取数据方式
远程读写的操作都集中在这个getprocessvalue函数(自命名)中
根据不同情况选择用syscall或者process_vm_readv来进行远程读写
总结
第一次参与这个比赛,还是挺多收获的,感谢主办方和出题人了,也希望看wp的师傅们也能有所收获2333。
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。