Linux 内核 AF_PACKET 原生套接字漏洞。
参考文章:
1 漏洞简介
Linux内核AF_PACKET原生套接字内存破坏漏洞。根据细节描述,该漏洞出现在net/packet/af_packet.c中,由整数溢出导致越界写,可以通过它进行权限提升。
该漏洞影响Linux发行版高于4.6的内核版本,包括:
- Ubuntu Bionic (18.04) and newer
- Debian 9
- Debian 10
- CentOS 8/RHEL 8
Patch:
1 | diff --git a/net/packet/af_packet.c b/net/packet/af_packet.c |
关闭CAP_NET_RAW功能(针对RHEL8),具体关闭步骤如下:
1 | echo "user.max_user_namespaces=0" > /etc/sysctl.d/userns.conf |
针对一些受影响的容器产品,同样采取关闭CAP_NET_RAW功能进行缓解:
Kubernetes Pod安全策略:配置Pod安全策略以删除运行容器中的CAP_NET_RAW功能,参考链接:https://cloud.google.com/kubernetes-engine/docs/security-bulletins
2 相关概念
2.1 AF_PACKET套接字
网络协议栈中,原始套接字是一个特殊的套接字类型,从实现上可以分为两类,一类为链路层原始套接字;另一类为网络层原始套接字。链路层原始套接字可直接用于接收和发送链路层的MAC帧,在发送时需要调用者自行构造和封装MAC首部。链路层原始套接字调用socket()函数创建。第一个参数指定地址簇类型为AF_PACKET,第二个参数套接字类型为SOCK_RAW或SOCK_DGRAM,当类型指定为SOCK_RAW时,套接字接收和发送的数据都是从MAC首部开始的。在发送时需要由调用者从MAC首部开始构造和封装报文数据。
2.2 PACKET_MMAP
仅依靠AF_PACKET过滤数据包是非常低效的,内核又提供了PACKET_MMAP支持。PACKET_MMAP在内核空间中分配一块环形内核缓冲区,用户空间通过mmap将该内核缓冲区映射出来。收到的数据包拷贝到环形内核缓冲区中,用户层可以直接操作数据,通过内核空间和用户空间共享的缓冲区起到减少数据拷贝的作用,提高处理效率。
2.3 PACKET_MMAP的实现
通过setsockopt()函数设置环形缓冲区,option参数设置为PACKET_RX_RING或PACKET_TX_RING。为了方便内核与用户层管理和交互环形缓冲区中的数据帧,内核定义了TPACKET_HEADER结构体,该结构体存储着一些元信息如套接字地址信息、时间戳以及环形缓冲区管理信息等。如果通过setsockopt()函数设置了PACKET_VNET_HDR选项,还需添加一个virtio_net_hdr结构体。一个数据帧包含两个部分,第一部分为TPACKET_HEADER,第二部分为Data,而且要保证页面对齐,详细构造可见下列代码的注释部分。
目前TPACKET_HEADER存在三个版本,每个版本长度略有不同。对于v1和v2,收发环形缓冲区用tpacket_req结构体管理,该结构体包含四个数据域:分别为内存块的大小和数量、每个数据帧的大小和数据帧总数。如下所示:
1 | /* |
捕获的frame被划分为多个block,每个block是一块物理上连续的内存区域,有tp_block_size/tp_frame_size个frame,block的总数是tp_block_nr。例如,tp_block_size = 4096,tp_frame_size = 2048,tp_block_nr = 4,tp_frame_nr = 8。
每个frame必须放在一个block中,每个block保存整数个frame,也就是说一个frame不能跨越两个block。在用户层映射环形缓冲区可以直接使用mmap()函数。虽然环形缓冲区在内核中是由多个block组成的,但是映射后它们在用户空间中是连续的。
3 漏洞分析
该漏洞具体出现在tpacket_rcv()函数中,该函数是基于PACKET_MMAP的数据包接收函数。具体功能实现如下代码所示:
1 | if (sk->sk_type == SOCK_DGRAM) { // af_packet.c#L2226 |
行2226到行2228,如果sk_type为SOCK_DGRAM,表示不需要自行构造MAC首部,由内核填充,则macoff等于netoff,大小为TPACKET_ALIGN(tp_hdr_len) + 16 + tp_reserve。如果sk_type为SOCK_RAW,则进入行2230,表示需要自行构造MAC首部。行2231到行2233,首先计算netoff,大小为TPACKET_ALIGN(tp_hdrlen +(maclen < 16 ? 16 : maclen)) + tp_reserve。行2234到行2237,如果设置了PACKET_VNET_HDR选项,还需加上一个virtio_net_hdr结构体的大小,然后设置do_vnet为真。行2238,计算macoff。
由于macoff、netoff以及maclen被定义为unsigned short类型,最大值为0xffff。而tp_reserve被定义为unsigned int类型,最大值为0xffffffff,而且大小可以通过setsockopt()函数进行设置,如下代码所示:
1 | case PACKET_RESERVE: |
因此,在行2233计算netoff时,可以通过控制tp_reserve造成整数溢出,进而计算出错误的macoff。当执行到如下代码时:
1 | if (do_vnet && |
行2287,调用virtio_net_hdr_from_skb()函数从sk_buff中拷贝数据,该函数第二个参数为h.raw + macoff – sizeof(struct virtio_net_hdr),h.raw为tpacket_rcv_uhdr类型的指针,指向环形缓冲区的frame,由于macoff是可控的,可以让maoff小于sizeof(struct virtio_net_hdr),导致向前越界写,最多可写入sizeof(struct virtio_net_hdr)个字节。
4 PoC
1 |
|
5 Exploit 思路
5.1 原语
通过控制macoff的值,可以在环形缓冲区最多偏移10个字节的空间处初始化virtio_net_hdr。virtio_net_hdr_from_skb函数首先会将整个结构体置零,然后根据skb结构,初始化其中的某些字段。
1 | static inline int virtio_net_hdr_from_skb(const struct sk_buff *skb, |
然而,我们可以通过设置skb,使得只有\0会被写入结构体中,这样我们就可以在__get_free_pages分配的空间后清空1-10个字节。此时如果不执行任何堆控制策略,内核将马上崩溃。
5.2 利用思路
利用的思路是将原语变为UAF,为了达到这个目的,考虑将一些对象的引用计数减少,例如,一个对象的引用计数是0x10001。如果发生了前向溢出,引用计数将会变为1,再经历一次释放后,对象将会被free。但为了使其发生,需要满足下列条件:
- refcount需要在对象的最后1~10字节
- 需要对象被分配在页的末尾:
- 因为get_free_pages返回页对齐的地址
经过分析,下面这个对象满足条件:
1 | struct sctp_shared_key { |
看起来这个对象满足我们的条件限制:
- 我们可以在非特权用户上下文中创建一个sctp server和client
- 对象在sctp_auth_shkey_create函数中创建
- 我们可以在页的末尾分配对象:
- 对象大小为32字节,通过kmalloc分配,意味着它使用kmalloc-32
- 因为4096 % 32 = 0,所以在slab页的末尾没有空闲的空间,最后一个对象会正好在我们分配的空间前面。其他slab cache不一定合适,如96字节
- 对象大小为32字节,通过kmalloc分配,意味着它使用kmalloc-32
- 我们可以覆盖refcount的最高两字节
- 编译后,key_id和deactivated都占4字节
- 如果我们利用bug越界写9~10字节,我们可以覆盖refcnt的1~2字节