背景介紹
Nftables
Nftables 是一個基于內核的包過濾框架,用于 Linux作業系統中的網路安全和防火墻功能,nftables的設計目標是提供一種更簡單、更靈活和更高效的方式來管理網路資料包的流量,
鉤子點(Hook Point)
鉤子點的作用是攔截資料包,然后對資料包進行修改,比較,丟棄和放行等操作,

// include/uapi/linux/netfilter_ipv4.h
?
#define NF_IP_PRE_ROUTING 0 /* After promisc drops, checksum checks. */
#define NF_IP_LOCAL_IN 1 /* If the packet is destined for this box. */
#define NF_IP_FORWARD 2 /* If the packet is destined for another interface. */
#define NF_IP_LOCAL_OUT 3 /* Packets coming from a local process. */
#define NF_IP_POST_ROUTING 4 /* Packets about to hit the wire. */
#define NF_IP_NUMHOOKS 5
Nftables的架構
Nftables由四部分組成
-
table(表):用于指定網路協議的型別,如ip,ip6,arp等
-
chains(鏈):用于指定流量的型別,如流入的流量或者是流出的流量并可以指定網路介面,如本地回環介面或者以太網介面等,
-
rules(規則):規則是用于過濾資料包所依據的規則,例如檢查協議、來源、目的地、埠等規則,
-
express(運算式):運算式則是具體的操作,
使用非常形象的圖描述,如下

