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 机制约束, 因为execvexecve 两个函数仅用于新执行一个程序. 当然如果执行的是一个脚本, 而脚本中又有 execvexecve 相关的系统调用(比如脚本里调用系统命令), 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 机制, 预先加载封装好的 execvexecve 函数, 记录执行的命令, 则实际执行真实的命令.

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_execvsnoopy_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 而言, 其通过封装 execvexecve 函数来记录执行的命令, 从性能方面来看, 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 处理.