翻译:myswsun

0x00 前言


Android的内核是逆向工程师的好伙伴。虽然常规的Android应用被限制和沙盒化,逆向工程师可以按自己希望自定义和改变操作系统和内核中行为。这给了你不可多得的优势,因为大部分完整性校验和防篡改功能都依赖内核的服务。部署这种可以滥用信任并自我欺骗的内核和环境,可以避免走很多歪路。

Android应用有几种方式和系统环境交互。标准方法是通过安卓应用框架的API函数。然而在底层,很多重要的函数,例如分配内存和访问文件,都是被转化为linux的系统调用。在ARM linux中,系统调用的调用是通过SVC指令触发软件中断实现的。中断调用内核函数vector_swi(),用系统调用号作为一个函数指针表的偏移(如安卓上的sys_call_table)。

拦截系统调用最直接的方法是注入你的代码到内核内存中,覆盖系统调用表中的原始函数地址重定向执行。不幸的是,目前Android内核加强了内存限制阻止了这种操作。具体来说,内核是在启用CONFIG_STRICT_MEMORY_RWX选项的情况下构建的。这阻止了向只读内核内存区写入,意味着任何试图修改系统代码或者系统调用表的操作都将导致崩溃和重启。绕过这个的一个方法就是自己编译内核:能够禁用这种保护,做更多自定义的修改有利于逆向分析。如果你按常规方法逆向Android应用,构建你自己的逆向沙盒是不明智的。

注意:下面的步骤是最好在Ubuntu 14.04环境中用Android NDK 4.8完成。我个人在Mac上面失败了很多次才完成。我推荐用Ubuntu虚拟机,除非你是个受虐狂。

0x01 构建内核


为了hack的目的,我推荐使用支持AOSP的设备。Google的Nexus智能手机和平板电脑是最合理的选择,从AOSP构建的内核和系统组件在上面运行没有问题。另外,索尼的Xperia也可以。为了构建AOSP内核,需要一系列工具(交叉编译工具)和相应的内核源代码。根据谷歌的指导,确定了正确的git仓库和分支。

例如,获取匹配Nexus 5的Lollipop的内核源码,需要克隆“msm”仓库并检出一个分支“android-msm-hammerhead”。一旦源码下载了,用make hammerhead_defconfig(或者whatever_defconfig,取决于你的设备)命令创建默认内核配置。

1
2
3
4
5
6
7
$ 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

为了启动系统调用挂钩功能,我推荐增加可加载模块的支持,由/dev/kmem接口支持,同时导出全局内核符号表。不要忘了禁用内存保护。这些选项的值在配置文件中已经存在,只需要简单的设置以下值。

1
2
3
4
5
6
7
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 5.1创建一个工具链,运行Android NDK包的make-standalone_toolchain.sh:

1
2
$ 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构建内核。

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

当构建过程完成后,能在arch/arm/boot/zImage-dtb找到引导内核模块。

0x02 启动新内核


在启动新内核前,备份一个你设备的原始引导映像。找到启动分区的位置:

1
2
3
4
5
6
7
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

然后,将所有的内容转储到一个文件中:

1
2
$ 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工具。安装工具并执行以下的命令:

1
$ abootimg -x boot.img

在本地目录会创建bootimg.cfg、initrd.img和zImage(原始的内核)文件。

能用快速引导测试新内核。“fastboot boot”命令允许测试内核。在fastboot模式下用下面命令重启设备:

1
$ adb reboot bootloader

然后,用“fastboot boot”命令引导Android的新内核。除了新建的内核和原始ramdisk,还需指定内核偏移量,ramdisk偏移量,标签偏移量和命令行(使用在之前提取的bootimg.cfg中列出的值)。

1
$ 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"

现在应该手动重启。为了快速验证内核=正确运行了,通过校验Settings->About phone中的“内核版本”的值。

如果一切运行良好,将显示自定义构建的版本字符串。

0x03 用内核模块hook系统调用


