利用msg_msg实现任意地址读写

利用msg_msg实现任意地址读写msgsnd和msgrcv的源码分析内核通过msgsnd和msgrcv来进行IPC通信 。内核消息分为两个部分,一个是消息头msg_msg(0x30),以及后面跟着的消息数据 。整个内核消息的长度是从kmalloc-64到kmalloc-4096` 。
/* 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 */};msgsnd发送数据调用链及方法调用链:通过msgsnd() -> ksys_msgsnd() -> do_msgsnd() -> load_msg() -> alloc_msg()来分配消息头和消息的数据,接着通过load_msg() -> copy_from_user()来将用户数据拷贝进内核 。
使用方法:例如我们想要发送一个包含0x1000个'A'的消息,代码如下:
struct msgbuf{long mtype;char mtext[0x1000];} msg;msg.mtype = 1;memset(msg.mtext, 'A', sizeof(msg.mtext));qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT));msgsnd(qid, &msg, sizeof(msg.mtext), 0);此外如果消息长度超过0xfd0,那么就会采取分段储存的方式,采用单向链表进行连接 。第一个称作消息头,用msg_msg结构进行储存,第二个和第三个称作segment,用msg_msgseg结构进行储存 。消息的最大长度由/proc/sys/kernel/msgmax确定,默认大小为8192个字节,所以最多连接三个成员 。
msgrcv接收数据的调用链及方法调用链msgrcv() -> ksys_msgrcv() -> do_msgrcv() -> find_msg() & do_msg_fill() & free_msg() 。通过find_msg来定位消息,并将消息从队列中unlink,再调用do_msg_fill() -> store_msg()来将消息从内核空间拷贝到用户空间,最后调用free_msg释放消息 。
使用方法:例如我们想要接收一个包含0x1000个'A'的消息,代码如下:
void *memdump = malloc(0x1000);msgrcv(qid, memdump, 0x1000, 1, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);此外值得注意的是:如果用flag:MSG_COPY来调用msgrcv(),就会调用prepare_copy()分配临时消息,并调用copy_msg()将请求的数据拷贝到该临时消息 。在将消息拷贝到用户空间之后,原始消息会被保留,不会从队列中unlink,而是直接goto out_unlock0,然后调用free_msg()删除该临时消息,有些题目中这一点对于利用很重要 。为什么?因为有些题目漏洞在UAF的时候,没有泄露正确地址,所以会破坏msg_msg->m_list双链表指针,unlink会触发崩溃 。如果某漏洞可以跳过前16字节,那就不需要注意这一点 。
数据泄露越界读取数据在拷贝数据的时候,我们对数据长度的判断主要是依靠msg_msg->m_ts 。所以我们可以想到如果我们可以控制某一个消息的msg_msg使得msg_msg->m_ts被改为一个较大的数,那么我们就能够实现越界读取数据 。
任意地址读取对于大于0xfd0的数据,内核会在msg_msg的基础上再加上msg_msgseg结构体,形成一个单向链表,如果我们能够同时控制msg_msg->m_tsmsg_msg->next,我们便可以实现任意地址读 。但是这里需要注意的是,无论我们采用MSG_COPY还是常规消息接收,拷贝消息的主要依据还是msg_msg->next,所以为了避免遍历消息时出现访存崩溃,实现对特定地址以后数据的读取,我们需使得segment的前8字节为NULL
任意地址写我们可以通过结合

经验总结扩展阅读