前言
Netfilter是一個用於Linux操作系統的網絡數據包過濾框架,它提供了一種靈活的方式來管理網絡數據包的流動。Netfilter允許系統管理員和開發人員控制數據包在Linux內核中的處理方式,以實現網絡安全、網絡地址轉換(Network Address Translation,NAT)、數據包過濾等功能。
漏洞成因
在netfilter
中存在這nft_byteorder_eval
函數,該函數的作用是將寄存器中的數據以主機序或網絡序存儲。具體代碼如下,若採用的操作是NFT_BYTEORDER_NTOH
則是將數據從主機序轉化爲網絡序,而NFT_BYTEORDER_HTON
則是從網絡序轉換爲主機序。具體轉換多少個字節則是用priv->size
指定的,在該操作下可以轉換二、四、八字節。該漏洞也是由於在對兩字節數據進行大小端序轉存時出現了錯誤所導致的。
可以看到代碼【1】中使用了聯合體存儲了源地址和目的地址,聯合體的變量分別是u32
與u16
分別代表的是四字節與兩字節的空間大小。然後在代碼【2】與【3】處源地址是直接取出u16
的變量存儲到目的地址的u16
變量中。
乍一看似乎很符合常理,因爲在處理雙字節的時候,聯合體中的變量就以u16
存儲,若處理四字節就轉化爲u32
存儲,但是這裏存在個問題,在C語言中,聯合體的存儲空間是以最大空間爲標準,換句話說無論聯合體取出的變量是u16
還是u32
,聯合體的大小都是佔用四個字節的,而不會出現雙字節的情況,因此在對s
與d
兩個聯合體進行遍歷時,會以四字節爲單位找到下一個位置。但是在計算長度時是以雙字節進行計算的,因此就會導致拷貝時發生溢出。
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;
}
按照設想的情況,在使用雙字節變量進行遍歷的時候會以雙字節爲單位進行遍歷,但是實際的情況如下圖。可以發現即使每次賦值都是對雙字節的變量進行賦值,但是再遍歷的時候還是按照聯合體中最大的存儲空間(四字節)進行遍歷的。
因此漏洞的成因如下,因此在使用nft_byteorder
函數轉換雙字節的大小端序時溢出。
模塊地址泄露
在nft_byteorder_eval
函數內部,溢出的地址是在寄存器下方。因此可以通過控制寄存器的下標值選擇需要泄露的地址。
在此需要觀察通過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
的變量。因此每次泄露只能獲取低兩字節的值。因此需要尋找其他方法進行地址的泄露。
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
變量
結構體nft_jumpstack
的構成如下,由chain
、rule
、last_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, ®s);
259: else if (expr->ops == &nft_cmp16_fast_ops)
260: nft_cmp16_fast_eval(expr, ®s);
261: else if (expr->ops == &nft_bitwise_fast_ops)
262: nft_bitwise_fast_eval(expr, ®s);
263: else if (expr->ops != &nft_payload_fast_ops ||
264: !nft_payload_fast_eval(expr, ®s, pkt))
265: expr_call_ops_eval(expr, ®s, 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->trace
或info->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
操作可以首先可以將原先的chain
、rule
以及last_rule
的地址泄露,但是隻能泄露四字節。
由於我們需要找到符合上述條件的rule
,並且我們只有rule
的最低兩個字節,因此搜索範圍不大,因此需要在泄露的rule_low
附近尋找一個合適的模塊地址。在存儲泄露的rule
之前存儲利用immediate
以及meta_set
操作,我們選擇其中一個進行泄露即可。
僞造的方式也比較簡單,由於is_last
與dlen
都需要設置爲0,因此我們只需要找到兩個字節爲0的值,作爲僞造的rule
即可,僞造的rule
如下。
修改後的結果如下
由於handle
實際是佔用42比特,但是有3個比特被設置爲0了,因此實際泄露的值只有39比特,但是由於模塊地址的高4個字節都是固定的0xffffffff
,因此不影響模塊地址的泄露。通過從數據包中提取數據得到handle
的值爲後,簡單移位操作就可以還原。
module = ((leak << 13) >> 16);
最後泄露模塊基地址成功。
總結
總結一下模塊基地址的泄露流程
1. 構造基礎鏈
設置NFT_JUMP
表達式
通過meta
設置爲NFT_META_NFTRACE
2. 泄露鏈
byteorder
表達式觸發漏洞,第一次讀chain
、rule
以及last_rule
,第二次改寫爲chain
,fake rule
以及fake last_rule
dynset
表達式泄露chain
、rule
、last_rule
3. 訂閱NFNLGRP_NFTRACE
組,接收數據包
4. 後續接着分享如何繞過kaslr
以及最終提權的利用。
原版exp使用go
語言寫的,我使用c語言重寫了一版。
完整exp
:https://github.com/h0pe-ay/Vulnerability-Reproduction/tree/master/CVE-2023-35001(nftables)(c語言)
更多網安技能的在線實操練習,請點擊這裏>>