首页 > 代码库 > Hook android系统调用的实践

Hook android系统调用的实践

本文博客地址:http://blog.csdn.net/qq1084283172/article/details/71037182


一、环境条件

Ubuntukylin 14.04.5 x64bit
Android 4.4.4
Nexus 5


二、Android内核源码的下载
执行下面的命令,获取 Nexus 5手机 设备使用的芯片即获取Nexus 5手机设备内核源码的版本信息。

$ adb shell  
  
# 查看移动设备使用的芯片信息  
$ ls /dev/block/platform 
执行的结果,如下图所示:

技术分享

根据google官方的参考文档以及上面获取的Nexus 5手机设备芯片信息得到Nexus 5手机的内核源码的下载地址,具体的执行下面的命令:

$ git clone https://aosp.tuna.tsinghua.edu.cn/kernel/msm.git (清华的源)
# 或者  
$ git clone https://android.googlesource.com/kernel/msm.git   (或者谷歌官方的源需要翻墙)  

$ cd msm
# 查看可以下载的Linux内核源码的版本  
$ git branch -a 
Nexus 5手机内核源码版本的下载,需要根据Nexus 5手机的内核的版本信息来确定,具体的执行下面的命令:

$ adb shell

# 查看移动设备的内核版本
$ cat /proc/version
Linux version 3.4.0-gd59db4e (android-build@vpbs1.mtv.corp.google.com) (gcc version 4.7 (GCC) ) #1 SMP PREEMPT Mon Mar 17 15:16:36 PDT 2014
当然了,直接在 Nexus 5手机上,打开 关于手机选项,查看Nexus 5手机的内核版本信息也是可以的。 3.4.0-g 后面的7位十六进制数字 d59db4e 非常重要,使用这个字符串就可以 check out 出准确的commit。该内核版本字符串引用正好是AOSP的GIT仓库中的某个commit的哈希值,因此只要是使用google官方提供的Android内核源码的设备就可以通过该内核版本字符串下载到对应的Android内核的源码。执行下面的命令下载Nexus 5手机设备对应的Android内核源码:

# 下载对应的Android内核源码
$ git checkout d59db4e
Android内核源码下载好了,还需要下载编译Android内核源码的交叉编译工具链 arm-eabi-4.7 。为了方便起见,交叉编译工具链 arm-eabi-4.7 下载好以后,添加arm-eabi-4.7到 ubuntu14.04.5 x64bit 的系统环境变量中,具体的执行下面的命令:

# 下载编译工具链  
$ git clone https://aosp.tuna.tsinghua.edu.cn/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7/  
# 或者  
$ git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7/    


# 添加arm-eabi-4.7到系统环境变量中
$ sudo gedit /etc/profile      
    
# 添加到环境变量配置文件/etc/profile中的内容    
export ANDROID_TOOLCHAIN=/home/fly2016/Desktop/Android4.4.4r1/android-4.4.4_r1/kernel_d59db4e/msm/arm-eabi-4.7  
export PATH=$PATH:${ANDROID_TOOLCHAIN}/bin/  
  
# 更新系统环境变量    
$ source /etc/profile     
  
# 测试是否配置成功  
$ arm-eabi-gdb  


三、Android内核源码的编译
执行下面的命令,进行Android内核编译的配置,具体如下所示:

# 配置编译环境变量
$ export CROSS_COMPILE=arm-eabi-     
$ export ARCH=arm    
$ export SUBARCH=arm    

# 生成编译配置文件.config 
$ make hammerhead_defconfig

# 编辑编译配置文件.config 
$ gedit .config 
为了使Android内核支持自定义内核模块的加载、卸载和对Android内核内存空间的修改以支持对Android系统调用的Hook操作,需要对Android内核的编译配置文件.config 进行修改,具体的修改如下图:

技术分享


对Android内核编译配置文件 .config 的修改如下:

# 需要修改
CONFIG_MODULES=y
CONFIG_MODULE_UNLOAD=y
CONFIG_STRICT_MEMORY_RWX=n

# 不需要修改
CONFIG_DEVMEM=y
CONFIG_DEVKMEM=y
CONFIG_KALLSYMS=y
CONFIG_KALLSYMS_ALL=y
保存、关闭Android内核编译配置文件 .config ,执行下面的命令进行Android内核的编译。在编译的时候会有对Android内核编译选项的设置,当遇到我们上面设置的内核编译选项时,还是根据Android系统调用Hook的要求来。因为默认情况下Android内核为了移动设备的安全是没有开启这些选项,只有设置了这些选项,才能加载自定义的Android内核模块,实现对Android系统调用的Hook操作。

