首页
社区
课程
招聘
[原创]Linux CVE-2017-16995整数扩展问题导致提权漏洞分析
发表于: 2019-1-19 12:44 13397

[原创]Linux CVE-2017-16995整数扩展问题导致提权漏洞分析

2019-1-19 12:44
13397

学习内核调试没有很久,如有错误,欢迎指出,本篇文章同步到了我的blog

这个漏洞在2017年底被Google Project Zero团队的Jann Horn发现并修复,然而在2018年4月再次被国外安全研究者Vitaly Nikolenko发现,并可以对特定内核版本的Ubuntu 16.04进行提权,这个漏洞不包含堆栈攻击或者控制流劫持,仅用系统调用数据进行提权,是Data-Oriented Attacks在linux内核上的一个典型应用。

本文分析基于v4.4.110,可以从这里下载编译,也可以从这里在线阅读,本文涉及到的代码、镜像等可从这里下载。

之前在做pwnable.tw里的seccomp-tools一题时,曾经看过一部分bpf代码,但主要是为了逆向seccomp沙箱的规则。

BPF 的全称是 Berkeley Packet Filter,这是一个用于过滤(filter)网络报文(packet)的架构。Linux中常用的抓包软件tcpdump、wireshark都是基于这个模块来对用户提供抓包的接口的。在linux内核3.15以后,基于原有的BPF模块,Linux重新设计了BPF模块,并称之为extended BPF,简称EBPF。

EBPF主要可以为用户加载数据包过滤代码进入内核,并在收到数据包时触发这段代码。

一个常见的数据包过滤程序编写如下:

EBPF采用的指令集与内核使用的汇编指令不同,采用了一种基于bpf_insn数据结构的指令集,同时还维护了10个寄存器,一个栈,并且有与用户态交互的map结构。

首先是寄存器:

但内核寄存器的实现同EBPF模拟的栈一样,仍然依赖于栈上的临时变量,并不是直接映射为寄存器。后续将从代码层面分析。

接着是指令

熟悉seccomp-tools的同学可能发现,这个结构和seccomp的基本差不多。程序的功能主要取决于code这个字节,代表功能,其中code操作码共有8个比特,其中最低3个比特代表大类功能,从如下代码中看出EBPF共分7类功能,定义如下:

而对于各大类功能还可以从通过异或组成不同的新功能。具体的操作可以参考实现中的定义名,根据操作名就可以看出来每一种功能的大意了,我写了一个解码编码的小工具放在github连接中,可以用来翻译或者辅助编写EBPF程序。

dst_reg代表目的寄存器,限制为0~10,src_reg代表目的寄存器,限制为0~10,off代表地址偏移,imm代表立即数。

下面将从代码层面分析EBPF的运行流程。

这个系统调用首先调用map_create函数,这个函数就是之前分析的bpf模块整数溢出漏洞所在的函数,具体内容可以参照上一篇博客,其核心思想是对申请出一块内存空间,其大小是管理块结构体+attr参数中的size大小,为其分配fd,并将其放入到map队列中,可以用fd号来查找。此部分与本漏洞相关性不大。

map_create

这个系统调用用于将用户编写的EBPF规则加载进入内核,其中包含有多处校验。

首先进入bpf_prog_load函数中,首先[1]检查的ebpf license是否为GPL证书的一种,[2]检查指令条数是否超过4096,[3]处利用kmalloc新建了一个bpf_prog结构体,并新建了一个用于存放EBPF程序的内存空间。[4]处将用户态的EBPF程序拷贝到刚申请的内存中。[5]处来判断是哪种过滤模式,其中socket_filter是数据包过滤,而tracing_filter就是对系统调用号及参数的过滤,也就是我们常见的seccomp。最终到达[5]处开始对用户输入的程序进行检查。如果通过检查就将fp中执行函数赋值为 __bpf_prog_run也就是真实执行函数,并尝试JIT加载,否则用中断的方法加载。

下面进入加载的检查逻辑——bpf_check,首先在[1]处将特定指令中的mapfd换成相应的map实际地址,这里需要注意,map实际地址是一个内核地址,有8字节,这样就需要有两条指令的长度来存这个地址,具体可以看下面对这个函数的分析。[2]中借用了程序控制流图的思路来检查这个EBPF程序中是否有死循环和跳转到未初始化的位置,造成无法预期的风险。[3]是实际模拟执行的检测当上述有任一出现问题的检测,是检测的重点。

