前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从Falco看如何利用eBPF检测系统调用

从Falco看如何利用eBPF检测系统调用

作者头像
绿盟科技研究通讯
发布2022-06-06 12:07:26
2.2K0
发布2022-06-06 12:07:26
举报

一、eBPF

1.1简介

eBPF是一项革命性的技术,可以在操作系统内核中运行沙盒程序。它用于安全有效地扩展内核的功能,而无需更改内核源代码或加载内核模块。通过允许在操作系统中运行沙箱程序,应用程序开发人员可以运行eBPF程序,以便在运行时向操作系统添加额外的功能。然后,操作系统保证安全性和执行效率,就像在实时(JIT)编译器和验证引擎的帮助下进行本机编译一样。这导致了一波基于eBPF的项目,涵盖了广泛的用例,包括下一代网络、可观察性和安全功能。

eBPF程序是事件驱动的,当内核或应用程序通过某个hook时运行。预定义的hook包括system calls, 函数的entry/exit, kerneltracepoints, network 事件等等。图1展示了eBPF在hook系统调用时程序调用的实际以及如何获取系统的数据。

图1 eBPF在hook系统调用时的执行位置

eBPF可以对接多种类型的探针:

• Kprobes

• Kretprobes

• Tracepoints

• Uprobes

• Uretprobes

• ...

eBPF中有两个比较重要的部分:Prog和 Map。

图2 eBPF采用Map实现Prog 之前的数据通信

eBPF Prog

为了细分不同类型的eBPF程序,截止到Kernel 5.8, eBPF划分了30种不同的eBPF 程序类型[1]。每种eBPF只能实现的有限的功能 。内核提供了一系列可供eBPF程序调用的内核函数来实现功能的可控,保障eBPF程序的安全性。这些内核函数通常被称为 helper辅助函数[2]。不同类型的程序可以使用不同的helper辅助函数。有关Prog的类型详解:使用场景、函数签名、执行位置及程序示例可以查阅产考文献[3]。

eBPF Map

Map是一种用来存储不同数据的动态的数据类型,允许在内核态的程序之间共享数据,也能够在内核态和用户态应用之间共享数据。可以实现用户态和内核态的双向实时通信。

随着版本迭代与功能的扩展细化,目前bpf_map_type已经有28不同类型的map[4]。所以在使用map时,需要根据应用场景指定一个合适的类型。关于BPF Map 类型详解使用可以参考[5]。map大致可以分为下面几种类型:

• Hash tables, Arrays

• LRU (Least Recently Used)

• Ring Buffer

• Stack Trace

• LPM (Longest Prefix match)

• ...

1.2编写eBPF

内核中提供了bpf系统调用:

int bpf(int cmd,union bpf_attr *attr,unsigned int size);

但在许多场景中,bpf不是直接使用,而是通过Cilium[6]、bcc[7]或bpftrace[8],goebpf[9],libbpf[10]等项目间接使用。这些项目在eBPF之上提供了一个抽象,不需要直接编写程序,而是提供了指定基于意图定义的能力,然后用eBPF实现这些定义。在 linux 源码中的 samples/bpf [11] 目录下可以找到对应的例子。关于eBPF的编写和使用也可以参阅往期文章[12]。

1.3eBPF应用场景

得益于eBPF的强大能力,一大批基于eBPF的优秀开源项目相继涌现,如BPFTrace,Katran[13],Cilium,Falco[14],Pixie[15],eBPF Exporter[16]等等,覆盖观测跟踪,网络,安全等各个方面。在国内,各家云厂商也开始采用eBPF技术来优化系统设计。下面我们将以Falco为例,展示下eBPF是如何实现安全监控的能力的。

二、Falco

2.1简介

Falco最初是由Sysdig[17]创建的,后来加入CNCF孵化器,成为首个加入CNCF的运行时安全项目。可以实现对调用行为的监控,并依赖于强大的规则引擎,对异常的系统调用行为进行告警。

2.2Falco架构

图3 Falco架构

