让我看看你的系统调用 - ebpf常见挂载点

基本上所有设备都能用的常见挂载点,目前考虑6.1内核,可惜的是pixel 6 的6.1内核还不支持fentry/fexit

Kprobe

Kprobe是比较常见的附加到内核函数的方法,kprobe/kretprobe分别负责在进入前和返回前hook内核函数
用法是使用SEC(kprobe/name),直接使用内核函数的签名即可,比如SEC(kprobe/kernel_clone),这个签名可以在/proc/kallsym中获取
获取调用参数有两种方案,最基础的方法是使用struct pt_regs *ctx作为参数,这个ctx结构体是具有架构依赖的,然后使用PT_REGS_PARM*(ctx)宏读取参数,arm64架构中这个可以填1~8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
SEC("kprobe/kernel_clone")
int bpf_prog(struct pt_regs *ctx) {
const u64 CLONE_THREAD = 0x00010000;
struct kernel_clone_args *args_ptr =
(struct kernel_clone_args *)PT_REGS_PARM1(ctx);
struct kernel_clone_args args;
int ret = bpf_probe_read_kernel(&args, sizeof(args), args_ptr);
if (ret != 0) {
bpf_printk("Failed to read kernel_clone_args: %d\n", ret);
return ret;
}
u64 flags = args.flags;
u64 id = bpf_get_current_pid_tgid();
if (args.kthread == 0) {
bpf_printk("new %s created by PID : %d",
flags & CLONE_THREAD ? "thread" : "process", id >> 32);
}
return 0;
}

char _license[] SEC("license") = "GPL";

这就是一个比较简单的探测kernel_clone这个函数的kprobe探针,使用PT_REGS_PARM1读取第一个参数(每个具体参数要查阅内核源代码中的定义),使用bpf_probe_read_kernel读取这个指针指向的参数列表结构体(直接解引用会被检查器拒绝加载,因为这是不安全的行为),然后根据flag这个掩码参数简单区分下进程和线程,通过bpf_get_current_pid_tgid获取调用clone的进程pid,使用printk输出到trace_pipe中

可以看到捕捉到了adb服务的活动,和新创建的shell的活动
除了直接声明函数还可以使用BPF_KPROBE这个宏,上述的代码可以写成如下等价形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
SEC("kprobe/kernel_clone")
int BPF_KPROBE(handle_enter_kernel_clone, struct kernel_clone_args *args_ptr) {
const u64 CLONE_THREAD = 0x00010000;
struct kernel_clone_args args;
int ret = bpf_probe_read_kernel(&args, sizeof(args), args_ptr);
if (ret != 0) {
bpf_printk("Failed to read kernel_clone_args: %d\n", ret);
return ret;
}
u64 flags = args.flags;
u64 id = bpf_get_current_pid_tgid();
if (args.kthread == 0) {
bpf_printk("new %s created by PID : %d",
flags & CLONE_THREAD ? "thread" : "process", id >> 32);
}
return 0;
}

char _license[] SEC("license") = "GPL";

这个宏的第一个参数是声明的函数名,之后接受最多五个参数,作为前5个传参寄存器中读取的参数(PT_REGS_PARM1~5),写起来会比第一种写法简洁一点,基本所有探针类型都有类似的宏
以及对于这些宏,触发探针时的上下文环境会被以*ctx保存,可以通过ctx访问上下文环境,所以不要在函数中再次使用ctx作为变量名
另外还有一种libbpf提供的语法糖ksyscall,因为linux syscall函数的命名通常遵循一定标准,ksyscall可以根据部分提供的函数名自动选择对应的函数附加(比如ksyscall/openat就会选择一种openat的实现),不过由于syscall通常由多种实现所以这种方法很容易漏掉调用,所以其实不大好用

Uprobe

