kernel heap 利用技巧: msg_msg和pipe_buffer

2025-12-06

todo: mas_msgpipe_buffer在ctf/linux kernel漏洞利用的实际应用。

之前提到,在正常的linux kernel环境下,堆风水布局是十分困难的。主要原因是内核大都使用kmalloc来申请内存,有漏洞的驱动大概率也是用kmalloc申请的内存。
但是msg_msgpipe_buffer给我们提供了一个有效的进行堆布局的方法.

1. msg_msg

msg_msg是linux提供的一种进程间通信(IPC)的结构体。

1.1 msg常见用法

msg在linux本质上是通过内核消息队列来进行通信的。
主要函数如下:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

可以看一下demo:

//msg_send.c 用于发送msg
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <stdio.h>

int main(){
    // prepare message
    size_t target_cache_size = 128;
    struct msgbuf{
        long mtype;
        char message[100];
    };
    size_t msg_sz = target_cache_size-0x30;
    struct msgbuf msgbuf;
    strcpy(msgbuf.message,"hello world!");
    
    puts("before creating messages");
    getchar();

    int msqid;
    int key = ftok(".",0);
    msqid = msgget(key,0666| IPC_CREAT);
    for(int i=0;i<20;i++){
        msgbuf.mtype = i+1;//注意mtype不能为0,0会出问题。
        msgsnd(msqid,&msgbuf,msg_sz,0);
    } 
    puts("after creating messages");
    getchar();
}
//msg_recv.c 用于接收msg
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <stdio.h>

int main(){
    // prepare message
    size_t target_cache_size = 128;
    struct msgbuf{
        long mtype;
        char message[100];
    };
    size_t msg_sz = target_cache_size-0x30;
    struct msgbuf msgbuf;
    strcpy(msgbuf.message,"hello world!");
    
    puts("before creating messages");
    getchar();

    int msqid;
    int key = ftok(".",0);
    msqid = msgget(key,0666);
    for(int i=0;i<20;i++){
        msgrcv(msqid,&msgbuf,msg_sz,0,0);
        printf("received with type: %d, content: %s\n",msgbuf.mtype,msgbuf.message);
    } 
    puts("after creating messages");
    getchar();
}

1.2 为什么要介绍msg?

还记得上一节https://wsxk.github.io/kernel_heap2/中提到的理想的堆布局构造对象吗?
首先看内核中msg_msg对象的定义:

/* one msg_msg structure for each message */
// 每个消息的头,最多可容纳0x1000-0x30 = 0xfd0个用户数据
struct msg_msg {
	struct list_head m_list;
	long m_type;
	size_t m_ts;		/* message text size */
	struct msg_msgseg *next;   //****完美满足第3点!ptr可覆盖!****
	void *security;
	/* the actual message follows immediately */
};

//m_list contains pointers to messages in the message queue 
//m_ts determines this size of the message text

// 消息段,最多可容纳0x1000-0x08 = 0xff8个用户数据,利用时,因为msg_msg的字段太多,因此大多数利用都是依靠msg_msgseg来实现
struct msg_msgseg {
    struct msg_msgseg *next;   // 0x00
    char data[];               // 0x08
};

我们接下来看一下msg_msg是如何分配的:

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);
	msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);//0x28+8(mtype)+实际消息长度
  //****完美满足第一点!size可控!!!****
}

在调用msgrcv函数时,发生如下事件:

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);//****完美满足第二点!绕过harden_usercopy!!!****

}
//copy_msg can be triggered via  msgrcv(msgqid, msgp, msgsz, 0, MSG_COPY);

2. pipe_buffer

2.1 pipe_buffer 常见用法

pipe_buffer也是内核IPC通信的方法之一,通过管道pipe来进行通信;
在用户态创建pipe后,内核就会维护pipe_buffer

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    int pipe_fd[2];
    const char message[] = "hello from pipe_buffer";
    char buffer[128] = {0};
    size_t message_len = strlen(message);
    ssize_t written;
    ssize_t received;

    /* pipe_fd[0] is the read end; pipe_fd[1] is the write end. */
    if (pipe(pipe_fd) == -1) {
        perror("pipe");
        return EXIT_FAILURE;
    }

    written = write(pipe_fd[1], message, message_len);
    if (written == -1) {
        perror("write");
        close(pipe_fd[0]);
        close(pipe_fd[1]);
        return EXIT_FAILURE;
    }

    /* No more data will be written. A later read can therefore see EOF. */
    close(pipe_fd[1]);

    received = read(pipe_fd[0], buffer, sizeof(buffer) - 1);
    if (received == -1) {
        perror("read");
        close(pipe_fd[0]);
        return EXIT_FAILURE;
    }

    buffer[received] = '\0';
    printf("write: %zd bytes\n", written);
    printf("read:  %zd bytes\n", received);
    printf("data:  %s\n", buffer);

    close(pipe_fd[0]);
    return EXIT_SUCCESS;
}

2.2 为什么要介绍pipe_buffer?

pipe_buffer的定义如下:

struct pipe_buffer {
    /* 0x00 */ struct page *page;
    /* 0x08 */ unsigned int offset;
    /* 0x0c */ unsigned int len;
    /* 0x10 */ const struct pipe_buf_operations *ops;
    /* 0x18 */ unsigned int flags;
    /* 0x1c */ unsigned int padding;
    /* 0x20 */ unsigned long private;
}; /* sizeof = 0x28 */

3. 具体案例

todo,等到复现linux cve的时候应该就用上了