书籍《BPF 用于 Linux 监控》

书籍《BPF 用于 Linux 监控》哈布罗居民大家好! BPF 虚拟机是 Linux 内核最重要的组件之一。 正确使用它将使系统工程师能够发现故障并解决最复杂的问题。 您将学习如何编写监视和修改内核行为的程序,如何安全地实现代码来监视内核中的事件,等等。 David Calavera 和 Lorenzo Fontana 将帮助您释放 BPF 的力量。 扩展您在性能优化、网络、安全方面的知识。 - 使用BPF来监视和修改Linux内核的行为。 - 注入代码以安全地监视内核事件,而无需重新编译内核或重新启动系统。 — 使用 C、Go 或 Python 编写的方便的代码示例。 - 通过拥有 BPF 程序生命周期来进行控制。

Linux 内核安全、其特性和 Seccomp

BPF 提供了一种强大的方法来扩展内核,而无需牺牲稳定性、安全性或速度。 因此,内核开发人员认为,通过实现 BPF 程序(也称为 Seccomp BPF)支持的 Seccomp 过滤器,利用其多功能性来改进 Seccomp 中的进程隔离是一个好主意。 在本章中,我们将解释什么是 Seccomp 以及如何使用它。 然后您将学习如何使用 BPF 程序编写 Seccomp 过滤器。 之后,我们将了解 Linux 安全模块内核中包含的内置 BPF 挂钩。

Linux 安全模块 (LSM) 是一个框架,提供了一组可用于以标准化方式实现各种安全模型的功能。 LSM可以直接在内核源码树中使用,例如Apparmor、SELinux和Tomoyo。

我们首先讨论 Linux 的功能。

机会

Linux 能力的本质是,您需要授予非特权进程执行某项任务的权限,但无需为此目的使用 suid,或者以其他方式使该进程具有特权,从而减少攻击的可能性并允许该进程执行某些任务。 例如,如果您的应用程序需要打开特权端口(例如 80),您可以简单地为其提供 CAP_NET_BIND_SERVICE 功能,而不是以 root 身份运行该进程。

考虑一个名为 main.go 的 Go 程序:

package main
import (
            "net/http"
            "log"
)
func main() {
     log.Fatalf("%v", http.ListenAndServe(":80", nil))
}

该程序在端口 80(这是一个特权端口)上为 HTTP 服务器提供服务。 通常我们编译后立即运行:

$ go build -o capabilities main.go
$ ./capabilities

但是,由于我们没有授予 root 权限,因此这段代码在绑定端口时会抛出错误:

2019/04/25 23:17:06 listen tcp :80: bind: permission denied
exit status 1

capsh(shell 管理器)是一种运行具有一组特定功能的 shell 的工具。

在这种情况下,正如已经提到的,您可以通过提供 cap_net_bind_service 功能以及程序中已有的所有其他内容来启用特权端口绑定,而不是授予完全 root 权限。 为此,我们可以将程序封装在 capsh 中:

# capsh --caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' 
   --keep=1 --user="nobody" 
   --addamb=cap_net_bind_service -- -c "./capabilities"

让我们稍微了解一下这个团队。

  • capsh - 使用 capsh 作为 shell。
  • —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' - 由于我们需要更改用户(我们不想以 root 身份运行),因此我们将指定 cap_net_bind_service 以及实际更改用户 ID 的能力root 为无人,即 cap_setuid 和 cap_setgid。
  • —keep=1 — 我们希望在从 root 帐户切换时保留已安装的功能。
  • —user=“nobody” — 运行该程序的最终用户将是nobody。
  • —addamba=cap_net_bind_service —设置从root模式切换后相关能力的清除。
  • - -c "./capability" - 只需运行该程序。

链接功能是一种特殊的功能,当当前程序使用 execve() 执行子程序时,它们会被子程序继承。 只有允许关联的能力,即环境能力,才可以被继承。

您可能想知道在 --caps 选项中指定功能后 +eip 意味着什么。 这些标志用于确定该功能:

- 必须激活(p);

- 可供使用(e);

- 可以被子进程继承(i)。

由于我们想要使用 cap_net_bind_service,因此我们需要使用 e 标志来完成此操作。 然后我们将在命令中启动shell。 这将运行功能二进制文件,我们需要用 i 标志来标记它。 最后,我们希望使用 p 启用该功能(我们这样做时没有更改 UID)。 看起来像 cap_net_bind_service+eip。

