CVE-2022-27666:内核攻击-分析翻译
漏洞位于Ubuntu Desktop 21.10 相当的内核大概5.13左右的内核。
一、漏洞成因
int esp6_output_head(struct xfrm_state *x, struct sk_buff *skb, struct esp_info *esp)
{
...
int tailen = esp->tailen;
allocsize = ALIGN(tailen, L1_CACHE_BYTES);
spin_lock_bh(&x->lock);
if (unlikely(!skb_page_frag_refill(allocsize, pfrag, GFP_ATOMIC))) {
spin_unlock_bh(&x->lock);
goto cow;
}
...
}
bool skb_page_frag_refill(unsigned int sz, struct page_frag *pfrag, gfp_t gfp)
{
if (pfrag->offset + sz <= pfrag->size)
return true;
...
if (SKB_FRAG_PAGE_ORDER &&
!static_branch_unlikely(&net_high_order_alloc_disable_key)) {
pfrag->page = alloc_pages((gfp & ~__GFP_DIRECT_RECLAIM) |
__GFP_COMP | __GFP_NOWARN |
__GFP_NORETRY,
SKB_FRAG_PAGE_ORDER);
...
}
...
return false;
}
触发路径
esp6_output —> esp6_output_head —> skb_page_frag_refill(allocsize, pfrag, GFP_ATOMIC)
----> esp_output_fill_trailer();
static int null_skcipher_crypt(struct skcipher_request *req)
{
struct skcipher_walk walk;
int err;
err = skcipher_walk_virt(&walk, req, false);
while (walk.nbytes) {
if (walk.src.virt.addr != walk.dst.virt.addr)
// out-of-bounds write
memcpy(walk.dst.virt.addr, walk.src.virt.addr,
walk.nbytes);
err = skcipher_walk_done(&walk, 0);
}
return err;
}
walk.dst.virt.addr, walk.src.virt.addr分别为skcipher_map(&walk->in);,skcipher_map(&walk->out);复制的地址。
walk->total = req->cryptlen; //最终长度
scatterwalk_start(&walk->in, req->src);
scatterwalk_start(&walk->out, req->dst);
static inline void scatterwalk_start(struct scatter_walk *walk,
struct scatterlist *sg)
{
walk->sg = sg;
walk->offset = sg->offset;
}
[ 374.509524] ? null_skcipher_crypt+0x5/0x80
[ 374.509528] ? crypto_skcipher_encrypt+0x3b/0x60
[ 374.509532] crypto_authenc_encrypt+0xba/0xe0 [authenc]
[ 374.509538] crypto_aead_encrypt+0x3c/0x70
[ 374.509542] esp6_output_tail+0x225/0x600 [esp6]
[ 374.509548] esp6_output+0x11d/0x17b [esp6] //esp6_output先调esp6_output_head,之后是esp6_output_tailer.
[ 374.509554] xfrm_output_one+0x335/0x370
[ 374.509558] xfrm_output_resume+0x44/0x200
[ 374.509561] ? raw6_getfrag+0x97/0xf0
[ 374.509566] xfrm_output+0xbf/0x240
[ 374.509570] ? flow_hash_from_keys+0x35/0x90
int crypto_skcipher_encrypt(struct skcipher_request *req)
{
struct crypto_skcipher *tfm = crypto_skcipher_reqtfm(req);
struct crypto_alg *alg = tfm->base.__crt_alg;
unsigned int cryptlen = req->cryptlen;
int ret;
crypto_stats_get(alg);
if (crypto_skcipher_get_flags(tfm) & CRYPTO_TFM_NEED_KEY)
ret = -ENOKEY;
else
ret = crypto_skcipher_alg(tfm)->encrypt(req); //这里调用null_skcipher_crypt();
crypto_stats_skcipher_encrypt(cryptlen, ret, alg);
return ret;
}
int crypto_aead_encrypt(struct aead_request *req)
{
struct crypto_aead *aead = crypto_aead_reqtfm(req);
struct crypto_alg *alg = aead->base.__crt_alg;
unsigned int cryptlen = req->cryptlen;
int ret;
crypto_stats_get(alg);
if (crypto_aead_get_flags(aead) & CRYPTO_TFM_NEED_KEY)
ret = -ENOKEY;
else
ret = crypto_aead_alg(aead)->encrypt(req); //这里调用crypto_skcipher_encrypt
crypto_stats_aead_encrypt(cryptlen, alg, ret);
return ret;
}
这里尾部填充的数据并不可控,如果分配了8page, 但是发送了16page数据,尾部数据可以看作是垃圾数据。
static inline void esp_output_fill_trailer(u8 *tail, int tfclen, int plen, __u8 proto)
{
/* Fill padding... */
if (tfclen) {
memset(tail, 0, tfclen);
tail += tfclen;
}
do {
int i;
for (i = 0; i < plen - 2; i++)
tail[i] = i + 1;
} while (0);
tail[plen - 2] = plen - 2;
tail[plen - 1] = proto;
}
skb_page_frag_refill分配了八页连续内存。
此处页大小的判断有误,会造成页数据写越界。
SKB_FRAG_PAGE_ORDER为3
为了准确构造order-3大小相邻位置的页表,我们必须消除order-2在分配什释放是造成的free_list的一系列改变。
页表分配器包含一个页表释放管理的器叫 free_area。如图所示,包含不同order阶页表的free_list。
不同的内核slabs分配器要求不同的orders的页表,如果其对应的free_list消耗完毕的,将会从新申请。例如,ubuntu21.10的 kmalloc-256要求order-0 页,kmalloc-512要求order-1 page, kmalloc-4k要求 order-3页。
free_list会根据相同order中的free_list是否有相邻地址的page来进行页表合并,进行重新order free_list的归类,所以会造成order-3中的page随时可能发生order-2不够或有相邻地址的page合并造成的,释放page管理块发生改变,为了阻止order对ordere-3的影响,这里对order-2大小的内存进行了大量的制造,防止order-2消耗完毕。这是order-3中的page块就会只受我们进程的影响。
二、泄露及地址
这里作者采用了user_key_payload作为攻击载体。
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[] __aligned(__alignof__(u64)); /* actual data */
};
如上可知,ubuntu对于user_key_payload有个20000bytes和200keys的限制。user_key_payload 为2049 bytes,分配将会为4k的内存块。
4k的slab,会有8个object. 也就是2049*8=16392bytes。
(20000-16392)/2049 = 1.
这样最多只能分配两个slab, 对噪声的容错率比较低。
为了增加容错。这里作者在每个slab中分配一个user_key_payload,其他的用其他结构填充,由于freelist中page的随机性,所以会形成如下的内存结构。由于越界可以实现8page的内容改写,所以并不影响exploit结果。这样将可以分配9个slabs, 增大了对page allocater的容错能力。
进一步利用:
为了增加成功率,减缓噪声的影响。我们制造了9组slab构造。只要一个成功,我们就可以获得正确的struct msg_msg next指针。如下图,每三个一组。为1pair
这里只要有一组成功构造为我们想要的结构,就可以发生地址泄露。读取任意msg_msg结构的信息。接下来,泄露及地址:
攻击路径
Phase 1
- 消耗 order-3的page list_free. order-3的分配将会从order-4上借用,并且内存讲相邻。
- 分配三组 相邻的8-page dummy object.
- 释放的第二个dummy object。分配一个8-page slab 的包含user_key_payload和其他7个object内存。
- 释放第三个dummy object, 分配一个用struct msg_msg 完全占用8-page slab的内存块。消息大小要位于4056到4072当中,为了使struct msg_msgseg落在kmalloc-32的分配中。
- 分配大量的struct seq_operations. 这些结构体将会拥有和step 4 中msg_msgseg一样大小的内存块。
- 释放第一个dummy_object, 分配溢出的buffer, 开始越界写的操作。我们计划修改struct user_key_payload datalen的域。
- 如果step 6 成功,取回user_key_payload 的payload将会造成越界读取。越界读取又可以使 struct msg_msg 的内容,包括他的next指针被读取到。
- step 7成功后,我们有了真实的struct msg_msg object 的next指针。
Phase 2
- 分配两个相邻的 8-page dummy objects.
- 释放第二个 dummy_object, 分配一个占满slab的struct msg_msg.
- 释放第一个dummy_object, 分配一个溢出内存块, 利用一个很大的值占用他的m_ts filed,并且利用上Phase1 step7得到的next指针覆盖 struct msg_msg 的next。
- 如果Phase2的step3成功,我们将会有一个大概 kmalloc-32大小的越界读。将很可能读取到struct seq_operations 中的函数指针。之后就可以计算出内核偏移地址。
获取root权限
一旦获得了基地址,msg_msg 的任意写就会成为可能。思路就是挂起第一个copy_from_user, 覆盖next的指针,唤醒进程,接下来的copy_from_user就会是任意地址写操作(位于17行)。
struct msg_msg *load_msg(const void __user *src, size_t len)
{
...
// hang the process at the first copy_from_user
// modify the msg->next and resume the process
if (copy_from_user(msg + 1, src, alen)) // line 7
goto out_err;
// msg->next has been changed to an arbitrary memory
for (seg = msg->next; seg != NULL; seg = seg->next) { // line 11
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
// Now an arbitrary write happens
if (copy_from_user(seg + 1, src, alen)) // line 17
goto out_err;
}
...
}
实现任意写需要挂起进程,然后修改msg->next, 最后继续执行进程,将可以实现相应的任一地址写。早期在5.11之前可以利用userfaultfd挂起进程,但是后来userfaultfd被限制需要特定的权限了。现在我们利用FUSE可以实现同样的能力。
(关于userfaultfd的攻击, copy_from_user(kptr, user_buf, size), user_buf 是一个 mmap 的内存块,并且我们为它注册了 userfaultfd,那么在拷贝时出现缺页异常后此线程会先执行我们注册的处理函数,在处理函数结束前线程一直被暂停,结束后才会执行后面的操作,大大增加了竞争的成功率。)
get a root shell
- 分配两组相邻的8-page dummy object
- 映射消息内容到FUSE, 并且释放第二个 dummy object, 分配 一个8-page slab 并利用struct msg_msg占满。 进程将会在这一步挂起。(这里为了实现挂起程序,fuse映射poc程序本身到mmap上的地址fuse_addr上,当有程序读到fuse_addr, 利用fuse自定义的文件操作函数挂起进程)。
- 释放第一个dummy object, 分配造成溢出的 object, 替换struct msg_msg next指针到 modProbe_path的地址上。
- 释放挂起的进程(step2), 复制“/tmp/get_root”到modProbe_path
- 通过运行位置格式文件触发modprobe
- 打开 /bin/bash, 我们就获得了root权限。
(这里的原因是,执行未知文件格式文件会触发modprobe_path上的可执行文件,详细可以参考https://www.anquanke.com/post/id/236126)