pwn1 - beebee:学习eBPF的exploit
通过这个题目学习下eBPF方面内容。
eBPF(extended Berkeley Packet Filter),是一个基于寄存器的虚拟机,使用自定义的64位RISC指令集,能够在linux内核中运行JIT原生编译的BPF程序,并访问内核功能和内存的子集。它是主线内核的一部分,不需要像其他框架那样( LTTng or SystemTap)的第三方模块,并且在所有的linux发行版中都默认启用。
在内核内运行完整虚拟机的目的主要是为了便利和安全,虽然eBPF可以完成的操作都可以通过普通内核模块处理,但是直接内核编程是很危险的,可能会造成整个系统的崩溃。因此通过虚拟机运行字节码对安全监控,沙盒,网络过滤,程序跟踪,分析,调试等很有价值。
eBPF VM的设计不允许循环,因此可以保证每个eBPF程序都能执行完,不会有死循环的情况,而且所有的内存访问都是有边界检查和属性检查的。eBPF刚开始只用于过滤网络数据包。从linux-3.18开始,虚拟机就可以通过bpf()系统调用与用户层交互,当时存在的指令集成为公共ABI,后面仍然可以添加新指令。
eBPF的运行原理:
1.用户空间的程序发送bytecode(eBPF VM字节码)到内核中。
2.内核会对eBPF程序进行一次检查(kernel/bpf/verifier.c)。
3.内核将字节码使用JIT机制 编译为机器码,并将附加到指定的位置。
4.插入的代码将数据写入ringbuffers或key-value maps。
5.用户空间从共享maps或ringbuffers中读取结果。
ringbuffers和maps由内核管理,独立于程序。需要通过文件描述符异步访问。
事件可以从kprobes/uprobes、tracepoints、dtrace probes、sockets等中生成。当事件发生时,eBPF程序由内核运行,可以理解为一种函数挂钩或者事件驱动编程。这允许在内核和用户进程中的任何指令上连接和检查任何功能中的内存,拦截文件操作,检查特定的网络数据包等。
用户层编写程序可以通过libbpf库,其中包含bpf_load_program等syscall函数的包装器和一些数据结构的定义。linux源码的samples/bpf/目录下有很多使用示例。
eBPF架构设计:
cBPF是32位架构,但eBPF是 64 位架构,
BPF寄存器 | 对应的x64寄存器 | 作用 |
---|---|---|
R0 | rax | 函数调用结束储存返回值,当前程序推退出码 |
R1 | rdi | 作为函数调用参数使用,传递第一个参数 |
R2 | rsi | 传递第二个参数 |
R3 | rdx | 传递第三个参数 |
R4 | rcx | 传递第四个参数 |
R5 | r8 | 传递第五个参数 |
R6 | rbx | 被保留函数内部使用 |
R7 | r13 | |
R8 | r14 | |
R9 | r15 | |
R10 | rbp | 只读寄存器,指向512byte大小的栈空间 |
每个函数调用在寄存器r1-r5中最多可以有5个参数;寄存器r1-r5只能存储堆栈的数字或指针(作为参数传递给函数),永远不会将指针指向任意内存。所有内存访问必须先将数据加载到eBPF堆栈,然后再在eBPF程序中使用。此限制有助于eBPF验证器,简化内存模型。
指令集:
普通用户的BPF程序,最多可以使用4096个指令。root用户的话,最多可以加载100万个指令。因为BPF是RISC架构,所以指令是定长的64位。
比特 | 名字 | 意义 |
---|---|---|
0-7 | op | 操作码 |
8-11 | dst_reg | 目的寄存器 |
12-15 | src_reg | 源寄存器 |
16-31 | off | 偏移 |
32-63 | imm | 立即数 |
程序类型:
在加载时需要指定BPF程序的用途。cBPF中只有2种类型:套接字过滤器和系统调用过滤器,但eBPF提供了20多种类型。
例如BPF_PROG_TYPE_SOCKET_FILTER
是套接字过滤器,根据BPF程序的返回值,可以进行丢弃数据包等操作。这种类型的BPF程序,通过SO_ATTACH_BPF选项调用setsockopt系统调用,可以附加到套接字上。
辅助函数:
eBPF设计的一套安全的扩展功能的模式。字节码程序能做的事情毕竟有限,这时我们可以通过添加辅助函数来扩展其功能,然后在VM中调用。当然其内部也有很多已经写好的辅助函数,我们可以直接调用。
辅助函数可以通过struct bpf_func_proto
结构体描述了自身的定义、入参类型、返回值类型等。验证器可以通过这个结构体描述的信息来检查,传入的参数是否合法。比较复杂的就是指针类型的参数了。指针有类型信息,范围信息,访问权限信息,对齐信息……
普通用户是否可以使用bpf有个开关。可以通过/proc/sys/kernel/unprivileged_bpf_disabled控制。
介绍了这么多,来看看这个题目:
先看patch文件:
diff --color -ruN origin/include/linux/bpf.h aliyunctf/include/linux/bpf.h
--- origin/include/linux/bpf.h2025-01-23 10:21:19.000000000 -0600
+++ aliyunctf/include/linux/bpf.h2025-01-24 03:44:01.494468038 -0600
@@ -3058,6 +3058,7 @@
extern const struct bpf_func_proto bpf_user_ringbuf_drain_proto;
extern const struct bpf_func_proto bpf_cgrp_storage_get_proto;
extern const struct bpf_func_proto bpf_cgrp_storage_delete_proto;
+extern const struct bpf_func_proto bpf_aliyunctf_xor_proto;
const struct bpf_func_proto *tracing_prog_func_proto(
enum bpf_func_id func_id, const struct bpf_prog *prog);
diff --color -ruN origin/include/uapi/linux/bpf.h aliyunctf/include/uapi/linux/bpf.h
--- origin/include/uapi/linux/bpf.h2025-01-23 10:21:19.000000000 -0600
+++ aliyunctf/include/uapi/linux/bpf.h2025-01-24 03:44:11.814636836 -0600
@@ -5881,6 +5881,7 @@
FN(user_ringbuf_drain, 209, ##ctx)
FN(cgrp_storage_get, 210, ##ctx)
FN(cgrp_storage_delete, 211, ##ctx)
+FN(aliyunctf_xor, 212, ##ctx)
/* */
/* backwards-compatibility macros for users of __BPF_FUNC_MAPPER that don't
diff --color -ruN origin/kernel/bpf/helpers.c aliyunctf/kernel/bpf/helpers.c
--- origin/kernel/bpf/helpers.c2025-01-23 10:21:19.000000000 -0600
+++ aliyunctf/kernel/bpf/helpers.c2025-01-24 03:44:06.683490095 -0600
@@ -1745,6 +1745,28 @@
.arg3_type= ARG_CONST_ALLOC_SIZE_OR_ZERO,
};
+BPF_CALL_3(bpf_aliyunctf_xor, const char *, buf, size_t, buf_len, s64 *, res) {
+s64 _res = 2025;
+
+if (buf_len != sizeof(s64))
+return -EINVAL;
+
+_res ^= *(s64 *)buf;
+*res = _res;
+
+return 0;
+}
+
+const struct bpf_func_proto bpf_aliyunctf_xor_proto = {
+.func= bpf_aliyunctf_xor,
+.gpl_only= false,
+.ret_type= RET_INTEGER,
+.arg1_type= ARG_PTR_TO_MEM | MEM_RDONLY,
+.arg2_type= ARG_CONST_SIZE,
+.arg3_type= ARG_PTR_TO_FIXED_SIZE_MEM | MEM_UNINIT | MEM_ALIGNED | MEM_RDONLY,
+.arg3_size= sizeof(s64),
+};
+
const struct bpf_func_proto bpf_get_current_task_proto __weak;
const struct bpf_func_proto bpf_get_current_task_btf_proto __weak;
const struct bpf_func_proto bpf_probe_read_user_proto __weak;
@@ -1801,6 +1823,8 @@
return &bpf_strtol_proto;
case BPF_FUNC_strtoul:
return &bpf_strtoul_proto;
+case BPF_FUNC_aliyunctf_xor:
+return &bpf_aliyunctf_xor_proto;
default:
break;
}
这种题目需要知道eBPF的机制,并且熟悉它的基础设施,才能完成对它的攻击,以前没有遇到过,现在正好根据官方的writeup来学习下这方面的内容。使用的是内核6.6.74版本的源码,新增辅助函数bpf_aliyunctf_xor
函数编号212,然后bpf_aliyunctf_xor_proto
定义了参数的类型,属性的一些信息,第三个参数是一个指针类型。
可以根据调用堆栈分析检查过程,在跟踪的过程可以看到check_mem_access函数,只有对内存进行访问的指令会调用它来检查,从参数可以看出很多细节信息。
#0 check_mem_access (env=0xffff888004b58000, insn_idx=0x1, regno=0xa, off=0x6, bpf_size=0x18, t=BPF_WRITE,
value_regno=<error reading variable: Cannot access memory at address 0x0>,
strict_alignment_once=<error reading variable: Cannot access memory at address 0x8>,
is_ldsx=<error reading variable: Cannot access memory at address 0x10>) at kernel/bpf/verifier.c:6698
#1 0xffffffff812012a9 in do_check (env=<optimized out>) at kernel/bpf/verifier.c:17179
#2 do_check_common (env=0xffff888004b58000, subprog=0x0) at kernel/bpf/verifier.c:19643
#3 0xffffffff812064ba in do_check_main (env=<optimized out>) at kernel/bpf/verifier.c:19706
#4 bpf_check (prog=0xffff888004b58000, attr=0x1 <fixed_percpu_data+1>, uattr=..., uattr_size=0x18) at kernel/bpf/verifier.c:20333
#5 0xffffffff811df0c2 in bpf_prog_load (attr=0xffffc9000023fe58, uattr=..., uattr_size=0xfffffff0) at kernel/bpf/syscall.c:2743
#6 0xffffffff811e196a in __sys_bpf (cmd=0x5, uattr=..., size=0x0) at kernel/bpf/syscall.c:5465
#7 0xffffffff811e4059 in __do_sys_bpf (size=<optimized out>, uattr=<optimized out>, cmd=<optimized out>) at kernel/bpf/syscall.c:5569
#8 __se_sys_bpf (size=<optimized out>, uattr=<optimized out>, cmd=<optimized out>) at kernel/bpf/syscall.c:5567
#9 __x64_sys_bpf (regs=0xffff888004b58000) at kernel/bpf/syscall.c:5567
#10 0xffffffff81f38d39 in do_syscall_x64 (nr=<optimized out>, regs=<optimized out>) at arch/x86/entry/common.c:51
#11 do_syscall_64 (regs=0xffffc9000023ff58, nr=0x1) at arch/x86/entry/common.c:81
#12 0xffffffff82000134 in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:121
#13 0x0000000000000000 in ?? ()
这里利用了eBPF只在load的时候,对有内存操作的指令进行检查,这里有一个eBPF设计上的细节,就是它的只读权限不是真的只读不可写,而是对于eBPF字节码程序不可写,它是由自己的虚拟机进行内存检查,不是依靠操作系统,但是eBPF设计了辅助函数这个机制,可以在虚拟机中调用c代码,因此如果辅助函数的设计有缺陷,可以去写某些只读区域,而虚拟机字节码如果再次使用了这部分被修改的内存,虚拟机并不会对这个引用这个数据的寄存器进行检查,这是非常危险的。
由于bpf_aliyunctf_xor_proto
辅助函数第三个参数有个标识位为MEM_RDONLY
表示参数传入的地址可以是只读的。但是在函数实现中,这个内存地址是会被写入一个64位数据的。(刚开始我本来打算直接通过这个函数来修改内核的全局变量,发现不行诶,后来才知道,这些指针传递给辅助函数的时候也是有限制的,只能是eBPF内部的某些内存。)因此这里有一种利用方式是:我们可以利用这个设置来修改只读的maps,只读权限区域可以帮助我们找到一种控制寄存器绕过内存边界检查的方式,我们可以使用bpf_skb_load_bytes()
函数来破坏堆栈,覆盖函数ret地址,然后利用rop完成攻击。
官方writeup的攻击流程就是通过向eBPF申请一个只读的map,然后通过新添加的漏洞函数将原来的值改掉,然后把这个值取出来 当bpf_skb_load_bytes()
的第四个参数,因为数据是只读的,所以eBPF不会再去检查它的大小,这样就可以造成栈拷贝溢出,控制ret地址。这里还有一个细节就是刚刚进入虚拟机的时候寄存器R1被初始化为指向struct __sk_buff
的指针。
但是用gcc exploit.c -o exp -static
这种方式编出来的文件很大,当我写了个脚本把数据提取出来后,通过echo -e "" > exp
的方式粘贴进虚拟机的时候,不知道为啥我整个测试系统崩了。后来我自己写了一套syscall 调用,来精简exp。使用gcc exploit.c -o exp -nostdlib -static
命令来编译,编出来的文件大小不足1M。
看雪ID:dig_grave
https://bbs.kanxue.com/user-home-851021.htm
#
原文始发于微信公众号(看雪学苑):aliyunctf2025-beebee题目详解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论