[漏洞分析] CVE-2022-2766六 IPV6 ESP协议页溢出内核提权

CVE-2022-27666 IPV6 ESP协议页溢出内核提权

文章目录

  • CVE-2022-27666 IPV6 ESP协议页溢出内核提权
    • 漏洞简介
    • 环境搭建
      • 虚拟机
      • docker
    • 漏洞原理
      • 漏洞发生点
      • 调用栈
    • 漏洞利用
    • 参考

漏洞作者博客:https://etenal.me/archives/1825 写的非常详细,这里只是简单补充一下环境搭建和简单分析一下原理和利用等。

漏洞简介

漏洞编号: CVE-2022-27666

漏洞产品: linux kernel - esp6

影响版本: ~ linux kernel 5.17-rc5

漏洞危害: ESP6 协议中esp6_output_head 申请缓存和实际使用大小无关,导致页面溢出,可造成本地提权

​ 由于是协议的漏洞,理论上可以远程触发,但利用可能非常困难。因为无法自由操作内存。

源码获取:git clone git://kernel.ubuntu.com/ubuntu/ubuntu-focal.git -b Ubuntu-hwe-5.13-5.13.0-35.40_20.04.1 --depth 1(获取的源码不一定是能跑通exp 的版本,只是有漏洞并且可以编译调试漏洞发生点)

环境搭建

虚拟机

根据曝光exp 的环境:ubuntu 21.10 ;内核版本 5.13.0-35-generic,使用ubuntu desktop 21.10虚拟机搭建exp,该镜像内核默认版本即 5.13.0-35-generic,如果不是,可以手动替换内核:

apt-get install linux-image-5.13.0-35-generic
#如果是新版本更换旧版本需要修改grub,或者把/boot 目录里所有其他版本内核删除
reboot
cd CVE-2022-27666-main/
./compile.sh
./run.sh

提权成功:

[漏洞分析] CVE-2022-2766六 IPV6 ESP协议页溢出内核提权_第1张图片

docker

docker 调试环境:

自己编译内核需要注意,这个漏洞很多相关功能都是在内核模块上,除了esp6 模块之外还有一些其他模块。我不是很确定,反正我把跟以下关键词相关的编译选项(明显和漏洞不相关的除外)全部设置成了"y",然后可以断住关键函数了:

INET6
TUNNEL
XFRM
CONFIG_NET_KEY=y
CONFIG_NF_SOCKET_IPV6=y

貌似改了下面这么多?(肯定有很多没用的,反正我也不确定具体哪几个管用)。编译方法参考:https://blog.csdn.net/Breeze_CAT/article/details/123787636

root@kc:/tmp# cat .config |grep ESP
CONFIG_NAMESPACES=y
CONFIG_X86_ESPFIX64=y
# CONFIG_MODULE_ALLOW_MISSING_NAMESPACE_IMPORTS is not set
CONFIG_XFRM_ESP=y
CONFIG_XFRM_ESPINTCP=y
CONFIG_INET_ESP=y
CONFIG_INET_ESP_OFFLOAD=y
CONFIG_INET_ESPINTCP=y
CONFIG_INET6_ESP=y
CONFIG_INET6_ESP_OFFLOAD=y
CONFIG_INET6_ESPINTCP=y
CONFIG_NETFILTER_XT_MATCH_ESP=m
CONFIG_IP_VS_PROTO_AH_ESP=y
CONFIG_IP_VS_PROTO_ESP=y
CONFIG_KEYBOARD_APPLESPI=m
# CONFIG_HARDENED_USERCOPY_PAGESPAN is not set
root@kc:/tmp# cat .config |grep INET6
CONFIG_INET6_AH=y
CONFIG_INET6_ESP=y
CONFIG_INET6_ESP_OFFLOAD=y
CONFIG_INET6_ESPINTCP=y
CONFIG_INET6_IPCOMP=y
CONFIG_INET6_XFRM_TUNNEL=y
CONFIG_INET6_TUNNEL=y
root@kc:/tmp# cat .config |grep TUNNEL
CONFIG_NET_IP_TUNNEL=m
CONFIG_NET_UDP_TUNNEL=m
CONFIG_NET_FOU_IP_TUNNELS=y
CONFIG_INET_XFRM_TUNNEL=m
CONFIG_INET_TUNNEL=m
CONFIG_INET6_XFRM_TUNNEL=y
CONFIG_INET6_TUNNEL=y
CONFIG_IPV6_TUNNEL=y
CONFIG_IPV6_FOU_TUNNEL=m
CONFIG_IPV6_SEG6_LWTUNNEL=y
# CONFIG_IPV6_RPL_LWTUNNEL is not set
CONFIG_NFT_TUNNEL=m
CONFIG_NET_ACT_TUNNEL_KEY=y
CONFIG_MPLS_IPTUNNEL=m
CONFIG_LWTUNNEL=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_I2C_CROS_EC_TUNNEL=m
root@kc:/tmp# cat .config |grep XFRM
CONFIG_XFRM=y
CONFIG_XFRM_OFFLOAD=y
CONFIG_XFRM_ALGO=y
CONFIG_XFRM_USER=y
CONFIG_XFRM_USER_COMPAT=y
CONFIG_XFRM_INTERFACE=y
# CONFIG_XFRM_SUB_POLICY is not set
# CONFIG_XFRM_MIGRATE is not set
CONFIG_XFRM_STATISTICS=y
CONFIG_XFRM_AH=y
CONFIG_XFRM_ESP=y
CONFIG_XFRM_IPCOMP=y
CONFIG_XFRM_ESPINTCP=y
CONFIG_INET_XFRM_TUNNEL=m
CONFIG_INET6_XFRM_TUNNEL=y
CONFIG_NFT_XFRM=m
CONFIG_SECURITY_NETWORK_XFRM=y