您可以使用 ss 检查结果。 让我们稍微缩短输出以适合页面,但它将显示除 0 之外的关联端口和用户 ID,在本例中为 65:

# ss -tulpn -e -H | cut -d' ' -f17-
128 *:80 *:*
users:(("capabilities",pid=30040,fd=3)) uid:65534 ino:11311579 sk:2c v6only:0

在本示例中,我们使用了 capsh,但您可以使用 libcap 编写 shell。 有关详细信息,请参阅 man 3 libcap。

在编写程序时,开发人员通常并不提前知道程序在运行时所需的所有功能; 此外,这些功能在新版本中可能会发生变化。

为了更好地理解我们程序的功能,我们可以使用 BCC 功能工具,它为 cap_capable 内核函数设置 kprobe:

/usr/share/bcc/tools/capable
TIME      UID  PID   TID   COMM               CAP    NAME           AUDIT
10:12:53 0 424     424     systemd-udevd 12 CAP_NET_ADMIN         1
10:12:57 0 1103   1101   timesync        25 CAP_SYS_TIME         1
10:12:57 0 19545 19545 capabilities       10 CAP_NET_BIND_SERVICE 1

我们可以通过在 cap_capable 内核函数中使用 bpftrace 和单行 kprobe 来实现同样的效果:

bpftrace -e 
   'kprobe:cap_capable {
      time("%H:%M:%S ");
      printf("%-6d %-6d %-16s %-4d %dn", uid, pid, comm, arg2, arg3);
    }' 
    | grep -i capabilities

如果我们的程序的功能在 kprobe 之后启用,这将输出类似以下内容:

12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 10 1

第五列是流程所需的功能,由于此输出包括非审核事件,因此我们会看到所有非审核检查,最后是审核标志(输出中最后一个)设置为 1 的所需功能。功能。我们感兴趣的是 CAP_NET_BIND_SERVICE,它在内核源代码中被定义为一个常量,位于文件 include/uapi/linux/ability.h 中,标识符为 10:

/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */
#define CAP_NET_BIND_SERVICE 10<source lang="go">

通常会在运行时为 runC 或 Docker 等容器启用功能,以允许它们在非特权模式下运行,但它们只允许运行大多数应用程序所需的功能。 当应用程序需要某些功能时,Docker 可以使用 --cap-add 提供它们:

docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummy

此命令将为容器提供 CAP_NET_ADMIN 功能,允许其配置网络链接以添加 dummy0 接口。

下一节将展示如何使用过滤等功能,但使用不同的技术,允许我们以编程方式实现我们自己的过滤器。

赛康

Seccomp 代表安全计算,是在 Linux 内核中实现的安全层,允许开发人员过滤某些系统调用。 尽管 Seccomp 在功能上与 Linux 相当,但它管理某些系统调用的能力使其更加灵活。

Seccomp 和 Linux 功能并不相互排斥,通常一起使用以从这两种方法中受益。 例如,您可能希望为进程提供 CAP_NET_ADMIN 功能,但不允许它接受套接字连接,从而阻止accept 和accept4 系统调用。

Seccomp过滤方法基于在SECCOMP_MODE_FILTER模式下运行的BPF过滤器,系统调用过滤的执行方式与数据包相同。

Seccomp 过滤器是通过 PR_SET_SECCOMP 操作使用 prctl 加载的。 这些过滤器采用 BPF 程序的形式,针对由 seccomp_data 结构表示的每个 Seccomp 数据包执行。 该结构包含参考体系结构、系统调用时指向处理器指令的指针以及最多六个系统调用参数(表示为 uint64)。

这是 linux/seccomp.h 文件中内核源代码中的 seccomp_data 结构的样子:

struct seccomp_data {
int nr;
      __u32 arch;
      __u64 instruction_pointer;
      __u64 args[6];
};

正如您从该结构中看到的,我们可以按系统调用、其参数或两者的组合进行过滤。

接收到每个 Seccomp 数据包后,过滤器必须进行处理以做出最终决定并告诉内核下一步要做什么。 最终的决定由返回值(状态代码)之一来表示。

