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的一系列改变。

esp32产生100k方波_esp32产生100k方波


页表分配器包含一个页表释放管理的器叫 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块就会只受我们进程的影响。

二、泄露及地址

esp32产生100k方波_esp32产生100k方波_02


这里作者采用了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, 对噪声的容错率比较低。

esp32产生100k方波_linux_03


为了增加容错。这里作者在每个slab中分配一个user_key_payload,其他的用其他结构填充,由于freelist中page的随机性,所以会形成如下的内存结构。由于越界可以实现8page的内容改写,所以并不影响exploit结果。这样将可以分配9个slabs, 增大了对page allocater的容错能力。

esp32产生100k方波_运维_04

进一步利用:

esp32产生100k方波_linux_05

为了增加成功率,减缓噪声的影响。我们制造了9组slab构造。只要一个成功,我们就可以获得正确的struct msg_msg next指针。如下图,每三个一组。为1pair

esp32产生100k方波_esp32产生100k方波_06

esp32产生100k方波_服务器_07


esp32产生100k方波_服务器_08


这里只要有一组成功构造为我们想要的结构,就可以发生地址泄露。读取任意msg_msg结构的信息。接下来,泄露及地址:

esp32产生100k方波_运维_09


esp32产生100k方波_ci_10

攻击路径

Phase 1

  1. 消耗 order-3的page list_free. order-3的分配将会从order-4上借用,并且内存讲相邻。
  2. 分配三组 相邻的8-page dummy object.
  3. 释放的第二个dummy object。分配一个8-page slab 的包含user_key_payload和其他7个object内存。
  4. 释放第三个dummy object, 分配一个用struct msg_msg 完全占用8-page slab的内存块。消息大小要位于4056到4072当中,为了使struct msg_msgseg落在kmalloc-32的分配中。
  5. 分配大量的struct seq_operations. 这些结构体将会拥有和step 4 中msg_msgseg一样大小的内存块。
  6. 释放第一个dummy_object, 分配溢出的buffer, 开始越界写的操作。我们计划修改struct user_key_payload datalen的域。
  7. 如果step 6 成功,取回user_key_payload 的payload将会造成越界读取。越界读取又可以使 struct msg_msg 的内容,包括他的next指针被读取到。
  8. step 7成功后,我们有了真实的struct msg_msg object 的next指针。

Phase 2

  1. 分配两个相邻的 8-page dummy objects.
  2. 释放第二个 dummy_object, 分配一个占满slab的struct msg_msg.
  3. 释放第一个dummy_object, 分配一个溢出内存块, 利用一个很大的值占用他的m_ts filed,并且利用上Phase1 step7得到的next指针覆盖 struct msg_msg 的next。
  4. 如果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

  1. 分配两组相邻的8-page dummy object
  2. 映射消息内容到FUSE, 并且释放第二个 dummy object, 分配 一个8-page slab 并利用struct msg_msg占满。 进程将会在这一步挂起。(这里为了实现挂起程序,fuse映射poc程序本身到mmap上的地址fuse_addr上,当有程序读到fuse_addr, 利用fuse自定义的文件操作函数挂起进程)。
  3. 释放第一个dummy object, 分配造成溢出的 object, 替换struct msg_msg next指针到 modProbe_path的地址上。
  4. 释放挂起的进程(step2), 复制“/tmp/get_root”到modProbe_path
  5. 通过运行位置格式文件触发modprobe
  6. 打开 /bin/bash, 我们就获得了root权限。

(这里的原因是,执行未知文件格式文件会触发modprobe_path上的可执行文件,详细可以参考https://www.anquanke.com/post/id/236126)

esp32产生100k方波_服务器_11


esp32产生100k方波_linux_12