漏洞原理

漏洞发生点

漏洞所在点在于ESP6模块中的esp6_output_head 函数:

linux-5.13\net\ipv6\esp6.c : 478 : esp6_output_head

int esp6_output_head(struct xfrm_state *x, struct sk_buff *skb, struct esp_info *esp)
{
	··· ···
	if (!skb_cloned(skb)) {
		if (tailen <= skb_tailroom(skb)) {
			···
		} else if ((skb_shinfo(skb)->nr_frags < MAX_SKB_FRAGS)
			   && !skb_has_frag_list(skb)) {
			int allocsize;
			··· ···
			allocsize = ALIGN(tailen, L1_CACHE_BYTES);
			··· ···
			//调用skb_page_frag_refill 申请缓存,共8页
			if (unlikely(!skb_page_frag_refill(allocsize, pfrag, GFP_ATOMIC))) {
				spin_unlock_bh(&x->lock);
				goto cow;
			}
			page = pfrag->page;
			get_page(page);
			··· ···
		}
	}
··· ···
}

而函数skb_page_frag_refill 虽然参数有allocsize,但实际申请的空间跟allocsize 没啥关系,固定为8页:

linux-5.13\net\core\sock.c : 2479 : skb_page_frag_refill

bool skb_page_frag_refill(unsigned int sz, struct page_frag *pfrag, gfp_t gfp)
{
	if (pfrag->page) {//只有当pfrag->page已经存在的时候,才会用sz做判断
		if (page_ref_count(pfrag->page) == 1) {
			pfrag->offset = 0;
			return true;
		}
		if (pfrag->offset + sz <= pfrag->size)
			return true;
		put_page(pfrag->page);
	}

	pfrag->offset = 0;
	if (SKB_FRAG_PAGE_ORDER &&
	    !static_branch_unlikely(&net_high_order_alloc_disable_key)) {
		/* Avoid direct reclaim but allow kswapd to wake */
        //#define SKB_FRAG_PAGE_ORDER     get_order(32768) 8页
		pfrag->page = alloc_pages((gfp & ~__GFP_DIRECT_RECLAIM) |
					  __GFP_COMP | __GFP_NOWARN |
					  __GFP_NORETRY,
					  SKB_FRAG_PAGE_ORDER);
		if (likely(pfrag->page)) {
			pfrag->size = PAGE_SIZE << SKB_FRAG_PAGE_ORDER;
			return true;
		}
	}
	··· ···
	return false;
}

调用alloc_pages 申请SKB_FRAG_PAGE_ORDER 大小,而SKB_FRAG_PAGE_ORDERget_order(32768) 即3,使用alloc_pages 申请页面,是申请2^order 个页,所以这里是申请8页固定。

但是在使用该内存空间的null_skcipher_crypt 函数中并没有检查长度是否满足,直接拷贝,造成溢出:

linux-5.13\crypto\crypto_null.c : 76 : null_skcipher_crypt

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)
			memcpy(walk.dst.virt.addr, walk.src.virt.addr,
			       walk.nbytes);
		err = skcipher_walk_done(&walk, 0);
	}

	return err;
}

调用栈

调用栈比较复杂…,整体调用栈如下:

