CVE-2023-0179提權利用

前言

CVE-2023-0179-Nftables整型溢出中,分析了漏洞的成因,接下來分析漏洞的利用。

漏洞利用

根據漏洞成因可以知道,payload_eval_copy_vlan函數存在整型溢出,導致我們將vlan頭部結構拷貝到寄存器(NFT_REG32_00-NFT_REG32_15),而該變量時存在與棧上的,因此可以覆蓋棧上的其餘變量的。

image-20231120215905836

可以發現regs變量是無法覆蓋到返回地址。

image-20231120222728305

因此我們需要觀察源碼,jumpstack變量是在regs變量下方

image-20231120223552586

我們可以通過溢出regs變量覆蓋到jumpstack變量。

image-20231120224114239

那麼接下來需要觀察一下nft_jumpstack結構體中存在哪些變量

【----幫助網安學習,以下所有學習資料免費領!加vx:yj009991,備註 “博客園” 獲取!】

 ① 網安學習成長路徑思維導圖
 ② 60+網安經典常用工具包
 ③ 100+SRC漏洞分析報告
 ④ 150+網安攻防實戰技術電子書
 ⑤ 最權威CISSP 認證考試指南+題庫
 ⑥ 超1800頁CTF實戰技巧手冊
 ⑦ 最新網安大廠面試題合集(含答案)
 ⑧ APP客戶端安全檢測指南(安卓+IOS)

struct nft_jumpstack {
    const struct nft_chain *chain;
    const struct nft_rule_dp *rule;
    const struct nft_rule_dp *last_rule;
};
  • chain:用於指定在哪個流程進行hook

  • rule:以什麼樣的規則處理數據包

  • last_rule:規則可能不止一條,因此last_rule用於指向最後一條規則

