initial commit

This commit is contained in:
guochao 2023-08-19 11:58:49 +08:00
commit 72ead3f149
Signed by: guochao
GPG Key ID: 79F7306D2AA32FC3
11 changed files with 131186 additions and 0 deletions

79
README.md Normal file
View File

@ -0,0 +1,79 @@
# 练手用的 LSM-BPF 模块
## Quick Start
### 环境
- clang
- 我的在 clang15但是理论上略微旧一点没关系。如果提示头文件缺失或者是不支持 bpf target请升级
- kernel with BTF
- 参考https://github.com/aquasecurity/btfhub/blob/main/docs/supported-distros.md
- golang 1.17+
- bpftool
- `bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h`
- bpf2go
- cilium 的工具
### 启动
- 一个终端
```bash
go build -o bin/demo
sudo ./bin/demo $PWD
```
- 另一个终端
```bash
watch ls
```
- 如果要看日志不要tail
```bash
sudo cat /sys/kernel/debug/tracing/trace_pipe
```
## 代码解释
### 内核态的 eBPF 程序: [./bpf/lsm.c](./bpf/lsm.c)
内核态程序主要是三部分:
- 生成的 BTF 定义,这个理论上可以在支持 BTF 的发行版上直接用,不需要改
- map: 内核和用户态通信的数据结构
- hook: 入口点
和普通的 C 非常像,可以当 C 来写。但是栈空间只有 512 B。我没打错就 512 Byte。
#### BTF 定义生成
```bash
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
```
#### map
map 不在栈空间中,一般用来让
- 用户态配置 eBPF 程序
- eBPF 程序向用户态传递结果或者性能数据
参考
- https://docs.kernel.org/bpf/maps.html#map-types
#### hook
hook 就是一个函数,可以配置为在特定时机被 hook 点调起。
我们这个 demo 主要是希望尝试写 lsm 模块,比如说和 file_open 这个 hook 有关的信息在
- https://github.com/torvalds/linux/blob/master/security/security.c#L2793
主要是需要关注 hook 需要接受的参数。
也有很多其他的 hook 点,比如说 kprobe 和 xdp 有关的。我们这个仓库暂时不关心。
### 用户态
用户态我选择了用 cilium 的 wrapper理论上可以用 libbpf 啊或者类似的其他工具来实现。
### 构建
整体来说项目是
- (一次性)生成 vmlinux.h
- bpf2go 工具编译 ebpf 字节码,生成 stub
- 编译 go 的二进制

2
bin/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

130
bpf/lsm.c Normal file
View File

@ -0,0 +1,130 @@
// generated by command: bpftool btf dump file /sys/kernel/btf/vmlinux format c
// > vmlinux.h
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <errno.h>
#include <sys/cdefs.h>
char __license[] SEC("license") = "Dual MIT/GPL";
char log_fmt_timeout[] = "timeout: %lld %lld";
#define SECOND (1000 * 1000 * 1000)
typedef enum status {
FILE_PROTECT_ENABLED,
FILE_PROTECT_TICK,
FILE_PROTECT_MAX,
} file_protect_state;
typedef struct check_ctx {
struct dentry *dentry;
__u64 need_to_be_checked;
__u64 return_value;
} check_ctx;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, FILE_PROTECT_MAX);
__type(key, file_protect_state);
__type(value, __u64);
} states SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 256);
__type(key, unsigned long);
__type(value, __u8);
} roots SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024 * 1024);
__type(key, unsigned long);
__type(value, __u64);
} banned_access SEC(".maps");
#define MAX_PATH_FRAGEMENTS 256
static __u64 check_file_need_protection(struct bpf_map *map,
unsigned long *inode, __u8 *enabled,
check_ctx *ctx) {
if (!*enabled) {
return 0;
}
struct dentry *dentry = ctx->dentry;
int count_down = MAX_PATH_FRAGEMENTS;
// enumerate from the leaf to root
while (count_down-- > 0 && dentry != NULL) {
if (dentry->d_inode->i_ino == *inode) {
ctx->need_to_be_checked = 1;
return 1;
}
}
return 0;
}
static __u64 check_service_status(struct bpf_map *map, file_protect_state *kind,
__u64 *state, check_ctx *data) {
__u64 now;
switch (*kind) {
case FILE_PROTECT_ENABLED:
if (!*state) {
data->need_to_be_checked = 0;
data->return_value = 0;
return 1; // early return to improve performance. return 1 means to stop
// iteration.
}
break;
case FILE_PROTECT_TICK:
now = bpf_ktime_get_ns();
// now - last > 3 seconds
// but 3 * SECOND will overflow
if ((now - *state) / 3 > SECOND) {
data->return_value = EPERM;
bpf_trace_printk(log_fmt_timeout, sizeof(log_fmt_timeout), now, *state);
}
break;
case FILE_PROTECT_MAX: // this branch just tell clang to not complaint about
// FILE_PROTECT_MAX
// noop
break;
// default: // TAKE CARE!!!! default branch disable enum branch checking.
// // noop
// break;
}
return 0;
}
SEC("lsm/file_open")
int BPF_PROG(check_file_open, struct file *file, int ret) {
if (ret != 0)
return ret;
check_ctx data = {
.dentry = file->f_path.dentry,
.need_to_be_checked = 0,
.return_value = 0,
};
bpf_for_each_map_elem(&roots, check_file_need_protection, &data, 0);
if (!data.need_to_be_checked) {
return 0;
}
data.need_to_be_checked = 1;
bpf_for_each_map_elem(&states, check_service_status, &data, 0);
if (!data.need_to_be_checked) {
return 0;
}
return data.return_value;
}

