CVE-2017-5123 漏洞利用全攻略
原文:https://salls.github.io/Linux-Kernel-CVE-2017-5123/
譯者:hello1900@知道創宇404實驗室
本文介紹如何利用Linux核心漏洞CVE-2017-5123提升許可權,突破SEMP、SMAP、Chrome沙箱全方位保護。
背景
在系統呼叫處理階段,核心需要具備讀取和寫入觸發系統呼叫程序記憶體的能力。為此,核心設有copy_from_user
與put_user
等特殊函式,用於將資料複製進出使用者區。在較高級別,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是否位於使用者區而非核心記憶體。如果檢查通過,user_access_begin()
呼叫禁用SMAP,允許核心訪問使用者區。核心寫入記憶體後重新啟用SMAP。需要注意的一點是:這些使用者訪問函式在記憶體讀寫過程中處理頁面錯誤,在訪問未對映記憶體時不會導致崩潰。
漏洞
某些系統呼叫要求多次呼叫put/get_user
以實現核心與使用者區之間的資料複製。為避免重複檢查和SMAP啟用/禁用的額外開銷,核心開發人員將缺少必要檢查的不安全版本_put_user
與unsafe_put_user
涵蓋進來。這樣一來,忘記額外檢查就在意料之中了。CVE-2017-5123就是一個很好的例子。在核心版本4.13中,為了能夠正常使用unsafe_put_user
waitid
syscall進行了更新,但access_ok
檢查仍處於缺失狀態。漏洞程式碼如下所示。
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沙箱概況與工作原理。
谷歌Chrome採用沙箱保護瀏覽器,即便成功利用漏洞實現程式碼執行也無法touch系統其它部分。沙箱分兩層:第一層通過改變user id與chroot限制資源訪問;第二層嘗試通過seccomp filter限制核心攻擊面,阻止沙箱程序中不必要的系統呼叫。通常情況下,Chrome沙箱行之有效,因為Linux核心漏洞多位於syscall,由seccomp沙箱攔截。
然而,waitid syscall在seccomp沙箱中普遍存在,當然也包括Chrome沙箱(chrome seccomp source)。也就是說,可以通過攻擊核心實現Chrome沙箱逃逸!
沙箱的侷限性在於不允許使用fork,只能建立新執行緒而非程序。如果無法進行fork操作,waitid就會無法發揮作用,只能將0寫入核心記憶體。
喘口氣,進行 infoleak
所有困難都是暫時的,但無論採取哪種方式,都需要先獲取核心基地址。 unsafe_put_user
的一個優秀屬性是在訪問無效記憶體地址時不會崩潰,僅返回-EFAULT
。因此,我們僅需猜測核心資料段潛在地址,直至顯示不同錯誤程式碼、找到核心地址。有了核心地址就可以攻破KASLR了, 但注意不要覆蓋任何重要資訊 :)
我們可以用相同做法查詢核心堆疊地址或核心記憶體其他區域。
SMAP繞過存在的侷限性
現在,我想看看是否可以利用該漏洞突破所有防線。 結果發現目前能做的事情相當有限:
- 只能寫0;
- 寫24個位元組的0,破壞附近記憶體;
- 少量資訊滲出,包括核心基地址與堆疊位置,但不包括堆疊目標位置。
輾轉思考多種漏洞利用方法後確定了幾個方向:
- 在核心資料段找到一個物件,其索引/大小/值為零將導致超出記憶體訪問邊界;
- 在核心中覆蓋一個自旋鎖,用來建立競爭條件;
- 嘗試覆蓋核心堆疊上的基址指標或其他值;
- 觸發可能導致在核心堆疊上建立有用結構的操作,看看是否可以用任意寫入的0命中物件。
我最終選取了第四個策略,進行堆噴射。
堆噴射
task_struct
(代表每個程序和執行緒的結構)開始部分是一些flag,其中一個flag標記是否採用seccomp過濾器。如果能夠用task_structs進行堆噴射,並且只覆蓋那些起始flag,則可從其中一個程序移除seccomp,從而獲取更多可能。
考慮到Linux核心堆疊並非自身擅長領域,先噴射10000個執行緒,然後使用偵錯程式檢查任務結構在堆疊中的位置。我注意到,噴射物件達到一定數量後,大部分任務結構將在堆疊較低地址處結束。這似乎意味著隨著空閒槽被用完,堆疊將向下擴充套件。
接下來的計劃是:
- 建立10000個執行緒;
- 從堆疊最低地址起繼續猜測任務結構潛在地址;
- 讓10000個執行緒繼續自檢是否仍位於seccomp沙箱;
- 當發現某個執行緒不再受seccomp影響時停止。
結果竟然奏效了!這種做法雖不可靠,但作為PoC已經足夠。我認為增大噴射力度能夠提升可靠程度。如果先噴灑其他物件填充,再建立10000個執行緒釋放,可以更加確定目標任務結構將位於堆疊底部。截至目前,我電腦上的執行結果已達到50%成功率,其餘半數則以核心崩潰告終。
獲得更佳“任意”寫入效果
現在,我們面臨一項seccomp沙箱外圍任務,目前已從上一步獲知task_struct
地址,仍需弄清如何利用核心漏洞升級到root許可權並移除chroot。
好在原語已得到優化,可以使用fork() 來建立子物件,然後使waitid
寫入非零值。儘管如此,我們仍無法控制多數siginfo
結構。唯一可用值是pid
和status
,兩者都存在一定限制。 pid
最大值是0x8000,狀態是單位元組。
但是,由於pid緊挨著一些未使用的填充(如前文所述),可以執行5次寫入,每次都移回一個位元組,構造一個任意寫入的5位元組。
5位元組寫入+ Physmap
5位元組寫入的使用方法並非顯而易見,暫時仍無法建立任意地址。然而,我們可以建立外觀類似
0x**********000000
的地址,其中*
可以是任意值。
在此,我從ret2dir獲取靈感。有一段名為physmap的核心記憶體,其中核心保留一個對映到與使用者區記憶體具有相同實體記憶體的“alias”(虛擬地址)。因此,在使用者區建立一個填充0x41的頁面後,核心中確實存在一個可以找到與該頁面完全相同的網頁地址。
我的策略是在使用者區分配大量記憶體,然後嘗試隨機覆蓋核心physmap中的頁面,同時檢查使用者區頁面是否已經改變。如果發現變化,則說明我們已經找到了一個與使用者區地址相對應的核心虛擬地址,可以寫入使用者區並在核心記憶體中建立有效payload。我僅對核心physmap中以6個0結尾的頁面進行了嘗試,一旦找到“alias”,就可以構造一個指向核心地址的指標。
這部分內容非常可靠,但在罕見情況下也可以崩潰一個隨機過程。
真正的任意讀/寫和Root操作!
現在我覆蓋task_struct中的files
指標,使其指向核心中的“alias”,在使用者區構造一個偽造的files_struct
物件,該物件也將位於alias
.file
物件,好處在於它們包含函式指標,即用來控制使用函式(如read
,lseek
,ioctl
)的引數。通過將ioctl
指向核心中的各種ROP小工具可以建立一個任意讀寫原語。於是,我修復了task_struct的clobbered部分,將creds結構改為root。最後,通過重置當前的fs移除chroot。現在我們已經完全實現沙箱逃逸,能夠以root身份彈出一個計算器了!
完整漏洞參見https://github.com/salls/kernel-exploits/blob/master/CVE-2017-5123/exploit_smap_bypass.c
最後,感謝Chrome/Chromium安全團隊對我的漏洞報告給予快速響應!