運算式(express)
運算式是對一個資料包具體的操作,這里大致介紹后續需要用到的運算式,
nft_payload
nft_payload用于將資料包的值拷貝到暫存器中
struct nft_payload {
enum nft_payload_bases base:8;
u8 offset;
u8 len;
u8 dreg;
};
-
base:資料包型別
-
offset:資料包起始位置的偏移
-
len:拷貝的長度
-
dreg:目的暫存器
其中base的型別由enum nft_payload_bases指定
/* include/uapi/linux/netfilter/nf_tables.h */
/**
* enum nft_payload_bases - nf_tables payload expression offset bases
*
* @NFT_PAYLOAD_LL_HEADER: link layer header
* @NFT_PAYLOAD_NETWORK_HEADER: network header
* @NFT_PAYLOAD_TRANSPORT_HEADER: transport header
* @NFT_PAYLOAD_INNER_HEADER: inner header / payload
*/
enum nft_payload_bases {
NFT_PAYLOAD_LL_HEADER, //鏈路層
NFT_PAYLOAD_NETWORK_HEADER, //網路層
NFT_PAYLOAD_TRANSPORT_HEADER, //傳輸層
NFT_PAYLOAD_INNER_HEADER, //資料包內部
};
下面這個例子則是將傳輸層的包偏移16個位元組的位置,取出兩個位元組的內容存放到目的暫存器中,該暫存器的編號為2
base = NFT_PAYLOAD_TRANSPORT_HEADER
offset = 16 -> the checksum is 16 bytes away from the start of the TCP header
len = 2 -> the checksum is 2 bytes
dreg = NFT_REG32_02 (the small registers start frrom NFT_REG32_00)
nft_payload_set
nft_payload_set則是與nft_payload相反,該運算式是將指定暫存器的值存放到資料包里面
/* include/net/netfilter/nf_tables_core.h */
struct nft_payload_set {
enum nft_payload_bases base:8;
u8 offset;
u8 len;
u8 sreg;
u8 csum_type;
u8 csum_offset;
u8 csum_flags;
};
與nft_payload不同的是多了校驗和的可選選項
【----幫助網安學習,以下所有學習資料免費領!加vx:yj009991,備注 “博客園” 獲取!】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客戶端安全檢測指南(安卓+IOS)
nft_cmp_expr
nft_cmp_expr運算式則是用于比較,通常用于判斷資料包的埠號是否是需要符合要求,
struct nft_cmp_expr {
struct nft_data data;
u8 sreg;
u8 len;
enum nft_cmp_ops op:8;
};
-
data:用于設定比較的常量值
-
sreg:源暫存器,可以認為是資料包取出的內容
-
len:比較的長度
-
op:比較的操作,具體操作型別如下所示
<!-- -->
/**
* enum nft_cmp_ops - nf_tables relational operator
*
* @NFT_CMP_EQ: equal
* @NFT_CMP_NEQ: not equal
* @NFT_CMP_LT: less than
* @NFT_CMP_LTE: less than or equal to
* @NFT_CMP_GT: greater than
* @NFT_CMP_GTE: greater than or equal to
*/
enum nft_cmp_ops {
NFT_CMP_EQ,
NFT_CMP_NEQ,
NFT_CMP_LT,
NFT_CMP_LTE,
NFT_CMP_GT,
NFT_CMP_GTE,
};
nft_bitwise
nft_bitwise用于對資料包進行位元級別的操作,例如移位,掩碼設定等,
struct nft_bitwise {
u8 sreg;
u8 dreg;
enum nft_bitwise_ops op:8;
u8 len;
struct nft_data mask;
struct nft_data xor;
struct nft_data data;
};
-
sreg:源暫存器
-
dreg:目的暫存器,用于存放最后的結果
-
op:指定具體的位元操作,具體操作如下
<!-- -->
/**
* enum nft_bitwise_ops - nf_tables bitwise operations
*
* @NFT_BITWISE_BOOL: mask-and-xor operation used to implement NOT, AND, OR and
* XOR boolean operations
* @NFT_BITWISE_LSHIFT: left-shift operation
* @NFT_BITWISE_RSHIFT: right-shift operation
*/
enum nft_bitwise_ops {
NFT_BITWISE_BOOL,
NFT_BITWISE_LSHIFT,
NFT_BITWISE_RSHIFT,
};
-
mask:當op被指定為NFT_BITWISE_BOOL時,sreg的值會與mask中指定的值進行掩碼設定操作,并將結果存放到dreg中
-
xor:當op被指定為NFT_BITWISE_BOOL時,sreg的值會與xor中指定的值進行掩碼設定操作,并將結果存放到dreg中
-
data:當op被指定為NFT_BITWISE_LSHIFT或NFT_BITWISE_RSHIFT時,data需要被指定移位的數值,
暫存器(register)
在Nftables中是以暫存器作為存盤區,用于存放一段連續的記憶體,現在Nftables版本每個暫存器的值存放4位元組資料,而舊版的Nftables的每個暫存器是存放16個位元組的資料,為了保持兼容性,4位元組的寄存與16位元組的暫存器都被保留,暫存器的列舉值如下所示
enum nft_registers {
NFT_REG_VERDICT, //判定暫存器
NFT_REG_1,
NFT_REG_2,
NFT_REG_3,
NFT_REG_4,
__NFT_REG_MAX,
?
NFT_REG32_00 = 8,
NFT_REG32_01,
NFT_REG32_02,
...
NFT_REG32_13,
NFT_REG32_14,
NFT_REG32_15,
};

其中NFT_REG_VERDICT被稱之為判斷暫存器,這個暫存器比較特殊,是用于判定每個資料包需要怎么處理,判定的型別如下
-
NFT_CONTINUE:允許資料包通過防火墻
-
NFT_BREAK:跳過剩余的規則運算式
-
NF_DROP:直接丟棄資料包
-
NF_ACCEPT:接收資料包
-
NFT_GOTO:跳轉到其他鏈執行
-
NFT_JUMP:跳轉到其他鏈執行,若其他鏈將該資料包判定為NFT_CONTINUE則回傳當前鏈
libmnl與libnftnl
由于Nftables處于內核,需要從用戶層向內核發送訊息去設定需要攔截資料包的屬性,人工構造成本較大,因此使用現成的庫libmnl與libnftnl
環境搭建
環境版本
-
ubuntu 20.04
-
qemu-system-x86_64 4.2.1
-
Linux-5.17原始碼
設定編譯選項
cd /home/pwn/CVE/CVE-2022-1015/CVE-2022-1015/linux-5.17
sudo gedit .config
#將下列選項設定為y
CONFIG_NF_TABLES=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_USER_NS=y
CONFIG_E1000=y
CONFIG_E1000E=y
make -j32 bzImage #編譯
?
#安裝依賴庫
sudo apt-get install libmnl-dev
sudo apt-get install libnftnl-dev
漏洞驗證
若運行exp顯示超過邊界則代表沒有漏洞

