神奇的BPF四 用bpftrace 开个后门

一 前言

前面的文章聊到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 执行形式

  1. 一句话命令 执行bpftrace -e '命令' 有些单行命令只有结束,按ctrl+c结束了才会输出内容。
  2. 文件形式,文件开头写上#!/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