当前位置:安全客 >> 知识详情

【技术分享】如何编译Android内核Hook系统调用

2017-02-17 14:50:41 来源:vantagepoint.sg 作者:興趣使然的小胃
阅读:4705次 点赞(0) 收藏


http://p0.qhimg.com/t01cbdef51a4774c3d0.jpg

作者:興趣使然的小胃

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


前言


Android内核是逆向工程师手上强大的武器。通常Android应用程序会被系统加以严格控制并沙箱化运行,但我们却可以通过修改操作系统内核使系统按照我们规定的方式运行。大多数完整性检查和防篡改功能最终都依赖于系统内核所提供的服务,恶意软件作者(或普通开发人员)可以通过部署一个自定义的内核,让针对性的逆向防护方法无计可施。

Android应用程序有多种方法可以与系统环境进行交互。常用方法是通过Android应用程序框架提供的API接口。在最底层,许多重要功能(如内存分配和文件访问)会被转换为纯Linux系统调用来实现。在ARM Linux系统中,系统调用是通过SVC指令所触发的软件中断来调用的,软件中断将系统调用号作为函数指针表(在Android上是sys_call_table表)的偏移量,调用vector_swi()内核函数来完成调用过程。

截断系统调用的最直接方法是将自己的代码注入内核内存空间,覆盖系统调用表中的原始函数空间以便重定向运行。然而目前Android内核对内存空间做了严格的限制,导致这种方式无法正常工作。具体来说,Lollipop和Marshmallow内核是在启用了CONFIG_STRICT_MEMORY_RWX选项的情况下编译构建的,这意味着任何对内核代码或者系统调用表的修改将会导致内存分段错误,最终导致系统重启。解决这个问题的办法之一是构建自定义内核,通过自定义内核,我们可以停用内存保护机制,并进行其他自定义配置,使Android上的逆向工程事半功倍。如果你经常逆向Android应用程序,那么构建自己的逆向工程沙箱应该不费吹灰之力。

注意:以下步骤已在安装了Android NDK 4.8环境的Ubuntu 14.04上测试成功,我在Mac OS系统上还未能成功复现,建议你使用Ubuntu虚拟机完成这些工作。


编译Android内核


我推荐使用AOSP支持的设备进行操作,谷歌的Nexus手机和平板是最合适的候选设备(基于AOSP构建的内核和系统组件可以在它们身上正常运行),索尼的Xperia系列也可以作为备选。为了构建AOSP内核,你需要一组工具链(用来交叉编译源代码)以及对应版本的内核源代码。请按照Google官方说明文档,为你所选定的设备选择正确的代码仓库分支以及正确的Android系统版本。

比如,要获取与Nexus 5兼容的Lollipop内核源代码,你需要克隆“msm”代码库,导出“android-msm-hammerhead”分支(hammerhead是Nexus 5的代号,找到正确的分支是相当费神的一个工作)。下载源码后,使用“make hammerhead_defconfig”命令(或者xxx_defconfig命令,这取决于你所选定的设备)来创建默认内核配置。

$ git clone https://android.googlesource.com/kernel/msm.git
$ cd msm
$ git checkout origin/android-msm-hammerhead-3.4-lollipop-mr1 
$ export ARCH=arm 
$ export SUBARCH=arm
$ make hammerhead_defconfig
$ vim .config

要想启用系统调用hook功能,我建议添加可加载模块功能、开放/dev/kmem接口并导出全局内核符号,同时记得要停用内存保护功能。这些选项大部分都可以在配置文件中找到,将它们设置为如下的值即可。

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

修改后保存.config文件。现在你可以创建一个独立工具链,用来完成后续任务或对内核进行交叉编译。要创建Android 5.1的工具链,请从Android NDK包中运行make-standalone-toolchain.sh脚本,如下所示:

$ cd android-ndk-rXXX
$ build/tools/make-standalone-toolchain.sh --arch=arm --platform=android-21 --install-dir=/tmp/my-android-toolchain

将“CROSS_COMPILE”环境变量指向NDK目录,运行“make”命令来编译内核。

$ export CROSS_COMPILE=/tmp/my-android-toolchain/bin/arm-eabi- 
$ make

当编译过程成功完成后,你可以在“arch/arm/boot/zImage-dtb”目录找到可引导的内核镜像。


引导自定义内核


在引导自定义内核之前,可以先从设备中备份原始引导镜像。进入引导分区位置,如下所示:

