字节vArmor代码解读

说明

最近字节开源了vArmor,刚好最近在研究eBPF,所以就顺便看了一下vArmor的实现,发现vArmor的实现也是基于eBPF的,所以就顺便记录一下。

vArmor 通过以下技术实现云原生容器沙箱

  • 借助 Linux 的 AppArmor 或 BPF LSM,在内核中对容器进程进行强制访问控制(文件、程序、网络外联等)
  • 为减少性能损失和增加易用性,vArmor 的安全模型为 Allow by Default,即只有显式声明的行为会被阻断
  • 用户通过操作 CRD 实现对指定 Workload 中的容器进行沙箱加固
  • 用户可以通过选择和配置沙箱策略(预置策略、自定义策略)来对容器进行强制访问控制。预置策略包含一些常见的提权阻断、渗透入侵防御策略。

vArmor的实现

本文主要是关注vArmor如何借用eBPF中的LSM技术实现对容器加固的。vArmor的内核代码是在一个单独仓库 vArmor-ebpf

vArmor-ebpf中存在两个主要目录,分别是behaviorbpfenforcer

behavior就是观察模式,不会对容器的行为进行任何阻断。

bpfenforcer,按照官方的说法,就是强制访问控制器。通过对某些行为进行阻断达到加固的目的。

behavior

behavior中的核心入口文件是tracer.c。在这个文件中定义了两个raw_tracepoint事件。

  • raw_tracepoint/sched_process_fork
  • raw_tracepoint/sched_process_exec

以其中的sched_process_exec代码为例分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// https://elixir.bootlin.com/linux/v5.4.196/source/fs/exec.c#L1722
SEC("raw_tracepoint/sched_process_exec")
int tracepoint__sched__sched_process_exec(struct bpf_raw_tracepoint_args *ctx)
{
// TP_PROTO(struct task_struct *p, pid_t old_pid, struct linux_binprm *bprm)
struct task_struct *current = (struct task_struct *)ctx->args[0];
struct linux_binprm *bprm = (struct linux_binprm *)ctx->args[2];

struct task_struct *parent = BPF_CORE_READ(current, parent);

struct event event = {};

event.type = 2;
BPF_CORE_READ_INTO(&event.parent_pid, parent, pid);
BPF_CORE_READ_INTO(&event.parent_tgid, parent, tgid);
BPF_CORE_READ_STR_INTO(&event.parent_task, parent, comm);
BPF_CORE_READ_INTO(&event.child_pid, current, pid);
BPF_CORE_READ_INTO(&event.child_tgid, current, tgid);
BPF_CORE_READ_STR_INTO(&event.child_task, current, comm);
bpf_probe_read_kernel_str(&event.filename, sizeof(event.filename), BPF_CORE_READ(bprm, filename));

u64 env_start = 0;
u64 env_end = 0;
int i = 0;
int len = 0;

BPF_CORE_READ_INTO(&env_start, current, mm, env_start);
BPF_CORE_READ_INTO(&env_end, current, mm, env_end);

while(i < MAX_ENV_EXTRACT_LOOP_COUNT && env_start < env_end ) {
len = bpf_probe_read_user_str(&event.env, sizeof(event.env), (void *)env_start);

if ( len <= 0 ) {
break;
} else if ( event.env[0] == 'V' &&
event.env[1] == 'A' &&
event.env[2] == 'R' &&
event.env[3] == 'M' &&
event.env[4] == 'O' &&
event.env[5] == 'R' &&
event.env[6] == '=' ) {
break;
} else {
env_start = env_start + len;
event.env[0] = 0;
i++;
}
}

event.num = i;
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
return 0;
}

通过注释,可以看到主要是基于内核5.4.196版本开发的。

有关rawtracepoint的原理和机制,可以参考之前写的文章rawtracepoint机制介绍.

当一个进程执行新的可执行文件(例如通过 execve 系统调用)时,内核会发出 sched_process_exec 跟踪事件,以便跟踪和记录进程执行的相关信息。这个跟踪事件提供了以下信息:

  • common_type:跟踪事件的类型标识符。
  • common_flags:跟踪事件的标志位。
  • common_preempt_count:跟踪事件发生时的抢占计数。
  • common_pid:触发事件的进程 ID。
  • filename:新可执行文件的文件名。

