CVE-2022-34918


环境搭建

commit:2c85ebc57b3e1817b6ce1a6b703928e113a90442

config:

defconfig+:

CONFIG_CONFIGFS_FS=y
CONFIG_SECURITYFS=y
CONFIG_NET_SCHED=y
CONFIG_DEBUG_INFO=y
CONFIG_USER_NS=y #支持新的namespace
CONFIG_USERFAULTFD=y #支持userfaultfd

CONFIG_SLAB_FREELIST_RANDOM=y

CONFIG_NF_TABLES=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_BINFMT_MISC=y
CONFIG_USER_NS=y
CONFIG_E1000=y
CONFIG_E1000E=y

背景知识

请先看笔者的nf_tables入门!😮

漏洞分析

先放调用链:

nft_setelem_parse_data()

直接看源码:https://elixir.bootlin.com/linux/v5.10/source/net/netfilter/nf_tables_api.c#4865

nft_add_set_elem中有这样一段代码:

if (nla[NFTA_SET_ELEM_DATA] != NULL) {
err = nft_setelem_parse_data(ctx, set, &desc, &elem.data.val,
nla[NFTA_SET_ELEM_DATA]);
if (err < 0)
goto err_parse_key_end;

dreg = nft_type_to_reg(set->dtype);
list_for_each_entry(binding, &set->bindings, list) {
struct nft_ctx bind_ctx = {
.net = ctx->net,
.family = ctx->family,
.table = ctx->table,
.chain = (struct nft_chain *)binding->chain,
};

if (!(binding->flags & NFT_SET_MAP))
continue;

err = nft_validate_register_store(&bind_ctx, dreg,
&elem.data.val,
desc.type, desc.len);
if (err < 0)
goto err_parse_data;

if (desc.type == NFT_DATA_VERDICT &&
(elem.data.val.verdict.code == NFT_GOTO ||
elem.data.val.verdict.code == NFT_JUMP))
nft_validate_state_update(ctx->net,
NFT_VALIDATE_NEED);
}

nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len);
}

/* The full maximum length of userdata can exceed the maximum
* offset value (U8_MAX) for following extensions, therefor it
* must be the last extension added.
*/
ulen = 0;
if (nla[NFTA_SET_ELEM_USERDATA] != NULL) {
ulen = nla_len(nla[NFTA_SET_ELEM_USERDATA]);
if (ulen > 0)
nft_set_ext_add_length(&tmpl, NFT_SET_EXT_USERDATA,
ulen);
}

err = -ENOMEM;
elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,
elem.key_end.val.data, elem.data.val.data,
timeout, expiration, GFP_KERNEL);

  1. nft_data_init中,首先注意宏NFT_DATA_VALUE_MAXLEN的值是64,也就是昨天为什么datalen不能超过0x40的缘故;

  2. set->dlen应该是创建set的时候决定的;

  3. desc->lennla_len(nla)获取的,应该就是netlink里push进去那个长度;

  4. desc->type则是由nla_type(nla)

漏洞就是如果desc的type是NFT_DATA_VERDICT的话,那么就可以使desc->len != set-dlen成功过检查;

然后tmpl中的长度是根据desc.len增加的(NFTA_SET_ELEM_DATA == 2, 在nft_set_ext_types中对应的len==0):

因此elem就会为这个data分配desc->len的空间,但是在下面copy的时候可以看到长度用的是set->dlen

因此这里就存在了溢出:

触发

下面就剩下最后一个问题了,如何设置data的type为NFT_DATA_VERDICT

先看netlink的结构:

然后结合例子分析,一个nsmsghdr的构造应该是这样的:

  1. 最上面一个struct nlmsghdr头,长度用NLMSG_SPACE(other_size)来计算获取(其中other_size是消息体的长度),然后手动填充数据;
  2. 消息体最开始是一个struct nfgenmsg结构,长度是sizeof(struct fgenmsg),然后只需要设置其nfgen_familyNFPROTO_INET即可;
  3. 最后是消息了,每一个消息都是一个struct nlattr结构,这是一个变长结构,前2个字节是长度,再2个字节是type,后边则是内容。

