[Linux Kernel eBPF] Analyzing Negative Index Handling in BPF CO-RE
목차
배경
리눅스 커널의 eBPF 공부를 하기 위해서 관련 메일을 분석해봤다.
메일
원본 이메일은 여기서 볼 수 있다. -> lore.kernel.org
From: Weiming Shi <[email protected]>
Subject: [PATCH bpf] bpf: reject negative CO-RE accessor indices in bpf_core_parse_spec()
...
CO-RE accessor strings are colon-separated indices that describe a path
from a root BTF type to a target field, e.g. "0:1:2" walks through
nested struct members. bpf_core_parse_spec() parses each component with
sscanf("%d"), so negative values like -1 are silently accepted. The
subsequent bounds checks (access_idx >= btf_vlen(t)) only guard the
upper bound and always pass for negative values. When -1 reaches
btf_member_bit_offset() it gets cast to u32 0xffffffff, producing an
out-of-bounds read far past the members array.
A crafted BPF program with a negative CO-RE accessor on any struct that
exists in vmlinux BTF (e.g. task_struct) crashes the kernel during
BPF_PROG_LOAD:
BUG: unable to handle page fault for address: ffffed11818b6626
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page
PGD 7f74e067 P4D 7f74e067 PUD 0
Oops: Oops: 0000 [#1] SMP KASAN NOPTI
CPU: 0 UID: 0 PID: 85 Comm: poc Not tainted 7.0.0-rc6 #18 PREEMPT(full)
Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
RIP: 0010:bpf_core_parse_spec (tools/lib/bpf/relo_core.c:348)
RAX: 00000000ffffffff RBX: ffff88800c5b3128 RCX: 0000000000000000
Call Trace:
<TASK>
bpf_core_calc_relo_insn (tools/lib/bpf/relo_core.c:1319)
bpf_core_apply (kernel/bpf/btf.c:9507)
bpf_check (kernel/bpf/verifier.c:26031)
bpf_prog_load (kernel/bpf/syscall.c:3089)
__sys_bpf (kernel/bpf/syscall.c:6228)
__x64_sys_bpf (kernel/bpf/syscall.c:6339)
do_syscall_64 (arch/x86/entry/syscall_64.c:94)
</TASK>
CO-RE accessor indices are inherently non-negative (field index, array
index, or enumerator index), so reject them after parsing.
Fixes: ddc7c3042614 ("libbpf: implement BPF CO-RE offset relocation algorithm")
Reported-by: Xiang Mei <[email protected]>
igned-off-by: Weiming Shi <[email protected]>
마지막에 Signed-off-by에서 S가 빠진채로 메일을 보내셨다.
메일 내용 분석
먼저 CO-RE accessor strings 에 대해서 알아보자.
CO-RE accessor strings
CO-RE accessor strings는 이 필드에 접근이 원래 어떤 타입의 어느 내부 멤버를 가리켰는지를 표현하는 경로 문자열이다.
쉽게 설명하자면
task->mm->exe_file->f_inode->i_ino
처럼 생긴 task에서 i_ino까지 어떻게 들어갔는지를 저장하는 특정 필드까지의 접근 경로이다.
CO-RE: 소스 코드에 본 구조체 레이아웃과 실행 대상 커널의 구조체 레이아웃이 달라져도 동작하게 하려는 메커니즘
만드는 곳은 Clang의 __builtin_preserve_access_index()를 컴파일 할때 이다.
쓰는 곳은 libbpf가 대상 커널의 BTF와 대조해서 필드를 찾아낼 때 쓴다.
메일 내용에 따르면 "0:1:2" 처럼 생긴 것 같다.
bpf_core_parse_spec()
이 함수는 CO-RE relocation 하나에 들어있는 accessor string을 읽어서, libbpf가 실제로 따라갈 수 있는 내부 경로 표현으로 바꾸는 함수이다.
e.g.) "0:1:2" -> bpf_core_parse_spec() -> bpf_core_spec 내부의 데이터 채우기
근데 이 함수에서 각 숫자를 sscanf("%d") 로 파싱하는 바람에 -1같은 것들도 통과된다. 그리고 나중에하는 경계 검사에서 음수는 거르지 않기 때문에 결국은 통과되서 OOB read가 발생한다는 것이다.
이걸 실제 음수 CO-RE accessor string을 가지고 실험해본 것 같다.
실험 - 음수 CO-RE accessor string BPF 프로그램 만들기
계획: 정상 BPF 프로그램 -> 정교하게 바이너리 패치 -> 실험
정상 BPF 프로그램 제작
일단 먼저 bpftool로 vmlinux.h를 만든다.
bpftool btf dump file /path/to/vmlinux format c > vmlinux.h
bpf랑 btf관련 옵션 키고 다시 빌드했다.
vmlinux.h 뽑고나서는 아래 C코드를 컴파일 했다.
// task_core.bpf.c
// clang -O2 -g -target bpf -D__TARGET_ARCH_x86 \
-I./libbpf/src \
-I. \
-c task_core.bpf.c -o task_core.bpf.o
#include "vmlinux.h"
#include "bpf_helpers.h"
#include "bpf_core_read.h"
#include "bpf_tracing.h"
char LICENSE[] SEC("license") = "GPL";
struct event {
__u32 pid;
__u32 tgid;
__u32 ppid;
char comm[16];
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24);
} events SEC(".maps");
SEC("tp_btf/sched_switch")
int BPF_PROG(handle_switch)
{
struct task_struct *task;
struct task_struct *parent;
struct event *e;
task = (struct task_struct *)bpf_get_current_task_btf();
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e)
return 0;
e->pid = BPF_CORE_READ(task, pid);
e->tgid = BPF_CORE_READ(task, tgid);
bpf_core_read_str(&e->comm, sizeof(e->comm), task->comm);
parent = BPF_CORE_READ(task, real_parent);
e->ppid = parent ? BPF_CORE_READ(parent, tgid) : 0;
bpf_ringbuf_submit(e, 0);
return 0;
}
헤더 오류가 나서 bpf관련 헤더는 libbpf git 레포를 가져와서 해결했다.
여기서 문제는 바이너리 패치를 하려면 어느 위치를 음수로 바꿔야하는지를 알아야하는데...
이걸 모른다고 생각해서 포기하려고 했다가 gpt 살살 달래서 정상 bpf에서 단순히 0:81같이 생긴 것들 0:-1같이 바꾸면 된다는 사실을 알아냈다.
# patch.py
from elftools.elf.elffile import ELFFile
with open("task_core.bpf.o", "rb") as f:
data = bytearray(.())
with open("task_core.bpf.o", "rb") as f:
elf =()
btf = elf.(".BTF")
if not btf:
raise RuntimeError(".BTF not found")
blob = btf.()
needle = b"0:81\x00"
off = blob.()
if off < 0:
raise RuntimeError("accessor string not found")
file_off =['sh_offset'] + off
repl = b"0:-1\x00"
[:+len()] = repl
with open("repro-neg.bpf.o", "wb") as f:
f.()// loader.c
// cc -O2 -g loader.c -o loader -lbpf -lelf -lz
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <bpf/libbpf.h>
static volatile sig_atomic_t stop;
static void on_sigint(int sig)
{
stop = 1;
}
struct event {
__u32 pid;
__u32 tgid;
__u32 ppid;
char comm[16];
};
static int libbpf_print_fn(enum libbpf_print_level level,
const char *format, va_list args)
{
return vfprintf(stderr, format, args);
}
static int handle_event(void *ctx, void *data, size_t len)
{
const struct event *e = data;
printf("pid=%u tgid=%u ppid=%u comm=%s\n",
e->pid, e->tgid, e->ppid, e->comm);
return 0;
}
int main(void)
{
struct bpf_object *obj = NULL;
struct bpf_program *prog;
struct bpf_link *link = NULL;
struct ring_buffer *rb = NULL;
int map_fd;
int err;
signal(SIGINT, on_sigint);
signal(SIGTERM, on_sigint);
libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
libbpf_set_print(libbpf_print_fn);
obj = bpf_object__open_file("task_core.bpf.o", NULL);
if (!obj) {
fprintf(stderr, "failed to open BPF object\n");
return 1;
}
err = bpf_object__load(obj);
if (err) {
fprintf(stderr, "failed to load BPF object: %d\n", err);
goto cleanup;
}
prog = bpf_object__find_program_by_name(obj, "handle_switch");
if (!prog) {
fprintf(stderr, "failed to find program: handle_switch\n");
err = -ENOENT;
goto cleanup;
}
link = bpf_program__attach(prog);
if (!link) {
err = -errno;
fprintf(stderr, "failed to attach program: %d\n", err);
goto cleanup;
}
map_fd = bpf_object__find_map_fd_by_name(obj, "events");
if (map_fd < 0) {
err = map_fd;
fprintf(stderr, "failed to find map fd: %d\n", err);
goto cleanup;
}
/* ringbuf 소비는 선택 사항이지만, 안 읽으면 이벤트만 쌓입니다. */
// rb = ring_buffer__new(map_fd, NULL, NULL, NULL);
rb = ring_buffer__new(map_fd, handle_event, NULL, NULL);
if (!rb) {
err = -errno;
fprintf(stderr, "failed to create ring buffer: %d\n", err);
goto cleanup;
}
printf("program loaded and attached\n");
while (!stop) {
err = ring_buffer__poll(rb, 100 /* ms */);
if (err == -EINTR)
break;
if (err < 0) {
fprintf(stderr, "ring_buffer__poll failed: %d\n", err);
goto cleanup;
}
}
err = 0;
cleanup:
ring_buffer__free(rb);
bpf_link__destroy(link);
bpf_object__close(obj);
return err != 0;
}
해본 결과 커널 크래시는 아니고 libbpf 크래시다. 라고 생각하고 메일을 다시 한번 더 봤는데 누가봐도 저 메일은 커널 크래시였다. ㅜㅜ
libbpf: sec 'tp_btf/sched_switch': found 4 CO-RE relocations
libbpf: CO-RE relocating [13] struct task_struct: found target candidate [136253] struct task_struct in [vmlinux]
libbpf: prog 'handle_switch': relo #0: <byte_off> [13] struct task_struct.pid (0:80 @ offset 1456)
libbpf: prog 'handle_switch': relo #0: matching candidate #0 <byte_off> [136253] struct task_struct.pid (0:80 @ offset 1456)
libbpf: prog 'handle_switch': relo #0: patched insn #9 (ALU/ALU64) imm 1456 -> 1456
loader[70]: segfault at 55f4b45f4e68 ip 00007f320e693a6d sp 00007ffc5e7211c0 error 4 in libbpf.so.1[42a6d,7f320e65a000+45000]
Segmentation fault
아니 여기서 어느 함수에서 터지는 지랑 왜 저 코드가 문제가 됐는지랑 어떻게 고쳤는지를 확인해봐야하는데 qemu안의 gdbserver랑 호스트의 gdb랑 안 붙는다 ㅜㅜ
이리저리 GPT랑 씨름하다가 붙였다 !!!
gef> bt
#0 0x00007f83fa2ee665 in btf_member_bit_offset (t=0x556b18f35ec0, member_idx=0xffffffff)
at /home/rand/nomads/kernel-dev/kernel-lab/bpf/libbpf/src/btf.h:627
#1 0x00007f83fa2ef09b in bpf_core_parse_spec (prog_name=0x556b18f2db30 "handle_switch", btf=0x556b18f35d50, relo=0x556b18f3e65c,
spec=0x7fff5da89e90) at relo_core.c:348
#2 0x00007f83fa2f164b in bpf_core_calc_relo_insn (prog_name=0x556b18f2db30 "handle_switch", relo=0x556b18f3e65c, relo_idx=0x1,
local_btf=0x556b18f35d50, cands=0x556b18f2dd10, specs_scratch=0x7fff5da89e90, targ_res=0x7fff5da8ae90) at relo_core.c:1319
#3 0x00007f83fa2bc1c0 in bpf_core_resolve_relo (prog=0x556b18f2da10, relo=0x556b18f3e65c, relo_idx=0x1, local_btf=0x556b18f35d50,
cand_cache=0x556b18f2b5b0, targ_res=0x7fff5da8ae90) at libbpf.c:6022
#4 0x00007f83fa2bc53c in bpf_object__relocate_core (obj=0x556b18f2b310, targ_btf_path=0x0) at libbpf.c:6115
#5 0x00007f83fa2bef77 in bpf_object__relocate (obj=0x556b18f2b310, targ_btf_path=0x0) at libbpf.c:7379
#6 0x00007f83fa2c33c7 in bpf_object_prepare (obj=0x556b18f2b310, target_btf_path=0x0) at libbpf.c:8921
#7 0x00007f83fa2c355b in bpf_object_load (obj=0x556b18f2b310, extra_log_level=0x0, target_btf_path=0x0) at libbpf.c:8960
#8 0x00007f83fa2c36db in bpf_object__load (obj=0x556b18f2b310) at libbpf.c:8996
#9 0x0000556b149de1c4 in main () at repro-loader.c:61
#10 0x00007f83f9fb0b57 in ?? () from target:/lib64/libc.so.6
#11 0x00007f83f9fb0c15 in __libc_start_main () from target:/lib64/libc.so.6
#12 0x0000556b149de3c5 in _start ()
이게 libbpf가 터진 직후의 back trace이다.
메일에서 봤던 bpf_core_calc_relo_insn 가 보인다. 😀
자 이제 천천히 코드를 따라가보자.
코드를 보고 버그의 작동 원리에 대해서 알아보자
일단 bt의 역순을 따라가보자.
// repro-loader.c
err = bpf_object__load(obj);
if (err) {
fprintf(stderr, "failed to load BPF object: %d\n", err);
goto cleanup;
}
위 파일이 내가 만든 조작된 bpf프로그램 로더 코드다.
직접 libbpf의 bpf_object__load를 부르고 있다.
// libbpf.c
int bpf_object__load(struct bpf_object *obj)
{
return bpf_object_load(obj, 0, NULL);
}
단순하게 bpf_object_load를 부른다.
// libbpf.c
static int bpf_object_load(struct bpf_object *obj, int extra_log_level, const char *target_btf_path)
{
...
if (obj->state < OBJ_PREPARED) {
err = bpf_object_prepare(obj, target_btf_path);
if (err)
return libbpf_err(err);
}
err = bpf_object__load_progs(obj, extra_log_level);
err = err ? : bpf_object_init_prog_arrays(obj);
err = err ? : bpf_object_prepare_struct_ops(obj);
...
}
여기서 bpf_object_prepare를 보자.
// libbpf.c
static int bpf_object_prepare(struct bpf_object *obj, const char *target_btf_path)
{
...
err = err ? : bpf_object__relocate(obj, obj->btf_custom_path ? : target_btf_path);
err = err ? : bpf_object__sanitize_and_load_btf(obj);
...
if (err) {
bpf_object_unpin(obj);
bpf_object_unload(obj);
obj->state = OBJ_LOADED;
return err;
}
}
이 함수는 ELF에서 읽어온 BPF 오브젝트를 전처리해 두고, 커널에 올릴 준비 완료 상태로 만드는 함수이다.
그중에서 bpf_object__relocate가 컴파일 시점의 심볼, 오프셋 등을 지금 실행 중인 커널에 맞게 고쳐주는 함수이다.
// libbpf.c
static int bpf_object__relocate(struct bpf_object *obj, const char *targ_btf_path)
{
...
if (obj->btf_ext) {
err = bpf_object__relocate_core(obj, targ_btf_path);
if (err) {
pr_warn("failed to perform CO-RE relocations: %s\n",
errstr(err));
return err;
}
bpf_object__sort_relos(obj);
}
...
}
이 함수가 맨처음으로 하는 것은 obj에 .BTF.ext가 있다면 CO-RE relocation을 하는 것이다.
// libbpf.c
static int
bpf_object__relocate_core(struct bpf_object *obj, const char *targ_btf_path)
{
...
err = bpf_core_resolve_relo(prog, rec, i, obj->btf, cand_cache, &targ_res);
if (err) {
pr_warn("prog '%s': relo #%d: failed to relocate: %s\n",
prog->name, i, errstr(err));
goto out;
}
err = bpf_core_patch_insn(prog->name, insn, insn_idx, rec, i, &targ_res);
if (err) {
pr_warn("prog '%s': relo #%d: failed to patch insn #%u: %s\n",
prog->name, i, insn_idx, errstr(err));
goto out;
}
...
}
이 함수는 BPF 프로그램이 참조하는 커널 타입,필드,enum 등의 정보가 현재 타깃 커널에서는 어디에 해당하는지 계산해서, 해당 BPF instruction을 실제 값으로 고쳐 넣는 함수다.
// libbpf.c
static int bpf_core_resolve_relo(...)
{
...
return bpf_core_calc_relo_insn(prog_name, relo, relo_idx, local_btf, cands, specs_scratch,
targ_res);
}
이 함수는 로컬 BTF에서 relocation 대상 타입을 찾고, 최종 relocation 값을 계산하는 함수다.
// relo_core.c
int bpf_core_calc_relo_insn(...)
{
...
local_id = relo->type_id;
local_type = btf_type_by_id(local_btf, local_id);
local_name = btf__name_by_offset(local_btf, local_type->name_off);
if (!local_name)
return -EINVAL;
err = bpf_core_parse_spec(prog_name, local_btf, relo, local_spec);
if (err) {
const char *spec_str;
spec_str = btf__name_by_offset(local_btf, relo->access_str_off);
pr_warn("prog '%s': relo #%d: parsing [%d] %s %s + %s failed: %d\n",
prog_name, relo_idx, local_id, btf_kind_str(local_type),
str_is_empty(local_name) ? "<anon>" : local_name,
spec_str ?: "<?>", err);
return -EINVAL;
}
...
}
이 함수는 최종 relocation값을 확정짓는 함수다.
bpf_core_parse_spec을 통해서 relocation access string을 spec으로 파싱한다.
// relo_core.c
/*
* Turn bpf_core_relo into a low- and high-level spec representation,
* validating correctness along the way, as well as calculating resulting
* field bit offset, specified by accessor string. Low-level spec captures
* every single level of nestedness, including traversing anonymous
* struct/union members. High-level one only captures semantically meaningful
* "turning points": named fields and array indicies.
* E.g., for this case:
*
* struct sample {
* int __unimportant;
* struct {
* int __1;
* int __2;
* int a[7];
* };
* };
*
* struct sample *s = ...;
*
* int x = &s->a[3]; // access string = '0:1:2:3'
*
* Low-level spec has 1:1 mapping with each element of access string (it's
* just a parsed access string representation): [0, 1, 2, 3].
*
* High-level spec will capture only 3 points:
* - initial zero-index access by pointer (&s->... is the same as &s[0]...);
* - field 'a' access (corresponds to '2' in low-level spec);
* - array element #3 access (corresponds to '3' in low-level spec).
*
* Type-based relocations (TYPE_EXISTS/TYPE_MATCHES/TYPE_SIZE,
* TYPE_ID_LOCAL/TYPE_ID_TARGET) don't capture any field information. Their
* spec and raw_spec are kept empty.
*
* Enum value-based relocations (ENUMVAL_EXISTS/ENUMVAL_VALUE) use access
* string to specify enumerator's value index that need to be relocated.
*/
int bpf_core_parse_spec(const char *prog_name, const struct btf *btf,
const struct bpf_core_relo *relo,
struct bpf_core_spec *spec)
{
int access_idx, parsed_len, i;
struct bpf_core_accessor *acc;
const struct btf_type *t;
const char *name, *spec_str;
__u32 id, name_off;
__s64 sz;
spec_str = btf__name_by_offset(btf, relo->access_str_off);
if (str_is_empty(spec_str) || *spec_str == ':')
return -EINVAL;
memset(spec, 0, sizeof(*spec));
spec->btf = btf;
spec->root_type_id = relo->type_id;
spec->relo_kind = relo->kind;
/* type-based relocations don't have a field access string */
if (core_relo_is_type_based(relo->kind)) {
if (strcmp(spec_str, "0"))
return -EINVAL;
return 0;
}
/* parse spec_str="0:1:2:3:4" into array raw_spec=[0, 1, 2, 3, 4] */
while (*spec_str) {
if (*spec_str == ':')
++spec_str;
if (sscanf(spec_str, "%d%n", &access_idx, &parsed_len) != 1)
return -EINVAL;
if (spec->raw_len == BPF_CORE_SPEC_MAX_LEN)
return -E2BIG;
spec_str += parsed_len;
spec->raw_spec[spec->raw_len++] = access_idx;
}
if (spec->raw_len == 0)
return -EINVAL;
t = skip_mods_and_typedefs(btf, relo->type_id, &id);
if (!t)
return -EINVAL;
access_idx = spec->raw_spec[0];
acc = &spec->spec[0];
acc->type_id = id;
acc->idx = access_idx;
spec->len++;
if (core_relo_is_enumval_based(relo->kind)) {
if (!btf_is_any_enum(t) || spec->raw_len > 1 || access_idx >= btf_vlen(t))
return -EINVAL;
/* record enumerator name in a first accessor */
name_off = btf_is_enum(t) ? btf_enum(t)[access_idx].name_off
: btf_enum64(t)[access_idx].name_off;
acc->name = btf__name_by_offset(btf, name_off);
return 0;
}
if (!core_relo_is_field_based(relo->kind))
return -EINVAL;
sz = btf__resolve_size(btf, id);
if (sz < 0)
return sz;
spec->bit_offset = access_idx * sz * 8;
for (i = 1; i < spec->raw_len; i++) {
t = skip_mods_and_typedefs(btf, id, &id);
if (!t)
return -EINVAL;
access_idx = spec->raw_spec[i];
acc = &spec->spec[spec->len];
if (btf_is_composite(t)) {
const struct btf_member *m;
__u32 bit_offset;
if (access_idx >= btf_vlen(t))
return -EINVAL;
bit_offset = btf_member_bit_offset(t, access_idx);
spec->bit_offset += bit_offset;
m = btf_members(t) + access_idx;
if (m->name_off) {
name = btf__name_by_offset(btf, m->name_off);
if (str_is_empty(name))
return -EINVAL;
acc->type_id = id;
acc->idx = access_idx;
acc->name = name;
spec->len++;
}
id = m->type;
} else if (btf_is_array(t)) {
const struct btf_array *a = btf_array(t);
bool flex;
t = skip_mods_and_typedefs(btf, a->type, &id);
if (!t)
return -EINVAL;
flex = is_flex_arr(btf, acc - 1, a);
if (!flex && access_idx >= a->nelems)
return -EINVAL;
spec->spec[spec->len].type_id = id;
spec->spec[spec->len].idx = access_idx;
spec->len++;
sz = btf__resolve_size(btf, id);
if (sz < 0)
return sz;
spec->bit_offset += access_idx * sz * 8;
} else {
pr_warn("prog '%s': relo for [%u] %s (at idx %d) captures type [%d] of unexpected kind %s\n",
prog_name, relo->type_id, spec_str, i, id, btf_kind_str(t));
return -EINVAL;
}
}
return 0;
}
이 함수가 중요하다.
먼저 큰 그림 부터 그리자.
bpf_core_relo는 BPF명령의 어떤 타입/필드/enum정보를 로드 시점에 대상 커널에 맞게 다시 계산하는 relocation 기록이다.
.BTF.ext를 분석해서 현재 커널의 BTF를 보고 값을 다시 계산하는 것이다.
bpf_core_relo는 총 3종류이다.
- field-based relocation: 이 필드가 어디에 몇바이트로 존재하냐?
- type-based relocation: 타입 자체
- enum value-based relocation: enum 멤버 index가 어디에 존재하나?
다음으로 이 함수의 입출력을 보자.
input
- btf: 타입 정보
- relo: relocation 할 소스
- spec: 결과 저장 구조체
output
- raw_spec: access string을 숫자 배열로 파싱한 저수준 표현
- spec[]: 의미있는 지점만 뽑은 고수준 표현
- bit_offset: 최종 비트 오프셋
그림으로도 보자
struct sample {
int __unimportant;
struct {
int __1;
int __2;
int a[7];
};
};
struct sample *s = ...;
int x = &s->a[3]; // access string = '0:1:2:3'
위의 내용이 핵심이다.
이걸 그림으로 나타내면
+---------------------------+ struct sample
| __unimportant | field #0
+---------------------------+
| anonymous struct | field #1
| +-------------------+ |
| | __1 | | field #0
| +-------------------+ |
| | __2 | | field #1
| +-------------------+ |
| | a[0] | | field #2
| | a[1] | |
| | a[2] | |
| | a[3] <--- here | |
| | ... | |
| +-------------------+ |
+---------------------------+
access string "0:1:2:3" 뜻은:
- 0 : s[0] (s->... 는 사실상 s[0].... 과 같음)
- 1 : struct sample의 field #1 → anonymous struct
- 2 : 그 anonymous struct의 field #2 → a
- 3 : 배열 a[3]
즉
0:1:2:3
│ │ │ └─ array index 3
│ │ └─── field index 2 (a)
│ └───── field index 1 (anonymous struct)
└─────── root pointer first element
여기서 저수준이란
raw_spec = [0, 1, 2, 3]
이거이고 고수준이란 아래와 같이 의미있는 것만 남긴 것이다.
high-level spec = [
root[0],
field a,
array[3]
]
함수를 요약하면 이렇다.
- access string 꺼냄
- spec 초기화
- type-based relocation이면 특수 처리
- "0:1:2:3" -> raw_spec[] 파싱
- root type 정리(typedef/mod 제거)
- enum 기반 relocation이면 특수 처리
- field 기반 relocation이면 타입을 실제로 따라 내려감
- struct/union member 접근
- array index 접근
- bit offset 누적
- 의미 있는 accessor 기록
- spec 완성
문제는 4번에서 생긴다.
아래가 4번 코드이다.
/* parse spec_str="0:1:2:3:4" into array raw_spec=[0, 1, 2, 3, 4] */
while (*spec_str) {
if (*spec_str == ':')
++spec_str;
if (sscanf(spec_str, "%d%n", &access_idx, &parsed_len) != 1)
return -EINVAL;
if (spec->raw_len == BPF_CORE_SPEC_MAX_LEN)
return -E2BIG;
spec_str += parsed_len;
spec->raw_spec[spec->raw_len++] = access_idx;
}
변수:
- sepc_str: "0:1:2:3:4" 저장하는 문자열
- access_idx: 추출한 각 idx 임시 저장소
- parsed_len: sscanf으로 입력받은 수
- spec->raw_len: raw_spec 배열의 길이
- spec->raw_spec: [0, 1, 2, 3, 4] 같은 저수준 배열
- BPF_CORE_SPEC_MAX_LEN: 64 기본값
여기서 문제는 %d로 읽고 있기에 spec_str에 "0:-1"와 같이 음수를 넣는다면 spec->raw_sepc에 음수가 들어간다는 것이다. (spec_raw_spec은 int 배열이다)
음수가 들어가면 OOB가 난다.
이유는 다음과 같다.
- enum 타입일 경우에는 아래와 같이 btf의 길이보다 큰것만 검사하므로 통과된다.
if (!btf_is_any_enum(t) || spec->raw_len > 1 || access_idx >= btf_vlen(t))
return -EINVAL;
- field일 경우 아래와 같이 btf의 길이보다 큰것만 검사해서 통과하게 된다.
if (access_idx >= btf_vlen(t))
return -EINVAL;
bit_offset = btf_member_bit_offset(t, access_idx);
그리고 btf_member_bit_offset에서는 인자가 __u32이기 때문에 음수가 변환되면서 의도치 않은 값이 들어간다.
// btf.h
static inline __u32 btf_member_bit_offset(const struct btf_type *t,
__u32 member_idx)
{
const struct btf_member *m = btf_members(t) + member_idx;
bool kflag = btf_kflag(t);
return kflag ? BTF_MEMBER_BIT_OFFSET(m->offset) : m->offset;
}
- __u32타입인 bit_offset에 음수값을 대입하게 되면서 아래와 같이 의도하지 않은 값이 bit_offset에 들어간다.
- 그 후 그 주소에 접근하게 되면서 segmentation fault가 터진다.
624 const struct btf_member *m = btf_members(t) + member_idx;
625 bool kflag = btf_kflag(t);
626
// m = 0x00007ffd8d4792b8 -> 0x0000563b994b5ec0
-> 627 return kflag ? BTF_MEMBER_BIT_OFFSET(m->offset) : m->offset;
628 }패치
패치는 access_idx를 받은 직후 음수인지 검사하는 것으로 만들어 놓으셨다.
++spec_str;
if (sscanf(spec_str, "%d%n", &access_idx, &parsed_len) != 1)
return -EINVAL;
+ if (access_idx < 0)
+ return -EINVAL;
if (spec->raw_len == BPF_CORE_SPEC_MAX_LEN)
return -E2BIG;
spec_str += parsed_len;느낀 점
이 버그를 분석하면서 CO-RE에 대해서 많이 공부하게 된 것 같다.
환경 세팅이 쉽지 않았다.
다른 코드들을 보면서 비슷한 오류가 있나 찾아봤지만 못 찾았다.
이 버그를 악용해서 libbpf의 파싱을 잘해서 쉘을 딴다고 해도 어차피 libbpf를 쓰려면 대부분 이미 root이기 때문에 흠... 딱히 악용은 어려울 것 같다.