本文博客地址: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 ([email protected]) (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內核編譯的配置,具體如下所示:
# 配置編譯環境變量
$ 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
自定義內核模塊編譯成功後的結果截圖,如下所示:
# 查看編譯的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_64 對 kmem_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系統調用的樂趣和好處》,雖然原文中有不少的錯誤,但是還是值得研究和實踐,前面也寫過這篇博文的實踐但是各種錯誤和問題,只實踐了一半就繼續不下去了,本篇博文中一一將前面的遇到的問題都給解決了,遺憾的是還是失敗了~