【返回博客列表 】

[PSRC-B-20150009] Android系统调用hook研究(一)

编号:
PSRC-B-20150009
来源:
银河实验室
作者:
nabla
发布日期:
2016-01-14
阅读量:
7732

提到Xposed,大家应该都非常熟悉,它主要在Java层对目标函数进行hook,对于底层的系统调用则无能为力。由于hook系统调用需要在内核空间中完成,这大多是通过加载内核模块来实现。这里,我们将从最基本入手,一步步介绍如何通过LKM(loadable kernel module)对系统调用进行hook

 

具体内容将分为两部分,本篇文章主要介绍内核模块相关概念,并为Android编写运行一个简单的Hello world模块。

 

我们试验所使用手机的为小米4移动版,系统为MIUI 5.7.16开发版(自带root),内核版本3.4.0

 

1. 内核模块

如果需要向内核中添加功能,我们可以通过修改内核源码并重新编译的方式来完成。但这种方法需要在每次修改时都重新编译内核,因此不够灵活;此外,许多功能如硬件驱动,如果全部包含在内核中,无疑会增加内核的大小。因此,内核模块便应运而生:我们可以在内核运行时动态地加载所需的功能,模块化的设计也缩减了开发调试所需的时间。此外,作为内核的一部分,模块具有较高的权限。鉴于此,许多Rootkit便是以内核模块的形式出现。

 

下面,我们来看一个简单的内核模块:

#include<linux/module.h>

#include<linux/kernel.h>

 

MODULE_LICENSE("GPL");

 

inttest_init(void);

voidtest_exit(void);

module_init(test_init);

module_exit(test_exit);

 

int test_init(void) {

   printk(KERN_ALERT " Helloworld\n");

   return 0;

}

 

voidtest_exit(void) {

   printk(KERN_ALERT" Goodbye world\n");

}
 

module_initmodule_exit用于注册模块加载和卸载时需要执行的函数。我们在加载时输出"Hello World",在卸载时输出"Goodbyeworld"。注意到我们使用的是printk,这是由于内核模块中不能使用处于用户空间中的标准库,因此没有stdio.hstdlib.h等。printk便是内核所提供的,类似于printf的输出函数,其结果被输出至dmesg

 

将上述.c文件保存为testModule.c,并在同一文件夹下创建Makefile:

 

obj-m += testModule.o

 

CROSS_COMPILE=arm-none-eabi-

KERNEL_DIR =/mnt/hgfs/liuruikai756/src/Xiaomi_Kernel_OpenSource

 