下面根据源码来分析add_elem_to_set需要什么attr:https://elixir.bootlin.com/linux/v5.10/source/net/netfilter/nf_tables_api.c#L5419

首先是要有一个NFTA_SET_ELEM_LIST_ELENTS属性:

之后进入nft_ctx_init_from_elemattr函数,可以看到这里要用到NFTA_SET_ELEM_LIST_TABLE这个属性:

之后又用到了NFTA_SET_ELEM_LIST_SET这个属性,还有NFTA_SET_ELEM_LIST_SET_ID这个属性:

最后会添加NFTA_SET_ELEM_LIST_ELEMENTS中的每一个元素:

所以说一共需要三个LIST,以及一个ID,下面通过这个函数来看一下这个LIST是如何构造的:https://elixir.bootlin.com/linux/v5.10/source/net/netfilter/nf_tables_api.c#L509

可以看到TABLE_LIST就是字符串即可;

之后的SET_LIST也是字符串就行了,然后在这个地方开始遍历所有的element:

下面看一下这个宏https://elixir.bootlin.com/linux/v5.10/source/include/net/netlink.h#L1931:

经过分析,这个LIST结构是这样的:

我们可以先手工打造两个elem:

然后用任意长度的消息加进去:(注意这里会自动给我们加上那个总的头)

在此之前长度的计算直接手工算出来是0x64即可:

当我们能够成功进入到nft_add_set_elem函数之后:https://elixir.bootlin.com/linux/v5.10/source/net/netfilter/nf_tables_api.c#L5447

首先会遇到一个NFTA_SET_ELEM_KEY的判断:

nft_add_set_elem分析

下面从这个循环开始分析,这个循环中中调用nft_add_set_elem,attr是我们传进去的NFTA_SET_ELEM_LIST_ELEMENTS类型的nlattr中的每一个attr:

对于一个子attr中,取出其data和len进行parse,

对于每一个attr还要再迭代一次:

对于在nft_add_set_elem中会调用nft_set_elem_parse_key函数,其中上来先调用了nft_data_init函数,然后调用nla_parse_nested_deprecated的函数继续拆解key这个子属性,这不是套娃????😭

然后解出来有NFTA_DATA_VALUE或者NFTA_DATA_VERDICT即可;

我们再看看漏洞那块,不出所料,也是nft_data_init那也就是也走了个套娃,一共套了4层😡:

nft_value_init直接就是将这个属性拆解开,拿到data和len,赋值给desc、copy到data中就算完事:

nft_verdict_init那可就复杂多了,再来一次套娃拆解????😡,拆出来一个code,

然后各种switch,可以看到desc->len似乎是确定的了,

直接看下这个结构:

难道能从chain上做手脚?

总结

总结一下消息体:

数据流总结:

如下形式的消息中,导致在nft_setelem_parse_data中绕过检查造成不一致,然后此时desc.len会被设置为固定值sizeof(struct nft_verdict) == 0x10,而verdict中的信息会被复制到data中以供后续使用,然后后续分配elem的空间也是根据desc.len来的:

