4

字节vArmor代码解读

 11 months ago
source link: https://blog.spoock.com/2023/08/23/eBPF-vArmor/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近字节开源了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代码为例分析:

// 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为例,分析:

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的连接。

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中。

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

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和端口信息:

// 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;
}

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK