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