130595
bpf/vmlinux.h Normal file

File diff suppressed because it is too large Load Diff

127
fileprotector_bpfeb.go Normal file
View File

@ -0,0 +1,127 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
package main
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
type FileProtectorFileProtectState uint32
// LoadFileProtector returns the embedded CollectionSpec for FileProtector.
func LoadFileProtector() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_FileProtectorBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load FileProtector: %w", err)
}
return spec, err
}
// LoadFileProtectorObjects loads FileProtector and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *FileProtectorObjects
// *FileProtectorPrograms
// *FileProtectorMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func LoadFileProtectorObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := LoadFileProtector()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// FileProtectorSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type FileProtectorSpecs struct {
FileProtectorProgramSpecs
FileProtectorMapSpecs
}
// FileProtectorSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type FileProtectorProgramSpecs struct {
CheckFileOpen *ebpf.ProgramSpec `ebpf:"check_file_open"`
}
// FileProtectorMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type FileProtectorMapSpecs struct {
BannedAccess *ebpf.MapSpec `ebpf:"banned_access"`
Roots *ebpf.MapSpec `ebpf:"roots"`
States *ebpf.MapSpec `ebpf:"states"`
}
// FileProtectorObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to LoadFileProtectorObjects or ebpf.CollectionSpec.LoadAndAssign.
type FileProtectorObjects struct {
FileProtectorPrograms
FileProtectorMaps
}
func (o *FileProtectorObjects) Close() error {
return _FileProtectorClose(
&o.FileProtectorPrograms,
&o.FileProtectorMaps,
)
}
// FileProtectorMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to LoadFileProtectorObjects or ebpf.CollectionSpec.LoadAndAssign.
type FileProtectorMaps struct {
BannedAccess *ebpf.Map `ebpf:"banned_access"`
Roots *ebpf.Map `ebpf:"roots"`
States *ebpf.Map `ebpf:"states"`
}
func (m *FileProtectorMaps) Close() error {
return _FileProtectorClose(
m.BannedAccess,
m.Roots,
m.States,
)
}
// FileProtectorPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to LoadFileProtectorObjects or ebpf.CollectionSpec.LoadAndAssign.
type FileProtectorPrograms struct {
CheckFileOpen *ebpf.Program `ebpf:"check_file_open"`
}
func (p *FileProtectorPrograms) Close() error {
return _FileProtectorClose(
p.CheckFileOpen,
)
}
func _FileProtectorClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed fileprotector_bpfeb.o
var _FileProtectorBytes []byte

BIN
fileprotector_bpfeb.o Normal file

Binary file not shown.

127
fileprotector_bpfel.go Normal file
View File