root@hammerhead:/dev # ls -al /dev/block/platform/msm_sdcc.1/by-name/         
lrwxrwxrwx root     root              1970-08-30 22:31 DDR -> /dev/block/mmcblk0p24
lrwxrwxrwx root     root              1970-08-30 22:31 aboot -> /dev/block/mmcblk0p6
lrwxrwxrwx root     root              1970-08-30 22:31 abootb -> /dev/block/mmcblk0p11
lrwxrwxrwx root     root              1970-08-30 22:31 boot -> /dev/block/mmcblk0p19
(...)
lrwxrwxrwx root     root              1970-08-30 22:31 userdata -> /dev/block/mmcblk0p28

导出所有内容到一个文件中:

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

提取ramdisk以及与引导镜像有关的一些信息。有很多工具可以完成这个工作,我使用的是Gilles Grandou的abootimg工具。安装该工具并在你的启动镜像上运行以下命令:

$ abootimg -x boot.img

这条命令会在本地目录中创建bootimg.cfg、initrd.img以及zImage(你的原始内核)文件。

现在你可以使用fastboot来测试新内核。“fastboot boot“命令允许你在未刷入内核前引导它(一旦你确认内核工作正常后,你可以使用”fastboot flash”命令刷入内核并保存更改)。使用以下命令以fastboot模式重启设备:

$ adb reboot bootloader

下一步,使用“fastboot boot“命令启动Android新内核。除了指定新的内核以及原始的ramdisk,你还需要指定内核偏移量、ramdisk偏移量、标签偏移量以及控制台信息(这些信息可以在之前提取的bootimg.cfg中获得)。

$ fastboot boot zImage-dtb initrd.img --base 0 --kernel-offset 0x8000 --ramdisk-offset 0x2900000 --tags-offset 0x2700000 -c "console=ttyHSL0,115200,n8 androidboot.hardware=hammerhead user_debug=31 maxcpus=2 msm_watchdog_v2.enable=1"

现在系统应该可以正常引导。要快速验证系统是否正在运行正确的内核,你可以到“设置—关于手机“中检查”内核版本“字段。

内核版本检查

一切顺利的话,内核版本信息应该显示你定制的内核版本字符串。接下来是Hook的重头戏。


使用内核模块Hook系统调用


Hook系统调用可以让我们突破基于内核功能的反逆向保护机制。通过使用自定义内核,我们可以采用LKM将更多代码加载到内核空间,我们还可以访问/dev/kmem接口来实时修改内核内存空间。这是一项经典的Linux rootkit技术,在Android中已经有人进行了相关介绍[1]。

content_patching.jpg

我们首先需要知道sys_call_table的地址,这可以从Android内核的符号表中找到(iOS逆向人员就没那么走运了)。我们可以在“/proc/kallsyms“文件中查找地址:

$ adb shell "su -c echo 0 > /proc/sys/kernel/kptr_restrict"
$ adb shell cat /proc/kallsyms | grep sys_call_table
c000f984 T sys_call_table

这是我们在编写内核模块中所需要的唯一一个地址信息,其他的地址信息都可以从内核头部获取的偏移量计算得出。另外,你也可以从内核模块中动态获取到sys_call_table的地址(代码样例

在本文中,我们将使用内核模块来隐藏一个文件。首先让我们在设备上创建一个文件:

$ adb shell "su -c echo ABCD > /data/local/tmp/nowyouseeme"             
$ adb shell cat /data/local/tmp/nowyouseeme
ABCD

接下来是编写内核模块工作。为了达到文件隐藏目的,我们需要hook用来打开文件(或者检查文件是否存在)的系统调用。很多调用可以完成这个工作,比如open/openat/accessat/facessat/stat/fstat等,为了演示方便,在这里我们只hook openat系统调用(它是“/bin/cat“程序在访问文件时使用的系统调用)。

你可以在内核头文件“arch/arm/include/asm/unistd.h“中找到所有的系统调用函数原型。使用以下代码创建一个名为”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;
 
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);
 
  if (strcmp(kbuf, "/data/local/tmp/nowyouseeme") == 0) {
    printk("Hiding file!\n");
    return -ENOENT;
  }
 
  kfree(kbuf);
 
  return real_openat(dirfd, pathname, flags);
}
 
int init_module() {
 
  sys_call_table = (void*)0xc000f984;
  real_openat = (void*)(sys_call_table[__NR_openat]);
 
return 0;
 
}

要编译内核模块,你需要内核源代码以及一个工具链,由于前面你已经完成了一个完整内核的编译工作,因此这些条件你都已经具备了。创建包含如下内容的一个Makefile文件:

KERNEL=[YOUR KERNEL PATH]
TOOLCHAIN=[YOUR TOOLCHAIN PATH]
 
obj-m := kernel_hook.o
 
all:
        make ARCH=arm CROSS_COMPILE=$(TOOLCHAIN)/bin/arm-eabi- -C $(KERNEL) M=$(shell pwd) CFLAGS_MODULE=-fno-pic modules
 
