查看原文
其他

Linux内核eBPF RINGBUF越界访问漏洞(CVE-2021-3489)利用分析

启明星辰 ADLab 2023-01-16

更多安全资讯和分析文章请关注启明星辰ADLab微信公众号及官方网站(adlab.venustech.com.cn)















前 言

近年来,在PWN2OWN比赛Ubuntu桌面系统破解项目中,Linux内核eBPF机制一直是热门的攻击面。本文分析的CVE-2021-3489是在PWN2OWN 2021比赛中使用的漏洞,该eBPF漏洞和以往的逻辑验证漏洞不同,漏洞出现在新引入的eBPF RINGBUF功能中,导致内存访问越界,可实现越界读写达到权限提升。



BPF环形缓冲区及映射

eBPF提供多种类型的映射,环形缓冲区映射就是其中之一。该实现的动机之一是通过在CPU之间共享环形缓冲区来更有效地利用内存。单个RINGBUF环形缓冲区作为 BPF_MAP_TYPE_RINGBUF类型的BPF映射实例呈现给BPF程序。还提供多个BPF_CALL接口函数,其中bpf_ringbuf_output()功能为允许将数据从一个地方复制到环形缓冲区,bpf_ringbuf_reserve()/bpf_ringbuf_commit()/bpf_ringbuf_discard()这组函数将整个过程分为两个步骤。首先,预留固定数量的空间。如果成功,则返回指向环形缓冲区数据区域内数据的指针,BPF程序可以像使用数组/哈希映射内的数据一样使用该指针。一旦准备好,这块内存要么被提交,要么被丢弃。discard与commit类似。

在创建BPF_MAP_TYPE_RINGBUF映射时,内核将分配两个内存区域。一个是 bpf_ringbuf_map 结构,类似于其他的映射类型,另一个是bpf_ringbuf结构。该结构定义如下图所示:

其中,pages是内存分配的所有页面集合,consumer_pos为消费者计数器,producer_pos为生产者计数器,分别放在相邻的单独的页面中,在该漏洞修复前,这两个页面均可以通过MMAP映射到用户空间进行读写操作的。bpf_ringbuf_alloc()函数是实现bpf_ringbuf并初始化的,实现代码如下所示:

调用bpf_ringbuf_area_alloc()函数分配bpf_ringbuf,然后初始化rb->spinlock,rb->waitq和rb->work,最后设置rb->mask,rb->consumer_pos和rb->producer_pos。bpf_ringbuf_area_alloc()函数是用来具体分配ringbuf内存区域的,该实现如下代码所示:

第一个参数data_sz为申请分配内存的大小,nr_meta_pages为元数据页面数,包含一个不可映射页面和两个可映射页面,分别为consumer_pos和producer_pos,nr_data_pages为实际申请分配内存所需的内存页面数,nr_pages为nr_meta_pages和nr_data_pages之和,pages用于存放所有页面集合,内存分配如下代码所示:

调用bpf_map_area_alloc()函数分配pages指针数组,用于存放即将分配的内存页面。然后循环调用alloc_pages_node()函数分配页面并存放在pages中,注意到nr_data_pages是双份的。实际内存布局如下所示:

最后,调用vmmap()函数将pages中的页面映射到连续虚拟内存空间中,如下代码所示:

成功映射后返回ringbuf指针。 



漏洞原理与修复补丁

该漏洞发生在__bpf_ringbuf_reserve()函数中,该函数可以返回指向环形缓冲区数据区域内数据的指针,但是并没有判断访问长度大小,导致可以越界访问数据。该函数关键实现如下代码所示:

参数size为访问长度,首先判断size是否大于0x3fffffff,但是并没有判断len是否大于ringbuf的data_sz,即访问的范围是否大于实际分配的ringbuf内存范围。然后对size+8上限取整为len,接下来取出rb->producer_pos,通过prod_pos+len计算出new_prod_pos。

行333,首先判断新的生产者位置不超过ringbuf的data_sz-1,确保ringbuf内存空间是充足的。然后通过rb->data+prod_pos计算出hdr的位置,最后将rb->producer_pos更新为new_prod_pos,返回hdr+8位置的指针,如下代码所示:

根据前文分析,rb->consumer_pos和rb->producer_pos所在页面是可映射的,是可控的且没有检查,size访问长度也是可控的,因此可以构造如下条件达到大范围越界访问,令producer_pos = 0,consumer_pos= 0x3fffffff和size=0x3fffffff。这三个变量可以绕过所有检查,最后计算出的new_prod_pos为0x3fffffff+8,这是个很大的范围。

该漏洞修复补丁有两部分,第一部分是加上了和data_sz大小的判断,防止访问长度超出实际分配的空间范围,如下代码所示:

第二部分是对ringbuf_map的操作集函数map函数进行修改,如下代码所示:

不允许对rb->producer_pos所在内存页面进行写映射。



漏洞利用过程

(1)通过堆喷构造连续内存布局,给越界读写提供场景

连续创建多个size=0x1000000的ringbuf,这里mapfd的ringbuf和victimfd的ringbuf是连续的,中间间隔一个页面的guard page。

(2)通过eBPF指令构造出越界读写原语

通过eBPF指令访问mapfd的ringbuf,并调用bpf_ringbuf_reserve()函数获取mapfd的ringbuf->data指针。这里SIZE为0x30000000大于实际分配的内存空间。

寄存器r6存放ringbuf->data指针,然后将r6加上offset进行越界访问,同时还能绕过ebpf对寄存器加减的检测。然后通过BPF_LDX和BPF_STX进行内存读写。

根据前面堆喷布局,通过mapfd的ringbuf->data指针进行越界访问,可以越界读取到victimfd的ringbuf内存。

(3)泄露内核指针计算基地址

偏移size*2+0x1000跳过mapfd的ringbuf,再偏移8处是victimfd的ringbuf->wait_queue_head->list_head,偏移40处是ringbuf-> irq_work->func,初始化bpf_ringbuf时,func为bpf_ringbuf_notify,因此可以计算出内核基地址。

(4)劫持返回地址执行代码

通过大量fork子进程,在victimfd内存后面得到连续的task_struct内存布局。

同时,子进程和父进程通过pipe进行通信,这个过程中会调用__x64_sys_read和ksys_read函数并处于阻塞状态,然后不断搜索thread内核栈,搜索到这两个函数返回地址将其修改成commit_creds和prepare_kernel_cred,在父进程中解除阻塞状态便可劫持流程进行执行任意代码。      

综上利用过程,通过精心构造可实现对漏洞的提权效果。

 

        
参考链接: 
https://flatt.tech/reports/210401_pwn2own/





启明星辰积极防御实验室(ADLab)





ADLab成立于1999年,是中国安全行业最早成立的攻防技术研究实验室之一,微软MAPP计划核心成员,“黑雀攻击”概念首推者。截止目前,ADLab已通过CVE累计发布安全漏洞近1100个,通过 CNVD/CNNVD累计发布安全漏洞2000余个,持续保持国际网络安全领域一流水准。实验室研究方向涵盖操作系统与应用系统安全研究、移动智能终端安全研究、物联网智能设备安全研究、Web安全研究、工控系统安全研究、云安全研究。研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。







您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存