Uprobe是用来hook用户态函数的探针,原理是把目标地址的指令替换成int3(其他架构上就是对应的中断指令)跳到内核态执行hook逻辑(类似条件断点脚本),但是和调试器相比隐蔽性更强,这篇文章中总结了一些Uprobe的对抗手段https://www.cnxct.com/defeating-ebpf-uprobe-monitoring/
总结一下的话就是一下几点

  • 扫描中断指令
  • 在maps中扫描[uprobe]内存段(用于储存被中断替换的指令)
  • 将保护目标.text段的权限设置为VM_WRITE使得uprobe的valid_vma函数校验不通过(前提是要对目标二进制文件有修改权限)

Uprobe可以做到任意位置插入,因为只有一条断点指令所以也不会出现短指令问题(说的就是你frida),监测性能也会高很多,下面是几种UPROBE探针的示例

完整demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// bpf_test.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
SEC("uprobe")
int BPF_UPROBE(hookTestFunc1, int a, int b) {
bpf_printk("hookTestFunc1 called with args: %d, %d\n", a, b);
return 0;
}
SEC("uprobe")
int BPF_UPROBE(hookTestFunc2, int a, int b) {
bpf_printk("hookTestFunc2 called with args: %d, %d\n", a, b);
return 0;
}
SEC("uretprobe")
int BPF_URETPROBE(hookTestFunc3, int ret) {
bpf_printk("hookTestFunc3 called with return value: %d\n", ret);
return 0;
}
SEC("uprobe")
int BPF_UPROBE(insideFuncHook) {
bpf_printk("insideFuncHook called\n");
bpf_printk("w8 = %lld ,w9 = %lld", ctx->regs[8], ctx->regs[9]);
return 0;
}
SEC("uprobe")
int BPF_UPROBE(modifyArgProbe) {
bpf_printk("modify args...");
int valueToWrite = 114514;
bpf_probe_write_user((void *)(ctx->sp + 12), (void *)&valueToWrite,
sizeof(int));
valueToWrite = 1919810;
bpf_probe_write_user((void *)(ctx->sp + 8), (void *)&valueToWrite,
sizeof(int));
return 0;
}
char _license[] SEC("license") = "GPL";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// bpf_test_loader.cpp
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2021 Sartura
* Based on minimal.c by Facebook */

#include "bpf_test.skel.h"
#include <bpf/libbpf.h>
#include <cerrno>
#include <csignal>
#include <cstdio>
#include <cstring>
#include <format>
#include <sys/resource.h>
#include <unistd.h>
static int libbpf_print_fn(enum libbpf_print_level level, const char *format,
va_list args) {
return vfprintf(stderr, format, args);
}
size_t getFunctionOffsetReal(const char *soName, size_t staticOffset, int pid) {
char path[256];
snprintf(path, sizeof(path), "/proc/%d/maps", pid);
FILE *fp = fopen(path, "r");
if (!fp) {
perror("fopen");
return -1;
}
size_t start, end, base, inode, imageBase = -1;
char buf[5], dn[6], filePath[256];
bool found = 0;

while (fscanf(fp, "%zx-%zx %s %zx %s %lu %s\n", &start, &end, buf, &base, dn,
&inode, filePath) == 7) {
if (imageBase == -1 && strstr(filePath, soName)) {
imageBase = start;
}
if (buf[2] == 'x' && strstr(filePath, soName) != nullptr) {
printf("target excuteable segment found : %zx-%zx base: %zx file: %s\n",
start, end, base, filePath);
found = 1;
break;
}
}
fclose(fp);
if (!found) {
printf("can't find %s in /proc/%d/maps\n", soName, pid);
return -1;
}
return imageBase + staticOffset - start + base;
}
int main(int argc, char **argv) {
if (argc != 2) {
printf("need exact one pid\n");
return 1;
}
int pid = atoi(argv[1]);
struct bpf_test_bpf *skel;
int err;

/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);

/* Open load and verify BPF application */
skel = bpf_test_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}

