假如现在有这么一个需求:需要获取正在运行的 Linux 系统的信息,如我想知道系统什么时候发生系统调用,发生的是什么系统调用等这些信息,有什么解决方案呢?
通俗一点讲,上面的需求可以转化为应对日常应急可能遇到的一些场景:
1、谁在Linux系统上谁在发送DNS(UDP)、ICMP、ARP请求?例如挖矿病毒(矿池失活)、PingTunnel等。
2、哪个进程在对某个文件进行写入?
3、在没有网络设备的情况下,监控所有外对内的连接尝试(无论成功与否)。
4、进程创建、终止,进程模块的加载、卸载。
5、检测rootkit
6、等等
以上需求都可以总结为:对Linux内核调用进行调试跟踪。
Linux下的调试技术分为动态调试和静态调试。
静态调试又名 “插桩”,tracepoint。在Linux发展的早期,实现对Linux内核的调试,需要编写内核模块,对开发要求比较高,同时容易干扰内核的稳定运行。编写、编译、安装到内核,过程繁琐、效率低下、灵活性差。
类似Win下的动态调试。更加灵活地捕获内核调用,使用门槛低,稳定且几乎无干扰。早期的动态调试器有GDB、KDB等。
动态追踪技术的鼻祖是Dtrace,它于 21 世纪初诞生于 Solaris 操作系统,是由原来的 Sun Microsystems 公司的工程师编写的,先后被移植到 Linux、FreeBSD、NetBSD 及 Mac OS X 等操作系统上。iOS 系统上大名鼎鼎的 Instrument 工具就是基于 DTrace 实现的。Mac OS X 默认安装了dtrace工具;脚本使用D语言编写,也叫 d 脚本,Mac OS X的/usr/share/examples/DTTk/目录下有很多例子。Dtrace技术原理如下图:
Linux 在过去十多年的发展中, 演化了很多追踪与调试技术, 不过一直没有一款可以媲美 DTrace 的工具, 直到 Linux 4.1+ 版本 eBPF 机制的出现, 这种情况才得到了极大的改善. 不过 eBPF 也不是一蹴而就的, 而是经过了漫长的过程才得以完善。Linux下动态调试技术出现时间:
systemtap 是利用Kprobe 提供的API来实现动态地监控和跟踪运行中的Linux内核的工具,相比Kprobe,systemtap更加简单,提供给用户简单的命令行接口,以及编写内核指令的脚本语言,类似C语言、D语言。
SystemTap 基本思想是命名事件,并为它们提供处理程序。每当发生指定的事件时,内核都会将处理程序视为子例程运行,然后继续运行。有一系列的事件,例如进入或退出函数,计时器到期或整个SystemTap会话的开始和停止。处理程序是一系列脚本语言语句,用于指定事件发生时要完成的工作。这项工作通常包含从事件上下文中提取数据,将其存储到内部变量或打印结果。
SystemTap 的工作原理是将脚本翻译成C语言,执行C编译器创建一个内核模块。当模块被加载后,通过挂载到内核来激活所有的探测事件。然后,当事件发生再任何处理器上时,编译后的处理程序就运行,最终,SystemTap会话停止,Hook取消,内核模块被移除,整个过程由命令行程序stap驱动。
原理如下图所示:
以较老的系统(CentOS6.8 x64为例),查看内核版本,并下载与内核版本完全一致调试依赖包:
kernel-debuginfo
kernel-debuginfo-common
kernel-devel
手动安装
rpm -ivh kernel-debuginfo-common-x86_64-2.6.32-642.el6.x86_64.rpm
rpm -ivh kernel-debuginfo-2.6.32-642.el6.x86_64.rpm
rpm -ivh kernel-devel-2.6.32-642.el6.x86_64.rpm
yum install systemtap systemtap-runtime #yum安装systemtap
测试
stap -V
stap -h
stap -e 'probe begin{printf("Hello, World\n"); exit();}'
如果提示 Incorrect version or missing kernel-devel package, use: yum install kernel-devel-xxxxx
则安装的kernel-devel版本与内核不匹配。
/lib/modules/KERNEL_VERSION/systemtap/ 保存 SystemTap 工具模块。
/usr/share/systemtap/tapset/ 保存标准的 tapset 库。
/usr/share/doc/packages/systemtap/examples 保存用于各种目的的多个示例 SystemTap 脚本。仅当已安装 systemtap-docs 软件包时才可用。
~/.systemtap/cache 缓存的 SystemTap 文件的数据目录。
/tmp/stap* SystemTap 文件的临时目录,包含已转换的 C 代码和内核对象。
systemtap脚本默认文件后缀为.stp,文件内容有固定的格式,有完整的语法(变量、常量、注释、条件、循环、数组等等)。begin/end, 分别是systemtap会话的起始和结尾,IDE推荐VSCode。如下图所示:
systemtap脚本编写参考: https://sourceware.org/systemtap/tutorial/
Kprobes允许你为任何内核指令以及函数入口和函数返回处理程序安装预处理程序和后处理程序。
常用的可探测事件如下:
tid() 当前线程的 ID。
pid() 当前线程的进程 ID。
uid() 当前用户的 ID。
cpu() 当前 CPU 编号。
execname() 当前进程的名称。
gettimeofday_s() 自 Unix 纪元(1970 年 1 月 1 日)起经过的秒数。
ctime() 将时间转换为字符串。
pp() 用于描述当前正在处理的探测点的字符串。
函数大全:https://sourceware.org/systemtap/tapsets/index.html
监控DNS请求
脚本原理:判断外联目标端口为53
#! /usr/bin/env stap global the_dport = 53 probe netfilter.ip.local_out { if (the_dport == dport) printf("%s[PID:%d,TID:%d] sent packet to %s:%d\n", execname(), pid(), tid(), daddr, dport) }
监控ICMP请求
脚本原理: netfilter.ip.local_out 为所有协议外联,ICMP的目标端口为0
#! /usr/bin/env stap probe netfilter.ip.local_out { if (0 == dport) printf("%s[PID:%d,TID:%d] sent %d to %s:%d\n", execname(), pid(), tid(),length, daddr, dport) } probe netfilter.ip.local_in { if (0 == sport) printf("%s recv %d from %s:%d\n", execname(),length, saddr, sport) }
监控传入TCP连接(连接成功的)
脚本原理:传入握手成功,会有accept标识
#! /usr/bin/env stap probe begin { printf("%6s %16s %6s %6s %16s\n", "UID", "CMD", "PID", "PORT", "IP_SOURCE") } probe kernel.function("tcp_accept").return?, kernel.function("inet_csk_accept").return? { sock = $return if (sock != 0) printf("%6d %16s %6d %6d %16s\n", uid(), execname(), pid(),inet_get_local_port(sock), inet_get_ip_source(sock)) }
监控传入TCP连接尝试(不管成功与否)
脚本原理:监控receive,如果是传出则用sendmsg
#! /usr/bin/env stap probe tcp.receive { if(dport!=22) #exclude 22 port printf("%s:%d --> %s:%d\n", saddr, sport, daddr, dport) }
监控TCP连出(不管成功与否)
脚本原理:监控receive,如果是传出则用sendmsg
probe begin { printf("ok\n"); } probe syscall.connect { if (uaddr_af == "AF_INET" || uaddr_af == "AF_INET6") printf("%s[%d]: %s\n", execname(), pid(), argstr); }
curl baidu.com效果:
nc 8.8.8.8 9999,无法连接成功的端口,效果:
监控对文件的操作
#!/usr/bin/stap probe vfs.{write,read} { # dev and ino are defined by vfs.write and vfs.read if (dev == MKDEV($1,$2) && ino == $3) printf ("%s(%d) %s(%d) %s 0x%x/%u\n",execname(), pid(), pexecname(), ppid(), ppfunc(), dev, ino) }
步骤:
先获取要监控的文件信息:
使用df查看log.txt 所在的设备
log.txt在/的子文件夹下,所以要找/dev/mapper/centos-root的主从设备号
得到根目录的主从设备号为253 0
所以inodewatch.stp的参数为253 0 36398378
执行
stap /usr/share/systemtap/examples/io/inodewatch.stp 253 0 52092325
效果如下:
SystemTap的功能远不止这些,除了内核调试,还可以实现用户态的调试。本文仅抛砖引玉。
debuginfo下载
https://mirrors.ocf.berkeley.edu/centos-debuginfo/
https://oss.oracle.com/el6/debuginfo/
http://debuginfo.centos.org/6/x86_64/
kernel-devel下载
ftp://ftp.pbone.net/mirror/ftp.scientificlinux.org/linux/scientific/
https://www.central.org/dl/linuxdev/
systemtap 官方教程
https://sourceware.org/systemtap/tutorial/1_Introduction.html
《SystemTap: Instrumenting the Linux Kernel for Analyzing Performance and Functional Problems》
https://www.redbooks.ibm.com/redpapers/pdfs/redp4469.pdf
systemtap 内置函数库(tapset)
https://sourceware.org/systemtap/tapsets/index.html
Linux 系统动态追踪技术介绍
https://blog.arstercz.com/introduction_to_linux_dynamic_tracing/