hook系统调用能让我们绕过任何依赖内核提供的反逆向防御措施。在我们自定义的内核中,我们能用LKM加载例外的代码到内核中。我们也可以访问/dev/kmem接口,用来修改内核内存。这是个经典的linux rootkit技术。

首先需要的是sys_call_table的地址。幸运的是它被Android内核导出了符号(iOS没这么幸运)。我们在/proc/kallsyms寻找地址:

1
2
3
$ 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

这是我们仅需要写入的内核地址,其他的可以通过便宜计算出来。

我们将使用内核模块隐藏一个文件。让我们在设备创建一个文件,以便我们能在后面隐藏它:

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

最后时候写内核模块了。为了文件隐藏,我们需要挂钩用来打开文件的一个系统调用。有很多关于打开open, openat, access, accessat, facessat, stat, fstat,等等。现在我们只需要挂钩openat系统调用,这个系统调用被“/bin/cat”程序访问文件时使用。

你能在内核头文件中(arch/arm/include/asm/unistd.h)找到所有系统调用的函数原型。用下面代码创建一个文件kernel_hook.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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文件:

1
2
3
4
5
6
7
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。复制这个文件到设备并用insmod命令加载它。用lsmod命令验证模块是否加载成功。

1
2
3
4
5
6
7
$ 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)

0x04 修改系统调用表


现在,我们访问/dev/kmem来用我们注入的函数地址来覆盖sys_call_table中的原始函数的指针(这也能直接在内核模块中做,但是用/dev/kmem更加简单)。我参考了Dong-Hoon You的文章,但是我用文件接口代替nmap(),因为我发现会引起一些内核警告。用下面代码创建文件kmem_util.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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支持编译的。

1
2
3
$ /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

在我们开始修改内核内存前,我们需要知道的是系统调用表正确的偏移位置。这个openat调用在unistd.h中定义:

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

最后一个难题是我们替换openat函数的地址。我们能从/proc/kallsyms得到这个地址:

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

现在我们可以覆盖系统调用表的入口了。Kmem_util语法如下:

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

用下面的命令修改系统调用表指向我们的新函数。

1
2
3
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应该不能看见这个文件。

1
2
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

现在通过所有的用户进程已经无法看见隐藏的文件了(但是为了隐藏文件有许多需要做的,包括挂钩stat,access和其他系统调用,还有在文件夹中隐藏)。

文件隐藏的教程只是一个小例子:你可以完成一大堆事,包括绕过启动检测,完整性校验和反调试技巧。

尽管代码覆盖使用符号执行是一个好的方法,但它是个复杂的任务。路径遍历意味着内存消耗,并且一些情况下要计算的表达式太过复杂。目前,判定器非常慢,判定表达式非常慢。

0x05 总结


hook系统调用对于Android逆向分析是一个有用的技术。为了使用它,需要用自定义内核构建自己的逆向工程沙盒。这个文章介绍了如何在Nexus5运行Lollipop,其他AOSP设备也是类似的。