err = bpf_test_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
printf("binary path: ");
printf("%s\n", std::format("/proc/{}/exe", pid).c_str());
skel->links.hookTestFunc1 =
bpf_program__attach_uprobe(skel->progs.hookTestFunc1, false, pid,
std::format("/proc/{}/exe", pid).c_str(),
getFunctionOffsetReal("test", 0x1830, pid));
if (!skel->links.hookTestFunc1) {
fprintf(stderr, "Failed to attach uprobes1\n");
goto cleanup;
}
skel->links.hookTestFunc2 =
bpf_program__attach_uprobe(skel->progs.hookTestFunc2, false, pid,
std::format("/proc/{}/exe", pid).c_str(),
getFunctionOffsetReal("test", 0x1864, pid));
if (!skel->links.hookTestFunc2) {
fprintf(stderr, "Failed to attach uprobes2\n");
goto cleanup;
}
skel->links.hookTestFunc3 =
bpf_program__attach_uprobe(skel->progs.hookTestFunc3, true, pid,
std::format("/proc/{}/exe", pid).c_str(),
getFunctionOffsetReal("test", 0x1884, pid));
if (!skel->links.hookTestFunc3) {
fprintf(stderr, "Failed to attach uprobes3\n");
goto cleanup;
}
skel->links.insideFuncHook =
bpf_program__attach_uprobe(skel->progs.insideFuncHook, false, pid,
std::format("/proc/{}/exe", pid).c_str(),
getFunctionOffsetReal("test", 0x1878, pid));
if (!skel->links.insideFuncHook) {
fprintf(stderr, "Failed to attach insideFuncHook\n");
goto cleanup;
}
skel->links.modifyArgProbe =
bpf_program__attach_uprobe(skel->progs.modifyArgProbe, false, pid,
std::format("/proc/{}/exe", pid).c_str(),
getFunctionOffsetReal("test", 0x1870, pid));
if (!skel->links.modifyArgProbe) {
fprintf(stderr, "Failed to attach modifyArgProbe\n");
goto cleanup;
}
printf("while 1 \n");
while (1)
;
cleanup:
bpf_test_bpf__destroy(skel);
return -err;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 测试程序 test.cpp
#include <cstdio>
#include <unistd.h>
using namespace std;
struct Args {
int a;
int b;
int c;
};
void testFunc1(int a, int b) { printf("atest :%d %d\n", a, b); }
int testFunc2(int a, int b) { return a + b; }
int testFunc3(Args args) { return args.a + args.b + args.c; }
int main() {
while (1) {
testFunc1(1, 2);
printf("%d\n", testFunc2(3, 4));
printf("%d\n", testFunc3({1, 2, 3}));
printf("next trigger after 5s\n");
sleep(5);
}

return 0;
}

关于hook地址获取

不是很清楚linux内核的开发者是怎么想的,uprobe接受的偏移是目标地址相对其所在段起点的偏移再加上所在段的偏移值,而不是相对其所在二进制文件起点的偏移,通常就是理解为相对.text段起点的偏移加上maps中读取到的text段的偏移值

如下图所示,权限掩码带x的段就是可执行的段,我们在ida里看到的是这样的

这里.plt和.text在运行时被合并了,不过这个不重要,我们的目标函数如下图所示,显然是在.text段里的,然后0x1830这个地址,是base为0的情况下,相对二进制文件起始位置的偏移,也就是我们在frida等框架中使用的函数地址,在uprobe中不能直接传这个地址,在运行时,前面长度总计0x730的其他段会因为对齐变为长0x1000,然后经过计算,实际上要传入的地址应该是0x830(因为读取到的段偏移值为00000),也就是 目标实际地址(相对文件起点的偏移+基址)-所在段的基地址+所在段的偏移值