clean:
        make -C $(KERNEL) M=$(shell pwd) clean

运行“make“来编译代码,生成kernel_hook.ko文件。将kernel_hook.ko文件拷贝至设备中,使用insmod命令加载,使用lsmod命令验证模块是否已被成功加载。

$ make
(...)
$ adb push kernel_hook.ko /data/local/tmp/
[100%] /data/local/tmp/kernel_hook.ko
$ adb shell su -c insmod /data/local/tmp/kernel_hook.ko
$ adb shell lsmod
kernel_hook 1160 0 [permanent], Live 0xbf000000 (PO)


修改系统调用表


现在,我们可以访问/dev/kmem,将sys_call_table中的原始函数指针覆盖为我们新注入的函数地址(这也可以直接在内核模块中完成,但使用/dev/kmem可以让我们更容易启用和关闭hook功能)。我采用Dong-Hoon You在Phrack上的文章中介绍的方法来达到该目的,但我将文章中使用的mmap()方法替换为文件接口方法,由于某种原因,mmap()方法会导致内核出错。使用如下代码创建名为“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;
 
  if (argc < 3) { 
    return 0;
  }
 
  kmem=open("/dev/kmem",O_RDWR);
 
  if(kmem<0){
    perror("Error opening kmem"); return 0;
  }
 
  sscanf(argv[1], "%x", &sys_call_table); sscanf(argv[2], "%d", &sys_call_number);
  sscanf(argv[3], "%x", &addr_ptr); char buf[256];
  memset (buf, 0, 256); read_kmem2(buf,sys_call_table+(sys_call_number*4),4);
  printf("Original value: %02x%02x%02x%02x\n", buf[3], buf[2], buf[1], buf[0]);       
  write_kmem2((void*)&addr_ptr,sys_call_table+(sys_call_number*4),4);
  read_kmem2(buf,sys_call_table+(sys_call_number*4),4);
  printf("New value: %02x%02x%02x%02x\n", buf[3], buf[2], buf[1], buf[0]);
  close(kmem);
 
  return 0; 
}

使用前期准备的工具链编译kmem_util.c并将它拷贝至设备中。需要注意的是,从Android Lollipop开始,所有的可执行文件都必须在PIE选项下编译生成。

$ /tmp/my-android-toolchain/bin/arm-linux-androideabi-gcc -pie -fpie -o kmem_util kmem_util.c
$ adb push kmem_util /data/local/tmp/
$ adb shell chmod 755 /data/local/tmp/kmem_util

在修改内核内存空间前,我们需要知道系统调用表中的正确偏移量。我们可以在内核源代码的unistd.h文件中找到openat系统调用的定义:

$ grep -r "__NR_openat" arch/arm/include/asm/unistd.h
#define __NR_openat            (__NR_SYSCALL_BASE+322)

接下来要找到我们要替换的openat的地址。我们同样可以在/proc/kallsyms中获取该地址:

$ adb shell cat /proc/kallsyms | grep new_openat
bf000000 t new_openat    [kernel_hook]

现在我们已经准备好覆盖sys_call_table条目所需的一切信息了。Kmem_util的用法为:

./kmem_util <syscall_table_base_address> <offset> <func_addr>

以下命令可以修改openat系统调用表以指向我们的自定义函数:

berndt@osboxes:~/Host/Research/SoftToken/Android/Kernel/msm$ adb shell su -c /data/local/tmp/kmem_util c000f984 322 bf000000
Original value: c017a390
New value: bf000000

一切顺利的话,现在/bin/cat命令应该“看不见“我们之前创建的文件了。

berndt@osboxes:~/Desktop/Module$ adb shell su -c cat /data/local/tmp/nowyouseeme
tmp-mksh: cat: /data/local/tmp/nowyouseeme: No such file or directory

如上所示,文件“nowyouseeme“已经在所有用户进程的视图中消失了。为了完美隐藏该文件,你还需要对stat()、access()等其他系统调用进行hook。

文件隐藏只是的冰山一角,通过修改内核,你还可以完成很多事情,比如绕过许多root检测方法、完整性检查以及实现反调试功能等。


总结


Hook系统调用对Android逆向工程师而言非常有用。你需要使用自定义内核编译自己的逆向工程沙箱。本文介绍了如何在运行Lollipop的Nexus 5上执行上述操作,其他AOSP兼容设备上的过程应该与此类似。


参考文献


[1] Phrack Volume 0x0e, Issue 0x44 - Android Kernel Rootkit


本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。
原文链接:http://www.vantagepoint.sg/blog/82-hooking-android-system-calls-for-pleasure-and-benefit

参与讨论,请先 | 注册 | 匿名评论
发布
用户评论
无任何评论