hook Android系统调用的乐趣和好处的更多相关文章

  1. Hook android系统调用的实践

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/71037182 一.环境条件 Ubuntukylin 14.04.5 x64bit ...

  2. Hook android系统调用研究(一)

    本文的博客链接:http://blog.csdn.net/qq1084283172/article/details/55657300 一.Android内核源码的编译环境 系统环境:Ubuntu 14 ...

  3. 使用Cydia Substrate 从Native Hook Android Native世界

    同系列文章: 使用Cydia Substrate 从Native Hook Android Java世界 使用Cydia Substrate Hook Android Java世界 一.建立工程 手机 ...

  4. 玩转Hook——Android权限管理功能探讨(一)

    随着Android设备上的隐私安全问题越来越被公众重视,恶意软件对用户隐私,尤其是对电话.短信等私密信息的威胁日益突出,各大主流安全软件均推出了自己的隐私行为监控功能,在root情况下能有效防止恶意软 ...

  5. 使用Cydia Substrate 从Native Hook Android Java世界

    这里介绍了如何使用Cydia Substrate Hook安卓Java世界.这篇文章介绍如何从Native中Hook 安卓Java世界. 手机端配置见之前文章. 一.建立工程 建立一个Android工 ...

  6. 使用Cydia Substrate Hook Android Java世界

    从来没接触过Android的HOOK,在看雪上找到了一篇HOOK 的文章,但是太复杂了,应该是本地环境问题,测试不成功. 后来搜到Cydia Substrate,看了几篇文章,进入官网查看了一下文档, ...

  7. Linux、Android系统调用从上层到底层的调用路径浅析

    参考: https://blog.csdn.net/liuhangtiant/article/details/85149369 http://blog.sina.com.cn/s/blog_79433 ...

  8. Xposed框架Hook Android应用的所有类方法打印Log日志

    本文博客地址:https://blog.csdn.net/QQ1084283172/article/details/80954759 在进行Android程序的逆向分析的时候,经常需要Android程 ...

  9. Android系统调用

    android 中intent是经常要用到的.不管是页面牵转,还是传递数据,或是调用外部程序,系统功能都要用到intent. 在做了一些intent的例子之后,整理了一下intent,希望对大家有用. ...

随机推荐

  1. HDOJ-1176(数塔问题变形)

    免费陷阱 HDOJ-1176 一开始正向推的时候,一直wa,后来采用逆向推得到正确结果. 初始化的时候dp数组都初始化为0. #include<bits/stdc++.h> using n ...

  2. MVC base64加密的文件,前端下载

    后端代码: public FileResult OutPutFile(string  base64file,string filename) { buffer = Convert.FromBase64 ...

  3. MyBatis文档

    MyBatis 学习笔记 简介 什么是Mybatis MyBatis 是一款优秀的持久层框架,是Apache的一个Java开源项目 ,它支持自定义 SQL.存储过程以及高级映射, 免除了几乎所有的 J ...

  4. Python学习笔记 CH1-4:从入门到列表

    Python CH1 环境准备 因为已经有了C/C++.Java的基础,所以上手很快. 参考书:Eric Matthes -<Python编程 从入门到实践> 环境准备:python3.P ...

  5. golang 微服务以及相关web框架

    golang 中国gocn golang Applicable to all database connection pools xorm是一个简单而强大的Go语言ORM库,通过它可以使数据库操作非常 ...

  6. 【Azure 应用服务】App Service站点Header头中的中文信息显示乱码?当下载文件时,文件名也是乱码?

    问题描述 在本地开发的站点,响应头中的中文可以正常显示,部署到Azure App Service站点后,响应中文乱码.通过多方面验证,在代码中设置Response的Headers会显示乱码,而直接配置 ...

  7. OAuth2.0授权码模式实战

    OAuth2.0是目前比较流行的一种开源授权协议,可以用来授权第三方应用,允许在不将用户名和密码提供给第三方应用的情况下获取一定的用户资源,目前很多网站或APP基于微信或QQ的第三方登录方式都是基于O ...

  8. .Net5 WPF快速入门系列教程

    一.概要 在工作中大家会遇到需要学习新的技术或者临时被抽调到新的项目当中进行开发.通常这样的情况比较紧急没有那么多的时间去看书学习.所以这里向wpf技术栈的开发者分享一套wpf教程,基于.net5框架 ...

  9. 文字变图片——GitHub 热点速览 v.21.14

    作者:HelloGitHub-小鱼干 程序的力量,在 deep-daze 体现得淋漓尽致,你用一句话描述下你的图片需求,它就能帮你生成对应图片.同样的,appsmith 的力量在于你只要拖拽即可得到一 ...

  10. Spring Cloud 升级之路 - 2020.0.x - 3. Undertow 的 accesslog 配置

    上一节我们讲述了如何使用 Undertow 作为我们的 Web 服务容器,本小节我们来分析使用 Undertow 的另一个问题,也就是如何配置 accesslog,以及 accesslog 的各种占位 ...