Linux内核eBPF RINGBUF越界会见误差(CVE-2021-3489)使用剖析

宣布时间 2022-03-01

近年来,在PWN2OWN角逐Ubuntu桌面系统破解项目中,Linux内核eBPF机制一直是热门的攻击面。本文剖析的CVE-2021-3489是在PWN2OWN 2021角逐中使用的误差,该eBPF误差和以往的逻辑验证误差差别,误差泛起在新引入的eBPF RINGBUF功效中,导致内存会见越界,可实现越界读写抵达权限提升。


BPF环形缓冲区及映射


eBPF提供多种类型的映射,环形缓冲区映射就是其中之一。该实现的念头之一是通过在CPU之间共享环形缓冲区来更有用地使用内存。单个RINGBUF环形缓冲区作为 BPF_MAP_TYPE_RINGBUF类型的BPF映射实例泛起给BPF程序�;固峁┒喔鯞PF_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结构。该结构界说如下图所示:


代码文件.png


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


代码文件.png


挪用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内存区域的,该实现如下代码所示:


代码文件.png


第一个参数data_sz为申请分派内存的巨细,nr_meta_pages为元数据页面数,包括一个不可映射页面和两个可映射页面,划分为consumer_pos和producer_pos,nr_data_pages为现实申请分派内存所需的内存页面数,nr_pages为nr_meta_pages和nr_data_pages之和,pages用于存放所有页面荟萃,内存分派如下代码所示:


代码文件.png

挪用bpf_map_area_alloc()函数分派pages指针数组,用于存放即将分派的内存页面。然后循环挪用alloc_pages_node()函数分派页面并存放在pages中,注重到nr_data_pages是双份的。现实内存结构如下所示:


示例图.png

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


代码文件.png

误差原理与修复补丁


该误差爆发在__bpf_ringbuf_reserve()函数中,该函数可以返回指向环形缓冲区数据区域内数据的指针,可是并没有判断会见长度巨细,导致可以越界会见数据。该函数要害实现如下代码所示:


代码文件.png


参数size为会见长度,首先判断size是否大于0x3fffffff,可是并没有判断len是否大于ringbuf的data_sz,即会见的规模是否大于现实分派的ringbuf内存规模。然后对size+8上限取整为len,接下来取出rb->producer_pos,通过prod_pos+len盘算出new_prod_pos。


代码文件.png


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


代码文件.png


凭证前文剖析,rb->consumer_pos和rb->producer_pos所在页面是可映射的,是可控的且没有检查,size会见长度也是可控的,因此可以结构如下条件抵达大规模越界会见,令producer_pos = 0,consumer_pos= 0x3fffffff和size=0x3fffffff。这三个变量可以绕过所有检查,最后盘算出的new_prod_pos为0x3fffffff+8,这是个很大的规模。


该误差修复补丁有两部分,第一部分是加上了和data_sz巨细的判断,避免会见长度凌驾现实分派的空间规模,如下代码所示:


代码文件.png


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


误差使用历程


(1)通过堆喷结构一连内存结构,给越界读写提供场景

一连建设多个size=0x1000000的ringbuf,这里mapfd的ringbuf和victimfd的ringbuf是一连的,中心距离一个页面的guard page


代码文件.png

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

通过eBPF指令会见mapfd的ringbuf,并挪用bpf_ringbuf_reserve()函数获取mapfd的ringbuf->data指针。这里SIZE为0x30000000大于现实分派的内存空间。


代码文件.png


偏移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内存结构。


代码文件.png


同时,子历程和父历程通过pipe举行通讯,这个历程中会挪用__x64_sys_read和ksys_read函数并处于壅闭状态,然后一直搜索thread内核栈,搜索到这两个函数返回地址将其修改成commit_creds和prepare_kernel_cred,在父历程中扫除壅闭状态便可挟制流程举行执行恣意代码。      


综上使用历程,通过全心结构可实现对该误差的提权效果。


        

参考链接: 

https://flatt.tech/reports/210401_pwn2own/