vArmor-ebpf-loader 项目介绍和功能解读
2024-1-3 11:11:0 Author: paper.seebug.org(查看原文) 阅读量:21 收藏

作者:spoock
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]

在前面文章 字节vArmor代码解读 介绍了字节开源的vArmor项目。vArmor 是一个云原生容器沙箱系统,它借助 Linux 的 LSM 技术(AppArmor & BPF)实现强制访问控制器(即 enforcer),从而对容器进行安全加固。它可以用于增强容器隔离性、减少内核攻击面、增加容器逃逸或横行移动攻击的难度与成本。vArmor项目主要是由两个部分组成,分别是K8S CRD项目 vArmor,LSM项目vArmor-ebpf

两者之间的交互关系参考下图:

vArmor-ebpf是基于eBPF中的LSM实现针对特定系统调用函数监控,通过接受来自于vArmor下发得规则,针对特定的文件、进程、端口、IP等信息进行检测,判断是否需要阻断。

由于整个项目非常庞大,涉及到各种背景知识和机制也是非常多。如果需要针对 vArmor-ebpf 中的每个Hook函数进行分析,就必须运行整个项目。为了方便针对 vArmor-ebpf 项目进行测试,开发了vArmor-ebpf-loader 程序方便加载和调试ebpf程序。

vArmor-ebpf-loader介绍

由于整个vAmror项目非常庞大,尤其是涉及到helmk8s各种配置和部署,给个人开发环境运行调试造成了极大的调整。如果需要单独针对 vArmor-ebpf 项目中的功能测试分析和学习,十分地困难和不方便。为了方便单独针对项目分析,于是就单独创建了这个项目。一方面是为了了解vArmor项目的规则设置,另一方面是为了方便调试测试 vArmor-ebpf 中的规则。

当然也可以使用vArmor-ebpf 中的自带的测试用例enforcer_test.go测试。

编译eBPF

为了方便测试,项目需要先将 vArmor-ebpf 编译成为一个.o文件方便后面直接加载和使用。

make-ebpf:
    @echo " compiling ebpf"
    /usr/bin/clang-15 \
        -D__TARGET_ARCH_$(linux_arch) \
        -D__BPF_TRACING__ \
        -DDEBUG \
        -Wno-unused-value \
        -Wno-pointer-sign \
        -Wno-compare-distinct-pointer-types \
        -Wunused \
        -Wall \
        -Werror \
        -I ./pkg/bpfenforcer/bpf/headers \
        -I ./pkg/bpfenforcer/bpf \
        -target bpf \
        -O2 -g -emit-llvm \
        -c pkg/bpfenforcer/bpf/enforcer.c -o - | llc-15 -march=bpf -filetype=obj -o  varmor.o

    @echo "md5sum of ebpf"
    @md5sum varmor.o | cut -d ' ' -f 1 | xargs echo "MD5 of varmor.o is: "

通过上面的这段Makefile代码就可以将 vArmor-ebpf 项目编译得到一个varmor.o文件。

注意其中的-DDEBUG表示开启调试选项,这样vArmor-ebpf中的DEBUG_PRINT的调试日志才会打印出来。

加载eBPF

通过go:embed的方式加载varmor.o程序。

//go:embed bin/varmor.o
var _bytecode []byte

// 加载eBPF程序集合
spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(_bytecode))
if err != nil {
    log.Fatalf("failed to load collection spec: %v", err)
}

获得MntID

在原先的vAmror程序中,会根据容器的MntID定位是哪些容器要应用规则。如果我们在本机测试规则,就需要活得本机的MntID

func readMntID() (int, error) {
    // 读取文件内容
    content, err := os.Readlink("/proc/1/ns/mnt")
    if err != nil {
        fmt.Println("无法读取文件:", err)
        return -1, fmt.Errorf("无法读取文件: %w", err)
    }

    // 使用正则表达式提取数值
    re := regexp.MustCompile(`\[(\d+)\]`)
    match := re.FindStringSubmatch(string(content))
    if len(match) < 2 {
        return -1, fmt.Errorf("未找到匹配的数值")
    }

    // 提取到的数值
    mntValueStr := match[1]
    mntValue, err := strconv.Atoi(mntValueStr)
    if err != nil {
        return -1, fmt.Errorf("转换数值失败: %w", err)
    }
    return mntValue, nil
}

启动程序

加载默认空规则

在启动之前,程序需要默认先加载v_net_outerv_mount_outerv_file_outerv_bprm_outer对应的map,作为eBPF程序的初始化规则。

// 加载eBPF程序集合
spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(_bytecode))
if err != nil {
    log.Fatalf("failed to load collection spec: %v", err)
}

netInnerMap := ebpf.MapSpec{
    Name:       "v_net_inner_",
    Type:       ebpf.Hash,
    KeySize:    4,
    ValueSize:  4*2 + 16*2,
    MaxEntries: 1024,
}
spec.Maps["v_net_outer"].InnerMap = &netInnerMap

// 加载 bprm 规则
bprmInnerMap := ebpf.MapSpec{
    Name:       "v_bprm_inner_",
    Type:       ebpf.Hash,
    KeySize:    4,
    ValueSize:  4*2 + 64*2,
    MaxEntries: 50,
}
spec.Maps["v_bprm_outer"].InnerMap = &bprmInnerMap

// 加载文件规则
fileInnerMap := ebpf.MapSpec{
    Name:       "v_file_inner_",
    Type:       ebpf.Hash,
    KeySize:    4,
    ValueSize:  4*2 + 64*2,
    MaxEntries: 50,
}
spec.Maps["v_file_outer"].InnerMap = &fileInnerMap

mountInnerMap := ebpf.MapSpec{
    Name:       "v_mount_inner_",
    Type:       ebpf.Hash,
    KeySize:    4,
    ValueSize:  4*3 + 16 + 64*2,
    MaxEntries: 50,
}
spec.Maps["v_mount_outer"].InnerMap = &mountInnerMap

// 加载程序
// 加载eBPF程序集合
coll, err := ebpf.NewCollection(spec)
if err != nil {
    log.Fatalf("failed to load collection: %v", err)
}
defer coll.Close()

由于规则都是嵌套的,所以我们在进行规则设定的时候,也需要设置innerouterinner设置为ebpf.Hash的方式。

加载LSM

在整个 vArmor-ebpf项目中,存在11个LSM相关的函数点,我们可以单独针对某个Hook函数进行测试和设置对应的规则。如下所示:

prog := coll.Programs["varmor_file_open"]
if prog == nil {
    log.Fatalf("program not found in collection")
}
_, err = link.AttachLSM(link.LSMOptions{
    Program: prog,
})

这个就表示我们仅仅只是加载varmor_file_open这个对应的Hook点,其他的Hook点就不会加载。

设置规则

vArmor-ebpf项目中,前面已经说过了,是存在四种类型的规则。每种规则的设置方法都存在不同。具体到每种规则中的匹配模式,对于规则的设置也会存在差别。比如规则中的前缀和后缀匹配,对于路径的配置就完全不一样。

