@ETenal7的漏洞报告详细介绍了他对CVE-2022-27666采取的利用技巧。这是我在阅读报告的时候做的一些简单的笔记,希望可以给有同样疑惑的研究者提供一些帮助。
背景介绍
作为一个Linux
安全研究领域的新人,在阅读报告时我对该漏洞的攻击面和引起漏洞的Root Cause
产生了兴趣,结合自己的研究笔记完成了这一篇报告。内容也许会有错漏之处,仅代表个人观点。
友情提示,本报告不含任何与漏洞利用技巧相关的内容。
寻找 Root Cause: Part I
The basic logic of this vulnerability is that the receiving buffer of a user message in esp6 module is an 8-page buffer, but the sender can send a message larger than 8 pages, which clearly creates a buffer overflow.
这是CVE-2022-27666)原始报告中对Root Cause
的描述。当有了这样的认知之后问题就转化为了如果利用一个8-page buffer
的OOB Write
的问题。如果要研究漏洞利用技巧知道这一点就够了,原报告确实也是这么做的。
-
这个漏洞是如何被发现的?
-
这个漏洞的攻击面是什么?
-
这个漏洞相关代码是如何从应用层访问到的?
这个漏洞是如何被发现的?
根据原报告的介绍这个漏洞应该是被撞了,我找到了syzkaller的报告。根据原作者在报告底部的索引猜测原作者大概率也是通过fuzz
发现的该漏洞。
[3] https://googleprojectzero.blogspot.com/2017/05/exploiting-linux-kernel-via-packet.html
这个漏洞的攻击面是什么?
通过原报告该漏洞存在于esp6 modules
中。
这个漏洞相关代码是如何从应用层访问到的?
通过阅读syzkaller报告可以清楚的看到调用栈。
1memcpy+0x39/0x60 mm/kasan/shadow.c:66
2memcpy include/linux/fortify-string.h:191 [inline]
3null_skcipher_crypt+0xa8/0x120 crypto/crypto_null.c:85
4crypto_skcipher_encrypt+0xaa/0xf0 crypto/skcipher.c:630
5crypto_authenc_encrypt+0x3b4/0x510 crypto/authenc.c:222
6crypto_aead_encrypt+0xaa/0xf0 crypto/aead.c:94
7esp6_output_tail+0x777/0x1a90 net/ipv6/esp6.c:659
8esp6_output+0x4af/0x8a0 net/ipv6/esp6.c:735
9xfrm_output_one net/xfrm/xfrm_output.c:552 [inline]
10xfrm_output_resume+0x2997/0x5ae0 net/xfrm/xfrm_output.c:587
11xfrm_output2 net/xfrm/xfrm_output.c:614 [inline]
12xfrm_output+0x2e7/0xff0 net/xfrm/xfrm_output.c:744
13__xfrm6_output+0x4c3/0x1260 net/ipv6/xfrm6_output.c:87
14NF_HOOK_COND include/linux/netfilter.h:296 [inline]
15xfrm6_output+0x117/0x550 net/ipv6/xfrm6_output.c:92
16dst_output include/net/dst.h:448 [inline]
17ip6_local_out+0xaf/0x1a0 net/ipv6/output_core.c:161
18ip6_send_skb+0xb7/0x340 net/ipv6/ip6_output.c:1935
19ip6_push_pending_frames+0xdd/0x100 net/ipv6/ip6_output.c:1955
20rawv6_push_pending_frames net/ipv6/raw.c:613 [inline]
21rawv6_sendmsg+0x2a87/0x3990 net/ipv6/raw.c:956
22inet_sendmsg+0x99/0xe0 net/ipv4/af_inet.c:821
23sock_sendmsg_nosec net/socket.c:703 [inline]
24sock_sendmsg+0xcf/0x120 net/socket.c:723
25____sys_sendmsg+0x6e8/0x810 net/socket.c:2392
26___sys_sendmsg+0xf3/0x170 net/socket.c:2446
27__sys_sendmsg+0xe5/0x1b0 net/socket.c:2475
28do_syscall_x64 arch/x86/entry/common.c:50 [inline]
29do_syscall_64+0x35/0xb0 arch/x86/entry/common.c:80
30entry_SYSCALL_64_after_hwframe+0x44/0xae
结合syzkaller报告中的POC
代码可以获得对该漏洞跟直观的初步认识。(原报告提供的EXP包含了大量利用相关的内容,对于初学者来说有些吃力)
Root Cause A
不许要太多复杂的分析我们可以得到Root Cause A
-
猜测该漏洞是通过
Fuzzing
发现的 -
经过
socket
的sendmsg
系统调用后,在IPSec
的ESP6
的模块中,处理内存拷贝的时候触发了漏洞
有了Root Cause A
的初步认识后,结合syzkaller
给出的POC
代码后,就可以针对如果利用一个8-page buffer
的OOB Write
进行深入研究了。
但是作为一个漏洞研究人员的我觉得到这里为止的话对Root Cause
还不够,还有一些问题并没有交代清楚。
寻找RootCause:Part II
-
IPSec
和ESP6
是干什么用的?在内核里是怎么实现的? -
从
sendmsg
到最终触发溢出,中间到底发生了一些什么? -
POC
中的socket
操作是在干些什么? -
为什么溢出发生的地方只申请
8-page
,而输入的长度可以到达16-page? -
这个漏洞只能通过
Fuzz
发现么?或者说针对这单个漏洞来说人工审计和Fuzz
那种方法更具有优势?
`IPSec` 和 漏洞触发
相关内容推荐阅读《Linux Kernel Networking - Implementation and Theory》Chapter10: IPsec
中的相关内容,我没法介绍的比他更清楚和详细了。(该书内容不多且比较清晰,github
可以下载到该书的PDF
版本)。(这一段我没细写,我想如果读者读到这里一定是愿意追根溯源的研究者,应该不介意多读一份资料,况且我也没法写的比这本书更好了)。
-
详细解答了
IPSec
和ESP6
的作用和关系 -
详细解答了
xfrm
框架在IPsec
中的作用 -
解释了
POC
中socket
操作的背后代表的具体含义: 配置IPsec
相关策略,让sendmsg
的用户输入最终被ESP6
模块处理。
1esp6_output_tail+0x777/0x1a90 net/ipv6/esp6.c:659
2esp6_output+0x4af/0x8a0 net/ipv6/esp6.c:735
3xfrm_output_one net/xfrm/xfrm_output.c:552 [inline]
4xfrm_output_resume+0x2997/0x5ae0 net/xfrm/xfrm_output.c:587
5xfrm_output2 net/xfrm/xfrm_output.c:614 [inline]
6xfrm_output+0x2e7/0xff0 net/xfrm/xfrm_output.c:744
7__xfrm6_output+0x4c3/0x1260 net/ipv6/xfrm6_output.c:87
8NF_HOOK_COND include/linux/netfilter.h:296 [inline]
9xfrm6_output+0x117/0x550 net/ipv6/xfrm6_output.c:92
10dst_output include/net/dst.h:448 [inline]
11ip6_local_out+0xaf/0x1a0 net/ipv6/output_core.c:161
12ip6_send_skb+0xb7/0x340 net/ipv6/ip6_output.c:1935
13ip6_push_pending_frames+0xdd/0x100 net/ipv6/ip6_output.c:1955
14rawv6_push_pending_frames net/ipv6/raw.c:613 [inline]
15rawv6_sendmsg+0x2a87/0x3990 net/ipv6/raw.c:956
16inet_sendmsg+0x99/0xe0 net/ipv4/af_inet.c:821
17sock_sendmsg_nosec net/socket.c:703 [inline]
18sock_sendmsg+0xcf/0x120 net/socket.c:723
Why 8-Pages ?
1/**
2 * skb_page_frag_refill - check that a page_frag contains enough room
3 * @sz: minimum size of the fragment we want to get
4 * @pfrag: pointer to page_frag
5 * @gfp: priority for memory allocation
6 *
7 * Note: While this allocator tries to use high order pages, there is
8 * no guarantee that allocations succeed. Therefore, @sz MUST be
9 * less or equal than PAGE_SIZE.
10 */
11bool skb_page_frag_refill(unsigned int sz, struct page_frag *pfrag, gfp_t gfp)
12{
13 if (pfrag->page) {
14 if (page_ref_count(pfrag->page) == 1) {
15 pfrag->offset = 0;
16 return true;
17 }
18 if (pfrag->offset + sz <= pfrag->size)
19 return true;
20 put_page(pfrag->page);
21 }
22
23 pfrag->offset = 0;
24 if (SKB_FRAG_PAGE_ORDER &&
25 !static_branch_unlikely(&net_high_order_alloc_disable_key)) {
26 /* Avoid direct reclaim but allow kswapd to wake */
27 pfrag->page = alloc_pages((gfp & ~__GFP_DIRECT_RECLAIM) |
28 __GFP_COMP | __GFP_NOWARN |
29 __GFP_NORETRY,
30 SKB_FRAG_PAGE_ORDER);
31 if (likely(pfrag->page)) {
32 pfrag->size = PAGE_SIZE << SKB_FRAG_PAGE_ORDER;
33 return true;
34 }
35 }
36 pfrag->page = alloc_page(gfp);
37 if (likely(pfrag->page)) {
38 pfrag->size = PAGE_SIZE;
39 return true;
40 }
41 return false;
42}
43EXPORT_SYMBOL(skb_page_frag_refill);
更具漏洞报告和修复情况可以得知,内存溢出的数据保存在pfrag->page
的成员变量中。
1pfrag->page = alloc_pages((gfp & ~__GFP_DIRECT_RECLAIM) |
2 __GFP_COMP | __GFP_NOWARN |
3 __GFP_NORETRY,
4 SKB_FRAG_PAGE_ORDER);
同时SKB_FRAG_PAGE_ORDER
限定了pfrag->page
保存8-pages
的内存。
1#define SKB_FRAG_PAGE_ORDER get_order(32768)
同时我们再看个函数的注释
1/**
2 * skb_page_frag_refill - check that a page_frag contains enough room
3 * @sz: minimum size of the fragment we want to get
4 * @pfrag: pointer to page_frag
5 * @gfp: priority for memory allocation
6 *
7 * Note: While this allocator tries to use high order pages, there is
8 * no guarantee that allocations succeed. Therefore, @sz MUST be
9 * less or equal than PAGE_SIZE.
10 */
这个函数 提供了一个的功能为:
-
检查
page_frag
是否拥有足够多的空间。 -
sz
参数定义了这个所谓的“足够多”。
也就是说这个函数是为了能保证申请的page
的连续内存的比sz
大。但是他实际申请内存的时候又不是用sz
作为内存申请的长度,而是直接申请了8-page
。
于是我想通过历史版本了解一下这个函数最初被加入内核时的初衷,意外发现了一个commit。
1net: remove obsolete comment
2Commit b656722 ("net: Increase the size of skb_frag_t")
3removed the 16bit limitation of a frag on some 32bit arches.
4
5Signed-off-by: Eric Dumazet <[email protected]>
6Signed-off-by: David S. Miller <[email protected]>
7
8diff --git a/net/core/sock.c b/net/core/sock.c
9index 90509c37d291..b714162213ae 100644
10--- a/net/core/sock.c
11+++ b/net/core/sock.c
12@@ -2364,7 +2364,6 @@ static void sk_leave_memory_pressure(struct sock *sk)
13 }
14 }
15
16-/* On 32bit arches, an skb frag is limited to 2^15 */
17 #define SKB_FRAG_PAGE_ORDER get_order(32768)
18 DEFINE_STATIC_KEY_FALSE(net_high_order_alloc_disable_key);
到此我没有在深入去追究这个size
约定的来龙去脉,我只是猜测这个size
是一个约定的最大值,理论上不可能用来处理这个size
大的内存。
PS:在最新版本中 这个宏定义被转移到了/include/net/sock.h
头文件中。
换句话说在原本的理解里,这8-page
是不可能被溢出的。这里我只能猜测可能是开发人员对skb_page_frag_refill
这个函数的来龙去脉没有十分清楚的情况下使用了这个接口。
这个时候很自然的会有考虑skb_page_frag_refill
函数存在一些含糊不清的地方可能导致漏洞产生,那别的开发人员在使用这个函数时是否正确使用了呢?
我粗略的看了一下,没有发现什么特别的东西。但是值得注意的是,在别的模块调用skb_page_frag_refill
时,确实都有一套自己对size
的检测,而ESP6
模块中缺失了这一段。
以tun_build_skb
中的使用为例子:
1static struct sk_buff *tun_build_skb(struct tun_struct *tun,
2 struct tun_file *tfile,
3 struct iov_iter *from,
4 struct virtio_net_hdr *hdr,
5 int len, int *skb_xdp)
6{
7//...
8 alloc_frag->offset = ALIGN((u64)alloc_frag->offset, SMP_CACHE_BYTES);
9 if (unlikely(!skb_page_frag_refill(buflen, alloc_frag, GFP_KERNEL)))
10 return ERR_PTR(-ENOMEM);
11
12 buf = (char *)page_address(alloc_frag->page) + alloc_frag->offset;
13 copied = copy_page_from_iter(alloc_frag->page,
14 alloc_frag->offset + pad,
15 len, from);
16//...
1 if (!frags && tun_can_build_skb(tun, tfile, len, noblock, zerocopy)) { // check size
2 /* For the packet that is not easy to be processed
3 * (e.g gso or jumbo packet), we will do it at after
4 * skb was created with generic XDP routine.
5 */
6 skb = tun_build_skb(tun, tfile, from, &gso, len, &skb_xdp); // call skb_page_frag_refill
7 if (IS_ERR(skb)) {
8 atomic_long_inc(&tun->dev->rx_dropped);
9 return PTR_ERR(skb);
10 }
11 if (!skb)
12 return total_len;
13 }
1static bool tun_can_build_skb(struct tun_struct *tun, struct tun_file *tfile,
2 int len, int noblock, bool zerocopy)
3{
4 if ((tun->flags & TUN_TYPE_MASK) != IFF_TAP)
5 return false;
6
7 if (tfile->socket.sk->sk_sndbuf != INT_MAX)
8 return false;
9
10 if (!noblock)
11 return false;
12
13 if (zerocopy)
14 return false;
15
16 if (SKB_DATA_ALIGN(len + TUN_RX_PAD) +
17 SKB_DATA_ALIGN(sizeof(struct skb_shared_info)) > PAGE_SIZE) // check
18 return false;
19
20 return true;
21}
而原报告中漏洞的修复的部分确实也是如此体现的:
1diff --git a/include/net/esp.h b/include/net/esp.h
2index 9c5637d41d951..90cd02ff77ef6 100644
3--- a/include/net/esp.h
4+++ b/include/net/esp.h
5@@ -4,6 +4,8 @@
6
7 #include <linux/skbuff.h>
8
9+#define ESP_SKB_FRAG_MAXSIZE (PAGE_SIZE << SKB_FRAG_PAGE_ORDER)
10+
11 struct ip_esp_hdr;
12
13 static inline struct ip_esp_hdr *ip_esp_hdr(const struct sk_buff *skb)
14diff --git a/net/ipv4/esp4.c b/net/ipv4/esp4.c
15index e1b1d080e908d..70e6c87fbe3df 100644
16--- a/net/ipv4/esp4.c
17+++ b/net/ipv4/esp4.c
18@@ -446,6 +446,7 @@ int esp_output_head(struct xfrm_state *x, struct sk_buff *skb, struct esp_info *
19 struct page *page;
20 struct sk_buff *trailer;
21 int tailen = esp->tailen;
22+ unsigned int allocsz;
23
24 /* this is non-NULL only with TCP/UDP Encapsulation */
25 if (x->encap) {
26@@ -455,6 +456,10 @@ int esp_output_head(struct xfrm_state *x, struct sk_buff *skb, struct esp_info *
27 return err;
28 }
29
30+ allocsz = ALIGN(skb->data_len + tailen, L1_CACHE_BYTES);
31+ if (allocsz > ESP_SKB_FRAG_MAXSIZE)
32+ goto cow;
33+
34 if (!skb_cloned(skb)) {
35 if (tailen <= skb_tailroom(skb)) {
36 nfrags = 1;
37diff --git a/net/ipv6/esp6.c b/net/ipv6/esp6.c
38index 7591160edce14..b0ffbcd5432d6 100644
39--- a/net/ipv6/esp6.c
40+++ b/net/ipv6/esp6.c
41@@ -482,6 +482,7 @@ int esp6_output_head(struct xfrm_state *x, struct sk_buff *skb, struct esp_info
42 struct page *page;
43 struct sk_buff *trailer;
44 int tailen = esp->tailen;
45+ unsigned int allocsz;
46
47 if (x->encap) {
48 int err = esp6_output_encap(x, skb, esp);
49@@ -490,6 +491,10 @@ int esp6_output_head(struct xfrm_state *x, struct sk_buff *skb, struct esp_info
50 return err;
51 }
52
53+ allocsz = ALIGN(skb->data_len + tailen, L1_CACHE_BYTES);
54+ if (allocsz > ESP_SKB_FRAG_MAXSIZE)
55+ goto cow;
56+
57 if (!skb_cloned(skb)) {
58 if (tailen <= skb_tailroom(skb)) {
59 nfrags = 1;
很明显,内核的修复也是添加了对size
的检测。
选择Fuzz还是人工审计
这个漏洞的调用栈非常的深,根据我以往的经验,多半还没看到这里我就已经放弃对这个模块的审计了。但是有Fuzz工具之后,当工具发现了这个漏洞给我们提供的不仅仅是一个漏洞,而是通过理解分析这个漏洞可以加深对代码的熟悉,加强自己对攻击面的理解。
我们可以用Fuzz工具触发的漏洞,甚至只是崩溃作为人工审计的起点,跳过一些枯燥的调用栈分析跟踪直接开始人工审计一些原先自己不知道的攻击面。
我还没有学习使用过syzkaller
,但是单看他提供的报告已经收获颇丰了。
Root Cause B
-
猜测在版本更迭中
skb_page_frag_refill
的函数原先存在某些约定俗称的假设前提,但是并没有体现在函数定义里 -
其他开发人员没有按照规范使用
skb_page_frag_refill
函数导致了漏洞的产生
Root Cuase A
不能说不是这个漏洞的Root Cause
,但是从漏洞审计的角度来说,我觉得Root Cause B
才是我们更想要的结果。这是在漏洞审计中非常常见的一种情况,我认为如何积累并利用这种规律帮助我们在漏洞审计工作提高效率,才是阅读分析报告真正目的。
我认为某种角度来说,Root Cause A
和Root Cause B
的组合才是这个漏洞完整的Root Cause
。
寻找RootCause:PART III
当我决定分析告一段落的时候,发现了一个新的commit
。
1esp: limit skb_page_frag_refill use to a single page
2Commit ebe48d3 ("esp: Fix possible buffer overflow in ESP
3transformation") tried to fix skb_page_frag_refill usage in ESP by
4capping allocsize to 32k, but that doesn't completely solve the issue,
5as skb_page_frag_refill may return a single page. If that happens, we
6will write out of bounds, despite the check introduced in the previous
7patch.
8
9This patch forces COW in cases where we would end up calling
10skb_page_frag_refill with a size larger than a page (first in
11esp_output_head with tailen, then in esp_output_tail with
12skb->data_len).
13
14Fixes: cac2661 ("esp4: Avoid skb_cow_data whenever possible")
15Fixes: 03e2a30 ("esp6: Avoid skb_cow_data whenever possible")
16Signed-off-by: Sabrina Dubroca <[email protected]>
17Signed-off-by: Steffen Klassert <[email protected]>
18
在细看skb_page_frag_refill
的定义:
1/**
2 * skb_page_frag_refill - check that a page_frag contains enough room
3 * @sz: minimum size of the fragment we want to get
4 * @pfrag: pointer to page_frag
5 * @gfp: priority for memory allocation
6 *
7 * Note: While this allocator tries to use high order pages, there is
8 * no guarantee that allocations succeed. Therefore, @sz MUST be
9 * less or equal than PAGE_SIZE.
10 */
11bool skb_page_frag_refill(unsigned int sz, struct page_frag *pfrag, gfp_t gfp)
12{
13 if (pfrag->page) {
14 if (page_ref_count(pfrag->page) == 1) {
15 pfrag->offset = 0;
16 return true;
17 }
18 if (pfrag->offset + sz <= pfrag->size)
19 return true;
20 put_page(pfrag->page);
21 }
22
23 pfrag->offset = 0;
24 if (SKB_FRAG_PAGE_ORDER &&
25 !static_branch_unlikely(&net_high_order_alloc_disable_key)) {
26 /* Avoid direct reclaim but allow kswapd to wake */
27 pfrag->page = alloc_pages((gfp & ~__GFP_DIRECT_RECLAIM) |
28 __GFP_COMP | __GFP_NOWARN |
29 __GFP_NORETRY,
30 SKB_FRAG_PAGE_ORDER);
31 if (likely(pfrag->page)) {
32 pfrag->size = PAGE_SIZE << SKB_FRAG_PAGE_ORDER;
33 return true;
34 }
35 }
36 pfrag->page = alloc_page(gfp); //<============ !!!!!这里也是有可能被执行的!!!!!
37 if (likely(pfrag->page)) {
38 pfrag->size = PAGE_SIZE;
39 return true;
40 }
41 return false;
42}
43EXPORT_SYMBOL(skb_page_frag_refill);
一点反思
读到这里读者看到这个commit
应该是可以理解之前的分析哪里出现了遗漏。
-
对函数理解的不透彻不仅会让开发者制造一些糟糕的漏洞,同时也会让我们自己错过一些漏洞。
-
对漏洞产生的
Root Cause
的追根溯源其实更多的是为了反思自己为什么没有发现这个漏洞。
原文始发于微信公众号(7号攻防实验室):浅尝辄止: CVE-2022-27666
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论