作者: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程序。
由于整个vAmror
项目非常庞大,尤其是涉及到helm
,k8s
各种配置和部署,给个人开发环境运行调试造成了极大的调整。如果需要单独针对 vArmor-ebpf 项目中的功能测试分析和学习,十分地困难和不方便。为了方便单独针对项目分析,于是就单独创建了这个项目。一方面是为了了解vArmor
项目的规则设置,另一方面是为了方便调试测试 vArmor-ebpf 中的规则。
当然也可以使用vArmor-ebpf 中的自带的测试用例enforcer_test.go测试。
为了方便测试,项目需要先将 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
的调试日志才会打印出来。
通过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)
}
在原先的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_outer
,v_mount_outer
,v_file_outer
,v_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()
由于规则都是嵌套的,所以我们在进行规则设定的时候,也需要设置inner
和outer
。inner
设置为ebpf.Hash
的方式。
在整个 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
的例子。
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()
其中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
在本例中是设置的文件相关的规则。和文件有关的规则就涉及到需要匹配的路径,对应文件的操作方式(读取还是写入,追加),以及匹配模式。下面针对这几条分别进行讲解。
copy(prefix[:], "/etc/hostname")
,表示规则路径是/etc/hostname
,是在前缀位置设置的,说明我们的规则是和前缀相关的规则 pathRule.Permissions = 4
,表示规则检测的文件的读取。所以如果发现有读取/etc/hostname
文件的行为就会终止 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)
通过innerFileMap
和outerFileMap
加载上面设置的规则。
$ 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
行为。
在varmor
中虽然使用了lsm
拦截了很多函数,但是并没有针对每一个拦截函数都创建了一个对应的规则。在varmor
中仅仅只是存在四种规则。分别是:
v_file_outer
v_bprm_outer
v_net_outer
v_mount_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
规则,这些规则定义了哪些文件路径可以访问以及访问权限。这种设计使得可以为不同的命名空间设置不同的访问控制策略,以此来增强系统的安全性。
网络相关的规则定义,在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
映射是一个索引结构,用来存储和查找网络规则映射。每个网络规则映射(内部映射)包含了特定的网络访问控制规则,这些规则可以基于源地址、目的地址、端口等进行匹配。这个结构允许针对不同的命名空间或容器应用不同的网络访问策略。
进程限制相关的规则定义,在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
映射是一个索引结构,用于存储和查找与进程执行权限相关的规则映射(内部映射)。每个内部映射包含了特定的文件路径访问控制规则,这些规则可以基于文件路径模式匹配等进行访问控制。
挂载限制相关的规则定义,在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 程序可以根据进程的命名空间来应用不同的安全策略。
目前整个项目中很多的函数都应用到了v_file_outer
中的函数有:
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
)时被调用,并执行一系列检查,以确定是否允许这个操作。
主要功能用来实现基于路径的访问控制策略,其中规则可以定义哪些文件路径可以被访问,以及允许的操作类型(例如读、写、创建等)。
对应的测试项目是: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
文件,采用的匹配模式前缀匹配和精准匹配(PreciseMatch
,PrefixMatch
)
测试效果:
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
对应的函数原型是:
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
方式实现。
对应的测试项目是: 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
0x4
,rule 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
0x4
,rule permissions: 0x2
满足条件 目前应用网络规则的就只有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
系统中存在大量和网络相关的行为,比如常见的curl
和wget
,以及DNS相关的请求,通过这个程序基本上都可以控制的。
对应的测试项目是: 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地址,忽略端口号,所以就成功命中了规则。
函数原型是:
SEC("lsm/bprm_check_security")
int BPF_PROG(varmor_bprm_check_security, struct linux_binprm *bprm, int ret)
varmor_bprm_check_security
函数是一个用于执行二进制程序执行前安全检查的 eBPF 程序,主要是通过检查二进制的文件路进来限制。比如可以禁止常见的curl
和wget
命令从互联网上下载文件。
对应的测试项目是: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
curl
反转成为lruc/
Permissions
因为是二进制的执行权限相关,所以设置为AaMayExec
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
上面是采用后缀贪婪匹配的模式成功阻止了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
通过日志可以看出来
/usr/bin/curl
成功匹配被禁止了 /bin/curl
因为采用的是精确匹配/usr/bin/curl
的模式,所以还是成功被绕过然后执行了 这就是说明不能仅仅通过前面文件那中前缀精确匹配的模式禁止一个二进制程序执行,因为可能会存在二进程程序可能会存在多个文件路径,很多时候也很难找到一个二进程程序对应的所有路径。
所以最好的针对二进制程序的方法最好还是采用后缀贪婪匹配模式。
在Linux系统中存在多个和mount
有关的函数,所以就会存在多个与之相关的权限校验的函数,包括mount
和unmount
等等。
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_check
和 mount_fstype_check
函数来检查挂载请求是否符合这些规则。如果请求违反了任何规则,挂载操作将被拒绝,并返回 -EPERM
错误码。如果没有违反规则,挂载操作被允许。
所以权限设置会涉及到文件类型和对应的挂载目录。
对应的测试项目是: 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
对应的函数原型是:
SEC("lsm/sb_umount")
int BPF_PROG(varmor_umount, struct vfsmount *mnt, int flags)
和varmor_mount
相反,主要是针对用来执行卸载文件系统操作的安全检查
对应的测试项目是: 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
分析整个匹配过程:
mount_fstype_check()
之后成功执行,说明fstype
校验成功通过 umount
操作的路径是/mnt/mountpoint2
,名中了配置的规则/mnt/mountpoint
基于以上分析,umount
被阻止的行为就可以理解了。
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
为了方便,仅仅只是测试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
) 和两个标志 (e
和 p
):
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 是一个很好的学习项目,尤其是开发者响应速度也非常快。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3104/