Falco架构图如图3所示。有关Falco的具体的介绍和使用可以参考往期文章【探索SysdigFalco:容器环境下的异常行为检测工具】,本文重点关注Falco对系统调用的采集。Falco对系统调用的采集有两种模式:

• LKM (Linux Kernel Module)内核扩展模块

• eBPF Probe.

这两种方法在功能上是相同的,但内核模块的效率要高一些,而eBPF方法更安全。

Falco的eBPF模块主要是对内核的静态探针Tracepoints进行hook,之所以没有采用动态探针Kprobes,是因为Kprobes并不是一种稳定的探针,可能会随着内核的迭代更新而新增或者删除,对内核版本的依赖性较高,因此兼容性与稳定性也就较差。

sysdig在他们的官方博客对eBPFdriver的评价[18]如下:

可能是这个星球上最雄心勃勃的、最复杂的eBPF脚本。

下面我们从Falco利用eBPF监控系统调用的代码层面[19],了解下Falco如何利用eBPF实现系统调用的监控。

Falco主要是使用系统的raw_tracepoint或者tracepoint,这取决于不同内核所能提供的能力。

# 从linux kernel 4.17后,添加了raw_tracepoint类型。
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 17,0)
#define BPF_SUPPORTS_RAW_TRACEPOINTS
#endif

#ifdef BPF_SUPPORTS_RAW_TRACEPOINTS
#define TP_NAME "raw_tracepoint/"
#else
#define TP_NAME "tracepoint/"
#endif