@ -0,0 +1,127 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build 386 || amd64 || amd64p32 || arm || arm64 || loong64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
package main
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
type FileProtectorFileProtectState uint32
// LoadFileProtector returns the embedded CollectionSpec for FileProtector.
func LoadFileProtector() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_FileProtectorBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load FileProtector: %w", err)
}
return spec, err
}
// LoadFileProtectorObjects loads FileProtector and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *FileProtectorObjects
// *FileProtectorPrograms
// *FileProtectorMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func LoadFileProtectorObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := LoadFileProtector()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// FileProtectorSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type FileProtectorSpecs struct {
FileProtectorProgramSpecs
FileProtectorMapSpecs
}
// FileProtectorSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type FileProtectorProgramSpecs struct {
CheckFileOpen *ebpf.ProgramSpec `ebpf:"check_file_open"`
}
// FileProtectorMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type FileProtectorMapSpecs struct {
BannedAccess *ebpf.MapSpec `ebpf:"banned_access"`
Roots *ebpf.MapSpec `ebpf:"roots"`
States *ebpf.MapSpec `ebpf:"states"`
}
// FileProtectorObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to LoadFileProtectorObjects or ebpf.CollectionSpec.LoadAndAssign.
type FileProtectorObjects struct {
FileProtectorPrograms
FileProtectorMaps
}
func (o *FileProtectorObjects) Close() error {
return _FileProtectorClose(
&o.FileProtectorPrograms,
&o.FileProtectorMaps,
)
}
// FileProtectorMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to LoadFileProtectorObjects or ebpf.CollectionSpec.LoadAndAssign.
type FileProtectorMaps struct {
BannedAccess *ebpf.Map `ebpf:"banned_access"`
Roots *ebpf.Map `ebpf:"roots"`
States *ebpf.Map `ebpf:"states"`
}
func (m *FileProtectorMaps) Close() error {
return _FileProtectorClose(
m.BannedAccess,
m.Roots,
m.States,
)
}
// FileProtectorPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to LoadFileProtectorObjects or ebpf.CollectionSpec.LoadAndAssign.
type FileProtectorPrograms struct {
CheckFileOpen *ebpf.Program `ebpf:"check_file_open"`
}
func (p *FileProtectorPrograms) Close() error {
return _FileProtectorClose(
p.CheckFileOpen,
)
}
func _FileProtectorClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed fileprotector_bpfel.o
var _FileProtectorBytes []byte

BIN
fileprotector_bpfel.o Normal file

Binary file not shown.

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module git.jeffthecoder.xyz/public/demo-ebpf-lsm
go 1.21.0
require (
github.com/cilium/ebpf v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect
golang.org/x/sys v0.6.0 // indirect
)

6
go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y=
github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs=
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI=
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

111
main.go Normal file
View File

@ -0,0 +1,111 @@
package main
import (
"errors"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"syscall"
"time"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go FileProtector bpf/lsm.c -- -I./bpf -O2
/*
#include <time.h>
static unsigned long long get_nsecs(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (unsigned long long)ts.tv_sec * 1000000000UL + ts.tv_nsec;
}
*/
import "C"
const ( // bpf2go cannot restore enum to corresponding names. so i do it manually
FileProtectorFileProtectStateEnabled FileProtectorFileProtectState = iota
FileProtectorFileProtectStateTick
FileProtectorFileProtectStateMax
)
func main() {
fDebug := flag.Bool("debug", false, "toggle to show full ebpf verifier error")
flag.Parse()
if err := rlimit.RemoveMemlock(); err != nil {
panic(err)
}
fileProtectorObjects := FileProtectorObjects{}
if err := LoadFileProtectorObjects(&fileProtectorObjects, &ebpf.CollectionOptions{
Programs: ebpf.ProgramOptions{
LogSize: ebpf.DefaultVerifierLogSize * 1024,
},
}); err != nil {
var ve *ebpf.VerifierError
if *fDebug && errors.As(err, &ve) {
// Using %+v will print the whole verifier error, not just the last
// few lines.
fmt.Printf("Verifier error: %+v\n", ve)
}
log.Panic("failed to attach lsm: ", err)
}
defer fileProtectorObjects.Close()
log.Println("lsm loaded")
lsm, err := link.AttachLSM(link.LSMOptions{
Program: fileProtectorObjects.CheckFileOpen,
})
if err != nil {
log.Panic("failed to attach lsm: ", err)
}
defer lsm.Close()
log.Println("lsm attached")
log.Println("configure maps...")
for _, path := range flag.Args() {
path, _ = filepath.Abs(path)
log.Println("-", path)
stat, err := os.Stat(path)
if err != nil {
log.Println("W: ", err)
continue
}
switch stat := stat.Sys().(type) {
case *syscall.Stat_t:
if err := fileProtectorObjects.FileProtectorMaps.Roots.Update(stat.Ino, uint8(1), ebpf.UpdateAny); err != nil {
panic(err)
}
default:
log.Printf("W: incompatible type of stat: %T", stat)
continue
}
}
// you should also protect the daemon config itself
log.Println("configuration done. enabling...")
if err := fileProtectorObjects.FileProtectorMaps.States.Update(FileProtectorFileProtectStateEnabled, uint64(1), ebpf.UpdateAny); err != nil {
panic(err)
}
ticker := time.NewTicker(time.Second * 10)
defer ticker.Stop()
log.Println("ticking...but with one step slower intentionally!")
for {
now := uint64(C.get_nsecs())
log.Println("tick:", now)
if err := fileProtectorObjects.FileProtectorMaps.States.Update(FileProtectorFileProtectStateTick, uint64(now), ebpf.UpdateAny); err != nil {
panic(err)
}
<-ticker.C
}
}