在研究uprobe的过程中,发现了Linux内核一个好用的功能。
本来是打算研究一下,怎么写uprobe的代码,写好后怎么部署,然后又是怎么和相应的程序对应上的。但是资料太少了,基本上都是写使用bpftrace或者bcc的例子,但是都不是我想要的,后面考虑研究一下bpftrace或者bcc的源码。
不过在这个过程中,却发现了一个Linux系统内置的uprobe插桩的功能。
一般在/sys/kernel/debug/tracing/
目录下,有一个uprobe_events
文件,在Android设备下,没有debug
目录,所以路径一般为:/sys/kernel/tracing/uprobe_events
那么我们怎么通过这个文件进行uprobe插桩呢?
首先,我们写一个测试代码:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
一个很简单的,使用C语言开发的Hello World程序,编译一下:$ gcc test.c -o /tmp/test
接着,我们再写一个脚本:
#!/bin/bash
ADDR=`python3 -c 'from pwn import ELF,context;context.log_level="error";e=ELF("/tmp/test");print(hex(e.symbols["main"]))'`
echo "p /tmp/test:$ADDR %x0 %x1" > /sys/kernel/debug/tracing/uprobe_events
echo 1 | tee /sys/kernel/debug/tracing/events/uprobes/p_*/enable
echo 1 | tee /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace_pipe
把这个脚本运行起来,接着,我们再开一个终端,运行一下/tmp/test
,随后我们就能看到前一个终端里有输出了:
root@ubuntu:~# /tmp/test.sh
1
1
test-3326935 [001] ..... 1187528.405340: p_test_0x76c: (0xaaaaddbc076c) arg1=0x1 arg2=0xffffe00fb1d8
接下来,我来对这个解释一下,这个过程中我做的事情:
-
首先使用pwntools计算出/tmp/test的main函数的地址
-
因为我的测试环境是arm64的Linux,所以参数寄存器是
x0, x1......
,如果是amd64架构的,参数寄存器就是di, si, dx......
-
p /tmp/test:$ADDR %x0 %x1
的含意就是在/tmp/test程序的ADDR地址处进行插桩,插入的代码目的是输出第一个参数和第二个参数的值,所以我们可以从结果中看到arg1=0x1 arg2=0xffffe00fb1d8
,也就是说argc=0x1, argv = 0xffffe00fb1d8
-
当我们把上面的语句写入到
uprobe_events
中后,将会在events/uprobes
目录下生成相应的事件目录,默认情况下是以p_(filename)_(addr)
的形式命名,所以,在当前测试环境中,这个目录的路径为:/sys/kernel/debug/tracing/events/uprobes/p_test_0x76c/
-
把1写入到上面这个目录的enable文件中,表示激活该事件,接着就是把1写入到
tracing_on
,激活内核的日志跟踪功能。 -
最后,我们就能从
/sys/kernel/debug/tracing/trace_pipe
目录中看到相关的输出了。
再来看看输出的数据格式:
test-3326935, 监控到的程序名-该程序的pid
[001], CPUID
1187528.405340, 时间戳相关?
p_test_0x76c, 事件名
0xaaaaddbc076c, ELF地址
arg1 arg2, 就是我们自己定义的输出内容
当我发现Linux内核功能,我是很惊讶的,竟然能这么容易的监控到任意程序的指定地址的信息,就是不知道对于一个程序来说,是否能发现自己被uprobe插桩了。
接着,我就继续深入的研究了该功能,看看使用场景如何。
自定义事件名
事件名我们是可以自定义的,比如,我只要把事件语句改为:"p:tes