#ifdef BPF_SUPPORTS_RAW_TRACEPOINTS
#define BPF_PROBE(prefix,event,type)\
      __bpf_section(TP_NAME #event)int bpf_##event(struct type*ctx)
#else
#define BPF_PROBE(prefix,event,type)\
      __bpf_section(TP_NAME prefix #event)int bpf_##event(struct type*ctx)
#endif

Falco的eBPF驱动提供了以下几种类型的调用采集的能力。

BPF_PROBE("raw_syscalls/", sys_enter, sys_enter_args)
BPF_PROBE("raw_syscalls/", sys_exit, sys_exit_args)
BPF_PROBE("sched/", sched_process_exit, sched_process_exit_args)
BPF_PROBE("sched/", sched_switch, sched_switch_args)
BPF_PROBE("exceptions/", page_fault_user, page_fault_args)
BPF_PROBE("exceptions/", page_fault_kernel, page_fault_args)
BPF_PROBE("signal/", signal_deliver, signal_deliver_args)

下面以tracepoint:syscalls:sys_enter为例分析下,Falco是如何利用eBPF来采集系统调用的详细信息的。

BPF_PROBE("raw_syscalls/", sys_enter, sys_enter_args)
{
      ...
      // 获取系统调用的系统调用相关信息
      id =bpf_syscall_get_nr(ctx);
      sc_evt =get_syscall_info(id);
      evttype = sc_evt->enter_event_type;
      ...
      // 调用具体系统调用的信息采集方法
#ifdef BPF_SUPPORTS_RAW_TRACEPOINTS
      call_filler(ctx, ctx, evt_type, settings, drop_flags);
#else
      /* Duplicated here to avoid verifier madness */
      structsys_enter_args stack_ctx;

      memcpy(stack_ctx.args, ctx->args,sizeof(ctx->args));
      if(stash_args(stack_ctx.args))
            return0;
      call_filler(ctx,&stack_ctx, evt_type, settings, drop_flags);
#endif
      return0;
}

可以看到,这只是入口,实际的eBPF的程序是使用bpf_tail_call()尾调用机制调用的。有关bpf_tail_call的介绍可以从参考文献[20]中获取。

static __always_inline void call_filler(void *ctx,
                              void *stack_ctx,
                              enum ppm_event_typeevt_type,
                              struct sysdig_bpf_settings *settings,
                              enum syscall_flags drop_flags)
{
      ...
      bpf_tail_call(ctx,&tail_map, filler_info->filler_id);
      bpf_printk("Can't tail call fillerevt=%d, filler=%d\n",
               state->tail_ctx.evt_type,
               filler_info->filler_id);
      ...
}

bpf_tail_call是从tail_map获取指定的BPF 程序执行。tail_map是type为BPF_MAP_TYPE_PROG_ARRAY的map集合。这种类型的 array 存放的是BPF 程序的文件描述符(fd),在尾调用时使用。

struct bpf_map_def __bpf_section("maps") tail_map ={
      .type = BPF_MAP_TYPE_PROG_ARRAY,
      .key_size =sizeof(u32),
      .value_size =sizeof(u32),
      .max_entries = PPM_FILLER_MAX,
};

tail_map中的BPF 程序位于driver/bpf/fillers.h中,考虑代码复用,里面使用了大量的宏定义。

#define FILLER(x,is_syscall)                               \
static__always_inline int__bpf_##x(struct filler_data *data);         \
                                                      \
__bpf_section(TP_NAME "filler/" #x)                         \
static__always_inline int bpf_##x(void*ctx)                     \
{                                                     \
      struct filler_data data;                              \
      intres;                                        \
                                                      \
      res=init_filler_data(ctx,&data,is_syscall);             \
      if(res==PPM_SUCCESS){                             \
            if(!data.state->tail_ctx.len)                        \
                  write_evt_hdr(&data);                     \
            res=__bpf_##x(&data);                         \
      }                                               \
                                                      \
      if(res==PPM_SUCCESS)                               \
            res=push_evt_frame(ctx,&data);               \
                                                      \
      if(data.state)                                       \
            data.state->tail_ctx.prev_res=res;                  \
                                                      \
      bpf_tail_call(ctx,&tail_map,PPM_FILLER_terminate_filler);\
      bpf_printk("Can't tail call terminate filler\n");           \
      return 0;                                       \
}                                                     \
                                                      \
static__always_inline int __bpf_##x(struct filler_data *data)          \

FILLER(sys_open_x,true)
{
      ...
      // 调用参数采集
      res = bpf_val_to_ring(data, dev);
      return res;
}
FILLER(sys_empty,true)
...

可以将宏定义还原后,查看简化的代码结构如下:

static __always_inline int __bpf_sys_open_x(struct filler_data *data);

__bpf_section(TP_NAME "filler/" sys_open_x)                             
static __always_inline int bpf_sys_open_x(void *ctx)
{
      ...
      // 调用实际的信息获取的方法,采集系统调用信息
      res = __bpf_sys_open_x(&data);                              
      ...
      if(res == PPM_SUCCESS)
            // 将采集的数据发送给用户态程序
            res = push_evt_frame(ctx,&data);
      ...
}     

static __always_inline int __bpf_sys_open_x(struct filler_data *data)
{
...
      // 调用参数采集,并将结果填充到data结构体中
      if(retval <0||!bpf_get_fd_dev_ino(retval,&dev,&ino))
            dev =0;
      res = bpf_val_to_ring(data, dev);
      return res;
}
static __always_inline int push_evt_frame(void *ctx,
                                struct filler_data *data)
{
      ...

#ifdef BPF_FORBIDS_ZERO_ACCESS
      int res = bpf_perf_event_output(ctx,
                              &perf_map,
                              BPF_F_CURRENT_CPU,
                              data->buf,
                              ((data->state->tail_ctx.len -1)& SCRATCH_SIZE_MAX)+1);
#else
      int res = bpf_perf_event_output(ctx,
                              &perf_map,
                              BPF_F_CURRENT_CPU,
                              data->buf,
                              data->state->tail_ctx.len & SCRATCH_SIZE_MAX);
#endif

可以看到, push_evt_frame中实际是bpf_perf_event_output()将获取到的数据发送到perf_map,利用perf 缓冲区从内核向用户空间发送数据。但是perf 缓冲区基于单个CPU的设计本身会有一定的缺陷,因此Linux 5.8开始 ,BPF提供了新的数据结构:BPF环形缓冲区(ringbuf)。有兴趣的读者可以从参考文献中了解更为详细的细节。关于BPF perfbuf和ringbuf的详细资料可以参考[21]。

struct bpf_map_def __bpf_section("maps") perf_map ={
      .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
      .key_size =sizeof(u32),
      .value_size =sizeof(u32),// 只能是 sizeof(u32) ,代表的是 perf_event 的文件描述符
      .max_entries =0,//是perf_event 的文件描述符数量。
};

perf_map是一个type为BPF_MAP_TYPE_PERF_EVENT_ARRAY的map。与其他array、hash 类型的eBPF map不同,BPF_MAP_TYPE_PERF_EVENT_ARRAY采用bpf_perf_event_output()往map中填充数据。

在这时用户态程序就可以从perf 缓冲区中获取到记录数据,传入Falco的规则引擎中进行匹配分析。

三、总结

如今,eBPF被广泛用于推动各种各样的用例:在现代数据中心和云原生环境中提供高性能网络和负载均衡,以低开销提取细粒度的安全可观察性数据,帮助应用程序开发人员跟踪应用程序,为性能故障排除、预防性应用程序和容器运行时安全实施提供见解等等。

本文以Falco为例,分析了eBPF在安全监控领域的使用方式。这只是eBPF技术的一个很小的应用场景,其他诸如加速网络访问,远程调试程序等等场景使用eBPF技术都能有很好的效果,有兴趣的读者也可以关注下。

参考文献

[1]. https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md#program-types

[2]. https://man7.org/linux/man-pages/man7/bpf-helpers.7.html

[3]. http://arthurchiao.art/blog/bpf-advanced-notes-1-zh/

[4]. https://github.com/torvalds/linux/blob/v5.8/include/uapi/linux/bpf.h#L122

[5]. http://arthurchiao.art/blog/bpf-advanced-notes-3-zh/#3-bpf_map_type_prog_array

[6]. https://github.com/cilium/cilium

[7]. https://github.com/iovisor/bcc

[8]. https://github.com/iovisor/bpftrace

[9]. https://github.com/dropbox/goebpf

[10]. https://github.com/libbpf/libbpf

[11]. https://github.com/torvalds/linux/tree/master/samples/bpf

[12]. https://mp.weixin.qq.com/s?__biz=MzIyODYzNTU2OA==&mid=2247487440&idx=1&sn=cb6379cfc4a1bba0881363840afc438a&chksm=e84fa90fdf38201990781e3c95d9857e6d4ad083ce40a575e1cbdc12aaabb4f58ba527dc58c1&scene=21#wechat_redirect。

[13]. https://github.com/facebookincubator/katran

[14]. https://github.com/falcosecurity/falco

[15]. https://github.com/pixie-io/pixie

[16]. https://github.com/cloudflare/ebpf_exporter

[17]. https://sysdig.com/

[18]. https://sysdig.com/blog/sysdig-contributes-falco-kernel-ebpf-cncf

[19]. https://github.com/falcosecurity/libs/tree/master/driver/bpf

[20]. https://lwn.net/Articles/645169/

[21]. https://www.ebpf.top/post/bpf_ring_buffer

关于星云实验室

星云实验室专注于云计算安全、解决方案研究与虚拟化网络安全问题研究。基于IaaS环境的安全防护,利用SDN/NFV等新技术和新理念,提出了软件定义安全的云安全防护体系。承担并完成多个国家、省、市以及行业重点单位创新研究课题,已成功孵化落地绿盟科技云安全解决方案。

内容编辑:星云实验室 陈建军 责任编辑:高深

本公众号原创文章仅代表作者观点,不代表绿盟科技立场。所有原创内容版权均属绿盟科技研究通讯。未经授权,严禁任何媒体以及微信公众号复制、转载、摘编或以其他方式使用,转载须注明来自绿盟科技研究通讯并附上本文链接。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-04-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 绿盟科技研究通讯 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档