显然这一坨不可能每次手算然后硬编码,我们用一个函数把反编译工具中获取的偏移值转换为uprobe认可的偏移值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
size_t getFunctionOffsetReal(const char *soName, size_t staticOffset, int pid) {
char path[256];
snprintf(path, sizeof(path), "/proc/%d/maps", pid); // 读取maps获得内存布局
FILE *fp = fopen(path, "r");
if (!fp) {
perror("fopen");
return -1;
}
size_t start, end, base, inode, imageBase = -1;
char buf[5], dn[6], filePath[256];
bool found = 0;

while (fscanf(fp, "%zx-%zx %s %zx %s %lu %s\n", &start, &end, buf, &base, dn,
&inode, filePath) == 7) { // 解析maps各字段,重点关注start,buf,base,filePath
if (imageBase == -1 && strstr(filePath, soName)) { // 这里采用匹配字串的方法,目标二进制文件第一次出现的段的起点肯定是这个二进制文件的基地址
imageBase = start;
}
if (buf[2] == 'x' && strstr(filePath, soName) != nullptr) { //寻找可执行段,应该一个二进制文件在maps通常只有一个可执行段吧,大概,可能加上内存区间的校验会更保险一点(start<=staticOffset+imageBase<=end)
printf("target excuteable segment found : %zx-%zx base: %zx file: %s\n",
start, end, base, filePath);
found = 1;
break;
}
}
fclose(fp);
if (!found) {
printf("can't find %s in /proc/%d/maps\n", soName, pid);
return -1;
}
return imageBase + staticOffset - start + base; //根据我们的公式计算出正确地址
}

hookTestFunc1

1
2
3
4
5
6
7
8
9
10
11
// probe
SEC("uprobe")
int BPF_UPROBE(hookTestFunc1, int a, int b) {
bpf_printk("hookTestFunc1 called with args: %d, %d\n", a, b);
return 0;
}
// load
skel->links.hookTestFunc1 =
bpf_program__attach_uprobe(skel->progs.hookTestFunc1, false, pid,
std::format("/proc/{}/exe", pid).c_str(),
getFunctionOffsetReal("test", 0x1830, pid));

uprobe也可以使用BPF_UPROBE宏,事实上这个宏就是BPF_KPROBE的别名(ebpf将uprobe和kprobe视为等价的)
然后我们采用手动链接的方式(自动链接不能指定地址),注意这里hook可执行文件的话,我们的二进制文件要选/proc/pid/exe,这是对源文件的一个符号链接(快捷方式),这样我们就不用自己输入路径了,我们指定pid只hook测试文件,并设置uretprobe为false

hookTestFunc2

1
int testFunc2(int *a, int *b) { return *a + *b; } // 突然发现test2和test1一样,遂修改
1
2
3
4
5
6
7
8
9
10
11
12
13
SEC("uprobe")
int BPF_UPROBE(hookTestFunc2, int *a, int *b) {
int argA, argB;
bpf_probe_read_user(&argA, sizeof(int), a);
bpf_probe_read_user(&argB, sizeof(int), b);
bpf_printk("hookTestFunc2 called with args: %d, %d\n", argA, argB);
return 0;
}

skel->links.hookTestFunc2 =
bpf_program__attach_uprobe(skel->progs.hookTestFunc2, false, pid,
std::format("/proc/{}/exe", pid).c_str(),
getFunctionOffsetReal("test", 0x1864, pid));

如果要从用户空间读内存,则需要使用bpf_probe_read_user,原因和上文的bpf_probe_read_kernel同理

hookTestFunc3

1
2
3
4
5
6
7
8
9
10
SEC("uretprobe")
int BPF_URETPROBE(hookTestFunc3, int ret) {
bpf_printk("hookTestFunc3 called with return value: %d\n", ret);
return 0;
}

skel->links.hookTestFunc3 =
bpf_program__attach_uprobe(skel->progs.hookTestFunc3, true, pid,
std::format("/proc/{}/exe", pid).c_str(),
getFunctionOffsetReal("test", 0x1884, pid));

