环境搭建
commit:2c85ebc57b3e1817b6ce1a6b703928e113a90442
config:
defconfig+:
CONFIG_CONFIGFS_FS=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) { |
在
nft_data_init
中,首先注意宏NFT_DATA_VALUE_MAXLEN
的值是64,也就是昨天为什么datalen不能超过0x40的缘故;set->dlen
应该是创建set的时候决定的;desc->len
是nla_len(nla)
获取的,应该就是netlink里push进去那个长度;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的构造应该是这样的:
- 最上面一个
struct nlmsghdr
头,长度用NLMSG_SPACE(other_size)
来计算获取(其中other_size
是消息体的长度),然后手动填充数据; - 消息体最开始是一个
struct nfgenmsg
结构,长度是sizeof(struct fgenmsg)
,然后只需要设置其nfgen_family
为NFPROTO_INET
即可; - 最后是消息了,每一个消息都是一个
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{ |
但是到了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设置如下:
{ |
{ |
太巧妙了!
参考
https://bsauce.github.io/2022/07/26/CVE-2022-34918/
附录
nf_tables_newsetelem |
NFTA_SET_ELEM_LIST_TABLE->string |