1. 程式人生 > 其它 >CVE-2017-5123 漏洞利用全攻略

CVE-2017-5123 漏洞利用全攻略

原文:https://salls.github.io/Linux-Kernel-CVE-2017-5123/

譯者:hello1900@知道創宇404實驗室

本文介紹如何利用Linux核心漏洞CVE-2017-5123提升許可權,突破SEMP、SMAP、Chrome沙箱全方位保護。

背景

在系統呼叫處理階段,核心需要具備讀取和寫入觸發系統呼叫程序記憶體的能力。為此,核心設有copy_from_userput_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_userunsafe_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結構。唯一可用值是pidstatus,兩者都存在一定限制。 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物件,好處在於它們包含函式指標,即用來控制使用函式(如readlseekioctl)的引數。通過將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安全團隊對我的漏洞報告給予快速響應!