观察模式
跟踪和记录进程创建与执行
代码取自 vArmor-ebpf
tracepoint__sched__sched_process_fork 函数,通过 eBPF 机制监听 sched_process_fork 事件,在事件触发时,提取相关进程的信息和环境变量数据,并将这些数据输出到 events map 中以供进一步处理或分析。sched_process_fork 事件在 Linux 内核中是当新的进程被创建时触发的。当一个进程调用 fork() 系统调用创建了一个子进程时,就会触发这个事件。
tracepoint__sched__sched_process_exec 函数,通过 eBPF 机制监听 sched_process_exec 事件,在事件触发时,收集相关进程执行的信息,包括父进程、子进程的 PID、TGID、进程名,以及执行的文件名和特定的环境变量,并输出到 events map 中以供进一步处理或分析。sched_process_exec 事件在进程执行新程序时被触发。
// SPDX-License-Identifier: GPL-2.0
// Copyright 2023 vArmor-ebpf Authors
// 设置文件名和环境变量的最大长度,环境变量提取的最大循环次数
char __license[] SEC("license") = "GPL";
// 声明一个 map
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
} events SEC(".maps");
struct event {
u32 type; // 事件类型
u32 parent_pid; // 父进程的 PID
u32 parent_tgid; // TGID
u32 child_pid; // 子进程的 PID
u32 child_tgid; // TGID
unsigned char parent_task[TASK_COMM_LEN]; // 父进程任务名称
unsigned char child_task[TASK_COMM_LEN]; // 子进程任务名称
unsigned char filename[MAX_FILENAME_LEN]; // 文件名
unsigned char env[MAX_ENV_LEN]; // 环境信息
u32 num;
};
// 为了防止编译器给出未使用变量的警告
const struct event *unused __attribute__((unused));
// https://elixir.bootlin.com/linux/v5.4.196/source/kernel/fork.c#L2388
// https://elixir.bootlin.com/linux/v5.4.196/source/include/trace/events/sched.h#L287
// 监听 sched_process_fork 事件,当事件发生时调用 tracepoint__sched__sched_process_fork
SEC("raw_tracepoint/sched_process_fork")
int tracepoint__sched__sched_process_fork(struct bpf_raw_tracepoint_args *ctx)
{
// TP_PROTO(struct task_struct *parent, struct task_struct *child)
// 将 ctx 结构体中的参数解析为父进程和子进程的 task_struct 对象。
struct task_struct *parent = (struct task_struct *)ctx->args[0];
struct task_struct *child = (struct task_struct *)ctx->args[1];
struct event event = {};
// 设置事件的类型为1
event.type = 1;
// 从父任务结构体中读取父进程的 PID
BPF_CORE_READ_INTO(&event.parent_pid, parent, pid);
// 从父任务结构体中读取父进程的 TGID
BPF_CORE_READ_INTO(&event.parent_tgid, parent, tgid);
// 从父任务结构体中读取父进程的名称
BPF_CORE_READ_STR_INTO(&event.parent_task, parent, comm);
// 从当前任务结构体中读取当前进程的 PID
BPF_CORE_READ_INTO(&event.child_pid, child, pid);
// 从当前任务结构体中读取当前进程的 TGID
BPF_CORE_READ_INTO(&event.child_tgid, child, tgid);
// 从当前任务结构体中读取当前进程的名称
BPF_CORE_READ_STR_INTO(&event.child_task, child, comm);
u64 env_start = 0;
u64 env_end = 0;
int i = 0;
int len = 0;
// 从当前任务结构体的 mm 结构体中读取环境变量的起始地址
BPF_CORE_READ_INTO(&env_start, parent, mm, env_start);
// 从当前任务结构体的 mm 结构体中读取环境变量的结束地址
BPF_CORE_READ_INTO(&env_end, parent, mm, env_end);
// 循环,尝试从父进程的环境变量中提取特定格式的信息
while(i < MAX_ENV_EXTRACT_LOOP_COUNT && env_start < env_end ) {
// bpf_probe_read_kernel_str 用于从内核空间读取字符串,并将其存储在 event.env 中
len = bpf_probe_read_kernel_str(&event.env, sizeof(event.env), (void *)env_start);
if ( len <= 0 ) {
break;
} else if ( event.env[0] == 'V' && // vArmor把自己排除了
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;
// 将 event 事件数据输出到 events map 中
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
return 0;
}
// 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)
// 获取当前任务结构体的指针,获取 linux_binprm 结构体的指针
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; // 设置 event 的类型为 2(表示进程执行事件)
// 从父任务结构体中读取父进程的 PID
BPF_CORE_READ_INTO(&event.parent_pid, parent, pid);
// 从父任务结构体中读取父进程的 TGID
BPF_CORE_READ_INTO(&event.parent_tgid, parent, tgid);
// 从父任务结构体中读取父进程的名称
BPF_CORE_READ_STR_INTO(&event.parent_task, parent, comm);
// 从当前任务结构体中读取当前进程的 PID
BPF_CORE_READ_INTO(&event.child_pid, child, pid);
// 从当前任务结构体中读取当前进程的 TGID
BPF_CORE_READ_INTO(&event.child_tgid, child, tgid);
// 从当前任务结构体中读取当前进程的名称
BPF_CORE_READ_STR_INTO(&event.child_task, child, comm);
// 从 linux_binprm 结构体中读取执行文件的名称
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;
// 从当前任务结构体的 mm 结构体中读取环境变量的起始地址
BPF_CORE_READ_INTO(&env_start, current, mm, env_start);
// 从当前任务结构体的 mm 结构体中读取环境变量的结束地址
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;
}
阻断模式
阻止向黑名单地址/网段进行访问
vArmor 具备拦截禁止访问云服务器的 metadata service 的能力,
添加规则
vArmor-eBPF 中的拦截实现
SEC("lsm/socket_connect")
// 传参 正在进行连接的套接字、正在连接的远程地址、地址结构的长度
int BPF_PROG(varmor_socket_connect, struct socket *sock, struct sockaddr *address, int addrlen) {
// 查地址类型是否是IPv4或IPv6,都不是就不关心
if (address->sa_family != AF_INET && address->sa_family != AF_INET6)
return 0;
// 检索当前任务
struct task_struct *current = (struct task_struct *)bpf_get_current_task();
// 获取当前任务的挂载命名空间ID,并获取此命名空间的规则
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);
// 进行网络规则匹配检查
return iterate_net_inner_map(vnet_inner, address);
}
关键函数 iterate_net_inner_map
static __noinline int iterate_net_inner_map(u32 *vnet_inner, struct sockaddr *address) {
u32 inner_id, ip, i;
bool match;
for(inner_id=0; inner_id<NET_INNER_MAP_ENTRIES_MAX; inner_id++) {
struct net_rule *rule = get_net_rule(vnet_inner, inner_id);
if (rule == NULL) { // 规则为空情况下
DEBUG_PRINT("");
DEBUG_PRINT("access allowed");
return 0;
}
DEBUG_PRINT("---- rule id: %d ----", inner_id);
match = true;
if (address->sa_family == AF_INET) { // IPv4的情况
struct sockaddr_in *addr4 = (struct sockaddr_in *) address; //传入的地址转换为 struct sockaddr_in 类型
DEBUG_PRINT("IPv4 address: %x", addr4->sin_addr.s_addr); // 输出地址
DEBUG_PRINT("IPv4 port: %x", addr4->sin_port); // 输出端口
if (rule->flags & CIDR_MATCH) { // 如果规则设置了 CIDR_MATCH 标志,将会进行 CIDR 匹配
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;
}
}
} else if (rule->flags & PRECISE_MATCH) { // 如果规则设置了 PRECISE_MATCH 标志,则进行精确匹配
for (i = 0; i < 4; i++) {
ip = (addr4->sin_addr.s_addr >> (8 * i)) & 0xff;
if (ip != rule->address[i]) { // 直接比较是否完全相同
match = false;
break;
}
}
}
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;
}
} else {
// IPv6
struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *) address;
struct in6_addr ip6addr = BPF_CORE_READ(addr6, sin6_addr);
DEBUG_PRINT("IPv6 address: %d:%d", ip6addr.in6_u.u6_addr8[0], ip6addr.in6_u.u6_addr8[1]);
DEBUG_PRINT("IPv6 address: %d:%d", ip6addr.in6_u.u6_addr8[2], ip6addr.in6_u.u6_addr8[3]);
DEBUG_PRINT("IPv6 address: %d:%d", ip6addr.in6_u.u6_addr8[4], ip6addr.in6_u.u6_addr8[5]);
DEBUG_PRINT("IPv6 address: %d:%d", ip6addr.in6_u.u6_addr8[6], ip6addr.in6_u.u6_addr8[7]);
DEBUG_PRINT("IPv6 port: %d", bpf_ntohs(addr6->sin6_port));
if (rule->flags & CIDR_MATCH) {
for (i = 0; i < 16; i++) {
ip = ip6addr.in6_u.u6_addr8[i];
if ((ip & rule->mask[i]) != rule->address[i]) {
match = false;
break;
}
}
} else if (rule->flags & PRECISE_MATCH) {
for (i = 0; i < 16; i++) {
ip = ip6addr.in6_u.u6_addr8[i];
if (ip != rule->address[i]) {
match = false;
break;
}
}
}
if (match && (rule->flags & PORT_MATCH) && (rule->port != bpf_ntohs(addr6->sin6_port))) {
match = false;
}
if (match) {
DEBUG_PRINT("");
DEBUG_PRINT("access denied");
return -EPERM;
}
}
}
DEBUG_PRINT("");
DEBUG_PRINT("access allowed");
return 0;
}
禁用 capabilities
vArmor 具备禁用所有 capabilities、禁用特权 capability、禁用任意 capability 的能力,
vArmor-eBPF 中的拦截实现
SEC("lsm/capable")
// 传参 调用者的权限信息、正在执行的命名空间、检查的权限标志、附加选项、返回值
int BPF_PROG(varmor_capable, const struct cred *cred, struct user_namespace *ns, int cap, unsigned int opts, int ret) {
// 获取当前任务的指针
struct task_struct *current = (struct task_struct *)bpf_get_current_task();
// 获取当前任务的挂载命名空间ID,并获取此命名空间的规则
u32 mnt_ns = get_task_mnt_ns_id(current);
u64 *deny_caps = get_capability_rules(mnt_ns);
if (deny_caps == 0)
return ret;
DEBUG_PRINT("================ lsm/capable ================");
// 将权限 cap 转换为其对应的掩码
u64 request_cap_mask = CAP_TO_MASK(cap);
if (*deny_caps & request_cap_mask) {
// 获取当前任务的用户命名空间 current_ns,以及任务的权限
struct user_namespace *current_ns = get_task_user_ns(current);
kernel_cap_t current_cap_effective = get_task_cap_effective(current);
u64 current_effective_mask = *(u64 *)¤t_cap_effective;
DEBUG_PRINT("task(mnt ns: %u) current_effective_mask: 0x%lx, request_cap_mask: 0x%lx",
mnt_ns, current_effective_mask, request_cap_mask);
// 在写入/tmp目录时与overlayfs兼容
if (current_ns == ns && current_effective_mask == 0x1fffeffffff) {
return ret;
}
// 与cgroupv2环境中的containerd兼容
if (current_ns == ns && current_effective_mask == 0x1ffffffffff) {
return ret;
}
// 直接阻断相关 capability
DEBUG_PRINT("task(mnt ns: %u) is not allowed to use capability: 0x%x", mnt_ns, cap);
return -EPERM;
}
return ret;
}
参考文章
https://blog.spoock.com/2023/08/23/eBPF-vArmor/
原文始发于微信公众号(安全小将李坦然):eBPF-vArmor ①
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论