all:

    make -C$(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules

 

clean:

    make -C$(KERNEL_DIR) M=$(PWD) clean

 

Makefile中各变量意义较为明确,在此简要介绍如下:

obj-m: 构建模块所需的object文件

CROSS_COMPILE: 交叉编译器的前缀。可通过apt-getinstall gcc-arm-none-eabi安装所需的交叉编译工具链。

KERNEL_DIR: 内核源码所在路径,下文将详细解释

 

到此为止,模块的代码及Makefile均已就绪。而在我们真正编译模块前,我们还需要做内核的相关准备工作。

 

2. 准备内核

内核模块与内核是紧密联系的。由于模块处于内核空间,一旦发生问题便会直接造成严重的系统崩溃,因此编译模块时需要内核的相关信息。一般的流程是:首先编译内核,之后根据得到的内核配置、符号信息等再编译模块。这也意味着,当系统内核更新后,外部模块往往需要重新编译以兼容新内核。Linux系统下可通过Dynamic Kernel Module Support(DKMS)自动重新编译内核模块。

 

对于我们的Android手机来说也是如此。我们需要编译内核所使用的源码,或与其版本尽量接近的源码。小米提供了其MIUI的内核源码:

https://github.com/MiCode/Xiaomi_Kernel_OpenSource

 

根据页面上的说明,我们的小米4使用的是cancro-kk-oss分支,我们首先下载得到这一分支的代码:

git clone https://github.com/MiCode/Xiaomi_Kernel_OpenSource.git -b cancro-kk-oss--single-branch --depth 1

 

由于Linux支持多种平台,因此我们需要根据目标平台进行配置。小米4使用的芯片是高通MSM8974,我们可在内核源码顶级目录下,使用如下命令完成相关配置:

export ARCH=arm

export CROSS_COMPILE=arm-none-eabi-

make msm8974_defconfig

 

之后,运行

make prepare

make scripts

完成相关准备工作

 

之前提到过,编译模块之前,首先需要编译内核。这是为什么呢?到目前为止,我们还未开始编译内核,因此可以尝试下直接编译模块。在我们Hello world模块代码所在目录下运行make,得到输出如下:

 

虽然编译报出了Warning提示Module.symvers不存在,但模块testModule.ko也生成了。我们尝试忽略警告,直接加载得到的模块。将文件testModule.ko push到手机,并使用insmod进行加载:

 

可以看到,insmod加载内核失败,且dmesg输出的信息表明原因是未找到module_layout的符号版本。

 

那么,符号版本又是什么呢?实际上,之前编译内核模块时警告缺少Module.symvers,便是与此相关。

 

Module.symvers包含的是内核符号的版本信息,其具体格式可参考系统中已存在的该文件。在我的Debian上,其内容如下:

 

该文件分4列,依次为:

(1) 符号的版本(CRC)

(2) 符号的名称

(3) 符号所属,内核(vmlinux)或模块

(4) 符号类型

 

当内核.config中设置CONFIG_MODVERSIONS=y,那么在内核编译完成后,在顶级目录下便会生成Module.symvers文件,包含符号的版本信息。之后,在编译模块时,会根据此Module.symvers文件,将所需的符号版本包含在模块中。随后,在加载模块时,会首先比较模块所需要的符号版本和内核实际的符号版本,如果不相同则拒绝加载模块。这样,便保证了模块与内核的兼容性,从而避免因加载不兼容模块造成的系统崩溃。而正是由于缺失Module.symvers,我们之前编译得到的模块才无法被加载。

 

顺带一提,目前大多数Linux发行版均包含名为linux-headers或相似的软件包,其中便包含对应版本内核的相关配置文件、头文件,以及Module.symvers

 

回到我们的问题。既然Module.symvers在完整编译内核后生成,那么现在只需要编译一遍内核就可以继续了?理论上应该是正确的,但实际上,我并没有编译内核。由于尝试编译内核时发现存在较多问题,而且我依然不确定编译得到的内核符号版本与小米手机上实际的符号版本完全一致。因此,我决定绕过编译内核这一步。毕竟,我们需要的仅仅是Module.symvers文件而非内核本身。

 

3. 提取Module.symvers

既然决定不编译内核,那么符号版本信息从何而得呢?总不能自己仿照文件格式随便写一个吧。

 

回忆之前提到的加载模块时会对比检查符号版本,我们便想到,实际运行的内核中一定保存着这些信息用于检查。那么,只需得到手机中的内核,并将其所包含的符号版本导出,不就可以了么?幸运的是,已经有前人想到了这一点并提供了工具:

https://github.com/glandium/extract-symvers

 

该脚本可从内核镜像中提取出符号版本,那么接下来我们要做的就是得到手机中的内核了。

 

一般地,Linux系统的内核保存在/boot分区下。在Android手机上,我们首先找到该分区:

 

可以看到,/boot分区对应于/dev/block/mmcblk0p19,我们将其内容dump出来:

 

随后,在PC上使用abootimg(可通过apt-get install abootimg安装),得到boot.img中的内核镜像zImage

 

最后一步便是使用extract-symvers脚本提取Module.symvers了。该脚本需要内核基址作为参数,可从dmesg中找到.text段信息获取基址:

 

得到了基址0xc0008000,运行

extract-symvers.py -B c0008000 vmlinux >Module.symvers

便得到了期待已久的Module.symvers。我们可以检查得到的Module.symvers文件,其中确实包含了之前提示缺失的module_layout符号信息:

 

4. 编译内核模块

我们将上一步的Module.symvers文件复制到内核源码顶级目录下,再一次在模块代码目录下运行make。这次便没有出现缺少Module.symvers的警告了。事实上,如果我们查看模块源码所在目录,会发现生成了一个新的文件testModule.mod.c。该文件用于向最终生成的模块中添加一些ELF文件段信息,其中便包括了符号版本:

 

将编译得到的内核模块push到手机,并使用insmod加载,这次并没有提示错误,而且通过lsmod查看发现,模块确实已经加载成功。dmesg输出的内容中,也看到了熟悉的Hello world

 

至此为止,我们已经成功地完成了一个简单的内核模块,读者们应该对模块有了初步的了解。那么如何利用内核模块来hook系统调用呢?在下一篇中,我们将详细讨论这一部分。

 

参考资料:

http://www.tldp.org/LDP/lkmpg/2.4/html/book1.htm

http://glandium.org/blog/?p=2664

https://github.com/codeworkx/abootimg

欢迎社会各界向我们反馈平安集团所有产品和业务的安全问题,更多信息可见报告漏洞