#0  esp6_output_head  
#1  esp6_xmit  
#2  validate_xmit_xfrm  
#3  validate_xmit_skb  
#4  __dev_queue_xmit  
#5  dev_queue_xmit  
#6  neigh_hh_output  
#7  neigh_output  
#8  ip6_finish_output2  
#9  __ip6_finish_output  
#10 __ip6_finish_output  
#11 ip6_finish_output  
#12 NF_HOOK_COND 
#13 ip6_output  
#14 dst_output  
#15 xfrm_output_resume  
#16 xfrm_output2  
#17 xfrm_output  
#18 __xfrm6_output  
#19 NF_HOOK_COND 
#20 xfrm6_output  
#21 dst_output  
#22 ip6_local_out  
#23 ip6_send_skb  
#24 ip6_push_pending_frames  
#25 rawv6_push_pending_frames  
#26 rawv6_sendmsg  
#27 inet_sendmsg  
#28 sock_sendmsg_nosec  
#29 sock_sendmsg  
#30 ____sys_sendmsg 
#31 ___sys_sendmsg 
#32 __sys_sendmsg  
#33 __do_sys_sendmsg  
#34 __se_sys_sendmsg  
#35 __x64_sys_sendmsg  
#36 do_syscall_64  

然后对于null_skcipher_crypt 函数的调用栈:

#0  null_skcipher_crypt 
#1  crypto_skcipher_encrypt 
#2  crypto_authenc_copy_assoc 
#3  crypto_authenc_encrypt
#4  crypto_aead_encrypt 
#5  esp6_output_tail 
#6  esp6_xmit
···

主要在于esp6_xmit 函数,先调用esp6_output_head 在这里申请受漏洞影响的内存空间,再调用esp6_output_tail ,在esp6_output_tail 中会将该内存复制给req.dst,后续在crypto_aead_encrypt 函数中会调用null_skcipher_crypt 来进行拷贝(中间还有多次数据传递)

linux-5.13\net\ipv6\esp6.c : 565 : esp6_output_tail

int esp6_output_tail(struct xfrm_state *x, struct sk_buff *skb, struct esp_info *esp)
{
	··· ···
	if (esp->inplace)   
		dsg = sg;
	else
		dsg = &sg[esp->nfrags];
	··· ···        
    aead_request_set_crypt(req, sg, dsg, ivlen + esp->clen, iv);    
    ··· ··· 
	err = crypto_aead_encrypt(req);
	··· ···
}

漏洞利用

博主write up 写的非常详细了,动图做的也非常精美。可以参考write up 的原文链接。这里就简单讲一下:

整体思路和CVE-2022-0185 类似,首先泄露指针部分,喷射大量kmalloc-4k + kmalloc-32 的两段消息,再喷射seq_operations结构体,然后通过修改msg_msg 结构体中m_ts 结构改变消息大小,然后通过越界读到第二段消息后面的seq_operations 结构体来泄露地址。不过这里遇到的问题是,该越界写漏洞会在末尾添加一些脏数据,这样会破坏msg_msgmsg_msgseg next 指针,读的时候就会发生异常,这里作者采用了一个中间结构体“中转”一下,就是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 */
};

该结构体长度后面直接就是数据段,所以覆盖了长度之后,脏数据覆盖进数据段并不影响,然后通过读已经改过长度的user_key_payload 结构体来读到后面的msg_msg 结构体,获取它的msg_msgseg next值,大概的堆布局如下gif(黑色指针代表越界写,橙色指针代表越界读,为了上述布局能大概率成功,作者也尝试了多种方法,最后选择了最优的一种):

[漏洞分析] CVE-2022-2766六 IPV6 ESP协议页溢出内核提权_第2张图片

这样读到msg_msgseg next 之后,接下来只要按照原本的想法越界写直接覆盖msg_msgm_tsmsg_msgseg next(用刚泄露的msg_msgseg next) 就可以了,脏数据破坏后面的security 是不影响的,因为ubuntu 中没有使用这一字段。然后读msgrcv越界读即可。

后续越界写修改modprobe_path 也和CVE-2022-0185 的方法很像,使用msg+fuse userfaulted 来完成。之前分析过了不再重复分析。

总的来看这个漏洞复杂的点还在漏洞的触发,初始化相关就用了上千行代码。(我对网络编程不咋了解,看不懂)

参考

writeup:https://etenal.me/archives/1825

github:plummm/CVE-2022-27666

CVE-2022-0185:https://www.willsroot.io/2022/01/

msg_msg 技术:https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html

你可能感兴趣的:(漏洞分析,#,linux,kernel,CVE-2022-0847,内核提权,网络安全,漏洞分析,linux内核)