vArmor-eBPF 功能测试

admin 2023年11月25日23:04:22评论16 views字数 6013阅读20分2秒阅读模式

vArmor

vArmor 是字节开源的一个云原生容器沙箱系统,它借助 Linux 的 LSM 技术(AppArmor & BPF)实现强制访问控制器(即 enforcer),从而对容器进行安全加固。它可以用于增强容器隔离性、减少内核攻击面、增加容器逃逸或横行移动攻击的难度与成本。https://github.com/bytedance/vArmor

vArmor 的 eBPF 代码位于 https://github.com/bytedance/vArmor-ebpf,这篇文章是对 vArmor-ebpf 进行功能测试。


环境准备

本地进行项目编写

  • Windows10

  • GoLand,GO


服务器进行编译运行

  • Ubuntu 22.04 64位  5.15.0-88-generic

  • GO,clang,llvm


官方的运行环境要求,vArmor-eBPF 功能测试


确认 BPF LSM 是否可用

# 确认内核版本高于 5.7$ cat /boot/config-$(uname -r) | grep BPF_LSMCONFIG_BPF_LSM=y
# 内核是否支持 BPF LSM$ cat /sys/kernel/security/lsmlockdown,capability,yama,apparmor,bpfroot
# 如果上条命令结果不包含 bpf 选项,可以补充GRUB_CMDLINE_LINUX的值$ vim /etc/default/grub GRUB_CMDLINE_LINUX="lsm=ndlock,lockdown,yama,integrity,apparmor,bpf"
# 更新 grub 配置并重启$ update-grub2$ shutdown -r now

项目代码有 bug,一直还以为是我 GO 环境有问题,排查了几个小时,结果不是。已提 issues。vArmor-eBPF 功能测试

项目中存在 Makefile ,使用 make 命令,相关参数如下,需要全部运行成功。

help: 显示帮助信息,它会遍历 Makefile 中的目标和说明,然后格式化输出显示。generate-ebpf: 这个目标生成 EBPF 代码和库。它设置了环境变量 BPF_CLANG 和 BPF_CFLAGS,然后运行 go generate ./... 命令来生成 EBPF 代码。fmt:运行 go fmt 命令对代码进行格式化。如果找不到 goimports,则尝试安装。vet:运行 go vet 命令对代码进行静态分析。test-unit:运行单元测试,使用 go test 命令并将覆盖率输出到 coverage.out 文件。test:运行所有测试,包括单元测试。build:此目标通常用于构建项目。在这个 Makefile 中,它依赖于 generate-ebpf、fmt 和 vet 目标,因此会首先生成 EBPF 代码,然后对代码进行格式化和静态分析。

其中,generate-ebpf 操作是无报错的,需要出现下面的输出才是成功。vArmor-eBPF 功能测试

如果正在使用的 Linux 发行版(例如 Ubuntu )默认情况下没有启用跟踪子系统可能看不到任何输出,使用以下指令打开这个功能,这样子我们就能通过 cat /sys/kernel/debug/tracing/trace_pipe 看到内核态的日志输出了。

sudo suecho 1 > /sys/kernel/debug/tracing/tracing_on

上面把一些重点列出来了,实际编译运行中还会遇到其他的问题,比如要严格与官方 mod 中第三方包的版本对应,不要升级成最新版(已经发现有问题了),不过都不是什么大问题,祝你好运!


behavior(观察模式)

补充代码

vArmor-eBPF 功能测试

vArmor-ebpf/ 下添加 main.go

package mainimport (  "github.com/bytedance/vArmor-ebpf/pkg/behavior")
func main() { behavior.Test_loadEbpf() behavior.Test_customData()}


vArmor-ebpf/pkg/behavior/ 下添加 test.go (实际是作者的测试代码)

package behavior
import ( "fmt" "k8s.io/klog/v2/klogr" "os/exec" log "sigs.k8s.io/controller-runtime/pkg/log" "time")
func Test_loadEbpf() { log.SetLogger(klogr.New()) tracer := NewEbpfTracer(log.Log.WithName("ebpf"))
_ = tracer.InitEBPF() _ = tracer.RemoveEBPF()}
func Test_customData() { ... ... // 下面有该函数的完整代码}


测试 eBPF 加载

vArmor-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}

