漏洞编号: 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
提权成功:
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_ORDER
为get_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_msg
的msg_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(黑色指针代表越界写,橙色指针代表越界读,为了上述布局能大概率成功,作者也尝试了多种方法,最后选择了最优的一种):
这样读到msg_msgseg next
之后,接下来只要按照原本的想法越界写直接覆盖msg_msg
的m_ts
和msg_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