我对 eBPF 世界还很陌生,我从 https://eunomia.dev/tutorials/0-introduce/开始学习。
我按照示例进行操作,发现了https://eunomia.dev/tutorials/24-hide/。这是关于隐藏 PID 的教程,我在这个示例中发现有趣的一件事是使用函数
bpf_tail_call()
。
在下面的代码中如果PID匹配的话,
int j = 0;
for (j = 0; j < pid_to_hide_len; j++)
{
if (filename[j] != pid_to_hide[j])
{
break;
}
}
if (j == pid_to_hide_len)
{
// ***********
// We've found the folder!!!
// Jump to handle_getdents_patch so we can remove it!
// ***********
bpf_map_delete_elem(&map_bytes_read, &pid_tgid);
bpf_map_delete_elem(&map_buffs, &pid_tgid);
bpf_tail_call(ctx, &map_prog_array, PROG_02);
//
}
bpf_map_update_elem(&map_to_patch, &pid_tgid, &dirp, BPF_ANY);
bpos += d_reclen;
}
然后它会调用以下
_patch
函数,
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
size_t pid_tgid = bpf_get_current_pid_tgid();
long unsigned int *pbuff_addr = bpf_map_lookup_elem(&map_to_patch, &pid_tgid);
if (pbuff_addr == 0)
{
return 0;
}
// Unlink target, by reading in previous linux_dirent64 struct,
// and setting it's d_reclen to cover itself and our target.
// This will make the program skip over our folder.
long unsigned int buff_addr = *pbuff_addr;
struct linux_dirent64 *dirp_previous = (struct linux_dirent64 *)buff_addr;
short unsigned int d_reclen_previous = 0;
bpf_probe_read_user(&d_reclen_previous, sizeof(d_reclen_previous), &dirp_previous->d_reclen);
struct linux_dirent64 *dirp2 = (struct linux_dirent64 *)(buff_addr + d_reclen_previous);
short unsigned int d_reclen2 = 0;
bpf_probe_read_user(&d_reclen2, sizeof(d_reclen2), &dirp2->d_reclen);
// Debug print
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_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);
// Attempt to overwrite
short unsigned int d_reclen_new = d_reclen_previous + d_reclen2;
long ret = bpf_probe_write_user(&dirp_previous->d_reclen, &d_reclen_new, sizeof(d_reclen_new));
// Send an event
struct event *e;
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (e)
{
e->success = (ret == 0);
e->pid = (pid_tgid >> 32);
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
}
bpf_map_delete_elem(&map_to_patch, &pid_tgid);
return 0;
}
我不明白拥有
_patch
函数并使用 bpf_tail_call()
调用它的概念,这将导致 getdents64
系统调用返回。
在一次尝试中,我试图摆脱
bpf_tail_call()
,将逻辑从 handle_getdents_patch()
移至用 bpf_tail_call()
替换。下面是我更改后handle_getdents_exit()
的代码的样子,
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();
int total_bytes_read = ctx->ret;
// if bytes_read is 0, everything's been read
if (total_bytes_read <= 0)
{
return 0;
}
// Check we stored the address of the buffer from the syscall entry
long unsigned int *pbuff_addr = bpf_map_lookup_elem(&map_buffs, &pid_tgid);
if (pbuff_addr == 0)
{
return 0;
}
// All of this is quite complex, but basically boils down to
// Calling 'handle_getdents_exit' in a loop to iterate over the file listing
// in chunks of 200, and seeing if a folder with the name of our pid is in there.
// If we find it, use 'bpf_tail_call' to jump to handle_getdents_patch to do the actual
// patching
long unsigned int buff_addr = *pbuff_addr;
struct linux_dirent64 *dirp = 0;
int pid = pid_tgid >> 32;
short unsigned int d_reclen = 0;
char filename[MAX_PID_LEN];
unsigned int bpos = 0;
unsigned int *pBPOS = bpf_map_lookup_elem(&map_bytes_read, &pid_tgid);
if (pBPOS != 0)
{
bpos = *pBPOS;
bpf_printk("bpos = *pBPOS -------> %d\n", bpos);
}
for (int i = 0; i < 200; i++)
{
if (bpos >= total_bytes_read)
{
break;
}
dirp = (struct linux_dirent64 *)(buff_addr + bpos);
bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen);
bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp->d_name);
bpf_printk("> d_reclen : %d - filename : %s\n", pid_to_hide_len, filename);
int j = 0;
for (j = 0; j < pid_to_hide_len; j++)
{
if (filename[j] != pid_to_hide[j])
{
break;
}
}
if (j == pid_to_hide_len)
{
// ***********
// We've found the folder!!!
// Jump to handle_getdents_patch so we can remove it!
// ***********
bpf_map_delete_elem(&map_bytes_read, &pid_tgid);
bpf_map_delete_elem(&map_buffs, &pid_tgid);
//
long unsigned int buff_addr = *pbuff_addr;
struct linux_dirent64 *dirp_previous = dirp;//(struct linux_dirent64 *)buff_addr;
short unsigned int d_reclen_previous = 0;
bpf_probe_read_user(&d_reclen_previous, sizeof(d_reclen_previous), &dirp_previous->d_reclen);
struct linux_dirent64 *dirp2 = (struct linux_dirent64 *)(buff_addr + d_reclen_previous);
short unsigned int d_reclen2 = 0;
bpf_probe_read_user(&d_reclen2, sizeof(d_reclen2), &dirp2->d_reclen);
// Attempt to overwrite
short unsigned int d_reclen_new = d_reclen_previous + d_reclen2;
long ret = bpf_probe_write_user(&dirp_previous->d_reclen, &d_reclen_new, sizeof(d_reclen_new));
// bpf_tail_call(ctx, &map_prog_array, PROG_02);
//
}
bpf_map_update_elem(&map_to_patch, &pid_tgid, &dirp, BPF_ANY);
bpos += d_reclen;
}
// If we didn't find it, but there's still more to read,
// jump back the start of this function and keep looking
if (bpos < total_bytes_read)
{
bpf_map_update_elem(&map_bytes_read, &pid_tgid, &bpos, BPF_ANY);
bpf_tail_call(ctx, &map_prog_array, PROG_01);
}
bpf_map_delete_elem(&map_bytes_read, &pid_tgid);
bpf_map_delete_elem(&map_buffs, &pid_tgid);
return 0;
}
问题在于,进行此更改后,系统速度超级慢,并且 PID 未隐藏。
我想要实现的是能够隐藏多个 PID,但是对
bpf_tail_call()
的调用使系统调用在第一场比赛中返回,所以这是我的问题,
handle_getdents_patch()
并将所有代码放在 handle_getdents_exit()
中我做错了什么?为什么隐藏 PID(即使是一个 PID)会被破坏,系统过载又是为了什么?PS:我尝试遵守所有提问规则,希望不要受到惩罚。
谢谢
TL;DR. 您没有保留程序的控制流。尾部调用不会返回给调用者,因此您应该在
handle_getdents_patch
逻辑之后退出。
你想做什么
如果我理解正确的话:
sys_exit_getdents64
以找到要隐藏的文件夹;bpf_probe_write_user
编辑结果并将事件发送到用户空间。您要做的就是通过将目标代码 (
handle_getdents_patch
) 移动到调用者 (handle_getdents_exit
) 中来避免第一次尾部调用。
尾部调用如何工作
尾部调用 (
bpf_tail_call
) 与普通函数调用不同。假设程序A尾调用程序B。程序B完全执行后,控制权不会返回到程序A。我们将直接退出。
所以在原程序的情况下,当找到文件夹时
handle_getdents_exit
会跳转到handle_getdents_patch
。一旦执行了handle_getdents_patch
,它就不会返回到handle_getdents_exit
来迭代其他文件夹。
如果您想在替换
handle_getdents_patch
时模仿上述尾部调用行为,则必须在复制的代码后面加上 return 0
。所以你的代码应该类似于:
if (j == pid_to_hide_len)
{
// ***********
// We've found the folder!!!
// Jump to handle_getdents_patch so we can remove it!
// ***********
bpf_map_delete_elem(&map_bytes_read, &pid_tgid);
bpf_map_delete_elem(&map_buffs, &pid_tgid);
[...]
// Attempt to overwrite
short unsigned int d_reclen_new = d_reclen_previous + d_reclen2;
long ret = bpf_probe_write_user(&dirp_previous->d_reclen, &d_reclen_new, sizeof(d_reclen_new));
[... other code from handle_getdents_patch...]
return 0;
}
我不知道这是否是故意的,但您也没有复制代码来发送事件(
bpf_ringbuf_reserve
和其他人)。