tracepoint__sched__sched_process_exec整体的逻辑也比较简单,通过task_struct获得子父进程的pidtgidcomm等信息,然后通过bpf_perf_event_output将这些信息传递给用户态。

整体来说,就是一个观察模式,不会对容器的行为进行任何阻断。

bpfenforcer

enforcer入口文件是enforcer.c,在这个文件中定义了多个lsm事件。包括:

  • capable
  • file_open
  • path_symlink
  • path_link
  • path_rename
  • bprm_check_security
  • socket_connect

具体的函数逻辑是封装在capability.hfile.hprocess.hnetwork.h中。

具体以lsm/socket_connect为例,分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SEC("lsm/socket_connect")
int BPF_PROG(varmor_socket_connect, struct socket *sock, struct sockaddr *address, int addrlen) {
// Only care about ipv4 and ipv6 for now
if (address->sa_family != AF_INET && address->sa_family != AF_INET6)
return 0;

// Retrieve the current task
struct task_struct *current = (struct task_struct *)bpf_get_current_task();

// Whether the current task has network access control rules
u32 mnt_ns = get_task_mnt_ns_id(current);
u32 *vnet_inner = get_net_inner_map(mnt_ns);
if (vnet_inner == NULL)
return 0;

DEBUG_PRINT("================ lsm/socket_connect ================");

DEBUG_PRINT("socket status: 0x%x", sock->state);
DEBUG_PRINT("socket type: 0x%x", sock->type);
DEBUG_PRINT("socket flags: 0x%x", sock->flags);

// Iterate all rules in the inner map
return iterate_net_inner_map(vnet_inner, address);
}

通过address->sa_family != AF_INET && address->sa_family != AF_INET6,只关注ipv4ipv6的连接。

1
2
3
4
u32 mnt_ns = get_task_mnt_ns_id(current);
u32 *vnet_inner = get_net_inner_map(mnt_ns);
if (vnet_inner == NULL)
return 0;

获得当前进程的mnt_ns,然后通过mnt_ns获得vnet_innervnet_inner是一个bpf map,存储了当前进程的网络访问控制规则。

整个代码的核心关键是iterate_net_inner_map(vnet_inner, address)iterate_net_inner_map的实现是在network.h中。

由于整个函数体较长,逐步分析。

1
2
3
4
5
6
7
8
9
10
for(inner_id=0; inner_id<NET_INNER_MAP_ENTRIES_MAX; inner_id++) {
// The key of the inner map must start from 0
struct net_rule *rule = get_net_rule(vnet_inner, inner_id);
if (rule == NULL) {
DEBUG_PRINT("");
DEBUG_PRINT("access allowed");
return 0;
}
....
}

通过for循环,配合get_net_rule(vnet_inner, inner_id)获得vnet_inner中的每一条规则。

针对每条规则,匹配address是否符合规则,检查条件包括IP和端口信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Check if the address matches the rule
if (rule->flags & CIDR_MATCH) {
for (i = 0; i < 4; i++) {
ip = (addr4->sin_addr.s_addr >> (8 * i)) & 0xff;
if ((ip & rule->mask[i]) != rule->address[i]) {
match = false;
break;
}
}
}

// Check if the port matches the rule
if (match && (rule->flags & PORT_MATCH) && (rule->port != bpf_ntohs(addr4->sin_port))) {
match = false;
}

执行动作,如果发现匹配的规则,执行规则中定义的动作:

1
2
3
4
5
if (match) {
DEBUG_PRINT("");
DEBUG_PRINT("access denied");
return -EPERM;
}

通过返回 -EPERM,LSM 程序可以告知内核或调用者,当前的操作被拒绝,并且可能会触发相应的权限拒绝处理逻辑。至此整个处理流程结束。

其他类型的lsm事件,处理逻辑也是类似的,只是针对的对象不同。

说明

整体来说,vArmor-ebpf代码逻辑是很清晰的,通过eBPFLSM机制,实现了对容器的加固。通过behaviorbpfenforcer两种模式,可以实现观察模式和阻断模式。

vArmor-ebpf也是很好的eBPF学习资料,可以参考和学习,后续如果有机会,也会继续深入学习。

参考

https://mp.weixin.qq.com/s/5rmkALNMhA1cVsk5A14wbA
https://github.com/bytedance/vArmor
https://github.com/bytedance/vArmor-ebpf