linux内核 攻击,Linux内核攻击面之eBPF模块

reg->var_off = tnum_or(hi32, tnum_intersect(lo32, range));}

初看这个函数,很难理解tnum,mask,tnum_range,tnum_cast这些函数的作用,尽管ZDI博文中给了相关的解释,但我觉着还是对不了解eBPF模块的人不够友好,读完还是让人无法理解。由于这个漏洞和业务逻辑强相关,因此要想掌握漏洞原理,就必须能够理解代码的逻辑功能是什么,而代码中的tnum结构的数据类型是阻碍理解逻辑功能的关键。下面,本文将围绕tnum这个数据结构对此漏洞的根因进行分析。

Verifier

漏洞函数__reg_bound_offset32所在文件为verifier.c,verifier.c文件实现了上图中Verifier的功能。eBPF是用户侧编写的程序,但是却在内核虚拟机中执行,这显然是非常危险的,为了能够保障内核数据不被篡改和泄露,eBPF在真正被虚拟机执行之前都会被Verifier检查,Verifier会对eBPF指令的类型,跳转,是否有循环,以及操作数的取值范围进行检查,只有通过检查的eBPF的指令才可以被执行。

那么Verifier到底是如何保证不会有OOB这种情况发生的呢?

eBPF程序的每个操作数的属性都会被 bpf_reg_state数据结构进行追踪

bpf_reg_state的结构如下

enumbpf_reg_type type;union{u16range;structbpf_map *map_ptr;u32btf_id;unsignedlong raw;};s32off;u32id;u32ref_obj_id;structtnum var_off;s64smin_value;s64smax_value;u64umin_value;u64umax_value;structbpf_reg_state *parent;u32frameno;s32subreg_def;enumbpf_reg_liveness live;boolprecise;

可以看到对于每一个操作数,它的类型,值,取值范围都有详细的变量在追踪。常见的操作类型有PTR指针类型,或者Scalar这种常量类型的数据,为了防止越界,Verifier禁止了很多类型的操作,比如禁止两个PTR类型的操作数运算,但是允许PTR类型与Scalar类型的操作数运算。即使允许PTR类型与Scalar类型操作,也不能保证安全性,因为如果Scalar比较大的话,还是可以导致OOB,所以Verifier通过设置取值范围的方式来进行校验,如果操作数在运算后超过了被设定的最大最小值范围,也会被禁止。

我们可以看到 bpf_reg_state还定义了一个tnum变量,这个变量注释说是获得操作数各个位的信息的情况的,value,mask两个字段一起表达操作数各个位的0,1,或者未知的三种状态的。

tnum数据结构的逻辑意义

tnum是为了描述那些不能有明确值的操作数,那么什么情况下操作数的值是不能确定的呢,例如从一个packet中读取一个half word,这个值就是不能确定。而如果直接读取一个立即数,这种值就是确定的。对于这种不能确定的操作数,就可以用umax,umin,smax,smin这几种变量表示有符号和无符号的最大最小值,tnum描述他们的每个位的信息。总之配合最大最小值,tnum可以尽可能的对一个未知的变量进行预测。并且伴随着eBPF指令的执行,还会对tnum,最大最小值进行更新,举个例子

ifreg_0 < 7// 有符号比较reg_1 = reg_0elsereg_1 = 1

在这个例子中,reg_0这个操作数会被跟踪,如果它小于7,则可以对reg_0的最大值进行设置,最大值为7-1=6, 同时也得出高位都是0,所以也可以对tnum进行设置。本文的__reg_bound_offset32函数就是负责处理tnum与最大最小值同步更新的工作的。

tnum到底是如何描述未知值的?

假设拿到一个寄存器,这个寄存器就是不是一个确定值,用tnum表示他的位的状态,比如64位的一个数,那么某一位只可能三种状态,确定的0,确定的1,或者不知道是啥,就是这种数据结构是某个位有三种状态,而不是2种状态。

单纯的用一个64位的数据是不可能表达这种数据结构的,这种数据结构有3的64次方,而64位的二进制只有2的64方,但是如果有两个64位的数据就可以表达这个64位的三进制数据,2的128次方。

所以就需要一种编码方式,用2个64位数编码这个三进制的数。

