原文:https://salls.github.io/Linux-Kernel-CVE-2017-5123/
譯者:知道創(chuàng)宇404實驗室
本文介紹如何利用Linux內核漏洞CVE-2017-5123提升權限,突破SEMP、SMAP、Chrome沙箱全方位保護。
在系統調用處理階段,內核需要具備讀取和寫入觸發(fā)系統調用進程內存的能力。為此,內核設有copy_from_user與put_user等特殊函數,用于將數據復制進出用戶區(qū)。在較高級別,put_user的功能大致如下:
put_user(x, void __user *ptr) if (access_ok(VERIFY_WRITE, ptr, sizeof(*ptr))) return -EFAULT user_access_begin() *ptr = x user_access_end()
access_ok() 調用檢查ptr是否位于用戶區(qū)而非內核內存。如果檢查通過,user_access_begin()調用禁用SMAP,允許內核訪問用戶區(qū)。內核寫入內存后重新啟用SMAP。需要注意的一點是:這些用戶訪問函數在內存讀寫過程中處理頁面錯誤,在訪問未映射內存時不會導致崩潰。
某些系統調用要求多次調用put/get_user以實現內核與用戶區(qū)之間的數據復制。為避免重復檢查和SMAP啟用/禁用的額外開銷,內核開發(fā)人員將缺少必要檢查的不安全版本_put_user與unsafe_put_user涵蓋進來。這樣一來,忘記額外檢查就在意料之中了。CVE-2017-5123就是一個很好的例子。在內核版本4.13中,為了能夠正常使用unsafe_put_user,專門對waitid syscall進行了更新,但access_ok檢查仍處于缺失狀態(tài)。漏洞代碼如下所示。
SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *, infop, int, options, struct rusage __user *, ru) { struct rusage r; struct waitid_info info = {.status = 0}; long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL); int signo = 0; if (err > 0) { signo = SIGCHLD; err = 0; if (ru && copy_to_user(ru, &r, sizeof(struct rusage))) return -EFAULT; } if (!infop) return err; user_access_begin(); unsafe_put_user(signo, &infop->si_signo, Efault); <- no access_ok call unsafe_put_user(0, &infop->si_errno, Efault); unsafe_put_user(info.cause, &infop->si_code, Efault); unsafe_put_user(info.pid, &infop->si_pid, Efault); unsafe_put_user(info.uid, &infop->si_uid, Efault); unsafe_put_user(info.status, &infop->si_status, Efault); user_access_end(); return err; Efault: user_access_end(); return -EFAULT; }
缺少access_ok檢查意味著允許提供內核地址并將其作為waitid syscall的infop參數。syscall將使用unsafe_put_user覆蓋內核地址,因為此項操作可以逃避檢查。該原語的棘手部分在于無法對寫入內容(6個不同字段中的任何1個)施與足夠控制。info.status 是32位int,但被限制為0 < status < 256。info.pid可在某種程度上通過重復fork操作進行控制,但最大值為0x8000。
以下是漏洞利用階段將引用到的寫入字段概況。
struct siginfo { int si_signo; int si_errno; int si_code; int padding; // this remains unchanged by waitid int pid; // process id int uid; // user id int status; // return code }
該漏洞的特色在于可從Chrome瀏覽器沙箱內部實現提權。首先介紹Chrome沙箱概況與工作原理。
谷歌Chrome采用沙箱保護瀏覽器,即便成功利用漏洞實現代碼執(zhí)行也無法touch系統其它部分。沙箱分兩層:第一層通過改變user id與chroot限制資源訪問;第二層嘗試通過seccomp filter限制內核攻擊面,阻止沙箱進程中不必要的系統調用。通常情況下,Chrome沙箱行之有效,因為Linux內核漏洞多位于syscall,由seccomp沙箱攔截。
然而,waitid syscall在seccomp沙箱中普遍存在,當然也包括Chrome沙箱(chrome seccomp source)。也就是說,可以通過攻擊內核實現Chrome沙箱逃逸!
沙箱的局限性在于不允許使用fork,只能創(chuàng)建新線程而非進程。如果無法進行fork操作,waitid就會無法發(fā)揮作用,只能將0寫入內核內存。
所有困難都是暫時的,但無論采取哪種方式,都需要先獲取內核基地址。 unsafe_put_user的一個優(yōu)秀屬性是在訪問無效內存地址時不會崩潰,僅返回-EFAULT。因此,我們僅需猜測內核數據段潛在地址,直至顯示不同錯誤代碼、找到內核地址。有了內核地址就可以攻破KASLR了, 但注意不要覆蓋任何重要信息
我們可以用相同做法查找內核堆棧地址或內核內存其他區(qū)域。
現在,我想看看是否可以利用該漏洞突破所有防線。 結果發(fā)現目前能做的事情相當有限:
輾轉思考多種漏洞利用方法后確定了幾個方向:
我最終選取了第四個策略,進行堆噴射。
task_struct(代表每個進程和線程的結構)開始部分是一些flag,其中一個flag標記是否采用seccomp過濾器。如果能夠用task_structs進行堆噴射,并且只覆蓋那些起始flag,則可從其中一個進程移除seccomp,從而獲取更多可能。
考慮到Linux內核堆棧并非自身擅長領域,先噴射10000個線程,然后使用調試器檢查任務結構在堆棧中的位置。我注意到,噴射對象達到一定數量后,大部分任務結構將在堆棧較低地址處結束。這似乎意味著隨著空閑槽被用完,堆棧將向下擴展。
接下來的計劃是:
結果竟然奏效了!這種做法雖不可靠,但作為PoC已經足夠。我認為增大噴射力度能夠提升可靠程度。如果先噴灑其他對象填充,再創(chuàng)建10000個線程釋放,可以更加確定目標任務結構將位于堆棧底部。截至目前,我電腦上的運行結果已達到50%成功率,其余半數則以內核崩潰告終。
現在,我們面臨一項seccomp沙箱外圍任務,目前已從上一步獲知task_struct地址,仍需弄清如何利用內核漏洞升級到root權限并移除chroot。
好在原語已得到優(yōu)化,可以使用fork() 來創(chuàng)建子對象,然后使waitid寫入非零值。盡管如此,我們仍無法控制多數siginfo結構。唯一可用值是pid和status,兩者都存在一定限制。 pid最大值是0x8000,狀態(tài)是單字節(jié)。
但是,由于pid緊挨著一些未使用的填充(如前文所述),可以執(zhí)行5次寫入,每次都移回一個字節(jié),構造一個任意寫入的5字節(jié)。
5字節(jié)寫入的使用方法并非顯而易見,暫時仍無法創(chuàng)建任意地址。然而,我們可以創(chuàng)建外觀類似 0x**********000000的地址,其中*可以是任意值。
在此,我從ret2dir獲取靈感。有一段名為physmap的內核內存,其中內核保留一個映射到與用戶區(qū)內存具有相同物理內存的“alias”(虛擬地址)。因此,在用戶區(qū)創(chuàng)建一個填充0x41的頁面后,內核中確實存在一個可以找到與該頁面完全相同的網頁地址。
我的策略是在用戶區(qū)分配大量內存,然后嘗試隨機覆蓋內核physmap中的頁面,同時檢查用戶區(qū)頁面是否已經改變。如果發(fā)現變化,則說明我們已經找到了一個與用戶區(qū)地址相對應的內核虛擬地址,可以寫入用戶區(qū)并在內核內存中創(chuàng)建有效payload。我僅對內核physmap中以6個0結尾的頁面進行了嘗試,一旦找到“alias”,就可以構造一個指向內核地址的指針。
這部分內容非常可靠,但在罕見情況下也可以崩潰一個隨機過程。
現在我覆蓋task_struct中的files指針,使其指向內核中的“alias”,在用戶區(qū)構造一個偽造的files_struct對象,該對象也將位于alias.file對象,好處在于它們包含函數指針,即用來控制使用函數(如read,lseek,ioctl)的參數。通過將ioctl指向內核中的各種ROP小工具可以創(chuàng)建一個任意讀寫原語。于是,我修復了task_struct的clobbered部分,將creds結構改為root。最后,通過重置當前的fs移除chroot。現在我們已經完全實現沙箱逃逸,能夠以root身份彈出一個計算器了!
完整漏洞參見https://github.com/salls/kernel-exploits/blob/master/CVE-2017-5123/exploit_smap_bypass.c