学习内核调试没有很久,如有错误,欢迎指出,本篇文章同步到了我的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] ba1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6k6h3y4#2M7X3W2@1P5g2)9J5k6i4c8W2L8X3y4W2L8Y4c8Q4x3X3g2U0L8$3#2Q4x3V1k6A6L8X3c8W2P5q4)9J5k6i4m8Z5M7q4)9J5c8X3u0D9L8$3N6Q4x3V1k6E0M7$3N6Q4x3V1j5I4x3U0b7`.
[2] 119K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2A6j5X3#2Q4x3X3g2U0L8$3#2Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0%4L8%4u0C8M7#2)9J5c8X3y4F1i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9i4K6u0V1L8r3!0Q4x3X3c8W2b7W2m8r3i4K6u0V1K9r3W2K6N6r3!0J5P5g2)9J5c8X3W2F1k6r3g2^5i4K6u0W2K9s2c8E0L8l9`.`.
[3] 951K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2B7K9h3q4F1M7$3S2#2i4K6u0W2j5$3!0E0i4K6u0r3M7q4)9J5c8U0M7#2j5U0x3$3z5r3j5^5y4h3c8U0y4R3`.`.
[4] 7c0K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0k6i4u0@1i4K6u0W2x3K6j5H3i4K6u0W2j5$3&6Q4x3V1k6J5k6i4m8G2M7Y4c8Q4x3V1k6V1k6i4c8S2K9h3I4Q4x3@1k6A6k6q4)9K6c8r3k6X3x3U0S2X3j5K6S2V1z5r3y4T1x3X3t1%4x3U0p5@1z5r3x3&6x3U0x3%4y4U0p5J5z5e0x3K6j5K6p5I4
[5] 9cdK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5P5W2)9J5k6h3q4D9K9i4W2#2L8W2)9J5k6h3y4G2L8g2)9J5c8Y4c8Q4x3V1j5J5x3U0p5J5
[6] acfK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8Y4q4I4i4K6g2X3x3e0b7&6y4K6R3I4x3e0y4Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6r3g2@1j5h3W2D9M7#2)9J5c8U0R3H3y4o6R3^5y4K6p5I4
[7] 539K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2L8r3W2^5K9i4u0Q4x3X3g2T1L8$3!0@1L8r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6$3y4q4)9J5k6e0c8Q4x3X3f1I4x3e0m8Q4x3V1k6K6L8%4g2J5j5$3g2Q4x3V1k6C8k6i4u0F1k6h3I4Q4x3V1k6T1M7r3k6Q4x3V1k6K6P5i4y4U0j5h3I4D9i4K6u0W2j5H3`.`.
[8] 4acK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2L8r3W2^5K9i4u0Q4x3X3g2T1L8$3!0@1L8r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6$3y4q4)9J5k6e0c8Q4x3X3f1I4x3e0m8Q4x3V1k6K6L8%4g2J5j5$3g2Q4x3V1k6C8k6i4u0F1k6h3I4Q4x3V1k6T1M7r3k6Q4x3V1k6$3k6i4u0A6k6X3W2W2M7W2)9J5k6h3x3`.
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2019-3-12 09:24
被pwnda编辑
,原因: 选错格式