# 编译Android内核
$ make -j4
编译Android内核时遇到下面编译选项设置的情况,做如下的正确设置,其他的参数选项使用系统默认值,不作任何的修改。

有关Android内核支持自定义内核模块加载和卸载的设置,如下所示:

技术分享


重要提示

CONFIG_MODVERSIONS 和 CONFIG_MODULE_SRCVERSION_ALL 这两个选项一定不能配置,需要去掉,否则的话:在Android系统加载自定义内核模块时会对内核模块进行代码的校验和版本的检查,容易出现如下的错误。具体的出错原因可以参考博文《内核模块编译时怎样绕过insmod时的版本检查》。

技术分享


有关Android内核内存空间读写情况的设置,如下所示:

技术分享

Android内核编译配置文件 .config 经过修改后的结果如下图:

对自定义内核模块加载支持的设置

技术分享


对Android内核内存空间可读可写支持的设置

技术分享


Android内核源码编译成功以后会生成内核引导模块文件 /msm/arch/arm/boot/zImage-dtb ,操作结果如下图:

技术分享



四、替换Nexus 5 手机的内核并启动新内核

由于Nexus 5手机是高通的设备,因此可以执行下面的命令查找到启动分区 boot 的镜像位置。

$ adb shell  
  
# msm 代表高通的芯片,msm_sdcc.1是外接的SD卡挂载的目录,by-name指的是这个sd卡分区的名称  
$ ls -al /dev/block/platform/msm_sdcc.1/by-name/  
执行结果,如下图所示:

技术分享

root权限下 ,将boot镜像所有的内容转储到Nexus 5手机的 /sdcard/boot.img 文件夹下,然后导出到 Ubuntu 14.04.5 x64bit 系统主机上,具体的执行下面的命令:

$ adb shell "su -c dd if=/dev/block/mmcblk0p19 of=/sdcard/boot.img"  
  
$ adb pull /sdcard/boot.img  ./  

使用 abootimg工具 对导出的 boot.img 镜像文件进行解包,然后替换替换掉Android内核镜像文件,重新打包生成新的 boot.img文件,使用 fastboot工具 将新的boot.img镜像文件刷入到Nexus 5设备上引导内核启动,执行的命令如下:

# 安装刷机工具fastboot和adb
$ sudo apt-get install android-tools-adb android-tools-fastboot  

# 安装boot.img文件解包和打包工具abootimg  
$ sudo apt-get install build-essential abootimg  

# 对boot.img文件进行解包
$ abootimg -x boot.img  
导出的boot.img文件解包的结果,如下图所示:

技术分享

将编译生成的 新内核文件 msm/arch/arm/boot/zImage-dtb 替换掉原来的Android内核镜像文件,重新打包生成新的boot.img文件,然后重启Nexus 5手机进入刷机模式,用 “fastboot boot” 命令引导Android的新内核,执行下面的命令:

# 拷贝编译生成的Android内核文件zImage-dt到当前目录下
$ cp  ~/msm/arch/arm/boot/zImage-dtb .  

# 重新打包生成新的boot.img文件
$ abootimg --create myboot.img -f bootimg.cfg -k zImage-dtb -r initrd.img  

# 重启手机设备进入刷机模式 
$ adb reboot bootloader  

# 刷新boot.img文件,引导新的Android内核 
$ fastboot boot myboot.img  
fastboot boot 更新Nexus 5手机的内核成功后,重启手机设备。为了快速验证新的Android内核正确运行了,通过校验Settings->About phone中的“内核版本”的值,如下图。如果一切运行良好的话,内核版本下面将 显示自定义构建的版本字符串 ,结果如下图所示:

技术分享

五、自定义加载Android内核模块Hook系统调用

在我们自定义的内核中,能用LKM加载自定义的代码到内核中,也可以访问/dev/kmem接口,用来修改Android内核的内存,Hook Android内核系统的调用都是基于这些前提条件实现的。有关Hook Android系统调用的原理和详细描述,可以参考前面的博文《Hook android系统调用研究(一)》。

在进行Hook Android系统调用之前,需要 先找到的是 sys_call_table的地址 。root权限下,通过 /proc/kallsyms 可以寻找到 sys_call_table的地址,具体的执行下面的命令:

# 获取root权限
$ adb shell su

# 查看默认值    
$ cat /proc/sys/kernel/kptr_restrict 

# root权限下,关闭symbol符号屏蔽
# 将 /proc/sys/kernel/kptr_restrict 重置为0,就可以打印显示出来 
$ echo 0 > /proc/sys/kernel/kptr_restrict  

