借助 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 的黑名单,
newBpfNetworkRule 函数中的处理对应,
....
var networkRule bpfNetworkRule
if 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 ②
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论