NFTA_SET_ELEM_LIST_ELEMENTS{
每一个elem{
NFTA_SET_ELEM_DATA{
NFTA_DATA_VERDICT{
NFTA_VERDICT_CODE->int32
(NFTA_VERDICT_CHAIN)
}
}

但是到了memcpy的时候,长度却用的是set->dlen(由创建set的时候传进去的参数);

那么就要关心这个data下边的数据是什么了,可以看到是elem.data.val.data:

elem是在nft_add_set_elem函数中的局部变量:

因此我们需要在nft_set_elem

思考

netfilter的洞是人眼挖的吗?

netlink真是手搓能行的吗?

漏洞利用

根据上述描述,data是栈上的局部变量,似乎溢出不可控,具体的做法是添加两个elem,第一个为0x60,第二个为0x40,然后通过将第一个elem的data设置为普通的value类型,且长度大于第二个,那么就会在栈上形成残留数据,紧接着在添加第二个elem的的时候就可以将残留的数据越界写入到0x40的elem中!😮

两个elem设置如下:

{
"NFTA_SET_ELEM_KEY":{
"NFTA_DATA_VALUE":"aaaaaaaaaaaaaaaa"
},
"NFTA_SET_ELEM_DATA":{
"NFTA_DATA_VALUE":"aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbcccccccccccccccc12345678"
}
}
{
"NFTA_SET_ELEM_KEY":{
"NFTA_DATA_VALUE":"aaaaaaaaaaaaaaaa"
},
"NFTA_SET_ELEM_DATA":{
"NFTA_DATA_VERDICT":{
"NFTA_VERDICT_CODE":0
}
}
}

太巧妙了!

参考

https://veritas501.github.io/2022_08_02-CVE-2022-34918%20netfilter%20%E5%88%86%E6%9E%90%E7%AC%94%E8%AE%B0/

https://bsauce.github.io/2022/07/26/CVE-2022-34918/

附录

nf_tables_newsetelem
检查nla[NFTA_SET_ELEM_LIST_ELEMENTS]==NULL
通过nla[NFTA_SET_ELEM_LIST_SET]找set,就是个string
然后遍历nla[NFTA_SET_ELEM_LIST_ELEMENTS]中所有的子attr,并调用nft_add_set_elem:
首先调用nla_parse_nested_deprecated,将上述子attr中所有的孙attr解析出来到nla中
检查nla[NFTA_SET_ELEM_KEY]==NULL
调用nft_set_ext_preparse(&tmpl)//tmpl用于准备所有的ext扩展消息的offset等信息,这里初始化
nft_setelem_parse_flags(set, nla[NFTA_SET_ELEM_FLAGS], &flags)//分析flags
ifxxx->nft_setelem_parse_flags(set, nla[NFTA_SET_ELEM_FLAGS], &flags)
检查一些flags
......
nft_setelem_parse_key(ctx, set, &elem.key.val,nla[NFTA_SET_ELEM_KEY]);
nft_set_add_length(&tmpl, NFT_SET_EXT_KEY, set->klen);
if(xxx_KEY_END) //可选,略
if(timeout) //可选,略
......//可选,略
if(nla[NFTA_SET_ELEM_DATA] != NULL){
nft_setelem_parse_data(); //分析数据,出现漏洞在这里;
nft_data_init();//value or verdict
......
nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len);//用desc.len赋值tmpl,并影响后续开辟空间,desc.len是固定值0x10;
//这里会给tmpl的offset[NFT_SET_EXT_DATA]赋值,后边就会ext_exists
}
......
elem.priv = nft_set_elem_init(){ //初始化一个elem到链表上
kzalloc(); //根据tmpl->len分配空间
//从tmpl中copy offset数组等;
memcpy(nft_set_ext_key(ext), key, set->klen);
//各种if来copy其他attr
......
if(nft_set_ext_exists(ext, NFT_SET_EXT_DATA)){
memcpy(nft_set_ext_data(ext), data, set->dlen); //用set->dlen作为长度copy,oob!
}
......
}



NFTA_SET_ELEM_LIST_TABLE->string
NFTA_SET_ELEM_LIST_SET->string
NFTA_SET_ELEM_LIST_ELEMENTS{
每一个elem{
NFTA_SET_ELEM_KEY{
NFTA_DATA_VALUE->string
}
NFTA_SET_ELEM_DATA{
NFTA_DATA_VERDICT{
NFTA_VERDICT_CODE->int32
(NFTA_VERDICT_CHAIN)
}
}

}
}

文章作者: q1ming
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 q1ming !
  目录