# 查看修改后的值    
$ cat /proc/sys/kernel/kptr_restrict  
  
# 获取 sys_call_table的内存地址  
$ cat /proc/kallsyms | grep sys_call_table   
c000f884 T sys_call_table

执行操作的结果如下图:

技术分享


sys_call_table=0xc000f884 即为需要找到的Android系统调用表的内存基址,后面很多Android系统调用的系统函数调用地址都需要通过这个基址加函数的偏移计算出来。下面以使用Android内核模块隐藏一个文件 为例子进行学习,先在设备上创建一个文件,方便我们能在后面隐藏它:

$ adb shell su

# 创建文件nowyouseeme并输入内容HelloWorld
$ echo HelloWorld > /data/local/tmp/nowyouseeme             
  
# 显示新创建文件的内容  
$ cat /data/local/tmp/nowyouseeme  
HelloWorld  
为了隐藏文件,需要Hook用来打开文件的一个Android系统调用。有很多关于打开文件操作的系统调用,如 open, openat, access,accessat, facessat, stat, fstat 等等。这里只需要挂钩 openat 系统调用 ,这个系统调用被 "/bin/cat" 程序 访问文件时被调用。当 openat系统调用 被Hook以后,就可以进行文件显示的过滤,需要隐藏的文件就不显出来。


在Android内核源码的头文件(arch/arm/include/asm/unistd.h)中找到Android所有系统调用的函数原型,然后创建一个挂钩Android系统调用openat 的代码文件 kernel_hook.c

#include <linux/kernel.h>    
#include <linux/module.h>    
#include <linux/moduleparam.h>    
#include <linux/unistd.h>    
#include <linux/slab.h>    
#include <asm/uaccess.h>    
    
asmlinkage int (*real_openat)(int, const char __user*, int);    
    
void **sys_call_table;    
    
// 替换Android系统调用的新的new_openat函数    
int new_openat(int dirfd, const char __user* pathname, int flags)    
{    
  char *kbuf;    
  size_t len;    
    
  // 在内核中申请内存空间    
  kbuf=(char*)kmalloc(256, GFP_KERNEL);    
  // 获取需要打开的文件的文件路径    
  len = strncpy_from_user(kbuf, pathname,255);    
    
  // 过滤,隐藏掉/data/local/tmp/nowyouseeme文件    
  if (strcmp(kbuf, "/data/local/tmp/nowyouseeme") == 0)     
  {    
    printk("Hiding file!\n");    
        
    return -ENOENT;    
  }    
    
  // 释放申请的内存空间    
  kfree(kbuf);    
    
  // 调用Android系统原来的系统调用openat函数    
  return real_openat(dirfd, pathname, flags);    
}    
    
    
// ########### 将被加载的Android内核模块 ###############    
int init_module(void) {    
    
  // 前面查找的内存地址    
  sys_call_table = (void*)0xc000f884;    
      
  // 获取Android系统的openat函数的调用地址    
  real_openat = (void*)(sys_call_table[__NR_openat]);    
    
  return 0;    
}      

为了编译 kernel_hook.c文件 需要配置Android内核源码文件路径和交叉编译工具链路径,Makefile文件的编写如下:

KERNEL=/home/fly2016/Android4.4.4r1/android-4.4.4_r1/kernel_d59db4e/msm  
  
TOOLCHAIN=arm-eabi-  
  
obj-m := kernel_hook.o  
  
all:  
	make ARCH=arm CROSS_COMPILE=$(TOOLCHAIN) -C $(KERNEL) M=$(shell pwd) CFLAGS_MODULE=-fno-pic modules  
  
clean:  
	make -C $(KERNEL) M=$(shell pwd) clean  
提示
Android内核模块与内核是紧密联系的,由于内核模块也处于Android内核空间,一旦发生问题就会直接造成严重的系统崩溃,因此编译Android自定义内核模块时需要Android内核的相关信息 。一般的流程是:首先编译内核,之后根据得到的内核配置符号信息等再编译自定义内核模块。这也意味着,当系统内核更新后,外部模块往往需要重新编译以兼容新内核。Linux系统下可通过 Dynamic Kernel Module Support (DKMS) 自动重新编译内核模块。 


前面Andorid内核源码的编译配置环境下,继续直接执行 make 命令就可以编译 kernel_hook.c文件生成内核模块文件kernel_hook.ko 。如果前面的Android内核编译环境丢失,可以通过在Android内核源码的根目录下,执行下面的命令进行 kernel_hook.c文件的编译。和其他的Linux发行版类似,在编译自定义内核模块之前无需编译整个Android内核,只要生成编译内核模块必要的脚本和头文件就行

