透视Linux内核 神奇的BPF三

一 前言

前面介绍了利用BCC 写eBPF的代码,虽然可以利用python加载,说实话,写起来并不容易,程序本身难度不大,难度在什么地方那,我们利用eBPF的时候更多的时候是在想看看内核到底在干嘛,为什么这么慢的问题,我们就需要对需要监控的程序进行追踪了解到系统调用的位置,然后通过静态的内核插桩和动态内核插桩,跟踪点等 去检测是否存在性能问题。但是如果不是对内核很了解,很难知道我们应该动态追踪哪些内核函数,这些函数又有怎么样的参数,还有如果我只想简单的做个测试,有没有简单点的方法,比如一句话命令等,这就是本文要解决的问题。

二 查询可跟踪的跟踪点和相关参数

对于内核静态插桩和跟踪点的查询非常重要,我们只有知道了跟踪点,结合我们需要实现的功能,才可以写BPF程序,有两种方法可以查看:

2.1 通过内核符号表查询

为了方便调试内核,内核开发者把内核中所有函数和非栈变量抽出来,当然不是所有的函数都可以追踪,只有被显示导出的函数才可以被内核kprobe追踪:

# 查看所有内核符号
cat /proc/kallsyms 
# 如果不存在则这样挂载后查看
sudo mount -t debugfs debugfs /sys/kernel/debug
# 可追踪的函数
cat /sys/kernel/debug/tracing/available_filter_functions |wc -l
50627

函数的参数的查看:

[root@localhost ~]# cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/format
name: sys_enter_execve
ID: 718
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:int __syscall_nr; offset:8;   size:4; signed:1;
    field:const char * filename;    offset:16;  size:8; signed:0;
    field:const char *const * argv; offset:24;  size:8; signed:0;
    field:const char *const * envp; offset:32;  size:8; signed:0;
print fmt: "filename: 0x%08lx, argv: 0x%08lx, envp: 0x%08lx", ((unsigned long)(REC->filename)), ((unsigned long)(REC->argv)), ((unsigned long)(REC->envp))

2.2 利用bpftrace查看

bpftrace 是基于BCC和BPF的开源跟踪器,它比基于BCC开发的BPF程序程序更简单,有自己的一组语法规则,类似于awk,使用起来很方便,只是功能上没有BCC强大。

原理如下图:


bpftrace 原理图

它利用clang ,bcc等工具,将简单的bpftrace程序加载到内核中执行,并通过映射来获取结果。
安装比较简单(内核版本要求在4.9以上):

#ubuntu
sudo apt-get update
sudo apt-get bpftrace

#centos
yum install dnf
dnf install -y bpftrace

那具体利用bpftrace如何查询那,通过-l和-lv命令可以轻松查询插桩信息跟踪点信息:

# 查看所有插桩和追踪点等信息
[root@localhost ~]# bpftrace -l|wc -l
51009
# 查看参数和返回值,通过-v参数,注意可以使用通配符* 做匹配
# sys_enter_vfork 是
[root@localhost ~]# bpftrace -lv "tracepoint:syscalls:*vfork"
tracepoint:syscalls:sys_enter_vfork
    int __syscall_nr
tracepoint:syscalls:sys_exit_vfork
    int __syscall_nr
    long ret

sys_enter_vfork 即系统调用fork的时候入参为系统调用号,返回sys_exit_vfork 时候可用的两个参数为系统调用号和返回值。

三 利用bpftrace 开发点实用东西

3.1 系统新创建的进程,进程创建使用了哪些参数

这个使用场景很多,比如我们在清理木马的时候,发现明明删除了文件,却进程还是再被不断的拉起,我需要知道这个木马进程是哪个父进程启动的。一般情况下,是通过fork一个进程,然后调用execv等一系列函数来创建的,那么我们
先从fork开始分析:

[root@localhost ~]# bpftrace -lv "tracepoint:syscalls:*fork"
tracepoint:syscalls:sys_enter_fork
    int __syscall_nr
tracepoint:syscalls:sys_enter_vfork
    int __syscall_nr
tracepoint:syscalls:sys_exit_fork
    int __syscall_nr
    long ret
tracepoint:syscalls:sys_exit_vfork
    int __syscall_nr
    long ret

fork()子进程拷贝父进程的数据段和代码段,这里通过拷贝页表实现。
vfork()子进程与父进程共享地址空间,无需拷贝页表,效率更高。
fork()父子进程的执行次序不确定。
vfork()保证子进程先运行,在调用 exec 或 exit 之前与父进程数据是共享的。父进程在子进程调用 exec 或 exit 之后才可能被调度运行,如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁

分析好追踪点和参数就可以开始写了:

bpftrace -e 'tracepoint:syscalls:sys_enter_fork { printf("->fork by %s PID :%d,\n",comm,pid);}'

本来我想追踪fork的,为此我还写了c代码:

#include 
#include 
#include 

void fork_one() {
    if (fork() == 0) {
       printf("hello from child,pid:%d ppid:%d\n", getpid(),getppid());
    }
    else {
           printf("My is parent  pid:%d ppid:%d.\n",getpid(),getppid());
    }
}