outerFileMap, ok := coll.Maps["v_file_outer"]
if !ok {
    log.Fatalf("map not found in collection")
}
fileMapName := fmt.Sprintf("v_file_inner_%d", nsID)
innerFileMapSpec := ebpf.MapSpec{
    Name:       fileMapName,
    Type:       ebpf.Hash,
    KeySize:    4,
    ValueSize:  4*2 + 64*2,
    MaxEntries: uint32(50),
}

innerFileMap, err := ebpf.NewMap(&innerFileMapSpec)
if err != nil {
    log.Fatalf("failed to create inner map: %v", err)
    return
}
defer innerFileMap.Close()

var pathRule bpfPathRule
var prefix, suffix [64]byte
copy(prefix[:], "/etc/hostname")
copy(suffix[:], "")

// permissions: 4 for AaMayRead
pathRule.Permissions = 4
// flags: 5 for PreciseMatch | PrefixMatch
pathRule.Pattern.Flags = 5
pathRule.Pattern.Prefix = prefix
pathRule.Pattern.Suffix = suffix

var fileIndex = uint32(0)
err = innerFileMap.Put(&fileIndex, &pathRule)
if err != nil {
    log.Fatalf("failed to put rule: %v", err)
}
outerFileMap.Put(uint32(nsID), innerFileMap)

以上就是一个简单的设置文件规则v_file_outer的例子。

获取outer规则
outerFileMap, ok := coll.Maps["v_file_outer"]
if !ok {
    log.Fatalf("map not found in collection")
}
获取inner规则
fileMapName := fmt.Sprintf("v_file_inner_%d", nsID)
innerFileMapSpec := ebpf.MapSpec{
    Name:       fileMapName,
    Type:       ebpf.Hash,
    KeySize:    4,
    ValueSize:  4*2 + 64*2,
    MaxEntries: uint32(50),
}

innerFileMap, err := ebpf.NewMap(&innerFileMapSpec)
if err != nil {
    log.Fatalf("failed to create inner map: %v", err)
    return
}
defer innerFileMap.Close()

其中inner规则的规则名和nsID相关,设置方法是fileMapName := fmt.Sprintf("v_file_inner_%d", nsID)。其中nsID就是前面得到的MntID

配置规则
var pathRule bpfPathRule
var prefix, suffix [64]byte
copy(prefix[:], "/etc/hostname")
copy(suffix[:], "")

// permissions: 4 for AaMayRead
pathRule.Permissions = 4
// flags: 5 for PreciseMatch | PrefixMatch
pathRule.Pattern.Flags = 5
pathRule.Pattern.Prefix = prefix
pathRule.Pattern.Suffix = suffix

在本例中是设置的文件相关的规则。和文件有关的规则就涉及到需要匹配的路径,对应文件的操作方式(读取还是写入,追加),以及匹配模式。下面针对这几条分别进行讲解。

  1. copy(prefix[:], "/etc/hostname"),表示规则路径是/etc/hostname,是在前缀位置设置的,说明我们的规则是和前缀相关的规则
  2. pathRule.Permissions = 4,表示规则检测的文件的读取。所以如果发现有读取/etc/hostname文件的行为就会终止
  3. pathRule.Pattern.Flags = 5,匹配模式是PreciseMatch | PrefixMatch,表示前缀精确匹配,只有完全匹配到/etc/hostname这个路径才会阻止该操作
加载规则
var fileIndex = uint32(0)
err = innerFileMap.Put(&fileIndex, &pathRule)
if err != nil {
    log.Fatalf("failed to put rule: %v", err)
}
outerFileMap.Put(uint32(nsID), innerFileMap)

通过innerFileMapouterFileMap加载上面设置的规则。

查看日志

$ sudo cat /sys/kernel/debug/tracing/trace_pipe

因为在Makefile阶段开启了debug模式,通过trace_pipe就可以看到所有的eBPF调试打印出来所有信息。 为了防止刷屏太快,也可以采用如下的代码保持输出日志。

$ sudo cat /sys/kernel/debug/tracing/trace_pipe | tee /path/to/output.txt | grep denied

利用上面的规则,我们实际执行cat /etc/hostname行为:

cat /etc/hostname            
cat: /etc/hostname: Operation not permitted

执行失败,提示Operation not permitted 通过trace_pipe查看ebpf程序中的内核执行的调试日志

# cat /sys/kernel/debug/tracing/trace_pipe
bpf_trace_printk: ================ lsm/file_open ================
bpf_trace_printk: path: /etc/hostname
bpf_trace_printk: offset: 4082, length: 13
bpf_trace_printk: file name: hostname, length: 8
bpf_trace_printk: ---- rule id: 0 ----
bpf_trace_printk: requested permissions: 0x4, rule permissions: 0x4
bpf_trace_printk: old_path_check() - pattern flags: 0x5
bpf_trace_printk: old_path_check() - matching path
bpf_trace_printk: old_path_check() - pattern prefix: /etc/hostname
bpf_trace_printk: 
bpf_trace_printk: access denied

显示access denied,说明成功阻止了cat /etc/hostname行为。

rules

varmor中虽然使用了lsm拦截了很多函数,但是并没有针对每一个拦截函数都创建了一个对应的规则。在varmor中仅仅只是存在四种规则。分别是:

  • 和文件相关的规则,v_file_outer
  • 和执行进程权限检查相关的规则,v_bprm_outer
  • 和网络相关的规则 ,v_net_outer
  • 和挂载相关的规则,v_mount_outer

其余的各种权限检查都是基于以上四种规则的。下面就会针对每种规则进行判断分析。

有关规则和对应的函数之间的关系,可以参考下图:

v_file_outer

文件相关的规则定义,在pkg/bpfenforcer/bpf/file.h中。

struct {
  __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS);
  __uint(max_entries, OUTER_MAP_ENTRIES_MAX);
  __type(key, u32);
  __type(value, u32);
} v_file_outer SEC(".maps");

struct path_rule {
  u32 permissions;
  struct path_pattern pattern;
};

v_file_outer 提供了一个命名空间级别的文件访问控制机制,通过内部map的集合来实施访问控制策略。每个内部map包含了一组path_rule 规则,这些规则定义了哪些文件路径可以访问以及访问权限。这种设计使得可以为不同的命名空间设置不同的访问控制策略,以此来增强系统的安全性。

v_net_outer

网络相关的规则定义,在pkg/bpfenforcer/bpf/network.h

struct net_rule {
  u32 flags;
  unsigned char address[16];
  unsigned char mask[16];
  u32 port;
};

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");

v_net_outer映射是一个索引结构,用来存储和查找网络规则映射。每个网络规则映射(内部映射)包含了特定的网络访问控制规则,这些规则可以基于源地址、目的地址、端口等进行匹配。这个结构允许针对不同的命名空间或容器应用不同的网络访问策略。

v_bprm_outer

进程限制相关的规则定义,在pkg/bpfenforcer/bpf/process.h

struct {
  __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS);
  __uint(max_entries, OUTER_MAP_ENTRIES_MAX);
  __type(key, u32);
  __type(value, u32);
} v_bprm_outer SEC(".maps");

v_bprm_outer 映射是一个索引结构,用于存储和查找与进程执行权限相关的规则映射(内部映射)。每个内部映射包含了特定的文件路径访问控制规则,这些规则可以基于文件路径模式匹配等进行访问控制。