# 配置编译环境
$ export ARCH=arm   
$ export SUBARCH=arm  
$ export CROSS_COMPILE=arm-eabi- 

# 生成编译配置文件.config  
$ make hammerhead_defconfig  

# 修改编译配置文件.config 
$ gedit .config  
  
###################################################  
# 修改.config编译配置文件,保存、关闭
CONFIG_MODULES=y  
CONFIG_MODULE_UNLOAD=y  
CONFIG_STRICT_MEMORY_RWX=n  

CONFIG_DEVMEM=y  
CONFIG_DEVKMEM=y  
CONFIG_KALLSYMS=y  
CONFIG_KALLSYMS_ALL=y  
###################################################  

# 生成内核编译需要的脚本和头文件 
$ make prepare modules_prepare

# 或者
$ make prepare    
$ make scripts   
###################################################
  
# 切换到工作目录编译 kernel_hook.c 文件  
$ make
自定义内核模块编译成功后的结果截图,如下所示:

技术分享


拷贝 kernel_hook.ko文件 到Nexus 5手机设备的 /data/local/tmp/ 目录下并在 root权限下 ,用 insmod命令 加载编译后的内核模块 kernel_hook.ko 。用 lsmod 命令 查看该内核模块是否加载成功。

# 查看编译的Android内核模块文件的版本信息
$ modinfo kernel_hook.ko

$ adb shell rm /data/local/tmp/kernel_hook.ko

# 拷贝 kernel_hook.ko文件 到移动设备的/data/local/tmp/目录下
$ adb push kernel_hook.ko /data/local/tmp/  

# root权限下,加载自定义的内核模块kernel_hook.ko
$ adb shell su -c insmod /data/local/tmp/kernel_hook.ko  

# 查看自定义内核模块是否加载成功
$ adb shell lsmod  
执行的操作结果,如下图所示:
技术分享


六、修改Android的系统调用表

通过访问 /dev/kmem接口 将Hook的 新函数地址new_openat 来覆盖sys_call_table中的原始函数openat的调用地址(这也能直接在内核模块中做,但是用/dev/kmem更加简单)。在参考了Dong-Hoon You的文章后,决定使用文件接口代替nmap(),因为经过试验发现会引起一些内核警告。用下面代码创建文件kmem_util.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <asm/unistd.h>
#include <sys/mman.h>

#define MAP_SIZE 4096UL
#define MAP_MASK (MAP_SIZE - 1)


// 保存内存文件的句柄
int kmem;

// 读取内存文件中的数据
void read_kmem2(unsigned char *buf, off_t off, int sz)
{
  off_t offset;
  ssize_t bread;

  // 设置内存文件的偏移在(从文件头开始)
  offset = lseek(kmem, off, SEEK_SET);
  // 读取内存文件的数据
  bread = read(kmem, buf, sz);

  return;
}

// 向内存文件写入数据
void write_kmem2(unsigned char *buf, off_t off, int sz)
{
  off_t offset;
  ssize_t written;

  // 设置内存文件的偏移
  offset = lseek(kmem, off, SEEK_SET);
  // 向内存文件写入数据
  if (written = write(kmem, buf, sz) == -1)
  {
      perror("Write error");
      exit(0);
  }

  return;
}