uretprobe直接在宏里定义返回值即可,libbpf会根据 调用约定 自动解析返回值对应的寄存器并读取到ret里
这里就要设置uretprobe为true了,注意uretprobe只能设置在函数开头,然后会把函数return的地址替换成 蹦床 的地址(用于执行hook逻辑) 并保存原return地址,在执行完hook逻辑后跳回原执行流,因此uretprobe只能由return触发,灵活性要较差

insideFuncHook

1
2
3
4
5
6
7
8
9
10
11
SEC("uprobe")
int BPF_UPROBE(insideFuncHook) {
bpf_printk("insideFuncHook called\n");
bpf_printk("w8 = %lld ,w9 = %lld", ctx->regs[8], ctx->regs[9]);
return 0;
}

skel->links.insideFuncHook =
bpf_program__attach_uprobe(skel->progs.insideFuncHook, false, pid,
std::format("/proc/{}/exe", pid).c_str(),
getFunctionOffsetReal("test", 0x1878, pid));


这里我们想读w8和w9寄存器的值,直接在对应位置hook然后读取ctx里regs[8]和regs[9]即可

modifyArgProbe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SEC("uprobe")
int BPF_UPROBE(modifyArgProbe) {
bpf_printk("modify args...");
int valueToWrite = 114514;
bpf_probe_write_user((void *)(ctx->sp + 12), (void *)&valueToWrite,
sizeof(int));
valueToWrite = 1919810;
bpf_probe_write_user((void *)(ctx->sp + 8), (void *)&valueToWrite,
sizeof(int));
return 0;
}
char _license[] SEC("license") = "GPL";

skel->links.modifyArgProbe =
bpf_program__attach_uprobe(skel->progs.modifyArgProbe, false, pid,
std::format("/proc/{}/exe", pid).c_str(),
getFunctionOffsetReal("test", 0x1870, pid));


注意到对uprobe来说寄存器是只读的,所以如果参数通过寄存器传递,我们没办法直接替换参数,只能通过这种修改栈变量的方式简介修改参数,操作性比较差

运行情况


几点不足

uprobe最大的问题是没法修改寄存器,导致其很难影响用户空间的行为,同时uretprobe虽然可以通过bpf_override_return替换返回值,但前提是内核开启了CONFIG_KPROBE_OVERRIDE,而pixel 6的内核是未开启的,必须得重编译,如此下来用uprobe做拦截操作就非常不优雅,stackplz的解决方案是联动frida使用ipc调用来干涉用户空间,个人也认为联动frida或者集成ptrace做修改操作会比较好,而且集成ptrace可以在需要修改时才附加ptrace,可以同时发挥uprobe的隐蔽性和ptrace的修改能力

Tracepoint

tracepoint

Tracepoint其实没啥好说的,就是内核预埋的一些检测点,内核支持的Tracepoint全部在/sys/kernel/tracing/events目录下了,对应的区段名为SEC(tp/catalog/name),比如sys_enter对应的就是SEC(tp/raw_syscalls/sys_enter),对应事件目录下的format文件描述了这个检测点获取的参数列表
对逆向分析而言我们主要关注raw_syscalls,里面的sys_enter和sys_exit是所有libc函数调用syscall时都到经过的

1
2
3
4
5
6
7
8
9
10
11
12
13
oriole:/sys/kernel/tracing # cat events/raw_syscalls/sys_enter/format
name: sys_enter
ID: 23
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;

field:long id; offset:8; size:8; signed:1;
field:unsigned long args[6]; offset:16; size:48; signed:0;

print fmt: "NR %ld (%lx, %lx, %lx, %lx, %lx, %lx)", REC->id, REC->args[0], REC->args[1], REC->args[2], REC->args[3], REC->args[4], REC->args[5]
1
2
3
4
5
6
7
8
9
10
11
12
name: sys_exit
ID: 24
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;

field:long id; offset:8; size:8; signed:1;
field:long ret; offset:16; size:8; signed:1;

print fmt: "NR %ld = %ld", REC->id, REC->ret

