0x01 导言
本文将向读者介绍一个影响Linux内核的Use-After-Free漏洞,其漏洞编号为CVE-2014-2851,影响内核版本范围直至3.14.1版本。首先,我要感谢托马斯对我的帮助。
这个漏洞本身并不是非常有用(因为这需要花许多时间来溢出一个32位整数),但是,从漏洞利用的角度来看,却是一个非常令人着迷。在我们的测试机器上面,为了实现溢出,竟然花费了50多分钟的时间,部分是由于回调函数RCU导致的不确定性所致,这无疑增加了利用的难度。
我们的测试机器是一台运行32位Ubuntu 14.04 LTS(3.13.0-24-generic kernel)的SMP电脑。在后文中,我们首先将对这个漏洞及其利用方法进行介绍。然后,我们还会详细探讨在利用这个漏洞过程中所遇到的各种挑战及其解决方案。
0x02 漏洞详解
当创建ICMP套按字的时候,会到达如下所示的漏洞路径。需要注意的是,虽然普通用户不允许创建ICMP套按字,但是即使没有根访问权限的用户也照样能够访问下列代码:
int pinginitsock(struct sock *sk) { struct net *net=socknet(sk); kgidt group=currentegid(); struct groupinfo *groupinfo=getcurrent_groups(); [1] int i, j, count=groupinfo->ngroups; kgidt low, high;
inet_get_ping_group_range_net(net, &low, &high);
if (gid_lte(low, group) && gid_lte(group, high)) [2]
return 0;
...
当执行上面的代码,尤其是[1]的时候,就会在用户空间创建一个ICMP套按字:
socket(AFINET, SOCKDGRAM, IPPROTO_ICMP);
在代码[1]处的getcurrentgroups()函数是以宏代码的形式定义的,原型位于include/linux/cred.h文件中:
define getcurrentgroups() \
({ \ struct group_info *__groups; \ const struct cred *cred; \ cred=currentcred(); \ groups=getgroupinfo(cred->groupinfo); \ [3] __groups; \ })
在代码[3]处,getgroupinfo()函数会给计数器group_info自动加1,这个计数器被定义为一个带符号整数,具体如下:
type=struct groupinfo { atomict usage; int ngroups; int nblocks; kgid_t smallblock[32]; kgidt *blocks[]; }
typedef struct { int counter; } atomic_t;
每当新建一个ICMP套按字的时候,这个计数器就会被[1]处的代码加1。可是,对于没有特权的用户来说,[2]处的检验将无法通过,所以函数会返回0。于是,我们可以不断新建ICMP套按字,直到令这个带符号整数(0xffffffff+1=0)发生溢出为止。
这里的group_info可以在派生的子进程之间进行共享。当用户组的使用计数器变为0的时候,在内核中的许多代码就可以释放了。托马斯发现的一个这样的代码就是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: revertcreds(oldcred); putcred(overridecred); [5] return res;
在[4]处,会为新建的结构体cred分配内存,同时它的使用计数器(请不要与groupinfo->usage混淆了)将被设为1,同时计数器groupinfo->usage也会加1。之后,在[5]处的putcred将会令计数器cred->usage减1,并调用函数putcred():
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);
}
对于结构体cred的释放,主要是通过下面[6]处的RCU来实现的:
void _putcred(struct cred *cred) { ... BUGON(cred==current->cred); BUGON(cred==current->real_cred);
call_rcu(&cred->rcu, put_cred_rcu); [6]
} EXPORTSYMBOL(putcred);
下面给出的是回调函数putcredrcu,该函数会调用[7]处的put_groupinfo()函数,以便在结构体groupinfo的使用计数器为0的时候将其释放,代码如下所示:
static void putcredrcu(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);
}
函数putgroupinfo()也是以宏的形式进行定义的,它的作用是令使用计数器group_info递减1,并在到达0的时候释放分配的结构体,具体如下所示:
define putgroupinfo(group_info) \
do { \ if (atomicdecandtest(&(groupinfo)->usage)) \ groupsfree(groupinfo); \ } while (0)
0x03 利用方法
通过上面的代码,可以明显看出,通过令使用计数器的值变为0,然后从用户空间调用faccessat()就可以释放结构体group_info了,代码如下所示:
// increment the counter close to 0xffffffff (-10=0xfffffff6) for (i=0; i < -10; i++) { socket(AFINET, SOCKDGRAM, IPPROTO_ICMP); }
// increment the counter by 1 and try to free it for (i=0; i < 100; i++) { socket(AFINET, SOCKDGRAM, IPPROTO_ICMP); faccessat(0, "/", ROK, ATEACCESS); }
上面的代码,将会令使用计数器溢出,并释放结构体group_info。因为释放这个结构体是通过一个RCU调用来实现的,所以,这里存在一定的不确定性,这个我们将在后面专门介绍。
一旦结构体groupinfo被释放,分配程序SLUB就会将其链接到释放列表中。关于分配程序SLUB,可以在网上找到许多很好的介绍材料,所以这里就不再赘述了。我们需要注意的是,当一个对象被释放,它就被放入一个释放列表freelist中,并且它的前4字节(对于32位架构而言)将被一个指针覆盖掉,该指针指向slab中的下一个释放对象。换句话说,groupinfo的前4字节将被一个有效的内核内存地址所覆盖。这前面四个字节实际上是一个使用计数器,并且,我们还可以通过新建ICMP套按字来令这个指针加1。
当group_info被释放的时候,可能面临两种情况:
它是freelist中最后一个对象。它是freelist中的释放对象之一。
对于前一种情况,我们释放的group_info的“下一个”释放对象指针将会是NULL。因此,我们真正需要关注的是后面一种情况,这是这个指针将会指向slab中的下一个释放对象,这也是最为常见的情况。
在我们的测试系统中,该groupinfo结构体的大小是140字节,被分配在通用的kmalloc-192缓存中。当一个分配128-192字节的请求(通过kmalloc、kmemcache_alloc,等等)到达时,分配程序SLUB将遍历整个freelist,并将内存地址分配给我们覆盖掉的使用计数器所指向的那个地址。
我们也可以让它指向我们的用户空间的地址,方法是不断让使用计数器递增直至溢出,这样它就会指向我们可以mmap的某个用户空间地址了。举例来说,假设内核地址为0xf3XXXXXX,加上0xfffffff,我们就得到了一个可供我们mmap的用户空间地址0x3XXXXXX了。总起来说,利用过程如下所述:
? 通过新建ICMP套按字令使用计数器groupinfo不断递增,直至接近0xffffffff。 ? 反复令使用计数器增1,然后尝试利用faccessat()函数释放groupinfo。 ? 一旦释放,groupinfo中的使用计数器就会被指向slab中下一个释放对象的有效内核内存地址所覆盖。 ? 一直让使用计数器groupinfo递增(方法是创建更多的ICMP套按字),直到它指向某个用户空间内存地址为止。 ? 将这个内存区(例如0x3000000-0x4000000)映射到用户空间,并通过memset将其设为0。在内核空间中为结构体X(理想情况下会包含有些函数指针),其大小为128-192字节。 ? 分配程序SLUB将会把这个结构体X分配到介于0X3000000-0X4000000范围内的用户空间地址。 ? 只要这个结构体X含有任何函数指针,我们就能够令它们指向我们的有效载荷(就本例而言就是ROP链)
我们的利用代码中的结构体X是结构体file,它的大小与groupinfo相同,并且含有少量函数指针(例如*fop,更具体来说,就是一个指向含有函数指针的结构体的指针)。这个结构体,即file可以通过下列代码分配内存:
for (i=0; i < N; i++) fd=open("/etc/passwd", O_RDONLY);
如果需要1024个以上的文件描述符,你可以不断派生进程,从而分配更多的file结构体。
一旦这个结构体file被分配到我们的用户地址空间(范围介于0x3000000-0x4000000),我们只要在此范围内进行搜索,找到第一个非零字节即可。在我们的file结构体的前面部分的内容如下所示:
unsigned *p; struct file *f=NULL;
// find the file struct for (p=0x3000000; p < 0x4000000; p++) { if (*p) { f=(struct file *)p; break; } }
到了这一步,剩下的利用过程就易如反掌了。
0x04 挑战
前面我们曾经说过,回调RCU会有一定的不确定性。举例来说,循环中pinginitsock()后面跟着一个faccessat()函数的话,执行的时候未必也是这个顺序。理想的情况下,我们想要的顺序如下所示:
使用计数器group_info加1检测该计数器是否为0,如果是的话通过faccessat()将其释放
但是回调函数RCU经常会是以批处理方式执行的。因为这个回调函数是在系统中的所有CPU都至少经历一次休眠状态(例如上下文转换、空循环或用户代码)后才会被调用。"因此,通常情况是,执行了多次pinginitsock(令计数器溢出,并通过递增使其大于0),然后是多个针对put_credrcu()的RCU回调。 这会导致groupinfo释放被跳过的代码。不过,我们找到了一种方法来给上面的事件(即递增并检查使用计数器)来进行排序。
这个漏洞的利用所遇到的另一个问题出现在恢复阶段。也就是说,如果同一个slab中的其他对象被请求的话,将会发生什么情况? 在这种情况下,可以把我们的对象的“下一个”freelist指针设为空。这样,分配程序就会设置freelist指针为空,并将迫使分配程序创建一个新的slab,同时“忘掉”当前的slab。
现在,如果属于这个特定slab的一些对象被释放的话,将会发生什么情况? 这就提出了一个真正的挑战,但是可以作为post-exploitation LKM实现。
0x05 小结
就实用性而言,这个漏洞可能不是非常理想,因为它需要花费许多时间来使32-位的使用计数器发生溢出。并且,在我们的测试系统中,它竟然花费了50多分钟才完成了这个利用过程! 但是,一旦释放了group_info,这将会是一个非常稳定的漏洞利用,即使是在一个SMP平台上面也是稳如泰山。