理论
`kprobe/uprobe` BPF 程序的参数是 `struct pt_regs *ctx` 。在内核中,对该参数的访问有限制:
/* bpf+kprobe programs can access fields of 'struct pt_regs' */
static bool kprobe_prog_is_valid_access(int off, int size, enum bpf_access_type type,
const struct bpf_prog *prog,
struct bpf_insn_access_aux *info)
{
if (off < 0 || off >= sizeof(struct pt_regs))
return false;
if (type != BPF_READ)
return false;
if (off % size != 0)
return false;
/*
* Assertion for 32 bit to make sure last 8 byte access
* (BPF_DW) to the last 4 byte member is disallowed.
*/
if (off + size > sizeof(struct pt_regs))
return false;
return true;
}
可以看到,对 `struct pt_regs *ctx` 的访问,只能读取,其它类型的操作非法。
在 BPF 程序中,你不能执行这样的操作去尝试修改函数的参数:
di = 0;
`BPF` 提供了一个辅助函数 `bpf_probe_write_user`,可以使用它来修改用户空间的内存。当然有一定的风险,参考 `man` 手册:
Attempt in a safe way to write len bytes from the
buffer src to dst in memory. It only works for
threads that are in user context, and dst must be a
valid user space address.
This helper should not be used to implement any
kind of security mechanism because of TOC-TOU
attacks, but rather to debug, divert, and
manipulate execution of semi-cooperative processes.
Keep in mind that this feature is meant for
experiments, and it has a risk of crashing the
system and running programs. Therefore, when an
eBPF program using this helper is attached, a
warning including PID and process name is printed
to kernel logs.
结合上述两点,我们可以推断出:
如果函数参数是标量类型,如 `int` 或 `bool` 等,通过 `di` / `si` 等寄存器传递,我们无法修改参数
如果函数参数是浮点类型,如 `float` 或 `double`,通过 `XMM` 寄存器传递,当前我们在 BPF 程序中无法访问
如果函数参数是指针类型,我们可以利用 `bpf_probe_write_user` 通过修改内存的方式修改函数参数
实际
纸上得来终觉浅,绝知此事要躬行。
int add(int *a, int *b)
{
return *a + *b;
}
int main()
{
while (1) {
int a = 1;
int b = 1;
fprintf(stderr, "%d\n", add(&a, &b));
sleep(1);
}
return 0;
}
对于上述程序,正常情况下,每秒会输出 `2\n`。
我们再尝试 Attach 一个 BPF 程序上去,如下:
SEC("uprobe/magic")
int BPF_KPROBE(magic, int *a, int *b)
{
int x = 666;
int y = 0;
bpf_probe_write_user(a, &x, sizeof(x));
bpf_probe_write_user(b, &y, sizeof(y));
return 0;
}
输出开始变成:
666
666
...
结语
我们可以在 BPF 程序中通过修改内存的方式修改函数的参数。
对于 Go 语言的早期版本,函数参数是通过栈(内存)来传递的,大家可以尝试探索一下。
参考:https://github.com/chenhengqi/bpf-examples/tree/main/modify-arguments-on-the-fly