vArmor-eBPF ②

admin 2023年11月24日12:45:11评论30 views字数 6224阅读20分44秒阅读模式

目介绍

借助 Linux 的 LSM 技术(AppArmor & BPF)实现预览(告警)模式和强制访问控制器,用于增强容器隔离性、减少内核攻击面、增加容器逃逸或横行移动攻击的难度与成本。

项目源码:https://github.com/bytedance/vArmor-ebpf

该项目使用 https://github.com/cilium/ebpf 来管理 ebpf 程序并与之交互,共有两个目录 behavior & bpfenforcer,前者实现观察模式,后者实现阻断模式。


behavior

InitEBPF

InitEBPF 加载预编译程序和内核中的映射对象,用来初始化 eBPF 跟踪器。

func (tracer *EbpfTracer) InitEBPF() error {  // 日志记录,表明正在执行内存锁定的操作  tracer.log.Info("remove memory lock")
// RemoveMemlock 用于允许当前进程锁定内存以便 eBPF 资源使用 if err := rlimit.RemoveMemlock(); err != nil { return fmt.Errorf("RemoveMemlock() failed: %v", err) }
// 表明正在加载 eBPF 程序和映射到内核中。调用了 loadBpfObjects() 函数来加载预编译的程序和内核中的映射对象到指定的结构体 tracer.objs 中 tracer.log.Info("load ebpf program and maps into the kernel") if err := loadBpfObjects(&tracer.objs, nil); err != nil { return fmt.Errorf("loadBpfObjects() failed: %v", err) } return nil}


startTracing

启动系统跟踪功能,将内核代码和用户代码相互关联,这样就完成了 eBPF 代码的加载。

func (tracer *EbpfTracer) startTracing() error {  // 将 printk_ratelimit 设置为0,表示关闭内核日志的速率限制,以便记录 AppArmor 的审核日志  err := tracer.setRateLimit()  if err != nil {    return fmt.Errorf("setRateLimit() failed: %v", err)  }
// AttachRawTracepoint 函数将程序与 sched_process_exec 原始跟踪点进行关联 // 这里使用了 tracer.objs.TracepointSchedSchedProcessExec 作为程序 execLink, err := link.AttachRawTracepoint(link.RawTracepointOptions{ Name: "sched_process_exec", Program: tracer.objs.TracepointSchedSchedProcessExec, }) if err != nil { return fmt.Errorf("AttachRawTracepoint() failed: %v", err) } tracer.execLink = execLink
// AttachRawTracepoint 函数将程序与 sched_process_fork 原始跟踪点进行关联 // 这里使用了 tracer.objs.TracepointSchedSchedProcessFork 作为程序 forkLink, err := link.AttachRawTracepoint(link.RawTracepointOptions{ Name: "sched_process_fork", Program: tracer.objs.TracepointSchedSchedProcessFork, }) if err != nil { return fmt.Errorf("AttachRawTracepoint() failed: %v", err) } tracer.forkLink = forkLink
// 从内核空间中的 BPF_MAP_TYPE_PERF_EVENT_ARRAY 映射创建一个性能事件读取器,这个读取器用于读取跟踪事件 reader, err := perf.NewReader(tracer.objs.Events, 8192*128) if err != nil { return fmt.Errorf("perf.NewReader() failed: %v", err) } tracer.reader = reader // 启动一个新的 goroutine 来执行 traceSyscall 函数 // traceSyscall 函数作用是不断读取跟踪事件并传递给注册的事件通道,以便后续处理 go tracer.traceSyscall() tracer.enabled = true
tracer.log.Info("start tracing")
return nil}


bpfenforcer

InitEBPF

初始化 eBPF 相关的资源和设置,下方代码中类似 fileInnerMap 的用于保存规则。

func (enforcer *BpfEnforcer) InitEBPF() error {  // 允许当前进程锁定内存以供 eBPF 资源使用  enforcer.log.Info("remove memory lock")  if err := rlimit.RemoveMemlock(); err != nil {    return fmt.Errorf("RemoveMemlock() failed: %v", err)  }
enforcer.log.Info("parses the ebpf program into a CollectionSpec") // 通过 loadBpf 函数解析 eBPF 程序 collectionSpec, err := loadBpf() if err != nil { return err }
// 为每个需要内部映射的 eBPF Map 创建了相应的 MapSpec // MapSpec 描述了每个 Map 的特征,例如类型、键值大小和最大条目数 fileInnerMap := ebpf.MapSpec{ Name: "v_file_inner_", Type: ebpf.Hash, KeySize: 4, ValueSize: 4*2 + 64*2, MaxEntries: MAX_FILE_INNER_ENTRIES, } collectionSpec.Maps["v_file_outer"].InnerMap = &fileInnerMap
.... ....
// 将 eBPF 程序和 Maps 预编译到内核中 enforcer.log.Info("load ebpf program and maps into the kernel") err = collectionSpec.LoadAndAssign(&enforcer.objs, nil) if err != nil { return err } return nil}


StartEnforcing

通过 link.AttachLSM() 方法将预先编译的 eBPF 程序附加到 Linux Security Module (LSM) 钩子上,用于执行安全策略的检查。

func (enforcer *BpfEnforcer) StartEnforcing() error {  capableLink, err := link.AttachLSM(link.LSMOptions{ // 检查特权    Program: enforcer.objs.VarmorCapable,  })  openFileLink, err := link.AttachLSM(link.LSMOptions{ // 文件打开    Program: enforcer.objs.VarmorFileOpen,  })  pathSymlinkLink, err := link.AttachLSM(link.LSMOptions{ // 路径符号链接    Program: enforcer.objs.VarmorPathSymlink,  })  pathLinkLink, err := link.AttachLSM(link.LSMOptions{ // 路径链接    Program: enforcer.objs.VarmorPathLink,  })  bprmLink, err := link.AttachLSM(link.LSMOptions{ // bprm 安全检查    Program: enforcer.objs.VarmorBprmCheckSecurity,  })  sockConnLink, err := link.AttachLSM(link.LSMOptions{ // 套接字连接    Program: enforcer.objs.VarmorSocketConnect,  })  ptraceLink, err := link.AttachLSM(link.LSMOptions{ // ptrace 访问检查    Program: enforcer.objs.VarmorPtraceAccessCheck,  })  ....    ....  enforcer.log.Info("start enforcing")  return nil}


规则匹配

以 varmor_socket_connect 为例

在内核态 规则获取代码如下,v_net_outer 将 namespace 作为 key,对应的规则信息作为 value 保存在 map 中,

// v_net_outer 是一个 BPF_MAP_TYPE_HASH_OF_MAPS 类型的 map,用于保存规则信息,struct {  __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS);  __uint(max_entries, OUTER_MAP_ENTRIES_MAX);  __type(key, u32);  __type(value, u32);} v_net_outer SEC(".maps");
// get_net_inner_map 通过 namespace 信息得到对应得规则信息。static u32 *get_net_inner_map(u32 mnt_ns) { return bpf_map_lookup_elem(&v_net_outer, &mnt_ns);}

SEC("lsm/socket_connect")int BPF_PROG(varmor_socket_connect, struct socket *sock, struct sockaddr *address, int addrlen) { .... u32 mnt_ns = get_task_mnt_ns_id(current); // 获取到对应的规则信息 u32 *vnet_inner = get_net_inner_map(mnt_ns); // 规则为空时 if (vnet_inner == NULL) return 0; // 开始进行规则匹配 return iterate_net_inner_map(vnet_inner, address);}


执行 iterate_net_inner_map(vnet_inner, address) 进行规则匹配,代码如下,

目的是阻止向黑名单地址/网段进行访问,下方匹配规则有两种:一种是设置 CIDR_MATCH ,则进行 CIDR 匹配,比较 IP 地址的网络前缀,另一种是 PRECISE_MATCH,精准匹配,对完整的 IP 地址进行比较。二选一后再进行端口的比较,最终确定访问的ip是不是处于黑名单中,若是则阻断。

// 规则信息最终的格式struct net_rule {  u32 flags;  unsigned char address[16]; // 黑名单中的地址  unsigned char mask[16]; // 地址掩码  u32 port;};

static struct net_rule *get_net_rule(u32 *vnet_inner, u32 rule_id) { return bpf_map_lookup_elem(vnet_inner, &rule_id);}
for(inner_id=0; inner_id<NET_INNER_MAP_ENTRIES_MAX; inner_id++) { // 通过get_net_rule 得到对应的规则信息处理后存储到 rule,传参 vnet_inner 就是上文中的规则信息 struct net_rule *rule = get_net_rule(vnet_inner, inner_id); if (rule == NULL) { DEBUG_PRINT(""); DEBUG_PRINT("access allowed"); return 0; } .... // 以IPv4为例 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; // 端口没匹配上,放行 } ....}


在用户态代码中设置规则,下图中,调用 newBpfNetworkRule 来设置 ip 的黑名单,vArmor-eBPF ②

newBpfNetworkRule 函数中的处理对应,

....var networkRule bpfNetworkRuleif ip.To4() != nil {    networkRule.Flags |= IPV4_MATCH  copy(networkRule.Address[:], ip.To4())} else {  networkRule.Flags |= IPV6_MATCH  copy(networkRule.Address[:], ip.To16())}....
// 用户态的networkRule与内核态的net_rule定义一致type bpfNetworkRule struct { Flags uint32 Address [16]byte Mask [16]byte Port uint32}
struct net_rule {  u32 flags;  unsigned char address[16]; // 黑名单中的地址  unsigned char mask[16]; // 地址掩码  u32 port;};


参考文章

https://blog.spoock.com/2023/08/26/eBPF-vArmor-client/


原文始发于微信公众号(安全小将李坦然):vArmor-eBPF ②

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年11月24日12:45:11
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   vArmor-eBPF ②https://cn-sec.com/archives/2231315.html

发表评论

匿名网友 填写信息