一 前言
前面的文章聊到bpftrace,这是个强大简洁的编写bpf程序的利器,内部的语法看起来比较容易,功能一点也不弱,比如
我们想查看现在系统中谁在执行什么程序:
[root@localhost ~]# bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s-->%s\n",comm,str(args->filename));}'
Attaching 1 probe...
bash-->/usr/bin/cat
bash-->/usr/bin/more
系统中程序执行的系统调用次数统计:
[root@localhost ~]# bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[pid,comm]=count();}'
Attaching 1 probe...
^C
@[894, rpcbind]: 1
@[897, sedispatch]: 2
@[929, gmain]: 4
@[1004, sssd_be]: 5
@[931, HangDetector]: 8
@[730, systemd-journal]: 9
@[1106, systemd-logind]: 14
@[1, systemd]: 15
@[3936, sshd]: 17
二 bpftrace 基础语法
提供一个快速上手指南,并不讲究面面俱到,只说明重要内容。
2.1 执行形式
- 一句话命令 执行
bpftrace -e '命令'
有些单行命令只有结束,按ctrl+c结束了才会输出内容。 - 文件形式,文件开头写上
#!/usr/bin/bpftrace
如果用到system函数需要加上--unsafe
命令形式: probe[,probe,...] /filter/ { action }
即探测事件,过滤器和执行语句,多个语句的话用逗号分隔,最简单的:
bpftrace -e 'BEGIN { printf("Hello eBPF!\n"); }'
2.2 分析实际例子说明语法
1 #!/usr/bin/bpftrace
2
3
4 kprobe:vfs_read
5 {
6 @start[tid] = nsecs;
7 }
8
9 kretprobe:vfs_read
10 /@start[tid]/
11 {
12 $duration_us = (nsecs - @start[tid]) / 1000;
13 @us = hist($duration_us);
14 delete(@start[tid]);
15 }
这是一个统计read系统掉哦那个耗时的脚本, 第四行,kprobe说明插桩为内核动态函数插桩;
@start 说明定义一个名字为start的映射表变量,是用来保存bpftrace的临时数据,也会在程序结束的时候自动输出,@start的映射表为map类型,它的key为tid,这个tid为内部变量,具体说明见下面,nsecs是当前时间,纳秒为单位。这样我们在map中为每个线程在进入read函数的时候,保存各自的纳秒级别的时间戳了。
第九行,kretprobe为内核动态函数返回的插桩,第10行为匹配条件,即要存在这个线程的进入read函数的开始时间戳,不然这个值为0,计算的结果就有问题了。
第12行,$duration_us即为一个自定义变量,值为这个函数的执行时间,计算方法用当前函数返回时候的时间戳减去进入函数时候的时间戳,单位转成微秒;
第13行,定义一个名字为@us的映射表变量,保存的是耗时时间为值的2的幂次方的直方图(看输出结果就明白了),hist为生成直方图的内置函数;
第14行,delete函数删除@start映射表里面,key为tid的对象,为什么要删除那,是因为不删除的话,这个脚本结束的时候,凡是映射表的内容都会输出,影响结果查看。
执行下:(执行结果不符合预期,多打印了@start 映射,按道理是已经删除了)
为了更实用点,我们可以打印出不同的线程调用read的返回值分布情况,只要改下第13行,改成:
@us[comm,tid] = hist($duration_us);
结果就如下图:
说明下直方图,[ 表示大于等于,)表示小于,比如第一行sshd这个进程名,线程id为3936,它执行耗时在大于等于4微秒到小于等于8微秒中的情况有一条,耗时在大于等于8微秒,小于16微秒的数量有一条。
2.3 自己写的代码动态追踪
随便写个小程序,如下代码:
#include
#include
#include
int fun1(int a) {
return a+10;
}
int fun2(int a, int b) {
return a*b;
}
int fun3(int a, int b){
return a+b;
}
int main(int argc,char * argv[])
{
int a = 2,b=4;
printf("a*b=%d\n",fun2(a,b));
getchar();
printf("a+b=%d\n",fun3(a,b));
getchar();
printf("a+10=%d\n",fun1(a));
}
查看可以进行用户级动态函数:
[root@localhost ctest]# bpftrace -l 'uprobe:/root/ctest/a.out:*'
uprobe:/root/ctest/a.out:__do_global_dtors_aux
uprobe:/root/ctest/a.out:__libc_csu_fini
uprobe:/root/ctest/a.out:__libc_csu_init
uprobe:/root/ctest/a.out:_dl_relocate_static_pie
uprobe:/root/ctest/a.out:_fini
uprobe:/root/ctest/a.out:_init
uprobe:/root/ctest/a.out:_start
uprobe:/root/ctest/a.out:deregister_tm_clones
uprobe:/root/ctest/a.out:frame_dummy
uprobe:/root/ctest/a.out:fun1
uprobe:/root/ctest/a.out:fun2
uprobe:/root/ctest/a.out:fun3
uprobe:/root/ctest/a.out:main
uprobe:/root/ctest/a.out:register_tm_clones
写了个追踪脚本:
#!/usr/bin/bpftrace
uprobe:/root/ctest/a.out:fun2
{
@start[comm,tid] = nsecs;
}
uretprobe:/root/ctest/a.out:fun2
/@start[comm,tid]/
{
$duration_us = (nsecs - @start[comm,tid]) / 1000;
if ($duration_us >100) {
printf("func2 cost too long:%d\n",$duration_us);
}else {
printf("func2 cost normal:%d ret:%d\n",$duration_us,retval);
}
}
执行./user.bt 然后执行a.out结果如下:
[root@localhost bpfstest]# ./user.bt
Attaching 2 probes...
func2 cost normal:9 ret:8
^C
@start[a.out, 12459]: 16150154585278
是不是觉得很棒,不用改任何代码,就知道函数执行的耗时,对于耗时情况我们可以灵活设置策略,比如告警啥的。
三 bpftrace建立个后门
在写前几篇文章的时候,飞哥联系我说,这玩意不完全,容易建后面,刚好看到一篇用bpftrace建立后面的文章(有不少变动),刚好测试下。
原理比较简单,就是对监听的端口进行跟踪,收到远程的连接信息,分析下,如果是发送的秘密字符串,那就执行一个命令,返回执行命令的结果。
3.1 环境建立
随便下载个web容器运行,如下:
yum install nginx
nginx -c /etc/nginx/nginx.conf
确认启动如下:
[root@localhost ~]# curl http://127.0.0.1
Test Page for the Nginx HTTP Server on Red Hat Enterprise Linux