-
-
[原创]aliyunctf2025-beebee题目详解
-
发表于: 2025-2-28 16:03 5062
-
通过这个题目学习下eBPF方面内容。
eBPF(extended Berkeley Packet Filter),是一个基于寄存器的虚拟机,使用自定义的64位RISC指令集,能够在linux内核中运行JIT原生编译的BPF程序,并访问内核功能和内存的子集。它是主线内核的一部分,不需要像其他框架那样( LTTng or SystemTap)的第三方模块,并且在所有的linux发行版中都默认启用。
在内核内运行完整虚拟机的目的主要是为了便利和安全,虽然eBPF可以完成的操作都可以通过普通内核模块处理,但是直接内核编程是很危险的,可能会造成整个系统的崩溃。因此通过虚拟机运行字节码对安全监控,沙盒,网络过滤,程序跟踪,分析,调试等很有价值。ex
eBPF VM的设计不允许循环,因此可以保证每个eBPF程序都能执行完,不会有死循环的情况,而且所有的内存访问都是有边界检查和属性检查的。eBPF刚开始只用于过滤网络数据包。从linux-3.18开始,虚拟机就可以通过bpf()系统调用与用户层交互,当时存在的指令集成为公共ABI,后面仍然可以添加新指令。
eBPF的运行原理:
事件可以从kprobes/uprobes、tracepoints、dtrace probes、sockets等中生成。当事件发生时,eBPF程序由内核运行,可以理解为一种函数挂钩或者事件驱动编程。这允许在内核和用户进程中的任何指令上连接和检查任何功能中的内存,拦截文件操作,检查特定的网络数据包等。
用户层编写程序可以通过libbpf库,其中包含bpf_load_program等syscall函数的包装器和一些数据结构的定义。linux源码的samples/bpf/目录下有很多使用示例。
eBPF架构设计:
cBPF是32位架构,但eBPF是 64 位架构,
每个函数调用在寄存器r1-r5中最多可以有5个参数;寄存器r1-r5只能存储堆栈的数字或指针(作为参数传递给函数),永远不会将指针指向任意内存。所有内存访问必须先将数据加载到eBPF堆栈,然后再在eBPF程序中使用。此限制有助于eBPF验证器,简化内存模型。
指令集:
普通用户的BPF程序,最多可以使用4096个指令。root用户的话,最多可以加载100万个指令。因为BPF是RISC架构,所以指令是定长的64位
程序类型:
在加载时需要指定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文件:
这种题目需要知道eBPF的机制,并且熟悉它的基础设施,才能完成对它的攻击,以前没有遇到过,现在正好根据官方的writeup来学习下这方面的内容。使用的是内核6.6.74版本的源码,新增辅助函数 bpf_aliyunctf_xor
函数编号212,然后 bpf_aliyunctf_xor_proto
定义了参数的类型,属性的一些信息,第三个参数是一个指针类型。
可以根据调用堆栈分析检查过程,在跟踪的过程可以看到check_mem_access函数,只有对内存进行访问的指令会调用它来检查,从参数可以看出很多细节信息。
这里利用了eBPF只在load的时候,对有内存操作的指令进行检查,这里有一个eBPF设计上的细节,就是它的只读权限不是真的只读不可写,而是对于eBPF字节码程序不可写,它是由自己的虚拟机进行内存检查,不是依靠操作系统,但是eBPF设计了辅助函数这个机制,可以在虚拟机中调用c代码,因此如果辅助函数的设计有缺陷,可以去写某些只读区域,而虚拟机字节码如果再次使用了这部分被修改的内存,虚拟机并不会对这个引用这个数据的寄存器进行检查,这是非常危险的。
由于 bpf_aliyunctf_xor_proto
辅助函数第三个参数有个标识位为 MEM_RDONLY
表示参数传入的地址可以是只读的。但是在函数实现中,这个内存地址是会被写入一个64位数据的。(刚开始我本来打算直接通过这个函数来修改内核的全局变量,发现不行诶,后来才知道,这些指针传递给辅助函数的时候也是有限制的,只能是eBPF内部的某些内存。)因此这里有一种利用方式是: 我们可以利用这个设置来修改只读的maps,只读权限区域可以帮助我们找到一种控制寄存器绕过内存边界检查的方式,我们可以使用 bpf_skb_load_bytes()
函数来破坏堆栈,覆盖函数ret地址,然后利用rop完成攻击。
官方有writeup
官方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
exploit.c。
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大小的栈空间 |
比特 | 名字 | 意义 |
---|---|---|
0-7 | op | 操作码 |
8-11 | dst_reg | 目的寄存器 |
12-15 | src_reg | 源寄存器 |
16-31 | off | 偏移 |
32-63 | imm | 立即数 |
diff --color -ruN origin/include/linux/bpf.h aliyunctf/include/linux/bpf.h
--- origin/include/linux/bpf.h 2025-01-23 10:21:19.000000000 -0600
+++ aliyunctf/include/linux/bpf.h 2025-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.h 2025-01-23 10:21:19.000000000 -0600
+++ aliyunctf/include/uapi/linux/bpf.h 2025-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.c 2025-01-23 10:21:19.000000000 -0600
+++ aliyunctf/kernel/bpf/helpers.c 2025-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
;
}
diff --color -ruN origin/include/linux/bpf.h aliyunctf/include/linux/bpf.h
--- origin/include/linux/bpf.h 2025-01-23 10:21:19.000000000 -0600
+++ aliyunctf/include/linux/bpf.h 2025-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.h 2025-01-23 10:21:19.000000000 -0600
+++ aliyunctf/include/uapi/linux/bpf.h 2025-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.c 2025-01-23 10:21:19.000000000 -0600
+++ aliyunctf/kernel/bpf/helpers.c 2025-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;
+
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
赞赏
- [原创]linux内核SLUB机制介绍 4596
- [原创]aliyunctf2025-beebee题目详解 5063
- [讨论]VMProtect编译 6440
- [翻译]Intel虚拟化技术简介 6312
- [原创]Arm指令介绍 18399