- SECCOMP_RET_KILL_PROCESS - 在过滤因此未执行的系统调用后立即终止整个进程。

- SECCOMP_RET_KILL_THREAD - 在过滤因此未执行的系统调用后立即终止当前线程。

— SECCOMP_RET_KILL — SECCOMP_RET_KILL_THREAD 的别名,保留用于向后兼容。

- SECCOMP_RET_TRAP - 系统调用被禁止,并向调用它的任务发送SIGSYS(错误系统调用)信号。

- SECCOMP_RET_ERRNO - 系统调用不执行,SECCOMP_RET_DATA 过滤器返回值的一部分作为 errno 值传递到用户空间。 根据错误原因,返回不同的errno值。 下一节提供了错误号列表。

- SECCOMP_RET_TRACE - 用于通知 ptrace 跟踪器使用 - PTRACE_O_TRACESECCOMP 拦截执行系统调用以查看和控制该进程。 如果未连接跟踪器,则返回错误,errno 设置为 -ENOSYS,并且不执行系统调用。

- SECCOMP_RET_LOG - 系统调用已解析并记录。

- SECCOMP_RET_ALLOW - 简单地允许系统调用。

ptrace是一个系统调用,用于在称为tracee的进程中实现跟踪机制,具有监视和控制进程执行的能力。 跟踪程序可以有效地影响执行并修改被跟踪者的内存寄存器。 在Seccomp上下文中,由SECCOMP_RET_TRACE状态码触发时使用ptrace,因此跟踪器可以阻止系统调用执行并实现自己的逻辑。

安全计算错误

在使用 Seccomp 时,您有时会遇到各种错误,这些错误由 SECCOMP_RET_ERRNO 类型的返回值标识。 要报告错误,seccomp 系统调用将返回 -1 而不是 0。

可能出现以下错误:

- EACCESS - 不允许调用者进行系统调用。 这通常是因为它没有 CAP_SYS_ADMIN 权限或者没有使用 prctl 设置 no_new_privs (我们稍后会讨论这个);

— EFAULT — 传递的参数(seccomp_data 结构中的 args)没有有效地址;

— EINVAL — 这里可能有四个原因:

- 请求的操作未知或当前配置中的内核不支持;

-指定的标志对于请求的操作无效;

-操作包含BPF_ABS,但指定的偏移量有问题,可能超出seccomp_data结构体的大小;

-传递给过滤器的指令数量超过最大值;

— ENOMEM — 没有足够的内存来执行程序;

- EOPNOTSUPP - 该操作表明使用 SECCOMP_GET_ACTION_AVAIL 该操作可用,但内核不支持在参数中返回;

— ESRCH — 同步另一个流时出现问题;

- ENOSYS - SECCOMP_RET_TRACE 操作没有附加跟踪器。

prctl 是一个系统调用,允许用户空间程序操作(设置和获取)进程的特定方面,例如字节字节顺序、线程名称、安全计算模式 (Seccomp)、特权、Perf 事件等。

对您来说,Seccomp 可能看起来像是一种沙箱技术,但事实并非如此。 Seccomp 是一个允许用户开发沙箱机制的实用程序。 现在让我们看看如何使用由 Seccomp 系统调用直接调用的过滤器来创建用户交互程序。

BPF Seccomp 过滤器示例

在这里,我们将展示如何结合前面讨论的两个操作,即:

— 我们将编写一个 Seccomp BPF 程序,该程序将用作过滤器,根据所做的决策具有不同的返回代码;

— 使用 prctl 加载过滤器。

首先,您需要来自标准库和 Linux 内核的标头:

#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>

在尝试此示例之前,我们必须确保内核是在 CONFIG_SECCOMP 和 CONFIG_SECCOMP_FILTER 设置为 y 的情况下编译的。 在工作机器上,您可以这样检查:

cat /proc/config.gz| zcat | grep -i CONFIG_SECCOMP

其余代码是由两部分组成的 install_filter 函数。 第一部分包含我们的 BPF 过滤指令列表:

static int install_filter(int nr, int arch, int error) {
  struct sock_filter filter[] = {
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3),
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
  };

这些指令是使用 linux/filter.h 文件中定义的 BPF_STMT 和 BPF_JUMP 宏来设置的。
让我们看一下说明。

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, arch))) - 系统以字 BPF_W 的形式从 BPF_LD 加载并累加,数据包数据位于固定偏移量 BPF_ABS 处。

- BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) - 使用 BPF_JEQ 检查累加器常量 BPF_K 中的体系结构值是否等于 arch。 如果是,则在偏移量 0 处跳转到下一条指令,否则在偏移量 3 处跳转(在本例中)以抛出错误,因为 arch 不匹配。

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) - 以字 BPF_W 的形式从 BPF_LD 加载并累加,BPF_W 是包含在 BPF_ABS 固定偏移量中的系统调用号。

— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — 将系统调用号与 nr 变量的值进行比较。 如果它们相等,则继续执行下一条指令并禁用系统调用,否则允许使用 SECCOMP_RET_ALLOW 进行系统调用。

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)) - 使用 BPF_RET 终止程序,并因此产生错误 SECCOMP_RET_ERRNO,其编号来自 err 变量。

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) - 使用 BPF_RET 终止程序并允许使用 SECCOMP_RET_ALLOW 执行系统调用。

SECCOMP 是 CBPF
您可能想知道为什么使用指令列表而不是编译的 ELF 对象或 JIT 编译的 C 程序。

有两个原因。

• 首先,Seccomp 使用cBPF(经典BPF)而不是eBPF,这意味着:它没有寄存器,只有一个累加器来存储最后的计算结果,如示例中所示。

• 其次,Seccomp 直接接受指向 BPF 指令数组的指针,而不接受其他任何东西。 我们使用的宏只是帮助以程序员友好的方式指定这些指令。

如果您需要更多帮助来理解此程序集,请考虑执行相同操作的伪代码:

if (arch != AUDIT_ARCH_X86_64) {
    return SECCOMP_RET_ALLOW;
}
if (nr == __NR_write) {
    return SECCOMP_RET_ERRNO;
}
return SECCOMP_RET_ALLOW;

在socket_filter结构体中定义过滤器代码后,需要定义一个sock_fprog,其中包含代码和过滤器的计算长度。 需要此数据结构作为声明稍后运行的进程的参数:

struct sock_fprog prog = {
   .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
   .filter = filter,
};

install_filter 函数中只剩下一件事要做 - 加载程序本身! 为此,我们使用 prctl,将 PR_SET_SECCOMP 作为进入安全计算模式的选项。 然后我们告诉模式使用 SECCOMP_MODE_FILTER 加载过滤器,它包含在 sock_fprog 类型的 prog 变量中:

  if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
    perror("prctl(PR_SET_SECCOMP)");
    return 1;
  }
  return 0;
}

最后,我们可以使用install_filter函数,但在此之前我们需要使用prctl为当前执行设置PR_SET_NO_NEW_PRIVS,从而避免子进程获得比父进程更多的权限的情况。 这样,我们就可以在 install_filter 函数中进行以下 prctl 调用,而无需 root 权限。

现在我们可以调用 install_filter 函数。 让我们阻止所有与 X86-64 架构相关的写入系统调用,并简单地授予阻止所有尝试的权限。 安装过滤器后,我们使用第一个参数继续执行:

int main(int argc, char const *argv[]) {
  if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
   perror("prctl(NO_NEW_PRIVS)");
   return 1;
  }
   install_filter(__NR_write, AUDIT_ARCH_X86_64, EPERM);
  return system(argv[1]);
 }

让我们开始吧。 为了编译我们的程序,我们可以使用 clang 或 gcc,无论哪种方式,它都只是编译 main.c 文件,没有特殊选项:

clang main.c -o filter-write

如前所述,我们已阻止该程序中的所有条目。 要测试这一点,您需要一个可以输出某些内容的程序 - ls 似乎是一个不错的候选者。 她平时的表现是这样的:

ls -la
total 36
drwxr-xr-x 2 fntlnz users 4096 Apr 28 21:09 .
drwxr-xr-x 4 fntlnz users 4096 Apr 26 13:01 ..
-rwxr-xr-x 1 fntlnz users 16800 Apr 28 21:09 filter-write
-rw-r--r-- 1 fntlnz users 19 Apr 28 21:09 .gitignore
-rw-r--r-- 1 fntlnz users 1282 Apr 28 21:08 main.c

精彩的! 使用我们的包装程序的情况如下:我们只需将要测试的程序作为第一个参数传递即可:

