好友
阅读权限 10
听众
最后登录 1970-1-1
[翻译]CVE-2016-6187 Exploiting Linux kernel heap off-by-one 利用堆大小差一错误爆破Linux内核(上) 大家五一节快乐,小编也出去浪了所以更新迟了~正文: Background (背景)——off-by-one(大小差一)错误介绍
详细了解请移步这里 off-by-one( https://en.wikipedia.org/wiki/Off-by-one_error),大小差一错误是一类常见的程序设计错误。这方面有一个经典的例子 OpenSSH.去 Google搜索关键词“ OpenSSH off-by-one”可以了解相关状况。具体来说,
1. if(id < 0 || id > channels_alloc)...
2. if(id < 0 || id >= channels_alloc)...
第二句应该是正确的写法。举个更通俗的例子:
int a[5],i;
for(i = 1;i < = 5;i++)
a=0;
上述代码定义了长度为 5的数组 a,循环的目的是给数组元素初始化,赋值为 0.但是 ,循环下标从 1开始到 5,出现了 a[5]=0,这样的不存在的数组元素 .这就是典型的“差一错误” (off-by-one).
Introduction (前言)
我认为我决定介绍该漏洞的理由是因为当我把它发在推特上时,我收到了一些私信说这个内核路径不存在漏洞(找不到漏洞在哪)或者该漏洞不能利用。另一个的理由就是我想在实际漏洞利用中尝试 userfaultfd() 系统调用,我需要一个真实的 UAF漏洞来进行实验。
首先,我不知道这个漏洞影响到哪些 Linux发行版本的内核。我检查了 ubuntu的最新发行版本 Yakkety,发现其没有受该漏洞影响。漏洞在 https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=bb646cdb12e75d82258c2f2e7746d5952d3e321a被引入,在 https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=30a46a4647fd1df9cf52e43bf467f0d9265096ca中被修复。
因为我需要一个含有该漏洞的 ubuntu内核,我在 ubuntu16.04( x86_64)上编译了 4.5.1内核。另外该漏洞仅仅影响像 ubuntu一样默认使用 AppArmor作为 LSM( Linux安全模块)的发行版本,像 centos使用 SELinux就不受该漏洞的影响。 Vulnerability (漏洞介绍) 当写入 /proc/self/attr/current 会使用内核中的 proc_pid_attr_write() 函数。接下来介绍的代码是漏洞被引入之前的:
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
static ssize_t proc_pid_attr_write(struct file * file, const char __user * buf,
size_t count, loff_t *ppos)
{
struct inode * inode = file_inode(file);
char *page;
ssize_t length;
struct task_struct *task = get_proc_task(inode);
length = -ESRCH;
if (!task)
goto out_no_task;
if (count > PAGE_SIZE) [1]
count = PAGE_SIZE;
/* No partial writes. */
length = -EINVAL;
if (*ppos != 0)
goto out;
length = -ENOMEM;
page = (char*)__get_free_page(GFP_TEMPORARY); [2]
if (!page)
goto out;
length = -EFAULT;
if (copy_from_user(page, buf, count)) [3]
goto out_free;
/* Guard against adverse ptrace interaction */
length = mutex_lock_interruptible(&task-;>signal->cred_guard_mutex);
if (length < 0)
goto out_free;
length = security_setprocattr(task,
(char*)file->f_path.dentry->d_name.name,
(void*)page, count);
...
buf参数代表用户提供的缓冲区(和长度count)被写入到/proc/self/attr/current 。在[1]处执行检查以确保用户缓冲区最多只会写入one page(默认4096个字节)。[2]和[3]中先在内核空间分配one page然后将用户空间缓冲区复制到新分配的页。该页将被传递给内核安全模块(例如AppArmour,SELinux,Smack)的security_setprocattr函数 。如果是ubuntu将触发apparmor_setprocattr() 函数,代码显示如下:
static int apparmor_setprocattr(struct task_struct *task, char *name,
void *value, size_t size)
{
struct common_audit_data sa;
struct apparmor_audit_data aad = {0,};
char *command, *args = value;
size_t arg_size;
int error;
if (size == 0)
return -EINVAL;
/* args points to a PAGE_SIZE buffer, AppArmor requires that
* the buffer must be null terminated or have size <= PAGE_SIZE -1
* so that AppArmor can null terminate them
*/
if (args[size - 1] != '\0') { [4]
if (size == PAGE_SIZE)
return -EINVAL;
args[size] = '\0';
}
...
在 [4]处,如果用户提供的缓冲区最后的字节不为空并且缓冲区大小不等于页的大小时,在缓冲区结尾添加一位字符串结束符;如果用户提供的缓冲区超过(或者等于)单页的大小(在 [2]处分配的),直接返回错误。 接下来显示的代码为引入该漏洞的(因为更改了 [3]处的代码) proc_pid_attr_write() :
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 ssize_t proc_pid_attr_write(struct file * file, const char __user * buf,
size_t count, loff_t *ppos)
{
struct inode * inode = file_inode(file);
void *page;
ssize_t length;
struct task_struct *task = get_proc_task(inode);
length = -ESRCH;
if (!task)
goto out_no_task;
if (count > PAGE_SIZE)
count = PAGE_SIZE;
/* No partial writes. */
length = -EINVAL;
if (*ppos != 0)
goto out;
page = memdup_user(buf, count); [5]
if (IS_ERR(page)) {
length = PTR_ERR(page);
goto out;
}
/* Guard against adverse ptrace interaction */
length = mutex_lock_interruptible(&task-;>signal->cred_guard_mutex);
if (length < 0)
goto out_free;
length = security_setprocattr(task,
(char*)file->f_path.dentry->d_name.name,
page, count);
...
不像 __get_free_page() , memdup_user() 分配了一块被 count参数指定大小的内存并且复制用户提供的数据到其中。因此,内核内存被分配的大小不再是严格的 4096个字节(最大也可能是 4096个字节)。现在假设用户提供的数据是 128个字节大小并且缓冲区的最后字节不为空。当 apparmor_setprocattr() 被触发,按照下面的代码逻辑 args[128] 将被设置为 0:
1
2
3
4
5
if (args[size - 1] != '\0') {
if (size == PAGE_SIZE)
return -EINVAL;
args[size] = '\0';
}
因为 args的内存在堆中被动态分配, args的内存的下一个字节会被重写为 NULL。因为该漏洞只能重写一个字节大小的内核内存,传统的重写函数指针的漏洞利用方法在这里并不适用。一个可行的方案是在一些对象上(像 args对象同样大小的)重写一个引用计数器并且然后触发一个 UAF(谢谢 Nicolas Tripar的建议)。如果你下一周将会在 Ruxcon , 尽管在计数器溢出的主题, 在内核中利用计数器溢出检验我的演讲吧。对象引用计数器( atomic_t type = signed int 所代表 )通常是结构的第一个成员。因为计数器的值通常对于大多数对象都是在 255 以下的,重写像一个对象最低有效的字节会清除计数器并且导致一个标准的 UAF 。然而为了利用这个漏洞,我决定用一个不同的方法:重写 SLUB freelist 指针。 未完待续……
译者:rodster
校对:song
原文链接:https://cyseclabs.com/blog/cve-2016-6187-heap-off-by-one-exploit
原文作者:Vitaly Nikolenko 微信公众号:看雪iOS安全小组 我们的微博:weibo.com/pediyiosteam 我们的知乎:zhihu.com/people/pediyiosteam 加入我们:看雪iOS安全小组成员募集中:http://bbs.pediy.com/showthread.php?t=212949
[看雪iOS安全小组]置顶向导集合贴: http://bbs.pediy.com/showthread.php?t=212685
免费评分
查看全部评分
本帖被以下淘专辑推荐:
· 学习及教程 | 主题: 1073, 订阅: 1130
· 教程类 | 主题: 262, 订阅: 43