若exp正常運行則代表漏洞

漏洞分析
原始碼分析
nft_parse_register_load
nft_cmp_expr:op=NFT_CMP_EQ sreg=8data=https://www.cnblogs.com/hetianlab/archive/2023/07/04/IPPROTO_TCP,該運算式是一個比較的運算式,用于比較下標為8的暫存器中的資料是否為TCP的協議,那么如何將下表為8的暫存器轉化為內核中暫存器的記憶體位置,則需要以來下面列舉的函式,
nft_parse_register_load函式就是將用戶設定的暫存器的下標轉化為內核暫存器的下標,然后存盤在源暫存器中,
File: net\netfilter\nf_tables_api.c
9325: int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
9326: {
9327: u32 reg;
9328: int err;
9329:
9330: reg = nft_parse_register(attr); //用于提取資料包中的暫存器的下標,并轉化為Nftables中暫存器的下標
9331: err = nft_validate_register_load(reg, len); //用于檢驗暫存器下表的合法性,漏洞點
9332: if (err < 0)
9333: return err;
9334:
9335: *sreg = reg; //然后將暫存器的下標值存盤在源暫存器中
9336: return 0;
9337: }
nft_parse_register
nft_parse_register函式用于將用戶設定的暫存器下標轉化為內核中暫存器的下標,
File: net\netfilter\nf_tables_api.c
9278: static unsigned int nft_parse_register(const struct nlattr *attr)
9279: {
9280: unsigned int reg;
9281:
9282: reg = ntohl(nla_get_be32(attr)); //提取資料包的暫存器下標,比如上述例子為8
9283: switch (reg) {
//0 - 4是16位元組暫存器
9284: case NFT_REG_VERDICT...NFT_REG_4:
9285: return reg * NFT_REG_SIZE / NFT_REG32_SIZE; //reg * 4
9286: default:
//由于4位元組暫存器起始下標為8,因此要減去起始下標
9287: return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00; // reg - 4
9288: }
9289: }
nft_validate_register_load
nft_validate_register_load函式則是用于校驗下標是否有問題,但是這個檢驗存在整型溢位的問題,reg是列舉值,而列舉通常會被編譯為int型別,len代表資料包的長度,
-
正常情況下:reg = 100,那么套入校驗則為100 * 4 + 0x10 = 0x1a0 >0x50,那么會檢驗出暫存器下標存在問題
-
漏洞情況:reg =0xffffffff(int情況下的最大值),那么逃入檢驗則為0xffffffff * 4 +0x10 =0x40000000c,由于int最大值為0xffffffff,那么最高4個位元會被舍棄,那么最后得到的值為0x0000000c,此時0xc< 0x50,就可以繞過檢驗,那么繞過檢驗后就會執行* sreg =reg,此時reg = 0xffffffff,就會導致*sreg = 0xff
<!-- -->
File: net\netfilter\nf_tables_api.c
9313: static int nft_validate_register_load(enum nft_registers reg, unsigned int len)
9314: {
9315: if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) // reg < 4則報錯
9316: return -EINVAL;
9317: if (len == 0) //長度為0則報錯
9318: return -EINVAL;
9319: if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) //reg * 4 + len > 0x50則報錯,存在整型溢位漏洞
9320: return -ERANGE;
9321:
9322: return 0;
9323: }
nft_do_chain
每一個被攔截的資料包都需要經過鏈上的運算式進行處理,而鏈處理的函式則為nft_do_chains,這個函式會提取出相應的運算式,最后呼叫expr_call_ops_eval函式進行處理,
File: net\netfilter\nf_tables_core.c
197: unsigned int
198: nft_do_chain(struct nft_pktinfo *pkt, void *priv)
199: {
...
224: for (; rule < last_rule; rule = nft_rule_next(rule)) {
225: nft_rule_dp_for_each_expr(expr, last, rule) {
226: if (expr->ops == &nft_cmp_fast_ops)
227: nft_cmp_fast_eval(expr, ®s);
228: else if (expr->ops == &nft_bitwise_fast_ops)
229: nft_bitwise_fast_eval(expr, ®s);
230: else if (expr->ops != &nft_payload_fast_ops ||
231: !nft_payload_fast_eval(expr, ®s, pkt))
232: expr_call_ops_eval(expr, ®s, pkt);
233:
234: if (regs.verdict.code != NFT_CONTINUE)
235: break;
236: }
...
expr_call_ops_eval
expr_call_ops_eval函式則是根據不同的運算式選擇不同的處理函式,例如若該資料包需要經過nft_payload的運算式處理,則會呼叫nft_payload_eval,
File: net\netfilter\nf_tables_core.c
161: static void expr_call_ops_eval(const struct nft_expr *expr,
162: struct nft_regs *regs,
163: struct nft_pktinfo *pkt)
164: {
165: #ifdef CONFIG_RETPOLINE
166: unsigned long e = (unsigned long)expr->ops->eval;
167: #define X(e, fun) \
168: do { if ((e) == (unsigned long)(fun)) \
169: return fun(expr, regs, pkt); } while (0)
170:
171: X(e, nft_payload_eval);
172: X(e, nft_cmp_eval);
173: X(e, nft_counter_eval);
174: X(e, nft_meta_get_eval);
175: X(e, nft_lookup_eval);
176: X(e, nft_range_eval);
177: X(e, nft_immediate_eval);
178: X(e, nft_byteorder_eval);
179: X(e, nft_dynset_eval);
180: X(e, nft_rt_get_eval);
181: X(e, nft_bitwise_eval);
182: #undef X
183: #endif /* CONFIG_RETPOLINE */
184: expr->ops->eval(expr, regs, pkt);
185: }
nft_payload_eval
這里可以看到regs存放在堆疊上面,dest這個變數值是通過®s->data[priv->dreg]取出來的,而priv->dreg則是通過上述的nft_parse_register_load函式進行提取的,那么這里就存在一個非常明顯的陣列越界的漏洞,
File: net\netfilter\nft_payload.c
121: void nft_payload_eval(const struct nft_expr *expr,
122: struct nft_regs *regs,
123: const struct nft_pktinfo *pkt)
124: {
125: const struct nft_payload *priv = nft_expr_priv(expr);
126: const struct sk_buff *skb = pkt->skb;
127: u32 *dest = ®s->data[priv->dreg];
...
165: if (skb_copy_bits(skb, offset, dest, priv->len) < 0) //拷貝資料
166: goto err;
167: return;
168: err:
169: regs->verdict.code = NFT_BREAK;
170: }
因此整型溢位結合越界就能夠使我們訪問到內核堆疊上的其他資料,如下圖所示,

漏洞利用
漏洞利用分析
現在我們擁有了訪問內核堆疊上其它地址的能力了,想要做到任意代碼執行則需要考慮下列幾種情況
-
由于回傳地址存在在堆疊上,需要判斷陣列越界是否能夠到達回傳地址的位置
-
如何通過陣列越界改寫回傳地址
-
由于需要進行任意代碼執行,那么需要用到內核函式,則需要得到內核的程式基地址才能夠根據函式偏移地址計算出函式的實際地址
由于運算式都會對暫存器空間進行操作,因此可以使用運算式對記憶體空間進行讀寫操作,
nft_bitwise運算式可以控制源暫存器和目的暫存器,那么采用nft_bitwise可以將源暫存器的內容放置到目的暫存器中,因此可以利用nft_bitwise進行越界讀,此時需要分析該陣列越界讀的邊界的大小是多少,這里需要注意的是由于len是sreg與dreg共同擁有的,為了dreg不越界,這里的長度最大值只能為0x40而不能為0xff,因為擁有16個暫存器,每個暫存器的值為4個位元組,因此16* 4 = 64 = 0x40
-
上界:(0xffffffff * 4) + 0x40 = 0x40000003c = 0x3c < 0x50 , 0xff* 4 = 0x3fc;由于可以拷貝0x40個位元組的長度,因此0x3fc + 0x40 =0x43c,
-
下界:(0xfffffff0 * 4 ) + 0x40 = 0x400000000 = 0x0 < 0x50, 0xf0 *4 = 0x3c0
內核地址泄露
接著查看regs偏移0x3c0處的地址資訊,結果發現在該片區域存在一個明顯的內核地址,因此若能將這個地址進行泄露,我們就能獲取內核的基地址,

回傳地址覆寫
由于需要構建的payload比較長,而我們如果利用nft_wise最多只能寫入0x43c -0x3c0 =0x7c的長度,是遠遠不夠的,因此對回傳地址進行覆寫時不能使用nft_bitwise,而得改用nft_payload,nft_payload需要dreg的下標以及修改的長度len,由于我們只需要考慮一個暫存器的值,因此該暫存器的長度最大可以達到0xff,因此我們可以在地址更低的位置去搜索有無可以覆寫的回傳地址,
可以發現在0x360的地址處也有一個內核的代碼段地址

并且可以發現該函式主要是處理udp包的發送

為了檢驗該地址是否能夠修改程式的執行流程,可以使用一個方法,將該地址的值修改為非法值并觀察內核是否會崩潰,這里將地址的內容修改為0x1122334455667788,接著運行程式,

可以看到內核報錯的資訊顯示RIP的地址為剛剛我們修改的地址,因此該地址可以作為被劫持程式執行流程的地址,

exp分析
現在我們已經具有了兩個利用條件
-
泄露內核的程式基地址
-
找到可以劫持程式執行流程的地址值
地址泄露
利用nft_bitwise泄露地址,這里注意的是在使用nft_bitwise泄露地址時,需要將data值設定為0,這樣就不會進行移位而導致我們的內核地址被修改存盤,最后將泄露的地址值放置在NFT_REG32_05下標的暫存器中

接著使用nft_set_payload將udp資料包的值修改為NFT_REG32_05暫存器的值,最后取出udp資料包的值,獲取內核程式地址值

回傳地址覆寫
利用nft_payload完成回傳地址的覆寫

在資料包中將payload填充進去,這里需要說明一下如何在內核中拿到shell權限
-
首先需要在內核中拿到root權限,需要呼叫commit_creds(prepare_kernel_cred(0))的內核函式獲取新的憑證結構,而該結構的uid = 0 ,gid =0即為root權限
-
其次需要切換命名空間,由于在普通用戶下是無法直接呼叫Nftables的,因為需要管理員的權限,因此在普通用戶下需要新開辟一個命名空間,使得該空間與正常的空間隔離,此時才能夠正常執行Nftales,那么如果逃逸這段命名空間則需要進行命名空間的切換,則依賴于switch_task_namespace函式,可以將命名空間切換為root的命名空間
-
最后則是實作從內核態切換到用戶態,由于我們是在內核空間拿到權限,而我們需要在用戶態執行,因此需要完成狀態的轉換,該狀態轉換依賴于swapgs_restore_regs函式

漏洞修復
補丁則是新增一條判斷條件,屬于4位元組暫存器的下標單獨處理,而不在16位元組暫存器以及4位元組暫存器的范圍內的下標都進行報錯處理

總結
Nftables堆疊溢位漏洞攻擊流程
-
首先利用nft_bitwise進行內核基地址的泄露,
-
其次是利用nft_payload改寫回傳地址,并將提權代碼注入進去,
-
最后等到代碼被觸發,
Nftables堆疊溢位漏洞利用的限制
-
不同的內核版本的內核堆疊布局幾乎不同,因此不同版本之間的利用手法相差較大,因此漏洞的利用十分依賴于內核版本,針對不同的版本需要做出針對性的漏洞利用的exp撰寫,差別存在于內核堆疊中存在的內核代碼段地址的偏移不同,例如有些內核代碼段地址偏移距離regs太大,導致無法利用漏洞進行泄露或者改寫,
更多網安技能的在線實操練習,請點擊這里>>
合天智匯:合天網路靶場、網安實戰虛擬環境
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/556601.html
標籤:其他
上一篇:分布式事務的幾種實作方式
下一篇:返回列表