// 主函数
int main(int argc, char *argv[])
{

  off_t sys_call_table;
  unsigned int addr_ptr, sys_call_number;

  // 对传入的参数的个数进行校验,不能少于3个
  if (argc < 3)
  {
    return 0;
  }

  // 打开内核文件接口/dev/kmem
  kmem = open("/dev/kmem", O_RDWR);
  // 判断文件是否打开成功
  if(kmem < 0)
  {
    perror("Error opening kmem");
    return 0;
  }

  // 获取输入的sys_call_table地址
  sscanf(argv[1], "%x", &sys_call_table);
  // 获取Android系统调用openat函数的偏移值
  sscanf(argv[2], "%d", &sys_call_number);
  // 获取新的new_openat的调用地址
  sscanf(argv[3], "%x", &addr_ptr);

  char buf[256];
  // 内存清零
  memset(buf, 0, 256);

  // 获取Android系统调用openat函数的原始调用地址
  read_kmem2(buf, sys_call_table+(sys_call_number*4), 4);
  // 打印Android系统调用openat函数的原始调用地址
  printf("Original value: %02x%02x%02x%02x\n", buf[3], buf[2], buf[1], buf[0]);

  // 将Android系统调用的openat函数的原始调用地址替换为新的new_openat的调用地址
  write_kmem2((void*)&addr_ptr,sys_call_table+(sys_call_number*4), 4);
  // 获取替换后的新new_openat函数的调用地址
  read_kmem2(buf,sys_call_table+(sys_call_number*4), 4);
  // 打印替换后的new_openat函数的调用地址
  printf("New value: %02x%02x%02x%02x\n", buf[3], buf[2], buf[1], buf[0]);

  // 关闭文件
  close(kmem);

  return 0;
}
编译构建 kmem_util.c文件 并拷贝编译后的文件 kmem_util 到Nexus 5手机设备的 /data/local/tmp/ 目录下。编译时生成可执行文件必须是PIE支持编译的,需要添加编译选项 -pie -fpie。直接使用 /arm-eabi-4.7/bin/arm-eabi-gcc编译工具 对 kmem_util.c文件 进行编译也是可以的,但是提示缺少系统头文件,为了不破坏Android源码的编译环境。这里使用Adt-bundle-x86_64kmem_util.c文件 进行 Android NDK 的编译,编译配置文件 Android.mk的编写如下:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := kmem_util
LOCAL_SRC_FILES := kmem_util.c

LOCAL_CFLAGS += -pie -fPIE  
LOCAL_LDFLAGS += -pie -fPIE

include $(BUILD_EXECUTABLE)
kmem_util.c文件 编译成功以后的结果截图如下:

技术分享

执行下面的命令,拷贝文件 kmem_util 到Nexus 5手机设备上。

# 拷贝文件kmem_util到手机设备上
$ adb push kmem_util /data/local/tmp/  

# 赋予文件可执行权限0755
$ adb shell chmod 755 /data/local/tmp/kmem_util  
在开始修改Android内核内存之前,需要先知道 Android系统调用表 中openat函数 的正确调用偏移位置。openat系统调用在Android内核源码的 arch/arm/include/asm/unistd.h中定义的,在Android内核源码的根目录下,执行下面的命令获取 openat系统调用 的调用偏移位置。

$ grep -r "__NR_openat" arch/arm/include/asm/unistd.h
#define __NR_openat			(__NR_SYSCALL_BASE+322)
从上面的操作结果获取到 openat系统调用的偏移量为322。自定义内核模块kernel_hook.ko已经加载到Android内核内存中,通过符号文件 /proc/kallsyms 可以得到 new_openat函数的调用地址,然后使用该new_openat函数的调用地址替换原来openat函数的调用地址。

$ adb shell cat /proc/kallsyms | grep new_openat  
bf000000 t new_openat	[kernel_hook]
执行操作的结果截图,如下所示:

技术分享

现在可以 覆盖Android内核内存中系统调用函数的调用地址了 ,kmem_util可执行程序 的用法如下:

./kmem_util <syscall_table_base_address> <offset> <new_fun_addr>
在root权限下,执行下面的命令 修改Android系统调用表中的系统函数调用地址,将系统函数调用地址替换为我们自定的Hook函数的调用地址。具体修改Android系统调用函数openat地址的示例,执行下面的命令:

$ adb shell su -c /data/local/tmp/kmem_util c000f884 322 bf000000 
Original value: c01734b4
New value: bf000000
执行操作的结果截图,如下所示:

技术分享

在root权限下,执行 /bin/cat 检查是否Hook Android系统调用函数openat成功。如果成功的话,执行 /bin/cat 不会显示我们隐藏的文件/data/local/tmp/nowyouseeme。具体的执行下面的命令:

$ adb shell su -c cat /data/local/tmp/nowyouseeme  
  
tmp-mksh: cat: /data/local/tmp/nowyouseeme: No such file or directory
执行操作的结果截图,如下所示:

技术分享


七、总结

本篇博文是在前面博文《Hook android系统调用研究(一)》的基础上进行实践和查错总结写出来的,非常遗憾的是在最后关键验证Hook是否成功的步骤上再一次出现错误,猜测可能还是前面的Hook代码或者操作步骤的细节上有问题,没有注意到,后面有时间会再进行研究。这篇博文最原始的参考还是文章《hook Android系统调用的乐趣和好处》,虽然原文中有不少的错误,但是还是值得研究和实践,前面也写过这篇博文的实践但是各种错误和问题,只实践了一半就继续不下去了,本篇博文中一一将前面的遇到的问题都给解决了,遗憾的是还是失败了~


Hook android系统调用的实践