replace_map_fd_with_map_ptr函数中,可以看到当满足[1]、[2]两个条件时,即opcode = BPF_LD | BPF_IMM | BPF_DW=0x18,且src_reg = BPF_PSEUDO_MAP_FD =1时,将根据imm的值进行map查找,并将得到的地址分成两部分,分别存储于该条指令和下一条指令的imm部分,与上文所说的占用两条指令是相符的。满足上述两个条件的语句又被命名为BPF_LD_MAP_FD,即把map地址放到寄存器里,该指令写完后,下一条指令应为无意义的填充。

下面进行check过程中最核心的do_check函数,首先可以看到整个程序处于一个for死循环中,其中维护了一系列寄存器,其寄存器变量定义和初始化如下,可以看到寄存器的值是一个int类型,并且有一个枚举的type变量,type类型包括未定义、位置、立即数、指针等,初始化时会将全部寄存器类型定义为未定义,赋值为0。第十个寄存器定义为栈指针,第一个定义为内容指针。

check函数的处理方式是逐条处理,按照不同的类型分别做check。由于指令比较多,不一样赘述了,下面从两个攻击角度去展示程序是如何检测的。

退出指令定义为BPF_EXIT,这个指令属于BPF_JMP大类,可以看到当指令为该条指令的时候会执行一个pop_stack操作,而当这个函数的返回值是负数的时候,用break跳出死循环。否则会用这个作为取值的位置去执行下一条指令。对于这个操作的理解是,当遇到条件跳转的时候,程序会默认执行一个分支,然后将另外一个分支压入stack中,当一个分支执行结束后,去检查另外一个分支,类似于迷宫问题解决里走到思路的退栈操作。

查看一下pop_stack函数,函数中先判断env->head是否为0,如果是就代表没有未检查的路径了。否则将保持的state恢复。

然后看一下条件分支的处理代码check_cond_jmp_op,我们可以看到这个检查将跳转分成两种,第一种[1]处是JEQ和JNE,并且是比较的值是立即数的情况,此时就判断立即数是不是等于要比较的寄存器,进行直接跳转。第二种[2]处是其他情况,均需把off+1的值压入栈中作为另一条分支。

内存读写需要用到的指令主要是BPF_LDX_MEM或者BPF_STX_MEM两类。如下,当r7和r8的值可控就可以达到内存任意写,类似于mov dword ptr[r7],r8这样的操作。

接下来分析一下ST和LD有哪些限制,check_reg_arg[1]处检查寄存器是否访问寄存器的序号是否超过最大值10,如果是SRC_OP检查是否是未初始化的值。否则检查是否要写的地方是rbp,并将要写的寄存器值置为UNKOWN。然后是check_mem_access检查,该函数会根据读写类型检查dst或src的值是否为栈指针、数据包指针、map指针,否则不允许读写。:

以上情况,如果采用MOV这样的赋值指令去读写的话,寄存器类型会判定为IMM,而拒绝。另外一种是用BPF_FUNC_map_lookup_elem这样的函数调用返回,再赋给某个寄存器,然后再进行读写。而这种方法会在赋值时被设定为UNKNOWN而拒绝读写。

以上就是对于加载指令的全部检查,可以看到我们能想到的内存读写方法都是会被检测出来的。真正执行的时候代码在__bpf_prog_run中,其中可以看到所谓的各个寄存器和栈只是这个函数的局部变量:

程序维护了一个跳表,根据opcode来进行跳转,而函数中没有任何check,具体实现代码十分简单,就不赘述了。

可以发现程序的寄存器变量与check中的寄存器变量不太一样,此时是unsigned long long类型。

本漏洞的原因是check函数和真正的函数的执行方法不一致导致的,主要问题是二者寄存器值类型不同。先看下面一段EBPF指令:

第0条指令是将0xffffffff放入r9寄存器中,当在do_check函数中时,在[1]处会直接将0xffffffff复制给r9,并将type赋值为IMM。在第[1]条指令,比较r9==0xffffffff,相等时就执行[2]、[3],否则跳到[4]。根据前文对退出的分析,这个地方在do_check看来是一个恒等式,不会将另外一条路径压入stack,直接退出。

而在真实执行的过程中,由于寄存器类型不一样,在执行第二条跳转语句时存在问题:

