简介
本案例研究 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