v_mount_outer

挂载限制相关的规则定义,在pkg/bpfenforcer/bpf/mount.h

struct {
  __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS);
  __uint(max_entries, OUTER_MAP_ENTRIES_MAX);
  __type(key, u32);
  __type(value, u32);
} v_mount_outer SEC(".maps");

struct mount_rule {
  u32 mount_flags;
  u32 reverse_mount_flags;
  unsigned char fstype[FILE_SYSTEM_TYPE_MAX];
  struct path_pattern pattern;
};

v_mount_outer 映射的目的是为不同的命名空间存储不同的挂载点规则集,每个规则集都在它自己的内部映射中。通过这种方式,eBPF 程序可以根据进程的命名空间来应用不同的安全策略。

lsm_file_rules

目前整个项目中很多的函数都应用到了v_file_outer中的函数有:

  • varmor_file_open
  • varmor_path_symlink
  • varmor_path_link
  • varmor_path_rename

varmor_file_open

varmor_file_open对应的函数原型是:

SEC("lsm/file_open")
int BPF_PROG(varmor_file_open, struct file *file)

varmor_file_open 函数是一个用于 Linux 内核 BPF (Berkeley Packet Filter) 程序的 LSM (Linux Security Modules) 钩子。这个函数在文件打开操作发生(调用file_open)时被调用,并执行一系列检查,以确定是否允许这个操作。

主要功能用来实现基于路径的访问控制策略,其中规则可以定义哪些文件路径可以被访问,以及允许的操作类型(例如读、写、创建等)。

test_varmor_file_open

对应的测试项目是:varmor_file_open

接下来,我们通过实际的代码来测试varmor_file_open功能实现。配置如下的规则

var pathRule bpfPathRule
var prefix, suffix [64]byte
copy(prefix[:], "/etc/hostname")
copy(suffix[:], "")

// permissions: 4 for AaMayRead
pathRule.Permissions = 4
// flags: 5 for PreciseMatch | PrefixMatch
pathRule.Pattern.Flags = 5
pathRule.Pattern.Prefix = prefix
pathRule.Pattern.Suffix = suffix

此规则的含义是表示禁止读取/etc/hostname文件,采用的匹配模式前缀匹配和精准匹配(PreciseMatchPrefixMatch)

测试效果:

cat /etc/hostname
cat: /etc/hostname: Operation not permitted

对应的trace_pipe执行结果是:

<...>-403730  [000] d...1 344555.287940: bpf_trace_printk: ================ lsm/file_open ================
<...>-403730  [000] d...1 344555.287947: bpf_trace_printk: path: /etc/hostname
<...>-403730  [000] d...1 344555.287948: bpf_trace_printk: offset: 4082, length: 13
<...>-403730  [000] d...1 344555.287950: bpf_trace_printk: file name: hostname, length: 8
<...>-403730  [000] d...1 344555.287952: bpf_trace_printk: ---- rule id: 0 ----
<...>-403730  [000] d...1 344555.287954: bpf_trace_printk: requested permissions: 0x4, rule permissions: 0x4
<...>-403730  [000] d...1 344555.287958: bpf_trace_printk: old_path_check() - pattern flags: 0x5
<...>-403730  [000] d...1 344555.287960: bpf_trace_printk: old_path_check() - matching path
<...>-403730  [000] d...1 344555.287961: bpf_trace_printk: old_path_check() - pattern prefix: /etc/hostname
<...>-403730  [000] d...1 344555.287962: bpf_trace_printk: 
<...>-403730  [000] d...1 344555.287963: bpf_trace_printk: access denied

通过trace_pipe的输出也可以看出来:

  • 请求的文件是,path: /etc/hostname
  • 请求的文件权限是requested permissions: 0x4,规则对应的校验文件权限是rule permissions: 0x4

完全名中了规则了,所以通过LSM程序就会拒绝执行这个函数,在用户态看到的就是Operation not permitted

varmor_path_symlink对应的函数原型是:

SEC("lsm/path_symlink")
int BPF_PROG(varmor_path_symlink, const struct path *dir, struct dentry *dentry, const char *old_name) 

varmor_path_symlink 函数的作用是在创建新的符号链接之前,检查与当前进程关联的文件访问控制规则,以确定是否允许该操作。

函数内部使用的规则是完全和varmor_file_open使用的规则一样,都是采用的文件规则。

对应的测试项目是: varmor_path_symlink

varmor_path_symlink设置规则:

var pathRule bpfPathRule
var prefix, suffix [64]byte
copy(prefix[:], "/tmp/hostname_link")
copy(suffix[:], "")

pathRule.Permissions = AaMayWrite
// flags: 5 for PreciseMatch | PrefixMatch
pathRule.Pattern.Flags = 5
pathRule.Pattern.Prefix = prefix
pathRule.Pattern.Suffix = suffix

表示禁止创建一个目标链接是/tmp/hostname_link这样的链接。其中的权限需要设置为AaMayWrite(表示禁止创建/写入) 最终实现的效果是:

ln -s /etc/hostname /tmp/hostname_link
ln: failed to create symbolic link '/tmp/hostname_link': Operation not permitted

当创建一个指向/etc/hostname文件的/tmp/hostname_link链接时,创建失败。 对应的trace_pipe结果是:

<...>-406237  [015] d...1 345631.499121: bpf_trace_printk: ================ lsm/path_symlink ================
<...>-406237  [015] d...1 345631.499123: bpf_trace_printk: path: /tmp/hostname_link
<...>-406237  [015] d...1 345631.499123: bpf_trace_printk: offset: 4077, length: 18
<...>-406237  [015] d...1 345631.499124: bpf_trace_printk: file name: hostname_link, length: 13
<...>-406237  [015] d...1 345631.499124: bpf_trace_printk: ---- rule id: 0 ----
<...>-406237  [015] d...1 345631.499125: bpf_trace_printk: requested permissions: 0x2, rule permissions: 0x2
<...>-406237  [015] d...1 345631.499125: bpf_trace_printk: old_path_check() - pattern flags: 0x5
<...>-406237  [015] d...1 345631.499125: bpf_trace_printk: old_path_check() - matching path
<...>-406237  [015] d...1 345631.499126: bpf_trace_printk: old_path_check() - pattern prefix: /tmp/hostname_link
<...>-406237  [015] d...1 345631.499126: bpf_trace_printk: 
<...>-406237  [015] d...1 345631.499126: bpf_trace_printk: access denied

通过trace_pipe的输出也可以看出来

  • 创建的链接文件是,path: /tmp/hostname_link
  • 请求的文件权限是requested permissions: 0x2,规则对应的校验文件权限是rule permissions: 0x2,表示创建权限

完全名中了规则了,所以通过LSM程序就会拒绝执行这个函数,在用户态看到的就是Operation not permitted

varmor_path_link对应的函数原型是:

SEC("lsm/path_link")
int BPF_PROG(varmor_path_link, struct dentry *old_dentry, const struct path *new_dir, struct dentry *new_dentry) 

varmor_path_link函数的目的是在创建硬链接之前进行安全检查,以确保操作符合相关的安全策略。通过检查文件访问控制规则,函数可以决定是否允许创建硬链接。根据后续的程序逻辑,varmor_path_link会同时检查源文件和目标文件。

