ebpf学习记录(一):实现简单的 ebpf 程序

前言

课程来源:极客时间《eBPF 核心技术与实战》
为什么突然开始学习 ebpf 了

因为基于 ebpf 的 rootkit 这类后门目前能很大程度上的 bypass 检测,同时也非常的隐蔽不好清除。在很多 apt 中也有进行使用,比如之前的 A国的那个顶级方程式的 case。

之前一直没开始动是因为 ebpf 需要涉及 Linux 内核相关内容所以迟迟没有开始,但是现在发现有现成的框架 bcc 所以我不需要太关注 Linux 内核中的一些事项。

ebpf 简介

ebpf 是什么?

在我的理解中 ebpf 是 bpf 的 plus ,bpf 是自 Linux 2.1.75 之后引入的技术。简单的说就是在 Linux 内核态中引入一个虚拟机,并且用户态的程序可以将编译好的 bpf 字节码传递给内核中的这个虚拟机来进行解释执行。

ebpf 在 bpf 的基础上做了一些演进,使得不再仅仅局限于 Linux 网络栈层面。

ebpf 可以在不需要重新编译内核的情况下来为内核提供一些拓展的能力,例如一些网络、进程侧日志采集的能力。

在运行过程中借助 LLVM 将 ebpf 程序编译成 BPF 字节码,然后通过 bpf 的系统调用来将字节码传递给内核进行执行 ,在通过一系列的检查和校验之后才会进行执行。

那么现在就有一个问题,由于 bpf 程序是运行在内核中的,那么如何来获取 bpf 采集到的一些信息,以及 bpf 如何将采集到的数据进行存储

bpf 通过引入 map 映射来解决这类问题,bpf 映射将键-值保存在内核中,但是可被 bpf 程序访问,bpf程序可以将收集到的数据存储到 map(映射)中,然后用户态通过 map (映射) 中将数据读取出来

ps:比较好奇这块映射是如何实现的,加到 todolist 中....

image-20220908235623583

运行 ebpf 程序

环境安装

sudo apt-get install -y make clang llvm libelf-dev libbpf-dev bpfcc-tools libbpfcc-dev linux-tools-$(uname -r) linux-headers-$(uname -r)

这里利用 bcc 框架来实现编写(bcc 是 bpf 编译器的集合)

开发主要有两个part:

  1. ebpf 程序的开发 (运行在内核中的代码)
  2. 编写用户态程序 (主要借助 bcc 来将字节码送到内核执行同时绑定对应的事件)
  3. 执行

新建 hello.c

输出 Hello,World,由于是在内核中进行运行所以结果输出到内核调试文件 /sys/kernel/debug/tracing/trace_pipe

int hello_world(void *ctx)
{
    bpf_trace_printk("Hello, World!");
    return 0;
}

开发用户态程序

#!/usr/bin/env python3
from bcc import BPF
# 加载 bpf 程序
b = BPF(src_file="hello.c")
# 将程序挂载到系统调用中
# do_sys_openat2 是 openat() 的实现,openat() 就是打开文件的系统调用
# 这里的逻辑就是监控打开文件这个系统调用
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
# 读取内核调试文件
b.trace_print()

运行 python 文件大致结果是这样的

image-20220909001824255

可以看到运行结果中正常输出了 hello,world ,但是有几个问题

  1. 既然我们是通过 ebpf 监控系统打开文件的操作,那么正常情况输出结果应该有打开的文件名
  2. 现在读取结果是直接读 /sys/kernel/debug/tracing/trace_pipe 这个内核调试文件的,这就有一个问题,当存在多个 ebpf 程序的时候输出结果都是到这个文件,这样的话就使得输出结果很不清晰,并且这样性能也很差

针对上述两个问题,需要利用在上文提到的映射来结果,通过映射来读取 ebpf 执行的结果并适当进行一些数据处理,因为上述截图中很多输出字段都是我们不需要的

BCC 定义了一系列的库函数和辅助宏定义。这里使用 BPF_PERF_OUTPUT 来定义一个 Perf 事件类型的 BPF 映射,这里先定义一个数据结构

// 包含头文件
#include <uapi/linux/openat2.h>
#include <linux/sched.h>

// 定义数据结构
struct data_t {
  u32 pid; // 进程pid
  u64 ts;  // 时间
  char comm[TASK_COMM_LEN]; // 进程名
  char fname[NAME_MAX]; // 文件名
};

// 定义性能事件映射
BPF_PERF_OUTPUT(events);

然后将数据填充到我们的数据结构里面

// 定义kprobe处理函数
int hello_world(struct pt_regs *ctx, int dfd, const char __user * filename, struct open_how *how)
{
  struct data_t data = { };

  // 获取PID和时间
  data.pid = bpf_get_current_pid_tgid();
  data.ts = bpf_ktime_get_ns();

  // 获取进程名
  if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0)
  {
    bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename);
  }

  // 提交性能事件
  events.perf_submit(ctx, &data, sizeof(data));
  return 0;
}

合起来就是下面这个文件,该文件最终在内核中进行运行,将采集到的数据输出到映射中

// 包含头文件
#include <uapi/linux/openat2.h>
#include <linux/sched.h>

// 定义数据结构
struct data_t {
  u32 pid; // 进程pid
  u64 ts;  // 时间
  char comm[TASK_COMM_LEN]; // 进程名
  char fname[NAME_MAX]; // 文件名
};

// 定义性能事件映射
BPF_PERF_OUTPUT(events);

// 定义kprobe处理函数
int hello_world(struct pt_regs *ctx, int dfd, const char __user * filename, struct open_how *how)
{
  struct data_t data = { };

  // 获取PID和时间
  data.pid = bpf_get_current_pid_tgid();
  data.ts = bpf_ktime_get_ns();

  // 获取进程名
  if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0) // 获取进程名并且存储到缓冲区中
  { 
    bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename); // 读取进程打开的文件名
  }

  // 提交性能事件
  events.perf_submit(ctx, &data, sizeof(data));
  return 0;
}

然后我们需要编写一个用户态的调用程序

from bcc import BPF

# 加载bpf代码
b = BPF(src_file="trace-open.c")
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")

# 格式化输出
print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))

# 这里是一个回调函数来进行数据处理 
start = 0
def print_event(cpu, data, size):
    global start
    event = b["events"].event(data)
    if start == 0:
            start = event.ts
    time_s = (float(event.ts - start)) / 1000000000
    print("%-18.9f %-16s %-6d %-16s" % (time_s, event.comm, event.pid, event.fname))

# 定义名为 “events” 的 Perf 事件映射,然后循环调用读取
b["events"].open_perf_buffer(print_event)
while 1:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

大致运行效果如下:

image-20220909003642981

结尾

其实文中还是有很多东西我没去仔细研究,奈何明天要打工今天先这样,明天来进行后续的补充

还有一些其他的思考,上面是针对文件追踪编写了小脚本,在安全的场景下我是不是可以通过这种方式来进行本机流量的窃取,是否可以bypass 检测,并且窃取到的数据如何隐蔽的传给我们的 c2

今天就写到这里吧....

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