这里重点关注id,args,ret即可,这里其实就是svc指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

SEC("tp/raw_syscalls/sys_enter")
int handle_sys_enter(struct trace_event_raw_sys_enter *args) {
int id = args->id;
// unsigned long callArgs[6];
// bpf_probe_read_kernel(callArgs, sizeof(callArgs),
// (void *)BPF_CORE_READ(args, args));
bpf_printk("sys_enter: id=%d, pid=%d\n", id,
bpf_get_current_pid_tgid() >> 32);
return 0;
}
SEC("tp/raw_syscalls/sys_exit")
int handle_sys_exit(struct trace_event_raw_sys_exit *args) {
int id = args->id;
long ret = args->ret;
bpf_printk("sys_exit: id=%d, pid=%d, ret=%ld\n", id,
bpf_get_current_pid_tgid() >> 32, ret);
return 0;
}
char _license[] SEC("license") = "GPL";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2021 Sartura
* Based on minimal.c by Facebook */

#include "bpf_test.skel.h"
#include <bpf/libbpf.h>
#include <cerrno>
#include <csignal>
#include <cstdio>
#include <cstring>
#include <format>
#include <sys/resource.h>
#include <unistd.h>
static int libbpf_print_fn(enum libbpf_print_level level, const char *format,
va_list args) {
return vfprintf(stderr, format, args);
}
int main(int argc, char **argv) {
// if (argc != 2) {
// printf("need exact one pid\n");
// return 1;
// }
// int pid = atoi(argv[1]);
struct bpf_test_bpf *skel;
int err;

/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);

/* Open load and verify BPF application */
skel = bpf_test_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
err = bpf_test_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}

printf("while 1 \n");
while (1)
;
cleanup:
bpf_test_bpf__destroy(skel);
return -err;
}


之前的格式表中的前8个字节的common_字段是不能直接读的,要用bpf_read_kernelBPF_CORE_READ读,下面的字段都是可以直接读取的
运行可以发现调用了101,64两个调用号,查表发现是nanosleep , write,符合我们的预期,之后根据不同调用号写case解析数据就可以实现stackplz相同的监控功能

btf_raw_tracepoint

btf_raw_tracepoint是相对tracepoint更加原始的检测点,使用的内存段是SEC(tp_btf/name)btf_raw_tracepointraw_tracepoint类似,访问的都是调用的原始参数,即直接返回调用号和寄存器信息pt_regs,而不是类似tracepoint的返回整理过的参数结构体,好处是通过raw_tracepoint可以获取全部的寄存器信息而不只是前6个,btf_的意思是使用btf类型增强兼容性,在应用中通常使用btf_raw_tracepoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

SEC("tp_btf/sys_enter")
int handle_enter(u64 *ctx) {
long int syscall_id = ctx[1];
struct pt_regs *regs = (struct pt_regs *)ctx[0];
if (syscall_id == 0x40) { // write
bpf_printk("%s %d\n", regs->regs[1], regs->regs[2]);
}
return 0;
}

char _license[] SEC("license") = "GPL";

上述就是一个简单的监测write的demo,其中ctx[0]是上下文信息(寄存器),ctx[1]是调用号,具体的参数需要自己查syscall表,arm64write的话就是x0fdx1bufx2len;

Syscall

如果内核开启了CONFIG_FTRACE_SYSCALLS的话就可以使用,使用方法是SEC(tp/syscall/name)宏,大部分手机的原厂镜像应该是不支持的

fentry/fexit

如果内核支持fentry可以使用,和kprobe语法类似,SEC(fentry/name),与kprobe不同的是fentry/fexit对每个内核函数提供了参数结构体,可以直接使用Args->fieldname的方法访问参数

prenext

让我看看你的系统调用 - ebpf常见挂载点

https://sgsgsama.github.io/ctf/ebpf/ebpf0x2/

Author

SGSG

Posted on

2025-06-28

Updated on

2025-08-25

Licensed under