规则使用方式还是采用的文件规则,同时因为是同时检测源文件和目标文件,所以就有设置两条规则。

对应的测试项目是: varmor_path_link

针对源文件的规则

varmor_path_link设置一条源文件的规则:

var pathRule bpfPathRule
var prefix, suffix [64]byte
copy(prefix[:], "/etc/hostname")
copy(suffix[:], "")

pathRule.Permissions = AaMayRead
// flags: 5 for PreciseMatch | PrefixMatch
pathRule.Pattern.Flags = 5
pathRule.Pattern.Prefix = prefix
pathRule.Pattern.Suffix = suffix

注意,因为源文件只需要读取权限,所以设置为AaMayRead 最终实现的测试效果是:

sudo ln  /etc/hostname /tmp/hostname_hard_link
ln: failed to create hard link '/tmp/hostname_hard_link' => '/etc/hostname': Operation not permitted

通过trace_pipe查看到的结果是:

<...>-411304  [008] d...1 347878.275643: bpf_trace_printk: ================ lsm/path_link ================
<...>-411304  [008] d...1 347878.275645: bpf_trace_printk: old path: /etc/hostname
<...>-411304  [008] d...1 347878.275646: bpf_trace_printk: offset: 4082, length: 13
<...>-411304  [008] d...1 347878.275646: bpf_trace_printk: file name: hostname, length: 8
<...>-411304  [008] d...1 347878.275647: bpf_trace_printk: new path: /tmp/hostname_hard_link
<...>-411304  [008] d...1 347878.275647: bpf_trace_printk: offset: 8168, length: 23
<...>-411304  [008] d...1 347878.275647: bpf_trace_printk: file name: hostname_hard_link, length: 18
<...>-411304  [008] d...1 347878.275648: bpf_trace_printk: ---- rule id: 0 ----
<...>-411304  [008] d...1 347878.275649: bpf_trace_printk: requested permissions: 0x40000, rule permissions: 0x4
<...>-411304  [008] d...1 347878.275649: bpf_trace_printk: old_path_check() - pattern flags: 0x5
<...>-411304  [008] d...1 347878.275649: bpf_trace_printk: old_path_check() - matching path
<...>-411304  [008] d...1 347878.275649: bpf_trace_printk: old_path_check() - pattern prefix: /etc/hostname
<...>-411304  [008] d...1 347878.275650: bpf_trace_printk: 
<...>-411304  [008] d...1 347878.275650: bpf_trace_printk: access denied

匹配方法和前面类似。通过trace_pipe的输出也可以看出来

  • 创建硬链接的源文件是old path: /etc/hostname,目标文件是new path: /tmp/hostname_hard_link
  • 请求的文件权限是requested permissions: 0x2,规则对应的校验文件权限是rule permissions: 0x2,表示创建权限,所以发现创建硬链接中的源文件就直接命中了规则

因为源文件直接名中了规则,所以就不需要进一步检测目标文件,直接拒绝本次操作。

针对目标文件的规则

创建一条针对/tmp/hostname_hard_link的目标文件的规则

var pathRule bpfPathRule
var prefix, suffix [64]byte
copy(prefix[:], "/tmp/hostname_hard_link")
copy(suffix[:], "")

pathRule.Permissions = AaMayWrite
// flags: 5 for PreciseMatch | PrefixMatch
pathRule.Pattern.Flags = 5
pathRule.Pattern.Prefix = prefix
pathRule.Pattern.Suffix = suffix

注意,因为目标文件只需要写入权限,所以设置为AaMayWrite 最终实现的测试效果是:

sudo ln  /etc/hostname /tmp/hostname_hard_link
ln: failed to create hard link '/tmp/hostname_hard_link' => '/etc/hostname': Operation not permitted

通过trace_pipe查看到的结果是:

<...>-412937  [012] d...1 348731.366822: bpf_trace_printk: ================ lsm/path_link ================
<...>-412937  [012] d...1 348731.366825: bpf_trace_printk: old path: /etc/hostname
<...>-412937  [012] d...1 348731.366825: bpf_trace_printk: offset: 4082, length: 13
<...>-412937  [012] d...1 348731.366826: bpf_trace_printk: file name: hostname, length: 8
<...>-412937  [012] d...1 348731.366826: bpf_trace_printk: new path: /tmp/hostname_hard_link
<...>-412937  [012] d...1 348731.366826: bpf_trace_printk: offset: 8168, length: 23
<...>-412937  [012] d...1 348731.366827: bpf_trace_printk: file name: hostname_hard_link, length: 18
<...>-412937  [012] d...1 348731.366827: bpf_trace_printk: ---- rule id: 0 ----
<...>-412937  [012] d...1 348731.366828: bpf_trace_printk: requested permissions: 0x40000, rule permissions: 0x2
<...>-412937  [012] d...1 348731.366828: bpf_trace_printk: new_path_check() - pattern flags: 0x5
<...>-412937  [012] d...1 348731.366828: bpf_trace_printk: new_path_check() - matching path
<...>-412937  [012] d...1 348731.366829: bpf_trace_printk: new_path_check() - pattern prefix: /tmp/hostname_hard_link
<...>-412937  [012] d...1 348731.366829: bpf_trace_printk: 
<...>-412937  [012] d...1 348731.366829: bpf_trace_printk: access denied

分析方法和上面的源文件匹配类似,就不在做分析了。

varmor_path_rename

varmor_path_rename对应的函数原型是:

SEC("lsm/path_rename")
int BPF_PROG(varmor_path_rename, const struct path *old_dir, struct dentry *old_dentry, const struct path *new_dir, struct dentry *new_dentry, const unsigned int flags) 

varmor_path_rename是在文件重命名时时进行安全检查,会检查重命名的源文件和目标文件。

Linux中,重命名文件一般都是采用mv方式实现。

test_varmor_path_rename

对应的测试项目是: varmor_path_rename

针对源文件的规则

针对重命名的源文件设置规则。设置的路径是/home/spoock/varmor_path_rename_source,如果有人需要在本机测试,那么就根据实际的路径自行修改。

var pathRule bpfPathRule
var prefix, suffix [64]byte
copy(prefix[:], "/home/spoock/varmor_path_rename_source")
copy(suffix[:], "")

pathRule.Permissions = AaMayRead
// flags: 5 for PreciseMatch | PrefixMatch
pathRule.Pattern.Flags = 5
pathRule.Pattern.Prefix = prefix
pathRule.Pattern.Suffix = suffix

因为是针对源文件的路径设置,所以只需要设置为AaMayRead(即读取权限就可以了)

实际测试效果如下所示:

$ cat varmor_path_rename_source             
source

$ mv varmor_path_rename_source varmor_path_rename_sink        
mv: cannot move 'varmor_path_rename_source' to 'varmor_path_rename_sink': Operation not permitted

源文件varmor_path_rename_source存在文件内容是source,当通过mv重命名时就出现了Operation not permitted错误。

通过trace_pipe查看到的结果是:

<...>-417941  [015] d...1 349857.745819: bpf_trace_printk: ================ lsm/path_rename ================
<...>-417941  [015] d...1 349857.745831: bpf_trace_printk: old path: /home/spoock/varmor_path_rename_source
<...>-417941  [015] d...1 349857.745835: bpf_trace_printk: offset: 4057, length: 38
<...>-417941  [015] d...1 349857.745837: bpf_trace_printk: file name: varmor_path_rename_source, length: 25
<...>-417941  [015] d...1 349857.745839: bpf_trace_printk: new path: /home/spoock/varmor_path_rename_sink
<...>-417941  [015] d...1 349857.745840: bpf_trace_printk: offset: 8155, length: 36
<...>-417941  [015] d...1 349857.745842: bpf_trace_printk: file name: varmor_path_rename_sink, length: 23
<...>-417941  [015] d...1 349857.745845: bpf_trace_printk: ---- rule id: 0 ----
<...>-417941  [015] d...1 349857.745848: bpf_trace_printk: requested permissions: 0x80, rule permissions: 0x4
<...>-417941  [015] d...1 349857.745849: bpf_trace_printk: old_path_check() - pattern flags: 0x5
<...>-417941  [015] d...1 349857.745851: bpf_trace_printk: old_path_check() - matching path
<...>-417941  [015] d...1 349857.745853: bpf_trace_printk: old_path_check() - pattern prefix: /home/spoock/varmor_path_rename_source
<...>-417941  [015] d...1 349857.745855: bpf_trace_printk: 
<...>-417941  [015] d...1 349857.745855: bpf_trace_printk: access denied

通过trace_pipe的日志可以看出来:

  • 重命名的源文件是old path: /home/spoock/varmor_path_rename_source,目标文件是new path: /home/spoock/varmor_path_rename_sink
  • 因为是针对源文件的分析判断,所以就需要判断规则是否是0x4rule permissions: 0x4满足条件

针对目标文件的规则

var pathRule bpfPathRule
var prefix, suffix [64]byte
copy(prefix[:], "/home/spoock/varmor_path_rename_sink")
copy(suffix[:], "")

pathRule.Permissions = AaMayWrite
// flags: 5 for PreciseMatch | PrefixMatch
pathRule.Pattern.Flags = 5
pathRule.Pattern.Prefix = prefix
pathRule.Pattern.Suffix = suffix

针对目标本机文件/home/spoock/varmor_path_rename_sink设置作为目标规则文件,同时需要将权限设置为AaMayWrite。 表示禁止针对/home/spoock/varmor_path_rename_sink文件的写入。

测试结果如下:

$ ll varmor_path_rename_sink
ls: cannot access 'varmor_path_rename_sink': No such file or directory

$ mv varmor_path_rename_source varmor_path_rename_sink
mv: cannot move 'varmor_path_rename_source' to 'varmor_path_rename_sink': Operation not permitted

说明之前不存在varmor_path_rename_sink文件,执行mv重命名时体现没有权限。 通过trace_pipe查看到的结果是:

mv-419513  [007] d...1 350650.158435: bpf_trace_printk: ================ lsm/path_rename ================
mv-419513  [007] d...1 350650.158447: bpf_trace_printk: old path: /home/spoock/varmor_path_rename_source
mv-419513  [007] d...1 350650.158450: bpf_trace_printk: offset: 4057, length: 38
mv-419513  [007] d...1 350650.158453: bpf_trace_printk: file name: varmor_path_rename_source, length: 25
mv-419513  [007] d...1 350650.158454: bpf_trace_printk: new path: /home/spoock/varmor_path_rename_sink
mv-419513  [007] d...1 350650.158456: bpf_trace_printk: offset: 8155, length: 36
mv-419513  [007] d...1 350650.158458: bpf_trace_printk: file name: varmor_path_rename_sink, length: 23
mv-419513  [007] d...1 350650.158461: bpf_trace_printk: ---- rule id: 0 ----
mv-419513  [007] d...1 350650.158463: bpf_trace_printk: requested permissions: 0x80, rule permissions: 0x2
mv-419513  [007] d...1 350650.158466: bpf_trace_printk: new_path_check() - pattern flags: 0x5
mv-419513  [007] d...1 350650.158472: bpf_trace_printk: new_path_check() - matching path
mv-419513  [007] d...1 350650.158475: bpf_trace_printk: new_path_check() - pattern prefix: /home/spoock/varmor_path_rename_sink
mv-419513  [007] d...1 350650.158476: bpf_trace_printk: 
mv-419513  [007] d...1 350650.158478: bpf_trace_printk: access denied

通过trace_pipe的日志可以看出来:

  • 重命名的源文件是old path: /home/spoock/varmor_path_rename_source,目标文件是new path: /home/spoock/varmor_path_rename_sink
  • 因为是针对目标文件的分析判断,所以就需要判断规则是否是0x4rule permissions: 0x2满足条件

lsm_net_rules

目前应用网络规则的就只有varmor_socket_connect

varmor_socket_connect

函数原型是:

SEC("lsm/socket_connect")
int BPF_PROG(varmor_socket_connect, struct socket *sock, struct sockaddr *address, int addrlen)

v_net_outer映射是一个索引结构,用来存储和查找网络规则映射。每个网络规则映射(内部映射)包含了特定的网络访问控制规则,这些规则可以基于源地址、目的地址、端口等进行匹配。这个结构允许针对不同的命名空间或容器应用不同的网络访问策略。 所有在Linux系统中存在大量和网络相关的行为,比如常见的curlwget,以及DNS相关的请求,通过这个程序基本上都可以控制的。

test_varmor_socket_connect

对应的测试项目是: varmor_socket_connect

为了方便测试,使用1.1.1.1作为目标IP,不限制任何的端口。

var rule bpfNetworkRule
rule.Port = 443
rule.Flags |= 0x00000001
ip := net.ParseIP("1.1.1.1")
if ip.To4() != nil {
    copy(rule.Address[:], ip.To4())
} else {
    copy(rule.Address[:], ip.To16())
}
var index uint32 = uint32(0)
err = innerMap.Put(&index, &rule)

测试结果

ping 1.1.1.1
ping: connect: Operation not permitted

访问1.1.1.1的行为成功被阻止了,说明规则生效了。

通过trace_pipe查看到的结果是:

<...>-430900  [004] d...1 358915.113391: bpf_trace_printk: ================ lsm/socket_connect ================
<...>-430900  [004] d...1 358915.113402: bpf_trace_printk: socket status: 0x1
<...>-430900  [004] d...1 358915.113404: bpf_trace_printk: socket type: 0x2
<...>-430900  [004] d...1 358915.113405: bpf_trace_printk: socket flags: 0x0
<...>-430900  [004] d...1 358915.113409: bpf_trace_printk: ---- rule id: 0 ----
<...>-430900  [004] d...1 358915.113411: bpf_trace_printk: IPv4 address: 1010101
<...>-430900  [004] d...1 358915.113412: bpf_trace_printk: IPv4 port: 104
<...>-430900  [004] d...1 358915.113413: bpf_trace_printk: 
<...>-430900  [004] d...1 358915.113414: bpf_trace_printk: access denied

ebpf程检测到的IP地址是0x1010101,端口号是0x104。通过代码转换为字节序:

c = ntohs(0x104);
printf("主机字节序端口号:%hu\n", c);

uint32_t addr = htonl(0x1010101);  // Convert to network byte order
struct in_addr in_addr;
in_addr.s_addr = addr;

char str[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &in_addr, str, INET_ADDRSTRLEN) == NULL) {
    perror("inet_ntop");
    return 1;
}

printf("The address is: %s\n", str);

得到的端口号是1025,IP地址是1.1.1.1

因为我们设置的匹配模式是只匹配IP地址,忽略端口号,所以就成功命中了规则。

lsm_bprm_rules

varmor_bprm_check_security

函数原型是:

SEC("lsm/bprm_check_security")
int BPF_PROG(varmor_bprm_check_security, struct linux_binprm *bprm, int ret)

varmor_bprm_check_security 函数是一个用于执行二进制程序执行前安全检查的 eBPF 程序,主要是通过检查二进制的文件路进来限制。比如可以禁止常见的curlwget命令从互联网上下载文件。

test_varmor_bprm_check_security

对应的测试项目是:varmor_bprm_check_security

对于二进制程序来说,可能会存在多个路径,所以很多时候并不能像文件一样只限制一个路径,比如常见的curl就存在/usr/bin/curl/bin/curl。所以针对二进制文件的限制,最好是采用后缀限制方式。这种方式和前面的几种方式的设置方法存在一些微小的差别。

var ExecpathRule bpfPathRule
var execPrefix, execSuffix [64]byte
copy(execPrefix[:], "")
copy(execSuffix[:], "lruc/")

ExecpathRule.Permissions = AaMayExec
ExecpathRule.Pattern.Flags = SuffixMatch | GreedyMatch
ExecpathRule.Pattern.Prefix = execPrefix
ExecpathRule.Pattern.Suffix = execSuffix
  1. 后缀匹配的特殊模式,需要将curl反转成为lruc/
  2. 权限Permissions因为是二进制的执行权限相关,所以设置为AaMayExec
  3. 匹配模式方便,因为我们本意就是为了匹配所有的curl对应的二进制程序,所以采用后缀匹配模式SuffixMatch因为不管具体的curl路径,所以还需要加上贪婪模式GreedyMatch

最终实现的效果是:

$ curl blog.spoock.com
zsh: operation not permitted: curl

说明curl的二进制程序成功被阻止了。

通过trace_pipe查看到的结果是:

<...>-146311  [001] d...1 73614.965331: bpf_trace_printk: ================ lsm/bprm_check_security ================
<...>-146311  [001] d...1 73614.965343: bpf_trace_printk: path: /usr/bin/curl
<...>-146311  [001] d...1 73614.965346: bpf_trace_printk: offset: 14, length: 13
<...>-146311  [001] d...1 73614.965347: bpf_trace_printk: file name: curl, length: 4
<...>-146311  [001] d...1 73614.965350: bpf_trace_printk: ---- rule id: 0 ----
<...>-146311  [001] d...1 73614.965351: bpf_trace_printk: rule permissions: 0x1
<...>-146311  [001] d...1 73614.965352: bpf_trace_printk: head_path_check() - pattern flags: 0xa
<...>-146311  [001] d...1 73614.965357: bpf_trace_printk: head_path_check() - matching path
<...>-146311  [001] d...1 73614.965359: bpf_trace_printk: head_path_check() - pattern suffix: lruc/
<...>-146311  [001] d...1 73614.965360: bpf_trace_printk: 
<...>-146311  [001] d...1 73614.965361: bpf_trace_printk: access denied
<...>-146311  [001] d...1 73614.965575: bpf_trace_printk: ================ lsm/bprm_check_security ================
<...>-146311  [001] d...1 73614.965582: bpf_trace_printk: path: /bin/curl
<...>-146311  [001] d...1 73614.965584: bpf_trace_printk: offset: 10, length: 9
<...>-146311  [001] d...1 73614.965585: bpf_trace_printk: file name: curl, length: 4
<...>-146311  [001] d...1 73614.965587: bpf_trace_printk: ---- rule id: 0 ----
<...>-146311  [001] d...1 73614.965588: bpf_trace_printk: rule permissions: 0x1
<...>-146311  [001] d...1 73614.965589: bpf_trace_printk: head_path_check() - pattern flags: 0xa
<...>-146311  [001] d...1 73614.965590: bpf_trace_printk: head_path_check() - matching path
<...>-146311  [001] d...1 73614.965592: bpf_trace_printk: head_path_check() - pattern suffix: lruc/
<...>-146311  [001] d...1 73614.965595: bpf_trace_printk: 
<...>-146311  [001] d...1 73614.965597: bpf_trace_printk: access denied

通过日志可以看到,内核是组织了来自两个路径的curl命令,分别是/bin/curl/usr/bin/curl。 通过测试,发现在当前系统中确实是存在/usr/bin/curl/bin/curl这两种路径

$ /bin/curl              
curl: try 'curl --help' or 'curl --manual' for more information

$ /usr/bin/curl          
curl: try 'curl --help' or 'curl --manual' for more information
test_varmor_bprm_check_security_2

上面是采用后缀贪婪匹配的模式成功阻止了curl进程的执行。如果采用和前面文件匹配一样的模式,仅仅只是禁止/usr/bin/curl的执行,观察curl命令是否可以成功执行。

重新修改规则

var ExecpathRule bpfPathRule
var execPrefix, execSuffix [64]byte
copy(execPrefix[:], "/usr/bin/curl")
copy(execSuffix[:], "")

// permissions: 4 for AaMayRead
ExecpathRule.Permissions = AaMayExec
ExecpathRule.Pattern.Flags = PreciseMatch | PrefixMatch
ExecpathRule.Pattern.Prefix = execPrefix
ExecpathRule.Pattern.Suffix = execSuffix

因为我们目标仅仅只是限制/usr/bin/curl的执行,所以最终匹配模式采用的是PreciseMatch | PrefixMatch,即前缀精确匹配。 最终执行命令的结果如下:

$ which curl
/usr/bin/curl

$ curl blog.spoock.com
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.4.6 (Ubuntu)</center>
</body>
</html>

发现curl blog.spoock.com执行成功。

通过查看到的结果是:

<...>-148170  [009] d...1 74767.657291: bpf_trace_printk: ================ lsm/bprm_check_security ================
<...>-148170  [009] d...1 74767.657299: bpf_trace_printk: path: /usr/bin/curl
<...>-148170  [009] d...1 74767.657302: bpf_trace_printk: offset: 14, length: 13
<...>-148170  [009] d...1 74767.657304: bpf_trace_printk: file name: curl, length: 4
<...>-148170  [009] d...1 74767.657306: bpf_trace_printk: ---- rule id: 0 ----
<...>-148170  [009] d...1 74767.657307: bpf_trace_printk: rule permissions: 0x1
<...>-148170  [009] d...1 74767.657309: bpf_trace_printk: head_path_check() - pattern flags: 0x5
<...>-148170  [009] d...1 74767.657310: bpf_trace_printk: head_path_check() - matching path
<...>-148170  [009] d...1 74767.657312: bpf_trace_printk: head_path_check() - pattern prefix: /usr/bin/curl
<...>-148170  [009] d...1 74767.657313: bpf_trace_printk: 
<...>-148170  [009] d...1 74767.657314: bpf_trace_printk: access denied
<...>-148170  [009] d...1 74767.657505: bpf_trace_printk: ================ lsm/bprm_check_security ================
<...>-148170  [009] d...1 74767.657511: bpf_trace_printk: path: /bin/curl
<...>-148170  [009] d...1 74767.657514: bpf_trace_printk: offset: 10, length: 9
<...>-148170  [009] d...1 74767.657515: bpf_trace_printk: file name: curl, length: 4
<...>-148170  [009] d...1 74767.657517: bpf_trace_printk: ---- rule id: 0 ----
<...>-148170  [009] d...1 74767.657518: bpf_trace_printk: rule permissions: 0x1
<...>-148170  [009] d...1 74767.657519: bpf_trace_printk: head_path_check() - pattern flags: 0x5
<...>-148170  [009] d...1 74767.657520: bpf_trace_printk: head_path_check() - matching path
<...>-148170  [009] d...1 74767.657522: bpf_trace_printk: head_path_check() - pattern prefix: /usr/bin/curl
<...>-148170  [009] d...1 74767.657523: bpf_trace_printk: 
<...>-148170  [009] d...1 74767.657524: bpf_trace_printk: access allowed

通过日志可以看出来

  1. /usr/bin/curl成功匹配被禁止了
  2. /bin/curl因为采用的是精确匹配/usr/bin/curl的模式,所以还是成功被绕过然后执行了

这就是说明不能仅仅通过前面文件那中前缀精确匹配的模式禁止一个二进制程序执行,因为可能会存在二进程程序可能会存在多个文件路径,很多时候也很难找到一个二进程程序对应的所有路径。

所以最好的针对二进制程序的方法最好还是采用后缀贪婪匹配模式。

lsm_mount_rules

在Linux系统中存在多个和mount有关的函数,所以就会存在多个与之相关的权限校验的函数,包括mountunmount等等。

varmor_mount

varmor_mount对应的函数原型是:

SEC("lsm/sb_mount")
int BPF_PROG(varmor_mount, char *dev_name, struct path *path, char *type, unsigned long flags, void *data) 

varmor_mount 函数在 LSM 挂载钩子中被调用时,它会首先检查当前任务是否有与之关联的挂载规则。如果有,它会使用 head_path_checkmount_fstype_check 函数来检查挂载请求是否符合这些规则。如果请求违反了任何规则,挂载操作将被拒绝,并返回 -EPERM 错误码。如果没有违反规则,挂载操作被允许。

所以权限设置会涉及到文件类型和对应的挂载目录。

varmor_mount_test

对应的测试项目是: varmor_mount 接下来就是针对varmor_mount的规则测试,为了便于测试,我们利用系统中自带的规则进行尝试。

newBpfMountRule("**", "proc", 0xFFFFFFFF&^AaMayUmount, 0xFFFFFFFF)
newBpfMountRule("/proc**", "*", unix.MS_BIND|unix.MS_REC|unix.MS_REMOUNT|unix.MS_MOVE|AaMayUmount, 0)

这两条规则的含义是禁止任何形式的/proc挂载。

原规则中使用的none可能会存在一些问题,相关的issue问题,可以参考 MountRule fstype misconfiguration。在最新的代码中,作者也已经修复了这个问题 Optimize mount access control primitives。如果要测试,可以更新到最新代码继续测试。

将上面两条规则成为代码就是如下格式:

outerMountMap, ok := coll.Maps["v_mount_outer"]
if !ok {
    log.Fatalf("outerMountMap not found in collection")
}

map_name := fmt.Sprintf("v_mount_inner_%d", nsID)
innerMapSpec := ebpf.MapSpec{
    Name:       map_name,
    Type:       ebpf.Hash,
    KeySize:    4,
    ValueSize:  4*3 + 16 + 64*2,
    MaxEntries: MAX_MOUNT_INNER_ENTRIES,
}

innerMountap, err := ebpf.NewMap(&innerMapSpec)
if err != nil {
    log.Fatalf("failed to create inner map: %v", err)
    return
}
defer innerMountap.Close()

var mountRule bpfMountRule
// 禁止挂载proc类型FsType
var prefix, suffix [64]byte
copy(prefix[:], "")
copy(suffix[:], "")

mountFlags := 0xFFFFFFFF &^ AaMayUmount
mountRule.Flags = GREEDY_MATCH
mountRule.MountFlags = uint32(mountFlags)
mountRule.ReverseMountFlags = 0xFFFFFFFF

var s [16]byte
copy(s[:], "proc")
mountRule.FsType = s

var index uint32 = 0
err = innerMountap.Put(&index, &mountRule)
if err != nil {
    fmt.Println("failed to put mount rule:", err)
    return
}
outerMountMap.Put(uint32(nsID), innerMountap)

// 禁止挂载/proc及其所有子目录
copy(prefix[:], "/proc")
copy(suffix[:], "")
mountRule.Prefix = prefix

mountFlags = unix.MS_BIND | unix.MS_REC | unix.MS_REMOUNT | unix.MS_MOVE | AaMayUmount
mountRule.Flags = GREEDY_MATCH | PREFIX_MATCH
mountRule.MountFlags = uint32(mountFlags)
mountRule.ReverseMountFlags = 0

copy(s[:], "*")
mountRule.FsType = s

var idx uint32 = 1
err = innerMountap.Put(&idx, &mountRule)
if err != nil {
    fmt.Println("failed to put mount rule:", err)
    return
}
outerMountMap.Put(uint32(nsID), innerMountap)

测试结果 在默认情况下使用docker挂载,成功挂载了宿主机目录。

$ docker run -v /proc:/host-proc:ro -it ubuntu  /bin/bash

root@10ed2617b101:/# 

加上了挂载命令限制之后,执行挂载命令如下所示:

$ docker run -v /proc:/host-proc:ro -it ubuntu  /bin/bash

docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error running hook #0: error running hook: exit status 1, stdout: , stderr: operation not permitted: unknown.
ERRO[0000] error waiting for container: 

通过trace_pipe查看到的结果是:

bpf_trace_printk: ================ lsm/sb_mount ================
bpf_trace_printk: dev path: /proc/self/exe
bpf_trace_printk: offset: 15, length: 14
bpf_trace_printk: dev name: exe, length: 3
bpf_trace_printk: fstype: 
bpf_trace_printk: flags: 4096
bpf_trace_printk: ---- rule id: 0 ----
bpf_trace_printk: rule mount_flags: 0xfffffdff, reverse_mount_flags: 0xffffffff
bpf_trace_printk: rule fstype: proc
bpf_trace_printk: mount_fstype_check()
bpf_trace_printk: ---- rule id: 1 ----
bpf_trace_printk: rule mount_flags: 0x7220, reverse_mount_flags: 0x0
bpf_trace_printk: rule fstype: *roc
bpf_trace_printk: mount_fstype_check()
bpf_trace_printk: head_path_check() - pattern flags: 0x6
bpf_trace_printk: head_path_check() - matching path
bpf_trace_printk: head_path_check() - pattern prefix: /proc
bpf_trace_printk: 
bpf_trace_printk: access denied

