影响范围
- 引入commit:fdb9c405e35bdc6e305b9b4e20ebc141ed14fc81
- 修复commit:7e6bc1f6cabcd30aba0b11219d8e01b952eacbb6
- 时间跨度:2020/04/28 ~ 2022/07/03
- 版本跨度:v5.8 ~ v5.19
简介
在netfilter模块的nft_setelem_parse_data()
中存在一处类型混淆(type confusion),从而在nft_set_elem_init()
中产生了堆越界问题。
修复方案
更新内核或使用下面命令禁止普通用户改变user namspace。
sysctl kernel.unprivileged_userns_clone=0
漏洞分析
漏洞的patch如链接:https://github.com/torvalds/linux/commit/7e6bc1f6cab.diff
diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 51144fc66889b5..d6b59beab3a986 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -5213,13 +5213,20 @@ static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
struct nft_data *data,
struct nlattr *attr)
{
+ u32 dtype;
int err;
err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
if (err < 0)
return err;
- if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
+ if (set->dtype == NFT_DATA_VERDICT)
+ dtype = NFT_DATA_VERDICT;
+ else
+ dtype = NFT_DATA_VALUE;
+
+ if (dtype != desc->type ||
+ set->dlen != desc->len) {
nft_data_release(data, desc->type);
return -EINVAL;
}
变化不是很大。仔细看的话,原代码其实强制把set->dtype
当做NFT_DATA_VERDICT
,但实际上set->dtype
有其他类型的可能性(类型混淆)。
这个函数其实是在5.8加入的。在5.8之前,调用逻辑是这样的:
```c
// >>> net/netfilter/nf_tables_api.c:4488
/ 4488 / static int nft_add_set_elem(struct nft_ctx ctx, struct nft_set set,
/ 4489 / const struct nlattr attr, u32 nlmsg_flags)
/ 4490 */ {/ 4500 / struct nft_data data;
/ 4595 / if (nla[NFTA_SET_ELEM_DATA] != NULL) {
/ 4596 / err = nft_data_init(ctx, &data, sizeof(data), &d2,
/ 4597 / nla[NFTA_SET_ELEM_DATA]);
/ 4598 / if (err < 0)
/ 4599 / goto err2;
/ 4600 /
/ 4601 / err = -EINVAL;
/ 4602 / if (set->dtype != NFT_DATA_VERDICT && d2.len != set->dlen)
/ 4603 / goto err3;/ 4629 /
/ 4630 / nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, d2.len);
/ 4631 / }```
在5.8之后添加了
nft_setelem_parse_data()
,逻辑变成如下:```c
// >>> net/netfilter/nf_tables_api.c:5697
/ 5697 / static int nft_add_set_elem(struct nft_ctx ctx, struct nft_set set,
/ 5698 / const struct nlattr attr, u32 nlmsg_flags)
/ 5699 */ {/ 5706 / struct nft_set_elem elem;
/ 5884 / if (nla[NFTA_SET_ELEM_DATA] != NULL) {
/ 5885 / err = nft_setelem_parse_data(ctx, set, &desc, &elem.data.val,
/ 5886 / nla[NFTA_SET_ELEM_DATA]);
/ 5887 / if (err < 0)
/ 5888 / goto err_parse_key_end;/ 5914 /
/ 5915 / nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len);
/ 5916 / }// >>> net/netfilter/nf_tables_api.c:5116
/ 5116 / static int nft_setelem_parse_data(struct nft_ctx ctx, struct nft_set set,
/ 5117 / struct nft_data_desc desc,
/ 5118 / struct nft_data data,
/ 5119 / struct nlattr attr)
/ 5120 / {
/ 5121 / int err;
/ 5122 /
/ 5123 / err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
/ 5124 / if (err < 0)
/ 5125 / return err;
/ 5126 /
/ 5127 / if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
/ 5128 / nft_data_release(data, desc->type);
/ 5129 / return -EINVAL;
/ 5130 / }
/ 5131 /
/ 5132 / return 0;
/ 5133 */ }
```仔细看!5.8之前调用
nft_data_init()
时第3个参数是值为sizeof(data),即16bytes。但到了5.8之后第3个参数莫名变成了NFT_DATA_VALUE_MAXLEN
,即64bytes。其实patch修复的那一行在5.8之前已经存在,但5.8前却不受影响,因为nft_data_init()
的第三个参数保证了datalen不会大于16bytes,从而不会导致溢出。
先看nft_setelem_parse_data()
函数的参数部分,attr
是用户可控的数据,将attr传入nft_data_init()
中对data
和desc
进行初始化。因此data
和desc
也是用户可控的。
// >>> net/netfilter/nf_tables_api.c:5116
/* 5116 */staticintnft_setelem_parse_data(struct nft_ctx*ctx,struct nft_set*set,
/* 5117 */struct nft_data_desc*desc,
/* 5118 */struct nft_data*data,
/* 5119 */struct nlattr*attr)
/* 5120 */{
/* 5121 */interr;
/* 5122 */
/* 5123 */err=nft_data_init(ctx,data,NFT_DATA_VALUE_MAXLEN,desc,attr);
下面是初始化的过程。
先进入nla_parse_nested_deprecated()
将nla
(即上层的attr)中的数据解析到tb中。然后根据tb中的值在设置data
和desc
(Line 9543 和 Line 9546)
// >>> net/netfilter/nf_tables_api.c:9530
/* 9530 */intnft_data_init(conststruct nft_ctx*ctx,
/* 9531 */struct nft_data*data,unsignedintsize,
/* 9532 */struct nft_data_desc*desc,conststruct nlattr*nla)
/* 9533 */{
/* 9534 */struct nlattr*tb[NFTA_DATA_MAX+1];
------
// 通过 nla 初始化 tb
/* 9537 */err=nla_parse_nested_deprecated(tb,NFTA_DATA_MAX,nla,
/* 9538 */nft_data_policy,NULL);
------
/* 9540 */returnerr;
------
// 初始化 data 和 desc
/* 9542 */if(tb[NFTA_DATA_VALUE])
/* 9543 */returnnft_value_init(ctx,data,size,desc,
/* 9544 */tb[NFTA_DATA_VALUE]);
/* 9545 */if(tb[NFTA_DATA_VERDICT]&&ctx!=NULL)
/* 9546 */returnnft_verdict_init(ctx,data,desc,tb[NFTA_DATA_VERDICT]);
// >>> net/netfilter/nf_tables_api.c:9486
/* 9486 */staticintnft_value_init(conststruct nft_ctx*ctx,
/* 9487 */struct nft_data*data,unsignedintsize,
/* 9488 */struct nft_data_desc*desc,conststruct nlattr*nla)
/* 9489 */{
------
/* 9495 */if(len>size)
/* 9496 */return-EOVERFLOW;
------
/* 9498 */nla_memcpy(data->data,nla,len);
/* 9499 */desc->type=NFT_DATA_VALUE;
/* 9500 */desc->len=len;
初始化完data
和desc
后就是下面这段判断了。
// >>> net/netfilter/nf_tables_api.c:5116
/* 5116 */staticintnft_setelem_parse_data(struct nft_ctx*ctx,struct nft_set*set,
/* 5117 */struct nft_data_desc*desc,
/* 5118 */struct nft_data*data,
/* 5119 */struct nlattr*attr)
/* 5120 */{
------
/* 5123 */err=nft_data_init(ctx,data,NFT_DATA_VALUE_MAXLEN,desc,attr);
------
// 判断是否畸形,但可以绕过
/* 5127 */if(desc->type!=NFT_DATA_VERDICT&&desc->len!=set->dlen){
/* 5128 */nft_data_release(data,desc->type);
/* 5129 */return-EINVAL;
/* 5130 */}
由于上面说了,desc
和data
是通过解析用户提供的attr
得到的,而set->dlen
又是用户在创建netfilter的set时可以控制的(但上层存在一个限制,即set->dlen < 64
)。
重点来了,假设此时set->dtype == NFT_DATA_VALUE
且desc->type == NFT_DATA_VERDICT
,则desc->len == 16
,set->dlen
和desc->len
可以不同,但由于这两个判断中间是“与”逻辑,因此并不会走到错误分支。
nft_setelem_parse_data()
外层由nft_add_set_elem()
调用,在其下还调用nft_set_elem_init()
。
// >>> net/netfilter/nf_tables_api.c:5697
/* 5697 */staticintnft_add_set_elem(struct nft_ctx*ctx,struct nft_set*set,
/* 5698 */conststruct nlattr*attr,u32nlmsg_flags)
/* 5699 */{
------
/* 5704 */struct nft_set_ext_tmpltmpl;
------
/* 5710 */struct nft_data_descdesc;
------
/* 5884 */if(nla[NFTA_SET_ELEM_DATA]!=NULL){
// 调用`nft_setelem_parse_data`,存在类型混淆问题
/* 5885 */err=nft_setelem_parse_data(ctx,set,&desc,&elem.data.val,
/* 5886 */nla[NFTA_SET_ELEM_DATA]);
/* 5887 */if(err<0)
// err就挂了,直接走到return
/* 5888 */gotoerr_parse_key_end;
------
// 根据 desc.len 修改 tmpl
/* 5915 */nft_set_ext_add_length(&tmpl,NFT_SET_EXT_DATA,desc.len);
/* 5916 */}
------
// 调用`nft_set_elem_init`,存在由类型混淆导致堆越界写
/* 5931 */elem.priv=nft_set_elem_init(set,&tmpl,elem.key.val.data,
/* 5932 */elem.key_end.val.data,elem.data.val.data,
/* 5933 */timeout,expiration,GFP_KERNEL);
------
/* 6010 */err_parse_key_end:
------
/* 6018 */returnerr;
/* 6019 */}
在nft_set_elem_init()
中存在一处memcpy,来源为data,长度为set->dlen
,但ext长度和分配和参数中的tmpl
有关,tmpl
在上层函数中根据desc
进行修改。又因为在nft_setelem_parse_data()
中我们提到,set->dlen
和desc->len
可以不同,前者最大可到64,而后者只为16定值,因此此处memcpy会造成堆越界写。
// >>> net/netfilter/nf_tables_api.c:5364
/* 5364 */void*nft_set_elem_init(conststruct nft_set*set,
/* 5365 */conststruct nft_set_ext_tmpl*tmpl,
/* 5366 */constu32*key,constu32*key_end,
/* 5367 */constu32*data,u64timeout,u64expiration,gfp_tgfp)
/* 5368 */{
/* 5369 */struct nft_set_ext*ext;
/* 5370 */void*elem;
/* 5371 */
// 根据 tmpl->len 分配空间
/* 5372 */elem=kzalloc(set->ops->elemsize+tmpl->len,gfp);
------
// 初始化 ext
/* 5376 */ext=nft_set_elem_ext(set,elem);
/* 5377 */nft_set_ext_init(ext,tmpl);
------
/* 5383 */if(nft_set_ext_exists(ext,NFT_SET_EXT_DATA))
// 触发堆越界写
/* 5384 */memcpy(nft_set_ext_data(ext),data,set->dlen);
根据数据的不同,分配的elem结构体可以位于kmalloc-64、kmalloc-128、kmalloc-192中。
漏洞利用
先观察一下这个发生堆越界写的对象:elem。
如下设置结构体可以在kmalloc-64中发生堆溢出:
如下设置结构体可以在kmalloc-128中发生堆溢出:
如下设置结构体可以在kmalloc-192中发生堆溢出:
这里可以选择kmalloc-64的排布,当然其他的也可以利用成功。
之后我们堆喷结构体user_key_payload
,做linux内核利用的朋友应该很熟悉这个结构体了。
struct user_key_payload{
struct rcu_headrcu;/* RCU destructor */
unsignedshortdatalen;/* length of this data */
chardata[]__aligned(__alignof__(u64));/* actual data */
};
pwndbg>pt/ostruct user_key_payload
/* offset | size */type=struct user_key_payload{
/* 0 | 16 */struct callback_head{
/* 0 | 8 */struct callback_head*next;
/* 8 | 8 */void(*func)(struct callback_head*);
/* total size (bytes): 16 */
}rcu;
/* 16 | 2 */unsignedshortdatalen;
/* XXX 6-byte hole */
/* 24 | 0 */chardata[];
/* total size (bytes): 24 */
}
结构体中的datalen
字段描述了后面data的长度。通过堆越界写修改它就能通过keyctl
syscall 来越界读取data中的数据。
我们先堆喷一些user_key_payload
来简单的做一下堆风水。然后再喷一些user_key_payload
,并隔空释放几个。之后触发nft_add_set_elem()
中的kzalloc时大概率会得到如下的堆布局:
之后越界写就可以修改user_key_payload
中的 datalen字段。我们可以使用keyctl遍历读取所有的key,并检查读取的长度。如果长度变成我们修改的长度,则说明修改成功,否则需要重复这一步。
成功后我们把其他所有的user_key_payload
通过keyctl的KEYCTL_REVOKE
删掉。需要注意的是,这里的删掉并不是直接调用kfree将堆块释放,而是通过向user_key_payload
中的rcu写入func值,并让这个key对用户不可见,之后在垃圾回收中将其释放。man中是这样描述的:
KEYCTL_REVOKE (since Linux 2.6.10)
Revoke the key with the ID provided in arg2 (cast to key_serial_t). The key is scheduled for garbage collection; it will no longer be findable, and will be unavailable for further operations. Further attempts to use the key will fail with the error EKEYREVOKED.
此时我们通过corrupted的user_key_payload
做越界读,就能读到rcu.func中的user_free_payload_rcu
这个函数指针,从而泄露出内核代码段地址。
稍微提一嘴,原作者博客中使用了另一个结构体作为泄露来源,即通过
io_uring
来分配percpu_ref_data
结构体。但我看了眼这个结构体是在Linux 5.10中加入的,这意味着受影响的5.8~5.9并不能使用这个方法来leak。
有了leak就是想办法通过越界写来提权了。这边我打算试试360在今年blackhat上提到的新利用思路USMA。
在raw_packet中存在这样一条路径:
// >>> linux-5.13/net/packet/af_packet.c:3695
/* 3695 */staticint
/* 3696 */packet_setsockopt(struct socket*sock,intlevel,intoptname,sockptr_toptval,
/* 3697 */unsignedintoptlen)
/* 3698 */{
------
/* 3706 */switch(optname){
------
/* 3711 */intlen=optlen;
------
/* 3728 */casePACKET_RX_RING:
/* 3729 */casePACKET_TX_RING:
/* 3730 */{
------
/* 3735 */switch(po->tp_version){
------
/* 3740 */caseTPACKET_V3:
------
// 调用
/* 3751 */ret=packet_set_ring(sk,&req_u,0,
/* 3752 */optname==PACKET_TX_RING);
// >>> linux-5.13/net/packet/af_packet.c:4306
/* 4306 */staticintpacket_set_ring(struct sock*sk,union tpacket_req_u*req_u,
/* 4307 */intclosing,inttx_ring)
/* 4308 */{
------
/* 4331 */if(req->tp_block_nr){
------
/* 4376 */order=get_order(req->tp_block_size);
// 调用
/* 4377 */pg_vec=alloc_pg_vec(req,order);
// >>> linux-5.13/net/packet/af_packet.c:4281
/* 4281 */staticstruct pgv*alloc_pg_vec(struct tpacket_req*req,intorder)
/* 4282 */{
/* 4283 */unsignedintblock_nr=req->tp_block_nr;
------
// 在slab中申请一段内存在存放pg_vec
/* 4287 */pg_vec=kcalloc(block_nr,sizeof(struct pgv),GFP_KERNEL|__GFP_NOWARN);
------
// 申请 n 个 page
/* 4291 */for(i=0;i<block_nr;i++){
/* 4292 */pg_vec[i].buffer=alloc_one_pg_vec_page(order);
平时我们只是拿它来做方便的page level 风水,即4292行的功能。而USMA使用的是4287行分配的数组。
首先我们设置block_nr
为5~8,这样pg_vec
就能分配到kmalloc-64中。之后我们触发nft_add_set_elem()
中的堆溢出就能覆盖到pg_vec中的虚拟地址。
之后通过packet_mmap
就能将这些page映射到用户态进行读写。
其中在mmap时还存在如下的校验:
// >>> mm/memory.c:1752
/* 1752 */staticintvalidate_page_before_insert(struct page*page)
/* 1753 */{
/* 1754 */if(PageAnon(page)||PageSlab(page)||page_has_type(page))
/* 1755 */return-EINVAL;
/* 1756 */flush_dcache_page(page);
/* 1757 */return0;
/* 1758 */}
即检查page是否为匿名页,是否为Slab子系统分配的页,以及page是否含有type,而内存页的type总共有以下四种。
#define PG_buddy 0x00000080
#define PG_offline 0x00000100
#define PG_table 0x00000200
#define PG_guard 0x00000400
PG_buddy为伙伴系统中的页,PG_offline为内存交换出去的页,PG_table为用作页表的页,PG_guard为用作内存屏障的页。可以看到如果传入的page为内核代码段的页,以上的检查全都可以绕过。
例如我们可以将pg_vec中的页修改为__sys_setresuid()
所在的页,从而直接patch它的代码让任意用户都可以直接提权到root。
// >>> kernel/sys.c:652
/* 652 */long__sys_setresuid(uid_truid,uid_teuid,uid_tsuid)
/* 653 */{
------
// patch 这个判断
/* 680 */if(!ns_capable_setid(old->user_ns,CAP_SETUID)){
/* 681 */if(ruid!=(uid_t)-1&&!uid_eq(kruid,old->uid)&&
/* 682 */!uid_eq(kruid,old->euid)&&!uid_eq(kruid,old->suid))
/* 683 */gotoerror;
/* 684 */if(euid!=(uid_t)-1&&!uid_eq(keuid,old->uid)&&
/* 685 */!uid_eq(keuid,old->euid)&&!uid_eq(keuid,old->suid))
/* 686 */gotoerror;
/* 687 */if(suid!=(uid_t)-1&&!uid_eq(ksuid,old->uid)&&
/* 688 */!uid_eq(ksuid,old->euid)&&!uid_eq(ksuid,old->suid))
/* 689 */gotoerror;
/* 690 */}
稍微提一嘴,原作者博客中使用这篇文章中所描述的手法来实现任意地址写。简单来说是借助了
simple_xattr
结构体中对list_head
的unlink操作,从而修改modprobe_path。
代码见: https://github.com/veritas501/CVE-2022-34918
参考资料
FROM:tttang . com
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论