[kernel exploit] 消息队列msg系列在内核漏洞利用中的应用
文章目录
简介
消息队列msg 和共享内存一样是linux 内核提供的一种进程间通信(IPC)方式,但它除了用于进程间通信,在内核堆漏洞利用中确是基本100%出场的常客,无论是堆占位、泄露地址、构造UAF、任意地址读写等操作它都能完成,所以这篇文章分析一下消息队列msg 的源码逻辑和在linux kernel 漏洞利用中的常见应用场景。
本篇分析使用内核代码版本5.13。
源码阅读与逻辑分析
在漏洞利用中,主要就使用msgget、msgsnd和msgrcv 三个函数。接下来分别分析这三个函数。
msgget 创建消息队列
msgget 原型
首先要使用消息队列,要调用msgget 创建消息队列。msgget 的原型如下:
int msgget(key_t key, int msgflg);
- 参数key:键值,linux IPC初始化基本都需要一个键值,通常是通过ftok 生成或是IPC_PRIVATE。在我们漏洞利用场景中一般就是用IPC_PRIVATE 就可以了。
- 参数msgflag:创建消息队列的操作和读写权限,可以设置IPC_CREAT(返回当前key 对应的msg队列id,若不存在则创建) 或IPC_CREAT | IPC_EXCL(返回当前key 对应的msg队列id,如果已经存在则返回错误)。在我们漏洞利用的场景中由于key 都是IPC_PRIVATE ,这里直接使用IPC_CREAT 即可,读写权限通常0666。
- 返回值:返回msg 队列id,用于后续msgsnd 和msgrcv。
msgget 源码分析
入口位置在ksys_msgget
linux\ipc\msg.c:
SYSCALL_DEFINE2(msgget, key_t, key, int, msgflg)
{
return ksys_msgget(key, msgflg);
}
调用栈
- ksys_msgget
- ipcget
- ipcget_new (key为IPC_PRIVATE)
- newque
其中在ipcget 中,会判断key,如果是IPC_PRIVATE,则会直接调用ipcget_new:
linux\ipc\util.c:
int ipcget(struct ipc_namespace *ns, struct ipc_ids *ids,
const struct ipc_ops *ops, struct ipc_params *params)
{
if (params->key == IPC_PRIVATE)
return ipcget_new(ns, ids, ops, params);
else
return ipcget_public(ns, ids, ops, params);
}
最后在newque 函数中,进行msg_queue 结构体的初始化,并且将该msg_queue 存入当前ipc_namespace中,消息队列msg_queue 结构体和相关代码如下;
linux\ipc\msg.c:
struct msg_queue {//msg队列结构体
struct kern_ipc_perm q_perm; //每个ipc 相关结构体都要有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; /* 当前消息队列中的字节数 */
unsigned long q_qnum; /* 当前消息队列中的消息数 */
unsigned long q_qbytes; /* 消息队列中允许的最大字节数 */
struct pid *q_lspid; /* pid of last msgsnd */
struct pid *q_lrpid; /* last receive pid */
struct list_head q_messages;/* 消息队列 */
struct list_head q_receivers;
struct list_head q_senders;
} __randomize_layout;
static int newque(struct ipc_namespace *ns, struct ipc_params *params)
{
struct msg_queue *msq;
··· ···
msq = kvmalloc(sizeof(*msq), GFP_KERNEL);//申请空间
··· ···
/*
* 初始化 msq-> q_perm 和其他成员
*/
retval = ipc_addid(&msg_ids(ns), &msq->q_perm, ns->msg_ctlmni); //[1]
··· ···
return msq->q_perm.id;
}
- [1] : 这里调用ipc_addid 将该msq 结构体的q_perm 成员加入到当前ipc_namespace 中,并返回对应该msg队列的id,在后续寻找的时候可以通过msq->q_perm 的地址用container_of 找到所属msq 的地址。
msq 没啥东西,主要看一下接下来的msgsnd 和msgrcv。
msgsnd 发送消息与消息队列模型
msgsnd 原型
msgsnd 用于向指定id 的有写权限的消息队列发送消息,原型如下:
int msgsnd(int msqid , const void * msgp , size_t msgsz , int msgflg );
-
参数msqid:指定消息队列id,由msgget 返回。
-
参数msgp:发送的消息结构体指针,结构体如下:
struct msgbuf { long mtype; /* 消息类型,后续也会用于接收消息 */ char mtext[1]; /* 用于发送的消息文本 */ };
-
参数msgsz:发送消息的大小
-
参数msgflg:一般会加一个IPC_NOWAIT,如果队列满了就不等待直接返回错误,默认状态会阻塞等待队列空出位置。
-
返回值:0成功,-1失败。
msgsnd 源码分析
入口位置在ksys_msgsnd
linux\ipc\msg.c:
SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz, int, msgflg)
{
return ksys_msgsnd(msqid, msgp, msgsz, msgflg);
}
调用栈:
- ksys_msgsnd
- do_msgsnd
- load_msg
- alloc_msg
- load_msg
- do_msgsnd
根据代码分析消息队列的结构,根据调用栈从后往前分析比较清晰,首先看一下msg 相关的结构体:
struct msg_msg {//主消息段头部
struct list_head m_list; //消息双向链表指针
long m_type;
size_t m_ts; /* 消息大小 */
struct msg_msgseg *next; //指向消息第二段
void *security;
/* 后面接着消息的文本 */
};
struct msg_msgseg {//子消息段头部
struct msg_msgseg *next; //指向下一段的指针,最多三段
/* 后面接着消息第二/三段的文本 */
};
消息分为两种结构体,主消息段头部和辅消息段头部,头部结构体后面紧跟着的就是消息的文本。然后分析申请结构体的alloc_msg 函数:
linux\ipc\msgutil.c:
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg)) //0xfd0
#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))//0xff8
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;
alen = min(len, DATALEN_MSG);//[1] 获得第一段消息长度
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);//为消息段申请内存
if (msg == NULL)
return NULL;
msg->next = NULL;
msg->security = NULL;
len -= alen;//[2]
pseg = &msg->next;
while (len > 0) {//[2] 查看消息是否需要分段
struct msg_msgseg *seg;
cond_resched();
alen = min(len, DATALEN_SEG); //获得第二段消息长度
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);//为消息段申请内存
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;
}
- [1] : 首先根据上面结构体和代码分析,一个消息分为三段,主消息段(msg_msg),和子消息段(msg_msgseg),每个消息段算上头结构最大0x1000 字节。主消息段头部大小0x30,所以主消息段中文本长度最大为0xfd0,这里取用户消息和主消息段最大长度的最小值。也就是判断消息长度是否大于0xfd0,如果不大于则消息一段就够了;如果大于则要先申请最大长度的主消息段,然后再进行分段。
- [2] : 申请完主消息段之后查看消息长度是否还有剩余,如果有则需要分段,子消息段也是最大0x1000字节,但子消息头部只有0x8字节长度,所以子消息段的最大文本长度为0xff8。
所以alloc_msg 函数按照每段消息的最大长度将单个消息分段存储,并使用单向链表链接。单个消息的最大长度是8192 字节,在do_msgsnd 函数中会进行判断,所以这里消息最多分为三段,最后组成如下图的结构:
然后查看看load_msg 函数
linux\ipc\msgutil.c:
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg)) //0xfd
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); //调用上面分析的alloc_msg申请消息结构体
if (msg == NULL)
return ERR_PTR(-ENOMEM);
alen = min(len, DATALEN_MSG);// [1] 和上面同理,获取主消息段长度
if (copy_from_user(msg + 1, src, alen))// [1] 从用户空间拷贝
goto out_err;
for (seg = msg->next; seg != NULL; seg = seg->next) {// [2] 查看是否有子消息段然后拷贝
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}
··· ···
}
- [1] : 跟alloc_msg 函数一样,获取主消息段的长度,然后调用copy_from_user 从用户空间拷贝消息内容。
- [2] : 根据单链表结构查看是否有子消息段,有的话则依次调用copy_from_user 从用户空间拷贝消息内容。
最后查看do_msgsnd 函数,将上面申请和拷贝好的消息放入消息队列。
linux\ipc\msg.c:
#define MSGMAX 8192
ns->msg_ctlmax = MSGMAX
static long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
struct msg_queue *msq;
struct msg_msg *msg;
if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0) // [1] 消息长度校验
return -EINVAL;
··· ···
msg = load_msg(mtext, msgsz); //使用load_msg函数申请消息和从用户空间拷贝内容
··· ···
msg->m_type = mtype;//记录消息类型
msg->m_ts = msgsz;
rcu_read_lock();
msq = msq_obtain_object_check(ns, msqid);// [2]通过msqid 找到msq 结构体
··· ···
for (;;) {
···
if (ipcperms(ns, &msq->q_perm, S_IWUGO))//检查权限是否允许
goto out_unlock0;
/* raced with RMID? */
if (!ipc_valid_object(&msq->q_perm)) {//检查该队列是否被删除
err = -EIDRM;
goto out_unlock0;
}
err = security_msg_queue_msgsnd(&msq->q_perm, msg, msgflg);//没啥用
···
if (msg_fits_inqueue(msq, msgsz))//[3]检查消息队列是否满了
break;
··· ···
··· ···
}
ipc_update_pid(&msq->q_lspid, task_tgid(current));//更新最后发送消息进程id
msq->q_stime = ktime_get_real_seconds();//更新最后发送消息时间
if (!pipelined_send(msq, msg, &wake_q)) {
/* no one is waiting for this message, enqueue it */
list_add_tail(&msg->m_list, &msq->q_messages);//[4]将消息添加进消息队列
msq->q_cbytes += msgsz;//更新目前队列中消息字节数
msq->q_qnum++;//更新目前队列中消息数
atomic_add(msgsz, &ns->msg_bytes);
atomic_inc(&ns->msg_hdrs);
}
··· ···
}
-
[1] : 判断消息最大长度不能超过ns->msg_ctlmax(默认8192),ns->msg_ctlmax 在ipc_namespace 初始化的时候调用msg_init_ns 函数进行初始化:
#define MSGMNI 32000 /* <= IPCMNI */ /* max # of msg queue identifiers */ #define MSGMAX 8192 /* <= INT_MAX */ /* max size of message (bytes) */ #define MSGMNB 16384 /* <= INT_MAX */ /* default max size of a message queue */ void msg_init_ns(struct ipc_namespace *ns) { ns->msg_ctlmax = MSGMAX;//消息最大长度8192 ns->msg_ctlmnb = MSGMNB;//一个消息队列最大总消息长度 ns->msg_ctlmni = MSGMNI;//消息队列最大数量 atomic_set(&ns->msg_bytes, 0); atomic_set(&ns->msg_hdrs, 0); ipc_init_ids(&ns->ids[IPC_MSG_IDS]); }
-
[2] : 这里调用msq_obtain_object_check 通过msqid 从namespace 中找到对应的msq->q_perm结构体,然后调用container_of通过偏移计算得到msg_queue结构体地址:
linux\ipc\msg.c:
static inline struct msg_queue *msq_obtain_object_check(struct ipc_namespace *ns, int id) { struct kern_ipc_perm *ipcp = ipc_obtain_object_check(&msg_ids(ns), id); if (IS_ERR(ipcp)) return ERR_CAST(ipcp); //container_of通过偏移计算得到msg_queue结构体地址 return container_of(ipcp, struct msg_queue, q_perm); }
-
[3] : 调用msg_fits_inqueue检查消息队列是否满了,每个消息队列有能容纳的最大字节数(而不是消息数),如果满了则查看flag是否需要等待,没满则直接break到后面将消息结构体合入消息队列中。
linux\ipc\msg.c:
static inline bool msg_fits_inqueue(struct msg_queue *msq, size_t msgsz) { return msgsz + msq->q_cbytes <= msq->q_qbytes && 1 + msq->q_qnum <= msq->q_qbytes; }
-
[4] : 将完成的消息结构体加入到消息队列中,最后消息队列中会形成一个双向链表(表头是msg_queue 结构体的q_messages 字段)如下表示:
msgrcv 接收消息
msgrcv 原型
msgsnd 用于向指定id 的有写权限的消息队列发送消息,原型如下:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 参数msqid:指定消息队列id,由msgget 返回。
- 参数msgp:接收消息用的结构体指针
- 参数msgsz:接收消息用的结构体大小
- 参数msgtyp:三种情况:
=0
: 读取消息队列中第一个消息>0
: 读取消息队列中类型为msgtyp 的第一个消息,如果msgflg 设置了MSG_EXCEPT 则会读取非msgtyp类型的第一个消息, 这个消息类型在msgsnd 里用msgbuf 结构体指定的;如果msgflg 设置了MSG_COPY则会读取队列中的第msgtyp个消息。<0
: 读取消息队列中最小类型且小于等于msgtyp 绝对值的消息。
- 参数msgflg:通常使用下面的一些flag:
- IPC_NOWAIT : 消息队列为空则不会阻塞。
- MSG_EXCEPT : 跟上面msgtyp 联用,读取类型不是msgtyp 的第一条消息。
- MSG_NOERROR : 消息长度超过msgsz 时截断消息。
- MSG_COPY : 漏洞利用中会用到,内核会把消息队列中的消息拷贝一份返回用户空间而不会释放该条消息结构。
- 返回值:成功时返回读取的消息字节数,失败返回-1。
msgrcv 源码分析
入口位置在ksys_msgrcv
linux\ipc\msg.c:
SYSCALL_DEFINE5(msgrcv, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz, long, msgtyp, int, msgflg)
{
return ksys_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg);
}
调用栈:
- ksys_msgrcv
- do_msgrcv
- prepare_copy (存在MSG_COPY 且有CONFIG_CHECKPOINT_RESTORE编译选项)
- load_msg
- convert_mode
- find_msg
- copy_msg (存在MSG_COPY 且有CONFIG_CHECKPOINT_RESTORE编译选项)
- msg_handler (函数指针指向do_msg_fill)
- store_msg
- free_msg
- prepare_copy (存在MSG_COPY 且有CONFIG_CHECKPOINT_RESTORE编译选项)
- do_msgrcv
首先ksys_msgrcv 会调用do_msgrcv ,并将do_msg_fill 函数作为msg_handler 传递给do_msgrcv :
linux\ipc\msg.c:
long ksys_msgrcv(int msqid, struct msgbuf __user *msgp, size_t msgsz, long msgtyp, int msgflg)
{
return do_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg, do_msg_fill);
}
然后分析主要的函数do_msgrcv:
linux\ipc\msg.c:
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);
ns = current->nsproxy->ipc_ns;
if (msqid < 0 || (long) bufsz < 0)//常规检查
return -EINVAL;
if (msgflg & MSG_COPY) {// [1] 存在MSG_COPY flag 的时候要备份一份消息
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
if (IS_ERR(copy))
return PTR_ERR(copy);
}
mode = convert_mode(&msgtyp, msgflg); //[2] 根据flag 修改搜索模式
rcu_read_lock();
msq = msq_obtain_object_check(ns, msqid);//获取msq结构,同msgsnd
if (IS_ERR(msq)) {
rcu_read_unlock();
free_copy(copy);
return PTR_ERR(msq);
}
for (;;) {//尝试读取消息
struct msg_receiver msr_d;
msg = ERR_PTR(-EACCES);
if (ipcperms(ns, &msq->q_perm, S_IRUGO))//检查权限
goto out_unlock1;
ipc_lock_object(&msq->q_perm);
/* raced with RMID? */
if (!ipc_valid_object(&msq->q_perm)) {//检查消息队列是否被删除
msg = ERR_PTR(-EIDRM);
goto out_unlock0;
}
msg = find_msg(msq, &msgtyp, mode);//找到消息队列中满足msgtyp的消息
if (!IS_ERR(msg)) {//存在符合的消息
if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
//若没有MSG_NOERROR 则进行长度检查
msg = ERR_PTR(-E2BIG);
goto out_unlock0;
}
if (msgflg & MSG_COPY) {// [1] 若有MSG_COPY flag 则备份一份消息
msg = copy_msg(msg, copy);
goto out_unlock0;//直接跳出循环,无需从消息队列unlink目标消息
}
list_del(&msg->m_list);// [3] 将该消息从消息链表中unlink
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();
if (IS_ERR(msg)) {
free_copy(copy);
return PTR_ERR(msg);
}
bufsz = msg_handler(buf, msg, bufsz);// [4] 调用do_msg_fill 将消息内容拷贝到用户空间
free_msg(msg);//[5] 释放消息
return bufsz;
}
-
[1] : 若用户传入msgflg 中存在MSG_COPY ,则会将需要被读取的消息拷贝一份,读取备份消息,而原本队列中的消息不会被释放,后续分析。prepare_copy 函数中会调用load_msg 申请一个临时消息结构;copy_msg 函数将消息内容拷贝到临时消息结构。
-
[2] : 由于msgtyp 在不同的msgflg 时代表的意义不同,这里通过convert_mode 函数设定搜索模式,后续msg_find会用到
static inline int convert_mode(long *msgtyp, int msgflg) { if (msgflg & MSG_COPY)//MSG_COPY 时按顺序搜索 return SEARCH_NUMBER; if (*msgtyp == 0)//msgtyp 为0 时返回第一个 return SEARCH_ANY; if (*msgtyp < 0) { if (*msgtyp == LONG_MIN) /* -LONG_MIN is undefined */ *msgtyp = LONG_MAX; else *msgtyp = -*msgtyp; return SEARCH_LESSEQUAL;//反向搜索 } if (msgflg & MSG_EXCEPT)//搜索非msgflg类型 return SEARCH_NOTEQUAL; return SEARCH_EQUAL;//正常搜索模式,搜索类型相等的 }
-
[3] : 如果没有MSG_COPY ,也就是消息没有被备份,则先将消息从消息队列中unlink ,然后再进行后续的操作(拷贝到用户空间并释放该msg);这里还要修改消息队列的一些值。
-
[4] : 调用msg_handler 处理消息,其实就是将msg 内容发送给用户空间。这里的msg 如果是在MSG_COPY 模式下就是备份消息,如果非MSG_COPY 模式就是刚从消息队列中unlink 的消息。msg_handler 是一个函数指针,在ksys_msgrcv 中指定的是调用do_msg_fill:
linux\ipc\msg.c:
static long do_msg_fill(void __user *dest, struct msg_msg *msg, size_t bufsz) { ··· ··· msgsz = (bufsz > msg->m_ts) ? msg->m_ts : bufsz;//长度大于用户空间长度则截断 if (store_msg(msgp->mtext, msg, msgsz))//调用store_msg 将消息内容拷贝到用户空间 return -EFAULT; return msgsz; } 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; }
-
[5] : free_msg 函数会将消息结构释放,除了释放几个消息段之外,还会释放security 指针,该指针通常情况下都是空(这里如果security 被篡改可以进行任意地址的kfree):
linux\ipc\msgutil.c :
void free_msg(struct msg_msg *msg) { struct msg_msgseg *seg; security_msg_msg_free(msg);//调用security_msg_msg_free释放security 指针 seg = msg->next; kfree(msg); while (seg != NULL) { struct msg_msgseg *tmp = seg->next; cond_resched(); kfree(seg); seg = tmp; } } void security_msg_msg_free(struct msg_msg *msg) { call_void_hook(msg_msg_free_security, msg); kfree(msg->security);//释放security 指针 msg->security = NULL; }
当编译选项开启CONFIG_CHECKPOINT_RESTORE 功能的时候,就会支持MSG_COPY flag,用户传入MSG_COPY flag来接收消息的时候,不会简单的从消息队列中找到符合的消息,然后将该消息从消息队列中unlink,然后发送给用户后free掉。而是会先准备一个空消息结构,然后将目标消息拷贝到空消息结构中,后续将该备份消息内容发送给用户然后free掉,而消息队列中的目标消息则还会放在消息队列中。想要使用MSG_COPY ,需要在C语言代码中定义_GNU_SOURCE 测试宏。
prepare_copy 函数中会调研上文在msgsnd 中分析的load_msg 函数,使用用户传入的接收消息长度作为长度来申请一个msg 空间,后文的copy_msg 按照消息结构体链表来进行分段拷贝消息:
linux\ipc\msg.c;linux\ipc\msgutil.c :
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;
}
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
struct msg_msgseg *dst_pseg, *src_pseg;
size_t len = src->m_ts;
size_t alen;
if (src->m_ts > dst->m_ts)
return ERR_PTR(-EINVAL);
alen = min(len, DATALEN_MSG);
memcpy(dst + 1, src + 1, alen);//先拷贝主消息段
for (dst_pseg = dst->next, src_pseg = src->next;
src_pseg != NULL;
dst_pseg = dst_pseg->next, src_pseg = src_pseg->next) {
//然后还有其他段的话分段拷贝
len -= alen;
alen = min(len, DATALEN_SEG);
memcpy(dst_pseg + 1, src_pseg + 1, alen);
}
dst->m_type = src->m_type;
dst->m_ts = src->m_ts;
return dst;
}
实际操作与漏洞利用应用
简单文字描述一下漏洞利用中怎么用,主要还要结合漏洞,下面漏洞分析可以用来参考:
使用场景
经过上面分析,msg_msg 具有如下特点:
-
堆结构可自由申请与释放,申请时可以定义内容,但无法重新编辑
-
可申请的堆大小属于kmalloc-16 ~ kmalloc-4k(其中主消息段属于kmalloc-64 ~ kmalloc-4k,子消息段最小kmalloc-16)
-
含有堆指针,但没有能泄露内核地址的指针
-
链表结构,存在俩链表指针
下面分析一些常见的操作,举例并不是全部,毕竟漏洞利用这东西靠的是想象力。
堆占位与堆喷
由于申请数量没什么限制并且可以自由释放,我们可以使用msgsnd 对kmalloc-16 ~ kmalloc-4k 范围内的kmalloc 区块进行堆喷。并且调用msgrcv 进行释放占位,几乎所有内核堆漏洞中都会用到。有时候需要将某个kmalloc free list清空,使用msg 是个不错的选择。
还可以使用msg来进行UAF 利用的编辑,不过头部的0x30大小是不可控的。而msg_msgseg 结构体头部只有0x8大小不可控。可以说msg 在UAF中的U 和F 阶段都可以参与。
构造UAF与任意堆释放
msg系列操作的free操作基本都在msgrcv 中,有两个操作我们可以使用,分别是释放msg_msg 消息结构体和释放security 指针。如果我们能通过堆溢出修改一个msg_msg 结构体的m_list->next
指针,让他指向另一个msg_msg的m_list->next
指针指向的消息,构成如下情况,就能组成UAF的经典场景(CVE-2021-22555、CVE-2022-0995):
如下场景就可以对同一个msg 释放两次,然后分别用sk_buf 和pipe 等占位,就可以释放修改rop 或者新dirty_pipe 漏洞利用原语啥的完美衔接。不只是m_list->next
,msg_msgseg next 也是差不多的。
除此之外,msg_msg->security 这个指针几乎没什么用途,但在free_msg 函数中却会将其free,如果通过溢出等操作或是不能控制写的内容的越界写将security 覆盖,那么调用msgrcv便可以将覆盖的地址的堆释放掉。也是一个好用的堆释放操作。
任意地址读(MSG_COPY 带来的容错)
如果通过堆溢出覆盖了msg_msg 中的msg_msgseg next 指针为想要读取的地址-8(有头部);和覆盖m_ts 消息长度,那么读取消息的时候就可以读到指定地址(覆盖msg_msgseg next 的地址)的指定长度(覆盖m_ts长度)的内容。一般可以跟seq_operations 结构体或者pipe 结构体等联用来泄露地址。
值得一提的是,如果只是通过常规连续的堆溢出写覆盖msg_msgseg next 指针的话,那么前面的m_list 结构体也会被覆盖,也就是造成了链表的破坏,这样调用msgrcv的时候在unlink 的时候就会异常。但由于CONFIG_CHECKPOINT_RESTORE 编译选项提供了MSG_COPY 功能,能让我们调用msgrcv 的时候COPY一个消息备份并返回,消息队列中的原本消息不会被释放,这样我们就达成了即读取到了想读取的内容,又避免了消息被释放时unlink报错。
任意地址写
由于消息结构是分段存储的,如果一个消息过长,那么从用户空间拷贝到内核msg_msg 结构体中也需要分段拷贝。这时如果在刚拷贝第一段的时候使用userfaulted 让其阻塞,然后使用溢出进程将主消息段的msg_msgseg next 指针覆盖为任意地址-8(有头部),那么当拷贝子消息段的时候就变成了任意地址写(CVE-2022-0185)。在新版本无法在非特权模式注册userfaulted 了,但可以使用fuse 文件系统来完成userfaulted。
基本使用代码
简单举个例子。
#define _GNU_SOURCE//要使用MSG_COPY 需要添加_GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
typedef struct//发送的消息结构体
{
long mtype;
char mtext[1];
}usrmsg;
void spray_4k(int spray)//堆喷举例
{
char buffer[0x2000] = {0};
usrmsg *message = (usrmsg *)buffer;
int size = 0x1000;
memset(buffer, 0x41, sizeof(buffer));
for (int i = 0; i < spray; i++)
{
int tmp = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
msgsnd(tmp, message, size - 0x30, 0);
}
}
#define TARGET_TYPE 9
#define TARGET_TYPE2 8
void main()
{
char buffer[0x2000] = {0};
usrmsg *message = (usrmsg *)buffer;
int size = 0x1000;
memset(message->mtext, 0x41, 0x10);
message->mtype = TARGET_TYPE;
int tmp = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
msgsnd(tmp, message, size - 0x30, 0);//发送消息1, 类型TARGET_TYPE
message->mtype = TARGET_TYPE2;
memset(message->mtext, 0x42, 0x10);
msgsnd(tmp, message, size - 0x30, 0);//发送消息2, 类型TARGET_TYPE2
char recievbuffer[0x2000] = {0};
usrmsg *recvmsg = (usrmsg *)recievbuffer;
//使用MSG_COPY 接收消息,mtype 参数代表的是“第几个消息”
msgrcv(tmp, recvmsg, size-0x30, 1, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
printf("%s\n",recvmsg->mtext);
memset(recvmsg->mtext, 0x0, 0x10);
//正常接收消息,没有使用MSG_COPY。mtype 参数代表接收的消息类型
msgrcv(tmp, recvmsg, size-0x30, TARGET_TYPE2, IPC_NOWAIT | MSG_NOERROR);
printf("%s\n",recvmsg->mtext);
//mtype 为0,接收第一个消息
msgrcv(tmp, recvmsg, size-0x30, 0, IPC_NOWAIT | MSG_NOERROR);
printf("%s\n",recvmsg->mtext);
}
参考
bsauce:Linux内核中利用msg_msg结构实现任意地址读写
https://www.willsroot.io/2022/01/cve-2022-0185.html