[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