How does snoopy log every executed command
snoopy 介绍
snoopy 是一个轻量级的lib库, 用来记录系统中所有执行过的命令(以及参数). 我们在实际环境的使用过程中, 结合 snoopy 和 rsyslog 可以很方便的搜集所有主机的历史执行命令, 这种方式给安全审计和故障排错带来了很大的便利. 不同于以往的 shell + history
方式, snoopy 是以预加载 (preload
) 的方式实现历史命令的记录, 整个会话环境的信息都可以记录下来, 而前者则仅仅记录执行的命令, 且容易绕过记录, 难以满足我们的需求. 安装部署可参考 install, rpm-install. 下文则详细介绍 snoopy 如何实现以及使用事项.
snoopy 如何工作
linux 的 ld.so, ld-linux.so
(动态链接)机制可以让程序在运行的时候加载或预处理需要的动态库文件(使用 --static
选项编译的程序除外). 其提供以下不同的文件:
/lib/ld.so
a.out dynamic linker/loader
/lib/ld-linux.so.{1,2}
ELF dynamic linker/loader
/etc/ld.so.cache
File containing a compiled list of directories in which to search for libraries and an ordered list of candidate libraries.
/etc/ld.so.preload
File containing a whitespace separated list of ELF shared libraries to be loaded before the program.
lib*.so*
shared libraries
snoopy 即是通过 preload
的方式在程序进行 execv()
和 execve()
系统调用的时候记录下所有需要的信息. 这种方式即意味着 snoopy 对用户和程序是透明的, 仅做记录处理, 不能改变用户或程序的命令. 已经运行的程序不受 preload 机制约束, 因为execv
和 execve
两个函数仅用于新执行一个程序. 当然如果执行的是一个脚本, 而脚本中又有 execv
和 execve
相关的系统调用(比如脚本里调用系统命令), snoopy 也会记录下来. 这在故障排错和审计的场景中是一个非常有用的功能.
系统调用
unix/linux
提供了 7 种不同的 exec 函数来初始执行新的程序, 如下所示:
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv []);
int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp []);
int execlp(const char *filename, const char *arg0,... /* (char *)0 */ );
int execvp(const char *filename, char *const argv []);
int fexecve(int fd, char *const argv[], char *const envp[]);
这些函数中前 4 个函数取路径名作为参数, 后两个取文件名作为函数, 最后一个取文件描述符作为参数.这几个函数的参数表传递略有不同, 含有 l
的函数为列表 list, 比如 execl, execlp, execle 要求将新程序的每个命令行参数都说明为一个单独的参数; 含有 v
的函数为矢量 vector, 比如 execv, execvp, execve, fexecve 等需要先构造一个指向各参数的指针数组, 再讲数组地址作为函数的参数; 含有 e
结尾的函数, 比如 execle, execve, fexecve 可以传递一个指向环境字符串指针数组的指针.
封装 execv, execve
snoopy 的内部则通过封装 execv
, execve
函数实现记录命令的目的. 即在执行程序之前, 通过 preload 机制, 预先加载封装好的 execv
和 execve
函数, 记录执行的命令, 则实际执行真实的命令.
execve_wrapper.c 源文件包含了这两个函数的封装:
#include <dlfcn.h>
...
#define FN(ptr, type, name, args) ptr = (type (*)args)dlsym (REAL_LIBC, name)
...
int execv (const char *filename, char *const argv[]) {
static int (*func)(const char *, char **);
FN(func, int, "execv", (const char *, char **const));
snoopy_log_syscall_execv(filename, argv);
return (*func) (filename, (char **) argv);
}
int execve (const char *filename, char *const argv[], char *const envp[])
{
static int (*func)(const char *, char **, char **);
FN(func, int, "execve", (const char *, char **const, char **const));
snoopy_log_syscall_execve(filename, argv, envp);
return (*func) (filename, (char**) argv, (char **) envp);
}
snoopy_log_syscall_execv
和 snoopy_log_syscall_execve
函数则无论成功与否都不会影响后续程序的真实执行, 在 log.c 源文件中处理, 两个都通过调用 snoopy_log_syscall_exec
函数进行处理, 该函数则包括解析配置, 初始化, 过滤, 输出等功能.
流程说明
我们以 strace uptime >/tmp/uptime.log 2>&1
命令为例, 追踪具体的处理流程:
execve("/usr/bin/uptime", ["uptime"], [/* 37 vars */]) = 0
brk(0) = 0xceb000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe960e52000
access("/etc/ld.so.preload", R_OK) = 0
open("/etc/ld.so.preload", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=28, ...}) = 0
mmap(NULL, 28, PROT_READ|PROT_WRITE, MAP_PRIVATE, 3, 0) = 0x7fe960e51000
close(3) = 0
open("/usr/local/lib/libsnoopy.so", O_RDONLY) = 3
......
......
系统直接以 execve
函数开始执行 uptime
程序, 第 4 行开始访问 /etc/ld.so.preload
, 进而加载 /usr/local/lib/libsnoopy.so
, 后续内容则为具体的执行信息. 而 snoopy 的输出则包含以下:
Jan 18 16:19:52 cz-test1 snoopy[16530]: [uid:0 sid:24493 tty:/dev/pts/3 cwd:/root filename:/usr/bin/strace]: strace uptime
Jan 18 16:19:52 cz-test1 snoopy[16533]: [uid:0 sid:24493 tty:/dev/pts/3 cwd:/root filename:/usr/bin/uptime]: uptime
snoopy 搜集了很全的信息, 包括 uid, sid, cwd 等. 更多输出选项可通过 snoopy.ini
配置文件查看.
配置处理
在较新的 2.x.x 版本中, snoopy 增加了 snoopy.ini
配置文件供用户配置记录所需的信息, 主要包含下面几个选项:
message_format
message_format
为输出格式选项, 支持的列都在配置文件中进行了说明, 上述示例的输出是通过以下面的配置获取的:
message_format = "[uid:%{uid} sid:%{sid} pid:%{pid} tty:%{tty} cwd:%{cwd} filename:%{filename}]: %{cmdline}"
filter_chain
filter_chain
为过滤规则, 可以只记录某个 uid 的所有操作, 也可以忽略记录某个 uid 的操作. 真实的环境中, 我们可能忽略一些监控用户的所有操作避免监控引起 snoopy 频繁的输出日志. 下面的配置则为忽略记录 uid 为 496 的用户的所有操作:
filter_chain = exclude_uid:496;exclude_spanws_of:crond,daemon
过滤规则在
(filtering.c - snoopy_filtering_check_chain)
函数实现, 由log.c - snoopy_log_syscall_exec
函数调用, 过滤规则为事后行为, 即在打印日志的时候判断是否满足过滤规则, 并非事前行为.
output
output 为输出选项, 支持的种类较多, 可以是 devlog, denull, devtty, file, socket, stderr, stdout, syslog 等. 默认为 devlog
, snoopy 通过 socket 方式输出到本地的 syslog, /dev/log
详见内核文件 devices.txt:
Sockets and pipes
Non-transient sockets and named pipes may exist in /dev. Common entries are:
/dev/printer socket lpd local socket
/dev/log socket syslog local socket
/dev/gpmdata socket gpm mouse multiplexer
file 选项使用的也比较多, 可以输出到指定的文件, stdout 则为标准输出, socket 方式则相对高级, 用户可以指定 snoopy 输出到指定的 socket 中, socket 文件的另一端有其它程序接收即可收到日志信息.
syslog 选项在旧版中存在比较严重的 bug, 可能会引起系统挂死, 详见 FAQ 1, 2 两个条目说明.
syslog_xxx
syslog_xxx
几个选项规定了以什么格式传给 syslog, syslog_level
为日志级别, 默认为 LOG_INFO
, syslog_facility
日志分类, 默认为 LOG_AUTHPRIV
, syslog_ident
为程序名, 默认为 snoopy
. rsyslog 将收到的信息归属到哪个日志文件, 由 rsyslog 配置的 authpriv
决定, 一般情况下都会在以下几个文件中:
/var/log/auth*
/var/log/messages
/var/log/secure
注意事项
FAQ 文档中描述了所有需要注意的问题. 实际上对于 snoopy 而言, 其通过封装 execv
和 execve
函数来记录执行的命令, 从性能方面来看, snoopy 可能延长正常的命令执行的时间.
如果中间的过程处理不当也可能引起其它方面的 bug, 比如 faq 中提到的 hangs systemd based system
以及 issue106 等问题, 所以在实际使用中, 尽量安装最新的版本, 也建议大家多看看 snoopy 的 issue 列表, 以及相关的 faq 文档.另外 snoopy 并不是万能的, 用户可以通过 LD_PRELOAD
环境变量绕过 snoopy 的记录, 详见 faq 文档说明;
同样的如果 snoopy 产生的日志过大, 可以在 snoopy.ini
中尽量配置需要忽略的选项, 配置完成后已经运行的程序不会立即生效, 需要重启程序以重新加载 preload
.
问题汇总
版本 | 漏洞 | 描述 |
---|---|---|
< 2.4.6 | issue-100 | keepalive 切换脚本执行过多的 execv 调用, 引起操作超时, 造成 keepalive 切换; |
< 2.4.6 | issue-106 | Centos 7 中重启 NetworkManager 服务崩溃, 主要为 dhcp 和 ini 配置解析出错; |
< 2.4.8 | issue-157 | cmdline 相关的指针和内存分配错误; |
< 2.4.14 | issue-119 | 解析 .ini 配置解析引起的系统异常,造成 php 无法启动; |
< 2.4.14 | issue-191 | 系统增加新用户时可能产生错误消息的异常; |
< 2.4.14 | pull-198 | 命令行太长可能引起程序崩溃异常; |
< 2.4.14 | pull-201 | 不太通用的空参数引起 execve 异常; |
总结
整体上看, snoopy 通过封装系统调用来实现记录执行的命令, 这就存在一定的风险, 比如降低系统性能, 和其它软件相冲突, 以及 hang 住系统等严重的问题, 但也带来了其它方面的好处, 在安全审计和故障排错的场景中尤为有用. 当然我们也可以按需开启 snoopy, 比如在排错的场景中, 排错前开启, 完成后再关闭即可. 不过已经运行的程序不受 preload
机制的影响, 毕竟 上述介绍的 exec 相关的函数仅用来执行新的程序, 未使用上述的两个系统调用则不会被 snoopy 处理.