./filter-write "ls -la"

执行时,该程序产生完全空的输出。 不过,我们可以使用 strace 来查看发生了什么:

strace -f ./filter-write "ls -la"

工作结果大大缩短,但其相应部分显示记录因 EPERM 错误而被阻止 - 与我们配置的错误相同。 这意味着程序不会输出任何内容,因为它无法访问 write 系统调用:

[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "write error", 11) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "n", 1) = -1 EPERM (Operation not permitted)

现在您了解了 Seccomp BPF 的工作原理,并清楚可以用它做什么。 但是您不想使用 eBPF 而不是 cBPF 来实现同样的目标,以充分利用其功能吗?

在考虑 eBPF 程序时,大多数人认为他们只是编写它们并使用管理员权限加载它们。 虽然这种说法通常是正确的,但内核实现了一组机制来保护各个级别的 eBPF 对象。 这些机制称为 BPF LSM 陷阱。

BPF LSM 陷阱

为了提供独立于体系结构的系统事件监视,LSM 实现了陷阱的概念。 钩子调用在技术上类似于系统调用,但它独立于系统并与基础设施集成。 LSM 提供了一个新的概念,其中抽象层可以帮助避免在处理不同架构上的系统调用时遇到的问题。

在撰写本文时,内核有 XNUMX 个与 BPF 程序相关的钩子,SELinux 是唯一实现它们的内置 LSM。

陷阱的源代码位于内核树的 include/linux/security.h 文件中:

extern int security_bpf(int cmd, union bpf_attr *attr, unsigned int size);
extern int security_bpf_map(struct bpf_map *map, fmode_t fmode);
extern int security_bpf_prog(struct bpf_prog *prog);
extern int security_bpf_map_alloc(struct bpf_map *map);
extern void security_bpf_map_free(struct bpf_map *map);
extern int security_bpf_prog_alloc(struct bpf_prog_aux *aux);
extern void security_bpf_prog_free(struct bpf_prog_aux *aux);

它们中的每一个都将在不同的执行阶段被调用:

— security_bpf — 对执行的 BPF 系统调用进行初始检查;

- security_bpf_map - 检查内核何时返回映射的文件描述符;

- security_bpf_prog - 检查内核何时返回 eBPF 程序的文件描述符;

— security_bpf_map_alloc — 检查 BPF 映射内的安全字段是否已初始化;

- security_bpf_map_free - 检查BPF映射内的安全字段是否被清除;

— security_bpf_prog_alloc — 检查安全字段是否在 BPF 程序内部初始化;

- security_bpf_prog_free - 检查 BPF 程序内的安全字段是否已清除。

现在,看到这一切,我们明白了:LSM BPF 拦截器背后的想法是它们可以为每个 eBPF 对象提供保护,确保只有具有适当权限的人才能对卡和程序执行操作。

总结

安全性并不是您可以以一刀切的方式针对您想要保护的所有内容实施的。 能够以不同方式保护不同级别的系统非常重要。 不管你相信与否,保护系统安全的最佳方法是从不同位置组织不同级别的保护,这样降低某一级别的安全性就不允许访问整个系统。 核心开发人员为我们提供了一组不同的层和接触点,做得非常出色。 我们希望我们已经让您很好地理解了什么是层以及如何使用 BPF 程序来处理它们。

关于作者

大卫·卡拉维拉 是 Netlify 的首席技术官。 他从事 Docker 支持工作,并为 Runc、Go 和 BCC 工具以及其他开源项目的开发做出了贡献。 因其在 Docker 项目和 Docker 插件生态系统开发方面的工作而闻名。 David 对火焰图非常热衷,并且始终致力于优化性能。

洛伦佐·丰塔纳 他在 Sysdig 的开源团队工作,主要关注 Falco,这是一个云原生计算基金会项目,通过内核模块和 eBPF 提供容器运行时安全性和异常检测。 他热衷于分布式系统、软件定义网络、Linux 内核和性能分析。

» 有关本书的更多详细信息,请访问 出版商的网站
» 目录
» 摘抄

对于 Khabrozhiteley 使用优惠券可享受 25% 折扣 - Linux

支付纸质版书籍的费用后,将通过电子邮件发送电子书。

来源: habr.com

添加评论