前言
在CVE-2023-0179-Nftables整型溢出中,分析了漏洞的成因,接下來分析漏洞的利用。
漏洞利用
根據漏洞成因可以知道,payload_eval_copy_vlan
函數存在整型溢出,導致我們將vlan
頭部結構拷貝到寄存器(NFT_REG32_00-NFT_REG32_15
),而該變量時存在與棧上的,因此可以覆蓋棧上的其餘變量的。
可以發現regs
變量是無法覆蓋到返回地址。
因此我們需要觀察源碼,jumpstack
變量是在regs
變量下方
我們可以通過溢出regs
變量覆蓋到jumpstack
變量。
那麼接下來需要觀察一下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
變量中存儲的chain
、rule
、lastrule
,然後就會跳轉到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, ®s);
else if (expr->ops == &nft_cmp16_fast_ops)
nft_cmp16_fast_eval(expr, ®s);
else if (expr->ops == &nft_bitwise_fast_ops)
nft_bitwise_fast_eval(expr, ®s);
else if (expr->ops != &nft_payload_fast_ops ||
!nft_payload_fast_eval(expr, ®s, pkt))
expr_call_ops_eval(expr, ®s, 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
就可以看到具體的結構體信息與偏移。
因此構造的流程如下,首先我們通過漏洞溢出到nft_jumpstack
結構體,並且修改rule
變量爲可控內容的地址同時需要將lastrule
的值篡改爲比rule
更大的值,原因上述已經說過。緊接着在可控內容中僞造一個nft_rule_dp
結構體,第一個八字節是填充位,而第二個八字節是需要僞造的函數表指針,同樣的我們也將該指針篡改爲可控內容的地址,然後再該地址處僞造nft_expr
,並且將ops
變量指向我們想要執行的函數即可。
通過上述分析已經知道了該如何通過漏洞完成程序流程的劫持,接下來需要分析如果僞造上述幾個結構體。
首先在nft_payload_copy_vlan
函數中,漏洞點是將vlan
頭的數據拷貝到指定的寄存器裏面,而vlan
頭的地址是低於寄存器的地址,這就會導致在拷貝完vlan
頭後會將寄存器中的值也進行拷貝的操作,而寄存器的值我們是能人爲控制的,因此就可以完成僞造的操作。
可以看到我們對NFT_REG32_00
的賦值會覆蓋到jumpstack[7].rule
的值,完成了對jumpstack
結構體的篡改,這裏我們可以通過NFT_REG32_00 - NFT_REG32_15
進行賦值,緊接着查看jumpstack
哪個值是被賦值。就可以知道哪個jumpstack
可以被篡改。
由於我們可以控制regs
變量的值,我們可以首先泄露regs
的地址,然後在regs
上僞造rule
即可。然後expr
重新指向爲jumpstack
即可,這裏採用了一個小技巧就是將last_rule
設置爲一個函數地址,由於函數地址的值是大於regs
變量的地址值的,因此我們可以節約八個字節。
但是這裏有個問題就是我們只能控制八個字節的函數指針,因此是無法構造一個完整的ROP
鏈的,而內核並不存在像用戶態下有one_gadget
可以只利用八個字節就能完成利用,因此在這裏必須使用棧遷移,遷移的目的是一段可以控制的內存,那麼這裏選用的目的自然就是regs
了。那麼該如何找棧遷移的gadget
呢?,這裏我首先採用的使用利用vmlinux-to-elf
將bzImage
的符號表提取出來,然後尋找對應的gadget
,gadget
類型如下
-
mov rsp,xxx
-
push xxx;pop rsp
-
add rsp,xxx
-
xchg rsp,xxx
上述指令都可以修改rsp
寄存器,完成棧遷移的效果。
首先通過vmlinux-to-elf ./bzImage ./vmlinux
去提取出符號表
然後通過ropper
進行gadget
的提取,ropper --file ./vmlinux --nocolor > g
最後這在搜索gadget
,cat g | grep 'add rsp.*ret'
,但是通過嘗試發現下述的地址都沒辦法使用,因爲下述地址都不具備可執行的權限。
然後嘗試了搜索上述所有的gadget
,我都沒有找到可以用的gadget
,唯一比較接近的gadget
是pop rsi
的,但是無法控制rsi
的寄存器,其實這裏一開始我使用的鏡像是自己編譯的,這裏搜索的gadget
是需要控制rdi
寄存器的,經過多次嘗試無果後才使用了作者的config
文件重新編譯發現還是不可行。
其實我們在編譯內核文件時是存在vmlinux
文件的,但是那個文件十分的大,使用ropper
工具無法分析,就在我準備放棄的時候,想到使用objdump
工具進行gadget
的提取
使用objdump -d -M intel vmlinux > ./gadget.txt
-
-d
是dump
代碼 -
-M
是指定彙編代碼的格式
objdump
提取的速度非常快,提取代碼如下,但是它沒有ropper
搜索gadget
那麼方便,但是會全的多
這裏我首先嚐試了搜索棧遷移的gadget
,cat gadget.txt | grep -E 'add rsp.*'
可以發現有非常多的匹配的gadget
,接着我們在gdb
中驗證可以使用的gadget
,通常在棧進行還原的時候會用到add rsp,xxx
,因此都是有效的gadget
,然後就是計算棧頂與resg
函數地址的差值找到相應的棧遷移gadget
即可。
接下就是考慮如何進行提權的利用了,雖然我們可以控制regs
但是可控的範圍也只有0x40
是不足於採用commit_creds(prepare_kernel_cred(0))
設置root
憑證然後返回到用戶空間執行後門的。那麼相當的一個辦法就是通過覆蓋modprobe_path
進行提權。這裏我找了下列gadget
進行modprobe_path
的覆蓋,將rdi
設置爲modprobe_path
,rax
設置爲覆蓋後的路徑即可。
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;
但是在我的環境下直接返回不行,這是因爲在返回到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
函數
該函數是一個軟中斷處理的函數,當時我就猜想,如果這個函數返回了,應該不會影響程序的執行。
嘗試運行之後,發現還是有內核異常,頓時有點失望。
但是在操控命令行的時候是能夠正常輸入命令的,說明我們成功返回到用戶態了。
最後就是查看是否將新用戶寫入到/etc/passwd
中了,最終完成寫入。完結撒花!。
完整exp可以參考
https://github.com/h0pe-ay/Vulnerability-Reproduction/blob/master/CVE-2023-0179(nftables)/poc.c
更多網安技能的在線實操練習,請點擊這裏>>