CVE-2023-35001-Nftables数组越界

本文首发于合天网安

前言

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两个联合体进行遍历时,会以四字节为单位找到下一个位置。但是在计算长度时是以双字节进行计算的,因此就会导致拷贝时发生溢出。

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: 【1union { 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进行赋值。

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;
}

按照设想的情况,在使用双字节变量进行遍历的时候会以双字节为单位进行遍历,但是实际的情况如下图。可以发现即使每次赋值都是对双字节的变量进行赋值,但是再遍历的时候还是按照联合体中最大的存储空间(四字节)进行遍历的。

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)范围内都是可以访问到的。但是现在存在一个问题,虽然我们可以越界访问,但是每次只能获取四字节中的低两个字节。

1
2
3
4
...
75: for (i = 0; i < priv->len / 2; i++)
76: 【2】d[i].u16 = ntohs((__force __be16)s[i].u16);//将源地址的数据拷贝到目的地址的低16位中
...

将下列值传参给nft_byteorder_eval函数

1
2
3
4
5
6
7
8
/*
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
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
/*函数递归
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

regs变量的下方存在jumpstack变量

image-20240226153618757

结构体nft_jumpstack的构成如下,由chainrulelast_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; /* for tracing */
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)) { //遍历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就可以直接跳出循环。

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->traceinfo->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操作可以首先可以将原先的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的值为后,简单移位操作就可以还原。

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

最后泄露模块基地址成功。

image-20240226192449806

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;
};

具体的布局如下

image-20240227185357673

可以看到data_fromdata_to都是从用户态中传递过去的数据,因此我们可以在这些区域内伪造表达式,这有点像在CTF中,我们泄露了堆块基址后,随意伪造堆块。

由于我们有0x28字节的空间,但是实际能够操作的空间是data_fromdata_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操作后还需要继续执行其他规则表达式。

image-20240227190243722

但是其他表达式都是需要大于0x8的,毕竟一个操作指针就占用八字节了,此时作者选用了meta表达式。可以看到meta表达式也只用到了三个字节,刚好对应range表达式的sreglen以及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 = &regs->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表达式。由于我们直接伪造了规则表达式,因此不会进行寄存器参数的校验,只要选择内核地址选择泄露即可。

image-20240227191748571

这里简单说一下伪造的规则头,此时的len需要设置为0x20以及islast需要设置为0。

最后泄露的效果如下,即使内核已经抛出了异常,但是不会影响后续表达式的正常执行。

image-20240227192131407

提权利用

既然可以随意伪造表达式,因此我们可以伪造payload表达式。

1
2
3
4
5
6
struct nft_payload {
enum nft_payload_bases base:8;
u8 offset;
u8 len;
u8 dreg;
};

image-20240227194109554

regs下方存在着nft_do_chain函数返回地址,因此可以直接通过payload表达式将提权payload注入进来。

image-20240227194544551

由于我们可以伪造payload表达式,因此注入的payload长度不受限,因此采用

  • commit_creds(prepare_kernel_cred(0)),构造root凭证
  • 接着利用find_task_by_vpidinit_nsproxy以及switch_task_namespaces切换命名空间。
  • 最后利用蹦床swapgs_restore_regs_and_retrun_to_usermode返回到用户空间完成提权利用。

image-20240227195613084

总结

总结一下模块基地址的泄露流程

  • 构造基础链
    • 设置NFT_JUMP表达式
    • 通过meta设置为NFT_META_NFTRACE
  • 泄露链
    • byteorder表达式触发漏洞,第一次读chainrule以及last_rule,第二次改写为chainfake rule以及fake last_rule
    • dynset表达式泄露chainrulelast_rule
  • 订阅NFNLGRP_NFTRACE组,接收数据包

原版exp使用go语言写的,我使用c语言重写了一版。

完整exphttps://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


CVE-2023-35001-Nftables数组越界
https://h0pe-ay.github.io/CVE-2023-35001-Nftables数组越界/
作者
hope
发布于
2024年4月25日
许可协议