int main(void)
{
    fork_one();
    return 0;
}

运行发现,并没有追踪到,于是我通过strace 去看下编译后的执行文件的系统调用,发现其实调用的是clone,fork是进程资源全部复制,vfork是共享内存,clone是有选择的复制,改下代码:

#在一个终端执行:
[root@localhost testc]# ./a.out 
My is parent  pid:6551 ppid:5823.
hello from child,pid:6552 ppid:6551

#另一个shell先执行bpftrace
[root@localhost ~]# bpftrace -e 'tracepoint:syscalls:sys_enter_clone { printf("--------\n[%s-%d]->clone\n",comm,pid);} tracepoint:syscalls:sys_exit_clone { printf("[%s-%d]->clone: ret:%d\n\n", comm,pid,args->ret);}'
Attaching 2 probes...

--------
[bash-5823]->clone
[bash-5823]->clone: ret:6551

[bash-6551]->clone: ret:0

--------
[a.out-6551]->clone
[a.out-6551]->clone: ret:6552

[a.out-6552]->clone: ret:0

我们执行./a.out的时候,先通过shell 创建a.out程序,进程id对的上,每次fork都返回两次,一次是返回子进程的id(对于父进程来说),一个是返回0(对子进程来说)。

我们在进一步完善下:

[root@localhost ~]#  bpftrace -e 'tracepoint:syscalls:sys_enter_clone {printf("\n"); printf("time:"); time(); printf("--------\nusername:%s userid:%d\n[%s-%d]->clone\n",username,uid, comm,pid);} tracepoint:syscalls:sys_exit_clone { printf("[%s-%d]->clone: ret:%d \n", comm,pid,args->ret);}'
Attaching 2 probes...

time:23:22:24
--------
username:root userid:0
[bash-5823]->clone
[bash-5823]->clone: ret:6599 
[bash-6599]->clone: ret:0 

time:23:22:24
--------
username:root userid:0
[a.out-6599]->clone
[a.out-6599]->clone: ret:6600 
[a.out-6600]->clone: ret:0 

有点意思了,那我们能不能根据进程名判断是否是木马进程,木马进程名一般比较奇怪,作为一个特征可以去处理,当然也可以判断下父进程的特定,判断如果进程是木马进程,则把进程kill掉,这样就算中了木马,也没多大危害了,因为它根本无法启动了,:)。

那首先我们改造下测试程序,子进程处于等待状态直到后台kill掉。

#include 
#include 
#include 
#include 

void fork_one(char *argv[]) {
        pid_t pid;
        if ( (pid= fork()) <0) {
             printf("fork error\n");
        }
        else if ( pid  == 0) {
                   printf("hello from child,pid:%d ppid:%d\n", getpid(),getppid());
                   // strcpy(argv[0],"a12345");
                   while(1) {
                     sleep(10);
                   }
        }
        else {
                  printf("My is parent  pid:%d ppid:%d.\n",getpid(),getppid());
                  printf("start to wait pid:%d",pid);
                  if (waitpid(pid,NULL,0)!= pid) {
                      printf("waitid pid:%d error.\n",pid);
                  }
        }
}

int main(int argc, char * argv[])
{
        fork_one(argv);
        return 0;
}

我们的bpftrace代码有点长了,那还是建个文件比较好,内容设置如下:

#!/usr/bin/bpftrace 
BEGIN 
{
  printf("watch create process....\n");
}

tracepoint:syscalls:sys_enter_clone 
{
    printf("\n"); 
    printf("time:"); time(); 
    printf("--------\nusername:%s userid:%d\n[%s-%d]->clone\n",username,uid, comm,pid);
} 


tracepoint:syscalls:sys_exit_clone 
{ 
    printf("[%s-%d]->clone: ret:%d \n", comm,pid,args->ret);
    if (comm == str($1) ) {
          system("kill -9 %d",pid );
          printf("kill pid:%d",pid);
    }
}

测试下:

[root@localhost bpfstest]# ./kill.bt a.out
Attaching 3 probes...
watch create process....

time:00:13:59
--------
username:root userid:0
[bash-5823]->clone
[bash-5823]->clone: ret:8649 
[bash-8649]->clone: ret:0 

time:00:13:59
--------
username:root userid:0
[a.out-8649]->clone
[a.out-8649]->clone: ret:8650 
kill pid:8649[a.out-8650]->clone: ret:0 
kill pid:8650
time:00:13:59
--------
username:root userid:0
[kill.bt-8647]->clone
[kill.bt-8647]->clone: ret:8651 
[kill.bt-8651]->clone: ret:0 

time:00:13:59
--------
username:root userid:0
[kill.bt-8647]->clone
[kill.bt-8647]->clone: ret:8652 
[kill.bt-8652]->clone: ret:0 

执行:

[root@localhost testc]# ./a.out
My is parent  pid:8649 ppid:5823.
hello from child,pid:8650 ppid:8649
已杀死

很棒,这个“木马” 被杀了,其他应用回头再聊。

你可能感兴趣的:(透视Linux内核 神奇的BPF三)