nft_jumpstack結構體在nft_do_chain函數的作用如下,當狀態寄存器被設置爲JUMP條件時,意味着需要跳轉到其他chain進行處理,因此需要先保存當前chain的狀態,這裏與函數調用時保存棧時的處理一樣,估計因此才命名爲jumpstack。並且使用一個全局變量stackptr用於確定保存的chain的先後順序。在保存完之後,就跳轉到目的chain,目的chain則是存儲在regs.verdict.chain中。

    ...
    switch (regs.verdict.code) {
    case NFT_JUMP:
        if (WARN_ON_ONCE(stackptr >= NFT_JUMP_STACK_SIZE))
            return NF_DROP;
        jumpstack[stackptr].chain = chain;
        jumpstack[stackptr].rule = nft_rule_next(rule);
        jumpstack[stackptr].last_rule = last_rule;
        stackptr++;
    case NFT_GOTO:
        chain = regs.verdict.chain;
        goto do_chain;
    ...

還原chain的過程如下,通過遞減stackptr來取出存儲在jumpstack變量中存儲的chainrulelastrule,然後就會跳轉到next_rule對還原的rule,進行rule的解析,這裏需要注意的是在遍歷rule的時候,循環是通過rule < last_rule進行遍歷的,因此我們在後續僞造last_rule的時候需要大於rule,否則是無法進入循環內部的。

    next_rule:
        regs.verdict.code = NFT_CONTINUE;
        for (; rule < last_rule; rule = nft_rule_next(rule)) {
            nft_rule_dp_for_each_expr(expr, last, rule) {
                if (expr->ops == &nft_cmp_fast_ops)
                    nft_cmp_fast_eval(expr, &regs);
                else if (expr->ops == &nft_cmp16_fast_ops)
                    nft_cmp16_fast_eval(expr, &regs);
                else if (expr->ops == &nft_bitwise_fast_ops)
                    nft_bitwise_fast_eval(expr, &regs);
                else if (expr->ops != &nft_payload_fast_ops ||
                     !nft_payload_fast_eval(expr, &regs, pkt))
                    expr_call_ops_eval(expr, &regs, pkt);
​
                if (regs.verdict.code != NFT_CONTINUE)
                    break;
            }
    ...
    if (stackptr > 0) {
            stackptr--;
            chain = jumpstack[stackptr].chain;
            rule = jumpstack[stackptr].rule;
            last_rule = jumpstack[stackptr].last_rule;
            goto next_rule;
        }
    ...

緊接着來看一下nft_rule_dp結構體,可以發現第一個八個字節是一些標誌位組成的,而後續的八個字節則是用於存儲nft_expr結構體的指針。

struct nft_rule_dp {
    u64             is_last:1,
                    dlen:12,
                    handle:42;  /* for tracing */
    unsigned char           data[]
        __attribute__((aligned(__alignof__(struct nft_expr))));
};

然後可以看到nft_expr結構體裏存儲了函數指針,如果我們能夠篡改該函數指針就可以劫持程序流程。

struct nft_expr {
    const struct nft_expr_ops   *ops;
    unsigned char           data[]
        __attribute__((aligned(__alignof__(u64))));
};

然後在這篇文章https://www.ctfiot.com/100156.html學習到了一個小技巧。使用ptype /o struct xxx就可以看到具體的結構體信息與偏移。

image-20231121105211637

因此構造的流程如下,首先我們通過漏洞溢出到nft_jumpstack結構體,並且修改rule變量爲可控內容的地址同時需要將lastrule的值篡改爲比rule更大的值,原因上述已經說過。緊接着在可控內容中僞造一個nft_rule_dp結構體,第一個八字節是填充位,而第二個八字節是需要僞造的函數表指針,同樣的我們也將該指針篡改爲可控內容的地址,然後再該地址處僞造nft_expr,並且將ops變量指向我們想要執行的函數即可。

image-20231121110958178

通過上述分析已經知道了該如何通過漏洞完成程序流程的劫持,接下來需要分析如果僞造上述幾個結構體。

首先在nft_payload_copy_vlan函數中,漏洞點是將vlan頭的數據拷貝到指定的寄存器裏面,而vlan頭的地址是低於寄存器的地址,這就會導致在拷貝完vlan頭後會將寄存器中的值也進行拷貝的操作,而寄存器的值我們是能人爲控制的,因此就可以完成僞造的操作。

image-20231121112915289

可以看到我們對NFT_REG32_00的賦值會覆蓋到jumpstack[7].rule的值,完成了對jumpstack結構體的篡改,這裏我們可以通過NFT_REG32_00 - NFT_REG32_15進行賦值,緊接着查看jumpstack哪個值是被賦值。就可以知道哪個jumpstack可以被篡改。

image-20231121113839312

由於我們可以控制regs變量的值,我們可以首先泄露regs的地址,然後在regs上僞造rule即可。然後expr重新指向爲jumpstack即可,這裏採用了一個小技巧就是將last_rule設置爲一個函數地址,由於函數地址的值是大於regs變量的地址值的,因此我們可以節約八個字節。

image-20231121115000312

但是這裏有個問題就是我們只能控制八個字節的函數指針,因此是無法構造一個完整的ROP鏈的,而內核並不存在像用戶態下有one_gadget可以只利用八個字節就能完成利用,因此在這裏必須使用棧遷移,遷移的目的是一段可以控制的內存,那麼這裏選用的目的自然就是regs了。那麼該如何找棧遷移的gadget呢?,這裏我首先採用的使用利用vmlinux-to-elfbzImage的符號表提取出來,然後尋找對應的gadgetgadget類型如下

  • mov rsp,xxx

  • push xxx;pop rsp

  • add rsp,xxx

  • xchg rsp,xxx

上述指令都可以修改rsp寄存器,完成棧遷移的效果。

首先通過vmlinux-to-elf ./bzImage ./vmlinux去提取出符號表

image-20231121115815899

然後通過ropper進行gadget的提取,ropper --file ./vmlinux --nocolor > g

最後這在搜索gadgetcat g | grep 'add rsp.*ret',但是通過嘗試發現下述的地址都沒辦法使用,因爲下述地址都不具備可執行的權限。

image-20231121120045631

然後嘗試了搜索上述所有的gadget,我都沒有找到可以用的gadget,唯一比較接近的gadgetpop rsi的,但是無法控制rsi的寄存器,其實這裏一開始我使用的鏡像是自己編譯的,這裏搜索的gadget是需要控制rdi寄存器的,經過多次嘗試無果後才使用了作者的config文件重新編譯發現還是不可行。

image-20231121120243094

其實我們在編譯內核文件時是存在vmlinux文件的,但是那個文件十分的大,使用ropper工具無法分析,就在我準備放棄的時候,想到使用objdump工具進行gadget的提取

使用objdump -d -M intel vmlinux > ./gadget.txt

  • -ddump代碼

  • -M是指定彙編代碼的格式

objdump提取的速度非常快,提取代碼如下,但是它沒有ropper搜索gadget那麼方便,但是會全的多

image-20231121120811426

這裏我首先嚐試了搜索棧遷移的gadgetcat gadget.txt | grep -E 'add rsp.*'

image-20231121135057224

可以發現有非常多的匹配的gadget,接着我們在gdb中驗證可以使用的gadget,通常在棧進行還原的時候會用到add rsp,xxx,因此都是有效的gadget,然後就是計算棧頂與resg函數地址的差值找到相應的棧遷移gadget即可。

image-20231121135250852

接下就是考慮如何進行提權的利用了,雖然我們可以控制regs但是可控的範圍也只有0x40是不足於採用commit_creds(prepare_kernel_cred(0))設置root憑證然後返回到用戶空間執行後門的。那麼相當的一個辦法就是通過覆蓋modprobe_path進行提權。這裏我找了下列gadget進行modprobe_path的覆蓋,將rdi設置爲modprobe_pathrax設置爲覆蓋後的路徑即可。

        0xffffffff810d1e6b: mov qword ptr [rdi], rax; ret; 
        0xffffffff81004165: pop rdi; pop rbp; ret

最後就是覆蓋完modprobe_path該如何返回到用戶態,因爲modprobe_path的提權需要在用戶態下執行非法文件頭的文件,這裏作者採用的是將棧還原,通過在rbp中的地址值覆蓋會rsp中即可,採用下述gadget

        0xffffffff810b47f0: mov rsp, rbp; pop rbp; ret;

image-20231121141008656

但是在我的環境下直接返回不行,這是因爲在返回到nf_hook_slow函數時,有對狀態碼的一個檢驗,而在上述覆蓋modprobe_path時,我們設置了rax值,就導致無法將狀態碼設置成合法值。那分支就會跳轉到default,導致報錯。在嘗試搜索了gadget之後,可以將rax設置爲0,但是這回進入到NF_DROP分支 中,但是此時skb變量也被我們破壞了,無法正常執行。

int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
         const struct nf_hook_entries *e, unsigned int s)
{
    unsigned int verdict;
    int ret;
​
    for (; s < e->num_hook_entries; s++) {
        verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
        switch (verdict & NF_VERDICT_MASK) {
        case NF_ACCEPT:
            break;
        case NF_DROP:
            kfree_skb_reason(skb,
                     SKB_DROP_REASON_NETFILTER_DROP);
            ret = NF_DROP_GETERR(verdict);
            if (ret == 0)
                ret = -EPERM;
            return ret;
        case NF_QUEUE:
            ret = nf_queue(skb, state, s, verdict);
            if (ret == 1)
                continue;
            return ret;
        default:
            /* Implicit handling for NF_STOLEN, as well as any other
             * non conventional verdicts.
             */
            return 0;
        }
    }
​
    return 1;
}

在嘗試很久之後,最終放棄正常返回的這個選項,然後我在rbp中搜索是否有合適的返回地址。最後在rbp中我找到了一個do_softirq函數

image-20231121142144155

該函數是一個軟中斷處理的函數,當時我就猜想,如果這個函數返回了,應該不會影響程序的執行。

image-20231121142513768

嘗試運行之後,發現還是有內核異常,頓時有點失望。

image-20231121142943996

但是在操控命令行的時候是能夠正常輸入命令的,說明我們成功返回到用戶態了。

image-20231121143107629

最後就是查看是否將新用戶寫入到/etc/passwd中了,最終完成寫入。完結撒花!。

image-20231121143252463

完整exp可以參考

https://github.com/h0pe-ay/Vulnerability-Reproduction/blob/master/CVE-2023-0179(nftables)/poc.c

更多網安技能的在線實操練習,請點擊這裏>>

  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章