该漏洞作者Andy Nguyen (theflow@) ,writeup已经公开。
简单概述
主要是在xt_compat_target_from_user()
中有溢出,经过特殊构造产生uaf,
具体exp中,使用了struct msg_msg
作为溢出,导致两个指针指向同一个chunk,使用套接字简化了uaf的使用流程,最后利用pipe()管道中的struct pipe_buf_operations
来泄露地址,以及伪造修改关闭pipes时调用的release
函数地址调用rop链,完成权限提升以及返回用户空间
修补方案
在查询源代码过程中发现某些版本的代码中选择将xt_compat_target_from_user()
的漏洞触发代码删除(简单明了)
漏洞具体分析
在兼容模式下,即存在#define CONFIG_COMPAT
条件下
套接字 IPT_SO_SET_REPLACE
或者IP6T_SO_SET_REPLACE
被调用,
在xt_compat_target_from_user()
中memset()
参数target->targetsize
没有检查偏移,导致可能出现字节越界。
#ifdef CONFIG_COMPAT
int xt_compat_target_offset(struct xt_target *target)
{
u_int16_t csize = target->compatsize ? : target->targetsize;
return XT_ALIGN(target->targetsize) - COMPAT_XT_ALIGN(csize);
}
EXPORT_SYMBOL_GPL(xt_compat_target_offset);
void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr, int *size)
{ //t 表示用户或者内核的target_size target_name
//
struct xt_target *target = t->u.kernel.target;//内核target 对应struct xt_target 即target基本类型
struct compat_xt_entry_target *ct = (struct compat_xt_entry_target *)t; //xt = t的副本
int pad, off = xt_compat_target_offset(target);
u_int16_t tsize = ct->u.user.target_size;
t = *dstptr;
memcpy(t, ct, sizeof(*ct));//将ct覆盖到dst
//当内核偏移与用户态偏移不同时使用
if (target->compat_from_user)
target->compat_from_user(t->data, ct->data);
else
memcpy(t->data, ct->data, tsize - sizeof(*ct));
pad = XT_ALIGN(target->targetsize) - target->targetsize;
//漏洞触发点
if (pad > 0)
memset(t->data + target->targetsize, 0, pad);//末位补0,控制target->targetsize偏大,可以造成\x00溢出
//这里有越界 off by null 设置偏移
tsize += off;
t->u.user.target_size = tsize;
*size += off;
*dstptr += tsize;
}
target->targetsize
不由用户控制,可以通过选择不同大小的target类型结构体来控制targetsize大小。
如何触发漏洞?
漏洞成因 在xt_compat_target_from_user()
中memset()
参数target->targetsize
溢出导致
memset(t->data + target->targetsize, 0, pad);
原本目的应该是将未满足的偏移填充0。targetsize不被用户直接控制,可以通过选择不同的target类型,但是targetsize不能是8位偏移整齐,满足pad>0
,
构造数据data,通过控制pad。控制溢出数量。
exp利用时,选择创建data数据,date数据长度为0x1012。这样刚好覆盖到接下来申请0x1000大小堆块的对应结构体的指针后2bit,造成指针的指向错误。
具体实现 uaf
由于在内核中无法直接申请固定大小堆块,exp选择使用创建msg_msg
结构体,
msgsend()内部使用alloc_msg(),
alloc_msg将一定长度的msg利用kmalloc多次分配每一段msg开头都有msg_msg结构体
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list;/*链表头*/
long m_type; /*类型*/
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};
//把id取出来
struct list_head {
struct list_head *next, *prev;
};
struct msg_msgseg {
struct msg_msgseg *next;
/* the next part of the message follows immediately */
};
msgget()
首先创建4096个消息队列,对每一个队列先发送msg_primary,大小为0x1000,再对每一队列发送msg_secondary,大小为0x400,这样对于每一个队列中都有两条消息,分别对应0x1000 和0x400大小的chunk,并且分别对每一个msg进行标号,使msg内部存有对应msg标号,struct msg_msg
位于整个chunk的头部,
以上是基础chunk的构造
通过读取msg,将对应chunk释放,由于释放的是0x1000大小,使用触发漏洞,将下一个堆块msg_msg
的struct list_head
的后两位溢出为0,如果正确运行,成功将一个primary_msg对应的secondary_msg指向不属于它本身队列的secondary _msg,此时就会有一个secondary_msg同时被两个指针指向,由此造成uaf。
为继续利用,我们需要找到哪两个primary_msg
指针指向了同一个secondary_msg
.由于在发送msg
时提前将标号放入msg
的内容中,可以使用msgrcv()
获取secondary_msg
,这时的获取通过primary_msg
的struct msg_msg->m_list
获取,被修改的msg
会指向id与消息队列id不同的secondary_msg
,这时可以通过当前消息队列的id与secondary_msg
的id获取具体哪两个指针指向同一个msg
。
SMAP
smap阻止内核直接访问用户空间的内容,这使泄露内核的地址会更复杂。
先将被双重指针指向的地址free掉,创建一个fakemsg内容如下:
build_msg_msg((void *)secondary_buf, 0x41414141, 0x42424242,
PAGE_SIZE - MSG_MSG_SIZE, 0);
采用套接字将fakemsg放到可以被uaf的地方,
int spray_skbuff(int ss[NUM_SOCKETS][2], const void *buf, size_t size) {
for (int i = 0; i < NUM_SOCKETS; i++) {
for (int j = 0; j < NUM_SKBUFFS; j++) {
if (write(ss[i][0], buf, size) < 0) {
perror("[-] write");
return -1;
}
}
}
return 0;
}
//原理 ss是由 socketpair制造一对相互连接的套接字
改变了fakemasg的m_ts位置,即扩大了读取范围,可以通过指针获取到
msg = (struct msg_msg *)&msg_fake.mtext[SECONDARY_SIZE - MSG_MSG_SIZE];
//获取相邻msg
fakechunk的相邻的chunk信息,通过该chunk找到 对应primary地址,以此构造一个合法的msg ,将他free掉。此时ss[i][1]产生的指针指向该地址。
这些对于msg的操作是为了更简便地uaf攻击。
攻击内核路径。
计算内核地址,采用pipe()
对应结构体,通过调用pipe()
可以分配到pipe_buffer
创建pipe
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
其中有const struct pipe_buf_operations *ops;
struct pipe_buf_operations {
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);
void (*release)(struct pipe_inode_info *, struct pipe_buffer *);
bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);
bool (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};
一旦有pipe被写入struct pipe_buffer
就会被写入内容,由于其中 ops
会定位到.data
段。泄露该地址,由于偏移固定,很容易可以计算出内核基地址。
创建攻击内核的rop链
通过uaf覆盖修改pipe。将对应release函数的地址伪造成rop链。
rop链需要完成更改creds(身份信息),commit_creds(prepare_kernel_cred(NULL))
并且返回命名空间 switch_task_namespaces(find_task_by_vpid(1), init_nsproxy)
。
由此提权
调用rop链
上面提到已经计算出内核基址,因此创建一个fakepipebuffer,伪造ops,更改ops中realese的指向为对应rop的起始地址。在关闭pipe时调用到realese。实际劫持流程执行提权rop。
实验演示
docker本地搭建
内核小白,如果理解上有偏差,请指正?
参考资料
–https://switch-router.gitee.io/blog/netfilter4/
–https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html#proof-of-concept
–https://blog.csdn.net/weixin_40039738/article/details/81095013