vArmor-eBPF 功能测试

编译运行vArmor-eBPF 功能测试


测试观察模式

这段代码创建了一个 eBPF 跟踪器,并使用它来跟踪和记录进程活动,每当一个新的进程被创建或执行时,都会生成一个事件,这个事件会被捕获并打印出来。同时,每2秒执行一次 "hostname" 命令,并为其设置一个特定的环境变量。如果环境变量为 "VARMOR=TEST",则计数器会增加。当计数器达到某个值或当程序运行了足够的时间后,程序会停止并退出。

behavior.Test_customData()

func Test_customData() {  // 创建一个容量为200的通道 eventCh,这个通道用于接收 bpfEvent 类型的事件  eventCh := make(chan bpfEvent, 200)  // 记录程序运行时的日志信息  log.SetLogger(klogr.New())    // 创建一个新的 eBPF 跟踪器实例  tracer := NewEbpfTracer(log.Log.WithName("ebpf"))    // 加载预编译程序和内核中的映射对象,用来初始化 eBPF 跟踪器  _ = tracer.InitEBPF()    // 函数结束时,删除 eBPF 跟踪器  defer tracer.RemoveEBPF()  // 将刚创建的通道 eventCh 添加到跟踪器中,并将其关联到 "TEST" 这个名称  tracer.AddEventCh("TEST", eventCh)  // 创建一个新的计时器 stopTicker,每3秒触发一次  stopTicker := time.NewTicker(3 * time.Second)    // 创建另一个计时器 runTicker,每2秒触发一次  runTicker := time.NewTicker(2 * time.Second)  count := 0
LOOP: for { // 开始无限循环 select { // 当 runTicker 计时器的定时器触发时,执行下面的代码块 case <-runTicker.C: // 创建一个新的子进程来执行 "hostname" 命令 cmd := exec.Command("hostname") // 将环境变量 "VARMOR=TEST" 添加到子进程的环境变量中 cmd.Env = append(cmd.Env, "VARMOR=TEST") // 在新的 goroutine 中运行子进程 go cmd.Run() // 当 stopTicker 计时器的定时器触发时,执行下面的代码块,这将结束循环 case <-stopTicker.C: break LOOP // 当从 eventCh 通道接收到新的事件时,执行下面的代码块 case event := <-eventCh: // 从事件的数据中提取环境变量、父进程任务名、子进程任务名和文件名 len := indexOfZero(event.Env[:]) env := string(event.Env[:len])
len = indexOfZero(event.ParentTask[:]) parentTask := string(event.ParentTask[:len])
len = indexOfZero(event.ChildTask[:]) childTask := string(event.ChildTask[:len])
len = indexOfZero(event.Filename[:]) fileName := string(event.Filename[:len])
// 根据事件的类型(fork 或 exec),设置事件类型字符串 eventType := "" if event.Type == 1 { eventType = "sched_process_fork" } else { eventType = "sched_process_exec" } // 格式化输出包括事件类型、父进程ID、子进程ID、父进程任务名、子进程任务名、环境变量、事件编号和文件名 output := fmt.Sprintf("%-24s |%-12d %-12d %-20s | %-12d %-12d %-20s | %-20s %-12d %sn", eventType, event.ParentPid, event.ParentTgid, parentTask, event.ChildPid, event.ChildTgid, childTask, env, event.Num, fileName, ) fmt.Println(output) // 如果环境变量为 "VARMOR=TEST",则增加计数器 if env == "VARMOR=TEST" { count += 1 } } }}


编译运行vArmor-eBPF 功能测试vArmor-eBPF 功能测试vArmor-eBPF 功能测试


bpfenforcer(阻断模式)

补充代码

原理同上,只不过调用的函数变了。设置打印日志,在代码 pkg/bpfenforcer/bpf/enforcer.h 中将 // #define DEBUG 1 的注释去掉,这样子我们就可以通过 cat /sys/kernel/debug/tracing/trace_pipe 看到内核态的日志输出了。然后需要 make generate-ebpf 将修改后的 C 代码重新转化成 GO 代码。


测试 eBPF 加载

同上。


测试建立 LSM 链接

StartEnforcing() 该函数尝试将多个 eBPF 程序链接到相应的 Linux 安全模块(LSM)钩子上,将链接后的对象保存在 enforcer 结构体中。

func (enforcer *BpfEnforcer) StartEnforcing() error {  capableLink, err := link.AttachLSM(link.LSMOptions{    Program: enforcer.objs.VarmorCapable,  })  if err != nil {    return err  }  enforcer.capableLink = capableLink  ...  ...

vArmor-eBPF 功能测试

测试网络规则

vArmor-eBPF 功能测试

pkg/bpfenforcer/bpf/enforcer.cSEC("lsm/socket_connect") 钩子函数内,修改一些代码,

// u32 mnt_ns = get_task_mnt_ns_id(current);// u32 *vnet_inner = get_net_inner_map(mnt_ns);// 修改成u32 mnt_ns = get_task_mnt_ns_id(current);DEBUG_PRINT("mnt_ns: %d", mnt_ns);u32 *vnet_inner = get_net_inner_map(4026533559);
// 修改内核态代码后,记得要 make generate-ebpf 一下哈

原因是在官方的用户态测试代码中,指定的命名空间 id 为 4026533559,然后将网络规则与该 id 的命名空间绑定:_ = tracer.SetNetMap(4026533559, rule)。因此在内核态代码中,我们需要从 id 为 4026533559 的命名空间中获取网络规则(通过 get_net_inner_map),如果不这样指定,则是通过 get_task_mnt_ns_id(current) 获取当前触发这个钩子的任务的命名空间 id,这个命名空间中是没有网络规则的。


网络规则如下,用同网段下另一个 ip 192.168.171.19 作为黑名单规则,进行五秒的跟踪与阻断,vArmor-eBPF 功能测试

一直打开 cat /sys/kernel/debug/tracing/trace_pipe 查看内核日志。

启动主程序,当出现 "msg"="start enforcing" 时,阻断模式启动,开始测试网络vArmor-eBPF 功能测试


在阻断模式下,第一个 ping 的 ip 是拦截规则内,故被拦截了,第二 ping 的 ip 不是拦截规则内,正常通信。vArmor-eBPF 功能测试


内核态代码日志输出,输出了相关日志,vArmor-eBPF 功能测试

测试 capabilities 规则

vArmor-eBPF 功能测试

pkg/bpfenforcer/bpf/enforcer.cSEC("lsm/capable") 钩子函数内,修改一些代码,

// u32 mnt_ns = get_task_mnt_ns_id(current);// u64 *deny_caps = get_capability_rules(mnt_ns);// 修改成u32 mnt_ns = get_task_mnt_ns_id(current);DEBUG_PRINT("mnt_ns: %d", mnt_ns);u64 *deny_caps = get_capability_rules(4026533394);
// 修改内核态代码后,记得要 make generate-ebpf 一下哈


CAP_NET_RAW 和 CAP_SYS_ADMIN 是 Linux 中的两种权限,分别允许用户执行网络原始I/O操作和系统管理操作,我这里对  CAP_SYS_ADMIN 进行禁用测试。vArmor-eBPF 功能测试

启动主程序,vArmor-eBPF 功能测试


第一个是在阻断模式下,被拦截,第二个是正常情况下。vArmor-eBPF 功能测试


内核态代码日志输出,输出了相关日志。vArmor-eBPF 功能测试

其余的拦截模块都和上面大同小异。


封面图,图源 https://github.com/bytedance/vArmor

vArmor-eBPF 功能测试





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

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年11月25日23:04:22
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   vArmor-eBPF 功能测试http://cn-sec.com/archives/2239622.html

发表评论

匿名网友 填写信息