技术细节
该漏洞来源于net/packet/af_packet.c 文件的tpacket_rcv 函数中,是由于算术问题引发的内存破坏。该漏洞是2008年7月引入的(commit 8913336),从2016年2月开始触发内存破坏(commit 58d19b19cd99),很多开发者都尝试修复该漏洞,但提出的补丁都不足以预防内存破坏。
为触发该漏洞需要创建一个含有TPACKET_V2 ring缓存和 PACKET_RESERVE为特定值的原始包(AF_PACKET domain, SOCK_RAW type )。
headroom 是用户指定大小的缓存,会在ring 缓存接收每个包的真实数据之前分配。该值可以通过setsockopt 系统调用在用户空间来设置:
图 1. Setsockopt设置 – PACKET_RESERVE
如图 1所示,会检查该值是否小于INT_MAX。该值是在补丁(https://lore.kernel.org/patchwork/patch/784412/)中新加的以防packet_set_ring 中最小帧大小计算溢出。然后回验证页面是否是为接收或者传输的ring缓存分配的。这么做的目的是预防tp_reserve 域和ring buffer之间的不连续。
在设置了tp_reserve 值后,就可以通过含有PACKET_RX_RING的setsockopt系统调用来触发ring缓存的分配:
图 2. From manual packet – PACKET_RX_RING option.
这是在packet_set_ring函数中实现的。在ring缓存分配之前,会有许多对从用户空间接收的 tpacket_req结构的检查:
图 3. packet_set_ring 函数中的安全检查
从图 3中可以看出,首先会计算最小的帧大小,然后与从用户空间接收到的值进行对比验证。检查确保了在 tpacket 头结构的每个帧和tp_reserve 字节数之间的有空间。
在做完所有检查之后,ring缓存本身就会通过 alloc_pg_vec调用来分配:
图 4. packet_set_ring 函数中调用ring缓存分配函数
如上图所示,block size(区块大小)是由用户空间控制的。alloc_pg_vec函数会分配 pg_vec数组,然后通过alloc_one_pg_vec_page 函数分配给每一个区块链:
图 5. alloc_pg_vec实现
alloc_one_pg_vec_page 函数会用 __get_free_pages 来分配区块页:
图 6. alloc_one_pg_vec_page 实现
区块分配后,pg_vec 数组就会保存在嵌入在 packet_sock结构中的packet_ring_buffer结构。
当接口接收到包后,与tpacket_rcv函数绑定的socket、包数据、TPACKET 元数据都会写入到ring缓存中。
漏洞
图7是 tpacket_rcv 函数的实现。首先,会调用skb_network_offset 来提取接收到的包的网络头的偏移值到maclen中。在本例中,大小为14字节,即以太网header的大小。之后,会根据TPACKET header、 maclen 和tp_reserve值来计算netoff。
但是计算的过程可能会溢出,因为 tp_reserve的类型是 unsigned int ,netoff的类型是unsigned short,而对tp_reserve 值的唯一限制是小于INT_MAX。
图 7. tpacket_rcv中的算术计算
如图 7所示,如果包中设置了PACKET_vnet_HDR ,就会加入sizeof(struct virtio_net_hdr) 。最后,以太网header的偏移量会计算会保存到macoff中。
如图 8所示, virtio_net_hdr结构会用 virtio_net_hdr_from_skb函数下入ring缓存中。 h.raw 指向ring 缓存中当前空闲的帧。
图 8. 调用tpacket_rcv中的 virtio_net_hdr_from_skb函数
研究人员设想有可能利用该溢出将netoff变成一个更小的值,所以macoff 可以接收一个大于block size的值,并尝试写入缓存中。
但是存在以下检查,所以无法实现:
图 9. tpacket_rcv 函数中的另一个检查
但是该检查并不足以预防内存破坏,因为仍然可以通过溢出netoff 将macoff变成一个小一点的值。比如,将macoff变成小于10字节的 sizeof(struct virtio_net_hdr),然后用 virtio_net_hdr_from_skb 写入缓存的边界。
原语
通过控制macoff的值,就可以在控制的偏移量中初始化 virtio_net_hdr 结构。 virtio_net_hdr_from_skb 函数会首先将整个struct 零化,然后根据skb结构初始化结构内的所有域。
图 10. virtio_net_hdr_from_skb 函数的实现
但可以设置skb 只让零写入结构中。因此,就可以在__get_free_pages 分配中零化1-10个字节。无需任何堆操作技巧就可以立刻引发kernel 奔溃。
POC
触发该漏洞的PoC代码参见:https://www.openwall.com/lists/oss-security/2020/09/03/3
漏洞利用
漏洞利用过程参见:https://unit42.paloaltonetworks.com/cve-2020-14386/
补丁
研究人员提出的补丁参见:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=acf69c946233259ab4d64f8869d4037a198c7f06
图 11. 研究人员提出的补丁
补丁的思想是如果将netoff 的类型从unsigned short 修改为unsigned int,就可以检查是否会超过USHRT_MAX,如果超过的化就丢弃该包,以防进一步利用。
总结
研究人员其实也很奇怪Linux kernel中至今还会存在如此简单的算术安全问题,而且之前没有被发现过。同时,非特权的用户空间也暴露出了本地权限提升的巨大攻击面。