mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-02-08 12:53:33 +08:00
Deploying to gh-pages from @ eunomia-bpf/bpf-developer-tutorial@c120bb4912 🚀
This commit is contained in:
@@ -177,25 +177,25 @@
|
||||
<p>eBPF(扩展的伯克利数据包过滤器)是 Linux 内核中的一个强大功能,可以在无需更改内核源代码或重启内核的情况下,运行、加载和更新用户定义的代码。这种功能让 eBPF 在网络和系统性能分析、数据包过滤、安全策略等方面有了广泛的应用。</p>
|
||||
<p>在本篇教程中,我们将展示如何利用 eBPF 来隐藏进程或文件信息,这是网络安全和防御领域中一种常见的技术。</p>
|
||||
<h2 id="背景知识与实现机制"><a class="header" href="#背景知识与实现机制">背景知识与实现机制</a></h2>
|
||||
<p>"进程隐藏" 能让特定的进程对操作系统的常规检测机制变得不可见。在黑客攻击或系统防御的场景中,这种技术都可能被应用。具体来说,Linux 系统中每个进程都在 /proc/ 目录下有一个以其进程 ID 命名的子文件夹,包含了该进程的各种信息。<code>ps</code> 命令就是通过查找这些文件夹来显示进程信息的。因此,如果我们能隐藏某个进程的 /proc/ 文件夹,就能让这个进程对 <code>ps</code> 命令等检测手段“隐身”。</p>
|
||||
<p>"进程隐藏" 能让特定的进程对操作系统的常规检测机制变得不可见。在黑客攻击或系统防御的场景中,这种技术都可能被应用。具体来说,Linux 系统中每个进程都在 /proc/ 目录下有一个以其进程 ID 命名的子文件夹,包含了该进程的各种信息。<code>ps</code> 命令就是通过查找这些文件夹来显示进程信息的。因此,如果我们能隐藏某个进程的 /proc/ 文件夹,就能让这个进程对 <code>ps</code> 命令等检测手段“隐身”。</p>
|
||||
<p>要实现进程隐藏,关键在于操作 <code>/proc/</code> 目录。在 Linux 中,<code>getdents64</code> 系统调用可以读取目录下的文件信息。我们可以通过挂接这个系统调用,修改它返回的结果,从而达到隐藏文件的目的。实现这个功能需要使用到 eBPF 的 <code>bpf_probe_write_user</code> 功能,它可以修改用户空间的内存,因此能用来修改 <code>getdents64</code> 返回的结果。</p>
|
||||
<p>下面,我们会详细介绍如何在内核态和用户态编写 eBPF 程序来实现进程隐藏。</p>
|
||||
<h3 id="内核态-ebpf-程序实现"><a class="header" href="#内核态-ebpf-程序实现">内核态 eBPF 程序实现</a></h3>
|
||||
<p>接下来,我们将详细介绍如何在内核态编写 eBPF 程序来实现进程隐藏。首先是 eBPF 程序的起始部分:</p>
|
||||
<pre><code class="language-c">// SPDX-License-Identifier: BSD-3-Clause
|
||||
#include "vmlinux.h"
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
#include <bpf/bpf_core_read.h>
|
||||
#include "common.h"
|
||||
#include "common.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "Dual BSD/GPL";
|
||||
char LICENSE[] SEC("license") = "Dual BSD/GPL";
|
||||
|
||||
// Ringbuffer Map to pass messages from kernel to user
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 256 * 1024);
|
||||
} rb SEC(".maps");
|
||||
} rb SEC(".maps");
|
||||
|
||||
// Map to fold the dents buffer addresses
|
||||
struct {
|
||||
@@ -203,7 +203,7 @@ struct {
|
||||
__uint(max_entries, 8192);
|
||||
__type(key, size_t);
|
||||
__type(value, long unsigned int);
|
||||
} map_buffs SEC(".maps");
|
||||
} map_buffs SEC(".maps");
|
||||
|
||||
// Map used to enable searching through the
|
||||
// data in a loop
|
||||
@@ -212,7 +212,7 @@ struct {
|
||||
__uint(max_entries, 8192);
|
||||
__type(key, size_t);
|
||||
__type(value, int);
|
||||
} map_bytes_read SEC(".maps");
|
||||
} map_bytes_read SEC(".maps");
|
||||
|
||||
// Map with address of actual
|
||||
struct {
|
||||
@@ -220,7 +220,7 @@ struct {
|
||||
__uint(max_entries, 8192);
|
||||
__type(key, size_t);
|
||||
__type(value, long unsigned int);
|
||||
} map_to_patch SEC(".maps");
|
||||
} map_to_patch SEC(".maps");
|
||||
|
||||
// Map to hold program tail calls
|
||||
struct {
|
||||
@@ -228,23 +228,23 @@ struct {
|
||||
__uint(max_entries, 5);
|
||||
__type(key, __u32);
|
||||
__type(value, __u32);
|
||||
} map_prog_array SEC(".maps");
|
||||
} map_prog_array SEC(".maps");
|
||||
</code></pre>
|
||||
<p>我们首先需要理解这个 eBPF 程序的基本构成和使用到的几个重要组件。前几行引用了几个重要的头文件,如 "vmlinux.h"、"bpf_helpers.h"、"bpf_tracing.h" 和 "bpf_core_read.h"。这些文件提供了 eBPF 编程所需的基础设施和一些重要的函数或宏。</p>
|
||||
<p>我们首先需要理解这个 eBPF 程序的基本构成和使用到的几个重要组件。前几行引用了几个重要的头文件,如 "vmlinux.h"、"bpf_helpers.h"、"bpf_tracing.h" 和 "bpf_core_read.h"。这些文件提供了 eBPF 编程所需的基础设施和一些重要的函数或宏。</p>
|
||||
<ul>
|
||||
<li>"vmlinux.h" 是一个包含了完整的内核数据结构的头文件,是从 vmlinux 内核二进制中提取的。使用这个头文件,eBPF 程序可以访问内核的数据结构。</li>
|
||||
<li>"bpf_helpers.h" 头文件中定义了一系列的宏,这些宏是 eBPF 程序使用的 BPF 助手(helper)函数的封装。这些 BPF 助手函数是 eBPF 程序和内核交互的主要方式。</li>
|
||||
<li>"bpf_tracing.h" 是用于跟踪事件的头文件,它包含了许多宏和函数,这些都是为了简化 eBPF 程序对跟踪点(tracepoint)的操作。</li>
|
||||
<li>"bpf_core_read.h" 头文件提供了一组用于从内核读取数据的宏和函数。</li>
|
||||
<li>"vmlinux.h" 是一个包含了完整的内核数据结构的头文件,是从 vmlinux 内核二进制中提取的。使用这个头文件,eBPF 程序可以访问内核的数据结构。</li>
|
||||
<li>"bpf_helpers.h" 头文件中定义了一系列的宏,这些宏是 eBPF 程序使用的 BPF 助手(helper)函数的封装。这些 BPF 助手函数是 eBPF 程序和内核交互的主要方式。</li>
|
||||
<li>"bpf_tracing.h" 是用于跟踪事件的头文件,它包含了许多宏和函数,这些都是为了简化 eBPF 程序对跟踪点(tracepoint)的操作。</li>
|
||||
<li>"bpf_core_read.h" 头文件提供了一组用于从内核读取数据的宏和函数。</li>
|
||||
</ul>
|
||||
<p>程序中定义了一系列的 map 结构,这些 map 是 eBPF 程序中的主要数据结构,它们用于在内核态和用户态之间共享数据,或者在 eBPF 程序中存储和传递数据。</p>
|
||||
<p>其中,"rb" 是一个 Ringbuffer 类型的 map,它用于从内核向用户态传递消息。Ringbuffer 是一种能在内核和用户态之间高效传递大量数据的数据结构。</p>
|
||||
<p>"map_buffs" 是一个 Hash 类型的 map,它用于存储目录项(dentry)的缓冲区地址。</p>
|
||||
<p>"map_bytes_read" 是另一个 Hash 类型的 map,它用于在数据循环中启用搜索。</p>
|
||||
<p>"map_to_patch" 是另一个 Hash 类型的 map,存储了需要被修改的目录项(dentry)的地址。</p>
|
||||
<p>"map_prog_array" 是一个 Prog Array 类型的 map,它用于保存程序的尾部调用。</p>
|
||||
<p>程序中的 "target_ppid" 和 "pid_to_hide_len"、"pid_to_hide" 是几个重要的全局变量,它们分别存储了目标父进程的 PID、需要隐藏的 PID 的长度以及需要隐藏的 PID。</p>
|
||||
<p>接下来的代码部分,程序定义了一个名为 "linux_dirent64" 的结构体,这个结构体代表一个 Linux 目录项。然后程序定义了两个函数,"handle_getdents_enter" 和 "handle_getdents_exit",这两个函数分别在 getdents64 系统调用的入口和出口被调用,用于实现对目录项的操作。</p>
|
||||
<p>其中,"rb" 是一个 Ringbuffer 类型的 map,它用于从内核向用户态传递消息。Ringbuffer 是一种能在内核和用户态之间高效传递大量数据的数据结构。</p>
|
||||
<p>"map_buffs" 是一个 Hash 类型的 map,它用于存储目录项(dentry)的缓冲区地址。</p>
|
||||
<p>"map_bytes_read" 是另一个 Hash 类型的 map,它用于在数据循环中启用搜索。</p>
|
||||
<p>"map_to_patch" 是另一个 Hash 类型的 map,存储了需要被修改的目录项(dentry)的地址。</p>
|
||||
<p>"map_prog_array" 是一个 Prog Array 类型的 map,它用于保存程序的尾部调用。</p>
|
||||
<p>程序中的 "target_ppid" 和 "pid_to_hide_len"、"pid_to_hide" 是几个重要的全局变量,它们分别存储了目标父进程的 PID、需要隐藏的 PID 的长度以及需要隐藏的 PID。</p>
|
||||
<p>接下来的代码部分,程序定义了一个名为 "linux_dirent64" 的结构体,这个结构体代表一个 Linux 目录项。然后程序定义了两个函数,"handle_getdents_enter" 和 "handle_getdents_exit",这两个函数分别在 getdents64 系统调用的入口和出口被调用,用于实现对目录项的操作。</p>
|
||||
<pre><code class="language-c">
|
||||
// Optional Target Parent PID
|
||||
const volatile int target_ppid = 0;
|
||||
@@ -262,7 +262,7 @@ const volatile char pid_to_hide[max_pid_len];
|
||||
// unsigned char d_type; /* File type */
|
||||
// char d_name[]; /* Filename (null-terminated) */ };
|
||||
// int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);
|
||||
SEC("tp/syscalls/sys_enter_getdents64")
|
||||
SEC("tp/syscalls/sys_enter_getdents64")
|
||||
int handle_getdents_enter(struct trace_event_raw_sys_enter *ctx)
|
||||
{
|
||||
size_t pid_tgid = bpf_get_current_pid_tgid();
|
||||
@@ -296,7 +296,7 @@ int handle_getdents_enter(struct trace_event_raw_sys_enter *ctx)
|
||||
<p>在接下来的代码段中,我们将要实现在 <code>getdents64</code> 系统调用返回时的处理。我们主要的目标就是找到我们想要隐藏的进程,并且对目录项进行修改以实现隐藏。</p>
|
||||
<p>我们首先定义了一个名为 <code>handle_getdents_exit</code> 的函数,它将在 <code>getdents64</code> 系统调用返回时被调用。</p>
|
||||
<pre><code class="language-c">
|
||||
SEC("tp/syscalls/sys_exit_getdents64")
|
||||
SEC("tp/syscalls/sys_exit_getdents64")
|
||||
int handle_getdents_exit(struct trace_event_raw_sys_exit *ctx)
|
||||
{
|
||||
size_t pid_tgid = bpf_get_current_pid_tgid();
|
||||
@@ -372,7 +372,7 @@ int handle_getdents_exit(struct trace_event_raw_sys_exit *ctx)
|
||||
<p>在这个函数中,我们首先获取了当前进程的 PID 和线程组 ID,然后检查系统调用是否读取到了目录的内容。如果没有读取到内容,我们就直接返回。</p>
|
||||
<p>然后我们从 <code>map_buffs</code> 这个 map 中获取 <code>getdents64</code> 系统调用入口处保存的目录内容的地址。如果我们没有保存过这个地址,那么就没有必要进行进一步的处理。</p>
|
||||
<p>接下来的部分有点复杂,我们用了一个循环来迭代读取目录的内容,并且检查是否有我们想要隐藏的进程的 PID。如果我们找到了,我们就用 <code>bpf_tail_call</code> 函数跳转到 <code>handle_getdents_patch</code> 函数,进行实际的隐藏操作。</p>
|
||||
<pre><code class="language-c">SEC("tp/syscalls/sys_exit_getdents64")
|
||||
<pre><code class="language-c">SEC("tp/syscalls/sys_exit_getdents64")
|
||||
int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)
|
||||
{
|
||||
// Only patch if we've already checked and found our pid's folder to hide
|
||||
@@ -398,10 +398,10 @@ int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)
|
||||
char filename[max_pid_len];
|
||||
bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp_previous->d_name);
|
||||
filename[pid_to_hide_len-1] = 0x00;
|
||||
bpf_printk("[PID_HIDE] filename previous %s\n", filename);
|
||||
bpf_printk("[PID_HIDE] filename previous %s\n", filename);
|
||||
bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp->d_name);
|
||||
filename[pid_to_hide_len-1] = 0x00;
|
||||
bpf_printk("[PID_HIDE] filename next one %s\n", filename);
|
||||
bpf_printk("[PID_HIDE] filename next one %s\n", filename);
|
||||
|
||||
// Attempt to overwrite
|
||||
short unsigned int d_reclen_new = d_reclen_previous + d_reclen;
|
||||
@@ -439,7 +439,7 @@ int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)
|
||||
<pre><code class="language-c"> skel = pidhide_bpf__open();
|
||||
if (!skel)
|
||||
{
|
||||
fprintf(stderr, "Failed to open BPF program: %s\n", strerror(errno));
|
||||
fprintf(stderr, "Failed to open BPF program: %s\n", strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
</code></pre>
|
||||
@@ -449,7 +449,7 @@ int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)
|
||||
{
|
||||
env.pid_to_hide = getpid();
|
||||
}
|
||||
sprintf(pid_to_hide, "%d", env.pid_to_hide);
|
||||
sprintf(pid_to_hide, "%d", env.pid_to_hide);
|
||||
strncpy(skel->rodata->pid_to_hide, pid_to_hide, sizeof(skel->rodata->pid_to_hide));
|
||||
skel->rodata->pid_to_hide_len = strlen(pid_to_hide) + 1;
|
||||
skel->rodata->target_ppid = env.target_ppid;
|
||||
@@ -458,13 +458,13 @@ int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)
|
||||
<pre><code class="language-c"> err = pidhide_bpf__load(skel);
|
||||
if (err)
|
||||
{
|
||||
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
|
||||
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
|
||||
goto cleanup;
|
||||
}
|
||||
</code></pre>
|
||||
<p>最后,我们等待并处理由 eBPF 程序发送的事件。这个过程是通过调用 <code>ring_buffer__poll</code> 函数实现的。在这个过程中,我们每隔一段时间就检查一次环形缓冲区中是否有新的事件。如果有,我们就调用 <code>handle_event</code> 函数来处理这个事件。</p>
|
||||
<pre><code class="language-c">printf("Successfully started!\n");
|
||||
printf("Hiding PID %d\n", env.pid_to_hide);
|
||||
<pre><code class="language-c">printf("Successfully started!\n");
|
||||
printf("Hiding PID %d\n", env.pid_to_hide);
|
||||
while (!exiting)
|
||||
{
|
||||
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
|
||||
@@ -476,7 +476,7 @@ while (!exiting)
|
||||
}
|
||||
if (err < 0)
|
||||
{
|
||||
printf("Error polling perf buffer: %d\n", err);
|
||||
printf("Error polling perf buffer: %d\n", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -487,9 +487,9 @@ while (!exiting)
|
||||
{
|
||||
const struct event *e = data;
|
||||
if (e->success)
|
||||
printf("Hid PID from program %d (%s)\n", e->pid, e->comm);
|
||||
printf("Hid PID from program %d (%s)\n", e->pid, e->comm);
|
||||
else
|
||||
printf("Failed to hide PID from program %d (%s)\n", e->pid, e->comm);
|
||||
printf("Failed to hide PID from program %d (%s)\n", e->pid, e->comm);
|
||||
return 0;
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
Reference in New Issue
Block a user