从案例中学习unidbg补系统调用(一)
一、引言
目标 APK:dewu.apk
目标方法:com.shizhuang.stone.main.SzSdk.lf
目标方法实现:libszstone.so
二、任务描述
unidbg 模拟执行如下函数。
1 |
|
三、初始化
基本框架以及补 JNI 环境代码如下
1 |
|
对于补 JNI 环境,这里不细讲,主要讲补系统调用。
四、补系统调用
4.1 报错分析
运行报错
1 |
|
这个报错主要信息在首行以及函数调用堆栈的第一行。
报错首行主要关注以下属性
intno
into
是异常类型,异常有很多种,比如未定义的指令,软中断、软断点等等,Unidbg 里对它们的定义如下。1
2
3int EXCP_UDEF = 1; /* undefined instruction */
int EXCP_SWI = 2; /* software interrupt */
int EXCP_BKPT = 7; /* software breakpoint */SVC
指令就是软中断,对应于定义里的EXCP_SWI
,我们熟悉的系统调用就是通过它发起。因此如果into
的值是 2,就说明这是一个软中断,也就是系统调用。如果是其他中断类型,即未定义的指令或者软件中断,在 Unidbg 里会直接断下,交由用户处置,但很少碰到这两种情况。NR
NR 就是所谓的调用号,32 位存在 R7 寄存器,64 位存在 X8 寄存器。以 64 位为例,Unidbg 代码如下,从 X8 寄存器取出值。
1
int NR = backend.reg_read(Arm64Const.UC_ARM64_REG_X8).intValue();
利用调用号通过查询系统调用表可以获取对应的系统调用的函数名,可依靠Linux System Call Table网站查询。
svcNumber
svcNumber
值为 0 表示系统调用,否则为 JNI 调用。PC
SO 中报错所在地址。
LR
PC指向的地址所处函数的返回地址。
syscall
syscall
字段提供系统调用的函数名,但只有很少的几个系统调用这么做了。因此在更多时候,我们都需要查看系统调用表,以便确定到底是哪个系统调用出了问题。
分析刚才所发生的报错,NR=263, svcNumber=0x0
,所以这个报错是系统调用引起的,而且具体的系统调用名我们在Linux System Call Table网站查询,这里是 32 位下的 263 调用号,查询可知它是 clock_gettime 这个系统调用。
接下来到 PC 和 LR 分别看看具体什么情况。
1 |
|
PC 是 libc.so 的 0x40b88 地址处,将src/main/resources/android/sdk23/lib/libc.so从 Unidbg 里拷贝出来,放到 IDA 里解析(千万不要从手机里 pull 出 libc 然后分析,这是明朝的剑斩清朝的官)。
1 |
|
LR 是执行完函数后的返回地址。
1 |
|
值为0x19ef3是因为arm32的情况下地址会多+1。
4.2 clock_gettime 系统调用处理
那遇到系统调用相关的报错怎么处理呢?
第一步肯定是要了解这个系统调用的基本语义。
clock_gettime
的语义和功能相当丰富,可以获取真实时间,进程时间等多种类型的时间,其函数原型如下。
1 |
|
timespec
结构体
1 |
|
参数1 是时钟类型,时钟类型列举如下。
1 |
|
简而言之,时钟可以分为真实时间、CPU 时间、开机时间、从不确定的某个时间点开始计时这四大类。
我们来看一下unidbg是怎么处理这些时间类型的。根据报错所打印的函数调用堆栈,来到ARM32SyscallHandler.java:1746
位置看一下。
1 |
|
再来看我们遇到的时间类型,结合报错信息java.lang.UnsupportedOperationException: clk_id=2
可知,clk_id=2
的情况没有处理。而clk_id=2
属于CPU时间类型,对于CPU
执行时间,Unidbg 则直接设置为 1 纳秒,也属于摆烂。这里我们也破罐子破摔,处理同 CLOCK_THREAD_CPUTIME_ID 一样。
由于我们模拟的样例是 32 位的,所以这里自定义deWuSyscallHandler
类继承自ARM32SyscallHandler
,重写clock_gettime
方法。
1 |
|
然后在创建模拟器的时候使用它。
1 |
|
4.3 popen 系统调用处理
运行后报错
1 |
|
首先是NR=190
,对应于vfork
,其次是 NR = 358
,对应于dup3
。
为什么这里有多个系统调用会WARN
?事实上这是库函数popen
惹的祸,只要看到com.github.unidbg.linux.file.PipedWriteFileIO
报错,那么就应该直接转入处理popen
的逻辑。
同样,我们首先需要了解这个系统调用的基本语义。popen
在 Android Native 上是一个高频函数,设备信息收集、环境检测都可以用到它。函数原型如下
1 |
|
popen
以创建管道的方式启动一个进程, 并调用 shell
,像下面这样使用,比如这里在作用上等价于 adb shell
里输入getprop ro.build.id
。
1 |
|
popen 在底层原理上是创建了一个新进程,然后通过管道去做通信,返回数据。但 Unidbg 在多进程相关的系统调用处理上十分不完善,无法支撑popen
正常的执行它的底层逻辑。
那如何处理它呢?一种思路就是越过创建多线程的相关逻辑,只处理好管道的返回值,来让它的功能基本满足,这可以当成固定处理模式,遇到了就这么做就行。
第一步是用 Dobby 或任意 Hook 工具,在库函数的层面 Hook 获取到 popen 的 command,放到 emulator 的全局变量里,方便后续处理。我这里使用的是 xhook,代码位于 hookPopen 函数内。
1 |
|
运行可以发现 Hook 生效,打印出了具体的命令。
1 |
|
通过adb shell 执行该指令,得到信息如下
1 |
|
获取的是文件的详细信息。接下来在系统调用端做处理,重写 pipe2(部分代码逻辑可参考AndroidSyscallHandler中的代码),根据当前的 command 返回从 adb shell 获取到的值,以及出现 popen 内部会使用的 vfork 和 wait4 系统调用。
1 |
|
运行发现一大堆 popen 调用,根据 command 逐一返回合适的值。
4.4 loctl 系统调用处理
继续运行,报错
1 |
|
NR=54
对应 ioctl
,在 Android Native 里,它常用来获取 MAC 地址。它的参数 request
用于指令操作类型,对于各种各样的文件和需求,有不同的request
,Unidbg 首先预处理了其中的一部分,但这里报错的 0x8927 尚未处理。
一个最简单的处理办法就是来到报错点com.github.unidbg.file.AbstractFileIO.ioctl(AbstractFileIO.java:64)
,添加判断request=0x8927
的情况,让其返回0。
1 |
|
最后运行成功,结果如下
1 |
|