APP启动外挂过程
首先打开apk,被BlackObfuscator混淆过
这里可以提取代码执行一下获得switch路径,代码不是很多,结合CFG图直接动态分析了
系统开启全局debuggable magisk resetprop ro.debuggable 1 stop;start;
使用 am start -D -n "com.tencent.esp/.MainActivity"
让app等待调试器
ddms中开启method trace
使用jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
恢复代码执行
MainActivity cinit
首先在主类的static块中加载了Putri的lib并初始化了一些值(这个库没用到?)
MainActivity.onCreate
根据调用顺序分析代码
736:
读取配置文件,判断是否存在配置,不存在则写入
797:
通过以下代码申请权限
1 | this.requestPermissions(new String[]{"android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION", "android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE"}, 102); |
962:
获取root权限,获取文件路径
1696:
检测APP能否绘制悬浮窗
2001:
检查写入权限
MainActivity.onClick
将
sock1复制出来
并给予777权限
调用 a 函数
向 /Android/data/.tyb 写入 DO NOT DELETE
并将assert内几个文件复制出来(到app目录)
对sock1文件进行解密
这两个函数不存在于这个类的源码中,是后加的
看一下两个函数
256字节为一块进行解密,看得出来是RC4,
h使用gamesec
作为密匙初始化Sbox
m进行加解密运算
对sock1进行RC4运算获得一个elf文件
然后
启动su向管道内写入命令启动刚才解密出的文件并启动服务
这里的f被修改过
su运行的命令为/data/data/com.tencent.esp/files/sock1 [x] [y]
APP绘制过程
Overlay.onCreate
该服务创建了一个悬浮窗并显示,同时创建了一个ESPView
EspView cinit
初始化Paint用于绘制
启动一个线程更新悬浮窗
线程内通过
在指定间隔时间(或者超时后时间)调用postInvalidate()
通知窗口重绘
通过学习AndroidView更新流程知晓窗口重绘会调用onDraw方法
ESPView onDraw
通过AS单步分析onDraw方法
在onDraw内判断窗口是否可见然后通过
1 | p0.drawColor(i, PorterDuff$Mode.CLEAR); |
清空内容
读取/sdcard/1A.txt
内容
1 | { |
将name绘制在 (x,y上)
此处 a 为绘制文本方法
DrawOn为Native方法(好像DrawOn没什么用)
二进制程序
脱壳
IDA打开发现存在壳,发现UE4!标记,怀疑是UPX,修复魔数
同时发现结尾多余的hack字符,删掉
此时upx -d
提示
CantUnpackException: ElfXX_Ehdr corrupted
补齐ELF魔术头,从结尾找到EC F9 04 00
修复上面的长度
还是旧提示
换用upx devel分支修改代码编译执行
直接通过了。。
这里有两个问题,一个是要用新版UPX 一个是要补好ELF魔术头跟被修改的长度数据
保护分析
脱壳后可以执行,详细分析
此处进入主函数
OLLVM 寄
通过OBPO还原控制流(OBPO的原理是修改ida微码,运行程序会改回微码)
可以看到还是有文本xor加密存在,这里写了个脚本反混淆,将IDA生成的伪代码传入即可
1 | def deObf(str): |
效果
main中申请了0xc8字节的空间存放动态获取的函数地址
使用刚才的脚本恢复一下名称,然后用以下脚本给结构体命名
1 | import re |
效果如下
有几个没识别到,字符串解密的伪代码不同,这里下个断点依据文本直接补上(这里有个更简单的办法,不去静态解密,直接下断然后用idapyhton去读内存中解密好的值并且更加通用,绕路了)
api偏移
初始化指针表之后读取x y参数,在loop(0x26378)函数内使用pthread_create创建线程,失败则退出
线程所执行的函数为一个死循环,调用检测函数后休眠
检测函数被高强度控制流混淆以及代码膨胀,OBPO无法进行重建函数
这里使用frida跟踪函数调用来辅助分析(脚本在附件,hook了指针表内所有函数)
首先将创建线程替换成直接运行便于分析
检测点1(0xe568)
解密文本后执行 sub_27420
其中调用 popen函数执行ls /proc/self/fd -all r
读取文件信息后pclose
使用strstr检测返回文本其中是否存在 frida
injector
字样
存在则获取自己pid并kill
若kill后代码还在执行(防止直接hook kill?)则exit(1)
检测点2 (0x15820)
opendir 打开 /data/local/tmp
目录(调用函数)
readdir 获取目录下内容
strcmp 与 re.frida.server
进行比较 (也就是检测是否存在frida临时文件)
存在则获取自己pid并kill
若kill后代码还在执行(防止直接hook kill?)则exit(1)
检测点3 (0xd83c)
这是一个信号反调试
这个函数注册了SIGTRAP的信号处理程序,并申请了一块内存放入代码执行
代码只有8字节
BKPT指令产生一个软中断,程序收到SIGTRAP
信号处理函数将501bc处数据解密并写入502ec
写入数据为
也就是把BKPT指令替换成了mov r0 ,#1
此时返回值为1
当返回值不为1时置检测不通过
绕过:修改状态为已解密并将数据替换成信号处理函数内写入的数据
检测点4 (0x16fcc)
读取/proc/self/task,其中tid文件大于2个就获取自己pid并kill
若kill后代码还在执行则exit(1)
绕过:readdir返回0
检测点5 (0x183c0)
这个函数的代码膨胀量巨大,可以考虑trace排除掉无用部分,实际上这个函数没有什么逻辑,只有部分有用代码,配合frida分析可知
该函数执行popen:
1 | cat /proc/net/tcp |grep :5D8A |
检测IDA,Frida默认端口号,并计算结果数目发现存在这两个端口则获取自己pid并kill
若kill后代码还在执行则exit(1)
绕过:修改程序内保存的Frida端口,IDA则用-p切换端口
至此主程序的5个检测函数分析完成
内部程序加载
内存加载sock流程
首先申请0x1c000大小的内存并给予可执行权限
随后将申请到的空间及文件句柄传给 0x7cf0函数
该函数中调用了0x67dc解密sock002文件
过程为将文件读入内存使用RC4进行解密
密匙为TencentGameSecurity
程序将几个部分解密后放在内存中并解析结构
判断解析的符号名是否为 main1
解析到main1后调用该函数
传入x,y及指针表作为参数
至此外挂程序正式被调用
修复外挂ELF
解密几个文件后发现可能是将sock001某些区段抽取后进行加密的
根据对比正常ELF文件可知sock002为ProgramHeader
从正常ELF文件中复制一份文件头并将002放在文件头后面
该头可以识别
004+003长度符合001中一段空位,参照其他ELF将两个bin合并贴回ELF中
005同样有空位,对比正常文件内容合理,贴之
随后修复ELF头
根据shstrtab找到节区所在位置0x1a48c
并修改e_shoff
根据文件长度减去e_shoff后除以节头大小40 得到节数量并写入ELF Header
至此elf可以被正常解析
外挂本体分析
获取游戏PID
main1将a3参数保存到全局变量后调用sub_8490
函数(传入 x y)
sub_8490中调用sub_6BF0
获取游戏进程ID
该函数内有两种获取方法,第一种是使用popen运行
pidof com.YourCompany.ThirdPerson
若运行失败则打开 /proc 目录遍历子目录读取cmdline文件
寻找游戏进程,最终通过atoi返回数字pid
读取游戏libUE4.so基址
调用sub_69c0
函数打开游戏进程/proc/pid/maps
文件,fgets
逐行读取,strstr
查找libUE4.so
匹配到后使用strtoull取出基址
读取内存方式
读取内存的函数有三种模式,通过rand生成随机数选择
rand % 3 = 0
时调用process_vm_readv
rand % 3 = 1
时调用syscall 提供系统调用号调用 (376)vm_readv
系统调用
rand % 3 = 2
时调用 sub_75F0()函数
其中申请内存给予可执行权限并将base64编码的shellcode写入
DcCg4fAALekAcKDhAQCg4QIQoOEDIKDheACc6AAAAO/wAL3oHv8v4Q==
然后将指向该shellcode的指针赋值给pointer表中的第13个
Shellcode Disassembly
1 | 0x0000000000000000: 0D C0 A0 E1 mov ip, sp |
可见这是使用svc指令产生0号中断,而零号中断代表系统调用
此处通过直接中断调用vm_readv
系统调用
解码shellcode函数返回后调用shellcode执行
arm下的系统调用号
透视实现
偏移总览:部分来源于SDK,部分为分析libUE4二进制文件及阅读源码获得
1 | GWorld = [libUE4.so + 0x491e6f0] |
该程序只有一个重要的函数sub_8490
,该函数是一个死循环,大可分为两部分,写出数据文件以及从游戏中读取数据更新缓存
第一次进入此函数时会将几个基址读取出来
透视函数的第一部分:写出文件
内容比较简单,从v62所申请的内存区域读出角色名,屏幕上的x,y坐标并拼装成json写入文件,代码也没什么混淆不多介绍
写出过程Frida记录
透视函数的第二部分:读取数据计算屏幕坐标
比较重要的是第二部分
遍历Actor对象的ID,使用ActorID从GNames中读取到对应的FName,在FName + 8 的地址读出字符串(具体偏移在偏移总览中)
判断名字是否符合一些条件,不符合则跳过
这里判断了是否为ThirdPersonCharacter4
测试可知ThirdPersonCharacter4是玩家操控的角色
这里判断有两个作用,一是不去绘制自己的名称,二十通过读取自己的组件获取了坐标(12字节向量但是好像没用?)
之后判断都不是目标Character则跳过
如果是目标 Character
首先读取相机的矩阵(个人理解是矩阵,实际上是一个)MinimalViewInfo 其中包含了相机的坐标,角度 FOV
之后读取Actor组件,从组件读取 Character绝对位置
将取得数据
1 | 目标绝对坐标 Vector |
交给函数 sub_7ed0
处理
该函数中进行了一些坐标的转换以及图形学矩阵操作,最终算出目标在相机平面内的二维坐标
(即已知相机坐标,摄像头角度,其他人坐标就可算出其他角色在2D屏幕上的显示坐标,由于游戏内存在遮挡关系, 自己获得坐标直接画上去就实现了透视效果)
将结果写在a1中
再次读取目标的绝对坐标,将数据写入进第一部分读取的内存区域,其中格式为
名字字符串指针 4字节
玩家绝对位置指针 4字节(没用到)
float x 4字节
float y 4字节
16字节为一组
遍历完成后跳转至该函数第一部分,
该函数第一部分读取此处写入的N组数据后写入文件,由APP读取文件并通过悬浮窗服务在屏幕的x,y位置上绘制名字文本。