systemtap是一个监控追踪系统调用、调试内核的工具,可以在系统运行时动态的调试内核,可以说是内核开发者必须要掌握的工具。

我使用这个工具是因为线上的应用存在fastjson的漏洞,被黑客执入了木马,在骨干交换机上抓到异常流量后,确定了内网服务器IP,登录服务器后使用lsmod、ps、netstat、ss、lsof这些命令无法发现异常,甚至连tcpdump都无法抓到这个木马,猜测是rootkit木马加载了内核模块、实现了无文件、无进程,只有每20分钟一次的心跳包,想揪出来非常困难。

一、安装

编译是比较麻烦的,centos base源里就有systemtap,主要有三个依赖:

  1. kernel-debuginfo 在CentOS-Debuginfo源里,默认没启用
  2. elfutils
  3. kernel-devel ,和 elfutils 一样,bases源里就有,yum会自己解决依赖
    kernel-debuginfo和kernel-devel 需要保证和当前运行的内核小版本号一致

使用yum安装:

yum install systemtap  systemtap-devel systemtap-runtime
yum --enablerepo=base-debuginfo install kernel-debuginfo kernel-debuginfo-common kernel-devel 

如果base源里没有找到systemtap,可以试试centosplus源:

yum --enablerepo=centosplus install  systemtap  systemtap-devel systemtap-runtime

当然也可以安装时直接指定版本,建议用这种方式:

yum install -y kernel-devel-$(uname -r)
yum --enablerepo=base-debuginfo install -y kernel-debuginfo-$(uname -r) kernel-debuginfo-common-$(uname -m)-$(uname -r)

按上面命令装好后验证一下kernel-debuginfo、kernel-devel版本是否和内核一致:

rpm -qa |grep kernel

如果系统内有多个版本的内核,建议卸载多余的内核、kernel-headers、kernel-devel和kernel-firmware,不然运行systemtap,容易出现错误。

二、systemtap脚本

目的是揪出来异常流量对应的进程、文件及文件位置,先来看看官网给的监控进程创建的脚本:

probe kprocess.create
{
    printf("%-25s: %s (%d) created %d\n",
    ctime(gettimeofday_s()), execname(), pid(), new_pid)
}

probe kprocess.exec
{
    printf("%-25s: %s (%d) is exec'ing %s\n",
    ctime(gettimeofday_s()), execname(), pid(), filename)
}

systemtap脚本执行需要root权限,将上述脚本保存为forktracker.stp,执行方法:

stap forktracker.stp 

可以将输出保存的文件:

stap -v forktracker.stp -o fork.log

改进一下,更详细一点:

probe kprocess.create {
     printf("%-25s: %s (%d:%d) created %d:%d\n",
         ctime(gettimeofday_s()), execname(), pid(), tid(), new_pid, new_tid)
}

probe kprocess.exec {
  printf("%-25s: %s (%d) is exec'ing %s\n",
     ctime(gettimeofday_s()), execname(), pid(), filename)
}

在来一个监控目的端口为443的流量。

cat tcp.stp 

#! /usr/bin/env stap
probe syscall.connect {
if(uaddr_ip_port=="443"){
    printf("ip:%s port:%s cmd:%s pid:%d ppid:%d\n",uaddr_ip, uaddr_ip_port,execname(),pid(),ppid())
    }
}

执行stap tcp.stp,然安在另外一个窗口执行 telnet 1.1.1.1 53进行测试,输出

ip:1.1.1.1 port:53 cmd:telnet pid:14934 ppid:10130

上面的信息不够详细,在来一个输出更详细的版本:

#! /usr/bin/env stap

probe syscall.connect {
    if(uaddr_ip_port=="1521"){
        printf("Time:%s remote_ip:%s remote_port:%s local_cmd:%s pid:%d local_pcmd:%s ppid:%d euid:%d egid:%d env_PWD:%s  \n",
           tz_ctime(gettimeofday_s()),uaddr_ip, uaddr_ip_port,execname(),pid(),pexecname(),ppid(),euid(),egid(),env_var("PWD"))
    }
}

执行 telnet 8.8.8.8 1521进行测试,输出

Time:Tue Jul 21 15:34:50 2020 HKT remote_ip:8.8.8.8 remote_port:1521 local_cmd:telnet pid:23892 local_pcmd:bash ppid:10130 euid:500 egid:500 env_PWD:/home/tomcat

上面两个只能监控tcp流量,在改进一下监控目的端口,不区分协议的,监控从本机出去到53端口的流量的进程

probe netfilter.ip.local_out {
  if (dport == 53)
      printf("%s[%d] %s:%d\n", execname(), pid(), daddr, dport)
      printf("Time:%s remote_ip:%s remote_port:%s local_cmd:%s pid:%d local_pcmd:%s ppid:%d euid:%d egid:%d env_PWD:%s  \n",
           tz_ctime(gettimeofday_s()),uaddr_ip, uaddr_ip_port,execname(),pid(),pexecname(),ppid(),euid(),egid(),env_var("PWD"))
    
}

输出

telnet[28032] 1.1.1.1:53
Time:Tue Jul 21 15:34:50 2020 HKT remote_ip:1.1.1.1 remote_port:53 local_cmd:telnet pid:28032 local_pcmd:bash ppid:10130 euid:500 egid:500 env_PWD:/home/tomcat

通过上面的操作基本上就确定了木马的文件名,接下来就是清理木马。centos单用户模式是维护模式,类似windows的安全模式,正常只会加载系统启动所需的最少服务,木马未针对单用户模式进行处理,所以在单用户模式下没有加载隐藏进程的内核模块,重启进入系统后直接find文件名清理即可。

参考文章:
https://sourceware.org/systemtap/documentation.html
https://sourceware.org/systemtap/examples/