因为挂载的宿主机路径是/proc/self/exe命中了规则1,内核拒绝了这个操作,所以在用户态执行失败,提示operation not permitted

varmor_umount

varmor_umount对应的函数原型是:

SEC("lsm/sb_umount")
int BPF_PROG(varmor_umount, struct vfsmount *mnt, int flags)

varmor_mount相反,主要是针对用来执行卸载文件系统操作的安全检查

varmor_umount_test

对应的测试项目是: varmor_umount

由于系统中并没有和umount相关的默认规则,所以我们自行测试一条规则。规则的主要目标就是禁止针对/mnt/mountpoint相关的挂载。如下所示:

var mountRule bpfMountRule
// 禁止挂载proc类型FsType
var prefix, suffix [64]byte
var s [16]byte
// 禁止挂载/mnt/mountpoint及其所有子目录
copy(prefix[:], "/mnt/mountpoint")
copy(suffix[:], "")
mountRule.Prefix = prefix

mountFlags := unix.MS_BIND | unix.MS_REC | unix.MS_REMOUNT | unix.MS_MOVE | AaMayUmount
mountRule.Flags = GREEDY_MATCH | PREFIX_MATCH
mountRule.MountFlags = uint32(mountFlags)
mountRule.ReverseMountFlags = 0

copy(s[:], "none")
mountRule.FsType = s

var idx uint32 = 0
err = innerMountap.Put(&idx, &mountRule)
if err != nil {
    fmt.Println("failed to put mount rule:", err)
    return
}
outerMountMap.Put(uint32(nsID), innerMountap)

设置的前缀路径是:`/mnt/mountpoint

对应的匹配规则是贪婪前缀匹配(GREEDY_MATCH | PREFIX_MATCH),表示如果执行umount操作的路径的前缀是/mnt/mountpoint就会被阻止。

测试命令如下:

$ sudo mount --bind /mnt/mountpoint1 /mnt/mountpoint2

$ sudo umount /mnt/mountpoint2                       
umount: /mnt/mountpoint2: must be superuser to unmount.

可以看到当执行umount操作时,提示umount: /mnt/mountpoint2: must be superuser to unmount,说明已经被阻止了。 通过trace_pipe查看到的结果是:

bpf_trace_printk: ================ lsm/move_mount ================
bpf_trace_printk: umount path: /mnt/mountpoint2, length: 16, umount path offset: 4079
bpf_trace_printk: umount name: mountpoint2, length: 11
bpf_trace_printk: mock fstype: none
bpf_trace_printk: mock flags: 512
bpf_trace_printk: ---- rule id: 0 ----
bpf_trace_printk: rule mount_flags: 0x7220, reverse_mount_flags: 0x0
bpf_trace_printk: rule fstype: none
bpf_trace_printk: mount_fstype_check()
bpf_trace_printk: old_path_check() - pattern flags: 0x6
bpf_trace_printk: old_path_check() - matching path
bpf_trace_printk: old_path_check() - pattern prefix: /mnt/mountpoint
bpf_trace_printk: 
bpf_trace_printk: access denied

通过trace_pipe分析整个匹配过程:

  1. mount_fstype_check()之后成功执行,说明fstype校验成功通过
  2. 执行umount操作的路径是/mnt/mountpoint2,名中了配置的规则/mnt/mountpoint

基于以上分析,umount被阻止的行为就可以理解了。

lsm_cap_rules

varmor_capable

varmor_capable程序原型是:

SEC("lsm/capable")
int BPF_PROG(varmor_capable, const struct cred *cred, struct user_namespace *ns, int cap, unsigned int opts, int ret)

LSM的 capable 钩子通常用于检查进程是否有权使用特定的能力(capability)。这个功能一般是用来限制某些容器具有过大的权限,在宿主机上反而使用淂不多。但是为了方便,后续我们的测试还是在宿主机上测试。

bpf.go 就存在各种针对CAP设置的规则。

varmor_capable_test

对应的测试项目是:varmor_capable 为了方便,仅仅只是测试unix.CAP_NET_RAW网络相关的封紧能力,因为其他的功能基本上都是一样的。

规则配置

var Capabilities uint64
// CAP_NET_RAW 网络相关的能力
Capabilities |= 1 << unix.CAP_NET_RAW
CapMap, ok := coll.Maps["v_capable"]
if !ok {
    log.Fatalf("CapMap not found in collection")
}
mntNsID := uint32(nsID)
CapMap.Put(&mntNsID, &Capabilities)

向比较其他类型的规则配置,v_capable能力配置就非常的简单。因为所有的规则其实对应的就是数字。

为了测试这个规则,我们需要编写一个创建原始套接字的代码,因为创建socket都需要CAP_NET_RAW的权限。

// 创建一个原始套接字
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
    fmt.Fprintf(os.Stderr, "Error creating socket: %v\n", err)
    os.Exit(1)
}

// 将文件描述符转换为net.FileConn以便使用Go的net包进行操作
conn, err := net.FileConn(os.NewFile(uintptr(fd), "rawsocket"))
if err != nil {
    fmt.Fprintf(os.Stderr, "Error wrapping socket: %v\n", err)
    syscall.Close(fd)
    os.Exit(1)
}
defer conn.Close()

fmt.Println("Raw socket created successfully")

上面就是一个简单的使用syscall.Socket创建socket的代码,需要需要程序成功运行,必须需要需要CAP_NET_RAW权限或者是以root的身份运行。

在本测试用例中,我们为其增加CAP_NET_RAW权限。

将上面的代码编译得到testnetcap的二进制程序

$ getcap testnetcap

$ sudo setcap cap_net_raw+ep testnetcap 

$ getcap testnetcap                    
testnetcap = cap_net_raw+ep

在这里,cap_net_raw+ep 指定了要赋予的能力 (cap_net_raw) 和两个标志 (ep):

  • e (effective):这意味着能力是激活的。
  • p (permitted):这意味着程序被允许使用这个能力。

在没有启动我们的测试运行上,运行:

$ ./testnetcap
Raw socket created successfully

testnetcap 程序成功运行,开启eBBPF程序之后:

$ ./testnetcap
Error creating socket: operation not permitted

testnetcap程序被阻止了,查看eBPF日志。通过trace_pipe查看到的结果是:

bpf_trace_printk: task(mnt ns: 4026531841) current_effective_mask: 0x2000, request_cap_mask: 0x2000
bpf_trace_printk: task(mnt ns: 4026531841) is not allowed to use capability: 0xd

其中0xd就是和unix.CAP_NET_RAW对应,十六进制0x2000对应的十进制就是8192,与1 << unix.CAP_NET_RAW结果是一致的。因为名中了规则,所以就会拒绝本次的行为,返回就是operation not permitted

总结

通过varmorebpfloader项目很方便针对各个功能进行测试,当然在测试过程中也加深了vArmor-ebpf 项目的理解。总的来说,vArmor-ebpf 是一个很好的学习项目,尤其是开发者响应速度也非常快。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3104/


文章来源: https://paper.seebug.org/3104/
如有侵权请联系:admin#unsafe.sh