而eBPF的tnum的编码方式就是能确定是1的位,就value标识为确定1,而能确定是0的位需要,value位0,并且mask对应位也为0,相当于用2位去表达这个状态,,所以本质是用2位去表达三种状态,就是x1,标识1,01标识0,00标识未知这种本质。

为了精确,模拟了一个mask和value的东西,就是value位能够决定某个位是1,对应mask位的值必须为0(有一个规定就是不能同时为1),而对于确定是0的位,则必须value位为0,mask也要为0,对于unknown的状态,需要value为0,而mask为1

所以最终的表达为

valuemask 预测值00 010 101 unknown11 禁止出现

__reg_bound_offset32漏洞函数解析

除了__reg_bound_offset32还有一个__reg_bound_offset函数,这个函数功能更加简洁

staticvoid __reg_bound_offset(struct bpf_reg_state *reg){reg->var_off = tnum_intersect(reg->var_off,tnum_range(reg->umin_value,reg->umax_value));}

__reg_bound_offset32是一种特殊情况,只有当操作数已经明知是32位的才会执行,而对于一般的是默认执行__reg_bound_offset操作,我们可以先从__reg_bound_offset去推测 __reg_bound_offset32的大概意义。

tnum_intersect函数的输入是两个tnum的变量,根据名字和源码我们可以简要总结:当有两个tnum对同一个操作数进行描述的时候,可以结合两个tnum的信息,这样可以对这个操作数的描述更加精确,结合的规则就是,如果一个tnum的某个位已知,另外一个tnum的对应位为未知,那么结合后新tnum对应位则是已知的。

tnum_range函数作用是,根据一个更新后的最大最小值得到一个tnum。这个tnum可以与目标操作数的tnum进行tnum_intersect,相当于融合了最大最小值的信息,这样可以实现对原来的操作数进行更准确的预测更新。

所以根据__reg_bound_offset的作用,我们知道了主要目的就是根据最大最小值对原来操作数的tnum进行更加准确的预测。那么__reg_bound_offset32又有什么不同呢?

__reg_bound_offset32源码如下:

staticvoid__reg_bound_offset32(struct bpf_reg_state *reg){u64 mask = 0xffffFFFF;structtnumrange= tnum_range( reg-> umin_value& mask,reg-> umax_value& mask);// ----->1structtnumlo32= tnum_cast( reg-> var_off, 4);structtnumhi32= tnum_lshift( tnum_rshift( reg-> var_off, 32), 32);reg->var_off = tnum_or(hi32, tnum_intersect(lo32, range));}

我们利用理解tnum_range函数的方法,可以推得tnum_cast,tnum_lshift,tnum_or的作用,可以感觉出整个函数的目的是同样根据最大最小值对已有的tnum值进行更新。而且,相比于__reg_bound_offset函数,__reg_bound_offset32还有一个隐藏的信息可以对操作数进行更加准确的预测:

32位数的最大最小值不会超过0xFFFFFFFF

这个隐藏条件的表达就是 标注1所做的工作,漏洞代码尝试用截断低32位的方式来表达32位数的最大最小值不会超过0xFFFFFFFF,但是实际上这个语句并不能表达这个功能。准确的表达是

new_umin_value = min(0xffffffff,umin_value)new_umax_value = min(0xffffffff,umax_value)range = tnum_range(new_umin_value, new_umax_value)

上面两句话是笔者自己理解的实现32位隐藏条件的代码。

漏洞根因

正是struct tnum range = tnum_range(reg->umin_value & mask,

reg->umax_value & mask); 这一条语句导致的漏洞,这句话实现的是截断功能,而不是对于超出32位的数直接取值为0xffff ffff的功能。

由于这个错误的实现导致Veifier并不能正确的验证eBPF指令的执行情况,所以对一些本应该禁止的OOB操作,Verifier还是通过了检查,最终可以实现对内核数据的越界读写。

小结

这个漏洞的Root Cause是和漏洞函数的业务功能逻辑强相关的,如果不理解代码的目的,很难对这个漏洞的根本原因理解,而由于eBPF的执行流程又比较特别,需要对背景知识,设计架构,运行机理有一定的了解才能够推理出漏洞函数的功能。为了能够让不熟悉eBPF的同学能够更加快速的了解eBPF,接触eBPF这一个理想的内核攻击面。eBPF程序由用户定义,但是在内核中执行,这是eBPF模块是一个值得重视的内核攻击面的根本原因。希望本文提供的思维路线,能够帮助到大家。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值