一 前言
前面介绍了利用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强大。
原理如下图:
它利用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 <stdio.h>
#include <sys/types.h>
#include <unistd.h>
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 <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
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 --unsafe
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
已杀死
很棒,这个“木马” 被杀了,其他应用回头再聊。