而翻译成汇编就非常明显了:

可以看到汇编指令被翻译成movsxd,而此时会发生符号扩展,由原来的0xffffffff扩展成0xffffffffffffffff,再次比较的时候二者并不相同,造成了跳转到[4]处执行,从而绕过了对[4]以后EBPF程序的校验。

当[4]以后的程序不经过check以后,就可以对[4]的内容进行构造了,利用真正执行时无类型就可以达到内存任意读写了。

利用本人写的小工具对已有的EBPF程序进行解码,可以看到程序逻辑如下:

下面对这个程序进行分析:

首先,[0]~[3]已经分析过了下面对后续指令进行分析:

第[4]~[5]条语句可用由上面的map知识得到,第五条语句是填充语句,当执行完后,会将map的地址存放在r9寄存器中。

[6]~[13]语句的类C代码如下,即调用BPF_FUNC_map_lookup_elem(map_add,idx),并将返回值存到r6寄存器中,即r6=map[0]

[14]~[21]同理,将r7=map[1]。[22]~[29]为r8=map[2],而map的内容可以由用户态传入。

最后[30]~[40]分为三个不分,map[0] = 0时,将map[1]地址所指的内容,写到map[3]中,用户态可以通过读map[3]来得到这个值,因此是内存任意读功能。map[0]=1时,将rbp的值写入map[3]中,由此可以泄露内核栈地址。map[0]=2时,将map[3]的值写入map[2]地址中,由此是个内存任意写。

漏洞利用也非常简单,首先利用2功能读取内核栈地址,这样通过栈地址& ~(0x4000 - 1)可以得到内核线程task_struct的地址,而这个数据结构中的cred指针指向该线程的cred数据块,但是这个偏移会随内核编译的改变而改变,从gdb中看这个结构的方法是:

因此,利用0功能可以读出cred的地址,同理找出cred中的uid偏移

再利用2功能向该地址里写入0,就可以成功提权了。

[1] https://security.tencent.com/index.php/blog/msg/124

[2] https://www.ibm.com/developerworks/cn/linux/l-lo-eBPF-history/index.html

[3] https://www.jianshu.com/p/75b368f85dc6

[4] https://cert.360.cn/report/detail?id=ff28fc8d8cb2b72148c9237612933c11

[5] https://xz.aliyun.com/t/2212

[6] https://blog.csdn.net/qq_14978113/article/details/80488711

[7] https://elixir.bootlin.com/linux/v4.4.110/source/kernel/bpf/syscall.c

[8] https://elixir.bootlin.com/linux/v4.4.110/source/kernel/bpf/verifier.c


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2019-3-12 09:24 被pwnda编辑 ,原因: 选错格式
收藏
免费 8
支持
分享
最新回复 (7)
雪    币: 8231
活跃值: (1301)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享!
2019-1-20 22:31
1
雪    币: 26205
活跃值: (63302)
能力值: (RANK:135 )
在线值:
发帖
回帖
粉丝
3
666 感谢分享!
2019-1-21 09:45
0
雪    币: 351
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
多谢分享
2019-1-22 11:32
1
雪    币: 28
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
666 感谢分享!
2019-1-22 17:22
1
雪    币: 1068
活跃值: (30)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
6
大佬好,想请教一下,怎么找到“栈地址& ~(0x4000 - 1)为task_struct的地址” 的?我看还有个poc时用sk_buff里面的*sk去修改cred 但这两个poc关于task_struct 、sk_buff地址都是直接减偏移的,所以想问下找这种结构体地址是有什么工具么?
2019-12-4 19:30
0
雪    币: 763
活跃值: (323)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
7
Ne0o0o 大佬好,想请教一下,怎么找到“栈地址& ~(0x4000 - 1)为task_struct的地址” 的?我看还有个poc时用sk_buff里面的*sk去修改cred 但这两个poc关于task ...
没有什么工具吧,都是一些小trick,比如每个内核进程的task_struct会放在当前进程栈的最开始部分,栈大小默认是一定的,所以与一下就可以找到了。
2019-12-19 11:55
0
雪    币: 1068
活跃值: (30)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
8
多谢大佬指点,后面又调过几个类似的cve,确实都是在函数入口,或者漏洞触发前调用的函数附近找这种有cred的结构体或者标志特权的变量的。
2019-12-19 15:41
0
游客
登录 | 注册 方可回帖
返回
//