freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

snoopy记录命令原理分析
2023-12-11 18:47:07

本文作者:pamela@涂鸦智能安全实验室

前言

在 Linux 操作系统的安全和审计领域中,Snoopy 是一个非常有用的工具,它能够拦截并记录所有通过 shell 执行的命令。

据snoopy的官方文档描述,“Snoopy is a small library that logs all program executions on your Linux/BSD system.”snoopy是一个开源的轻量级lib库, 可以记录系统中所有执行过的命令和参数。

虽然日常排查问题的过程中有用到,但是一直没有深入了解过,出于对其实现原理的好奇,于是有了这篇文章。所以这篇文章主要分析一下snoopy监控命令的原理。

snoopy

安装

我们在ubuntu22.04上面进行测试,我们直接采用apt install的方式安装.

sudo apt install snoopy

安装过程中会弹出如下一句话:

snoopy is a library that can only reliably do its work if it is mandatorily preloaded via /etc/ld.so.preload. Since this can potentially do harm to the system, your consent is needed. │ Install snoopy library to /etc/ld.so.preload? 即是否运行将snoopy安装到/etc/ld.so.preload中.选择是,成功安装之后,我们查看:

$ cat /etc/ld.so.preload
/lib/x86_64-linux-gnu/libsnoopy.so

在/etc/ld.so.preload中会出现/lib/x86_64-linux-gnu/libsnoopy.so. 根据文档的说法:

A list of additional, user-specified, ELF shared libraries to be loaded before all others. The items of the list can be separated by spaces or colons. This can be used to selectively override functions in other shared libraries. The libraries are searched for using the rules given under DESCRIPTION. For set-user-ID/set-group-ID ELF binaries, preload pathnames containing slashes are ignored, and libraries in the standard search directories are loaded only if the set-user-ID permission bit is enabled on the library file.

大致意思是在 Snoopy 的上下文中,LD_PRELOAD 用于引入 Snoopy 的共享库 libsnoopy.so,允许它在实际的系统函数(如 exec 系统调用)之前执行,从而拦截、记录命令,再将控制权传回到原本的系统函数。由于 LD_PRELOAD 可以使用户指定的库优先载入,Snoopy 能通过其来植入记录功能而不必修改任何现有的二进制可执行文件。

执行效果

如果要查看查看命令是否能被snoopy记录,我们可以通过ldd查看某个命令的链接库中是否包含snoopy来判断,以ls为例:

$ ldd /bin/ls
linux-vdso.so.1 (0x00007ffdd713e000)
/lib/x86_64-linux-gnu/libsnoopy.so (0x00007fa823311000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fa8232df000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa8230b7000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fa823020000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa823346000)

可以看到/lib/x86_64-linux-gnu/libsnoopy.so (0x00007fa823311000)的确已经在动态链接库中。所以我们猜想snoopy实现的原理就是覆盖了libc.so中的某些库函数从而实现了命令记录的功能。

$ tail -f /var/log/auth.log
Dec 10 21:29:13 localhost snoopy[49249]: [login:ubuntu ssh:((undefined)) sid:49211 tty:/dev/pts/3 (0/root) uid:root(0)/root(0) cwd:/root]: ldd /bin/ls
Dec 10 21:29:13 localhost snoopy[49251]: [login:ubuntu ssh:((undefined)) sid:49211 tty:/dev/pts/3 (0/root) uid:root(0)/root(0) cwd:/root]: /lib64/ld-linux-x86-64.so.2 --help
Dec 10 21:29:13 localhost snoopy[49252]: [login:ubuntu ssh:((undefined)) sid:49211 tty:/dev/pts/3 (0/root) uid:root(0)/root(0) cwd:/root]: /lib64/ld-linux-x86-64.so.2 --verify /bin/ls
Dec 10 21:29:13 localhost snoopy[49255]: [login:ubuntu ssh:((undefined)) sid:49211 tty:/dev/pts/3 (0/root) uid:root(0)/root(0) cwd:/root]: /lib64/ld-linux-x86-64.so.2 /bin/ls

代码分析

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

系统调用

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 可以传递一个指向环境字符串指针数组的指针。

snoopy 即是通过 preload 的方式在程序进行 execv() 和 execve() 系统调用的时候记录下所有需要的信息. 这种方式即意味着 snoopy 对用户和程序是透明的, 仅做记录处理, 不能改变用户或程序的命令. 已经运行的程序不受 preload 机制约束, 因为execv 和 execve 两个函数仅用于新执行一个程序. 当然如果执行的是一个脚本, 而脚本中又有 execv 和 execve 相关的系统调用(比如脚本里调用系统命令), snoopy 也会记录下来. 这在故障排错和审计的场景中是一个非常有用的功能。

snoopy 的内部则通过封装 execv, execve 函数实现记录命令的目的. 即在执行程序之前, 通过 preload 机制, 预先加载封装好的 execv 和 execve 函数, 记录执行的命令, 则实际执行真实的命令。

封装 execv, execve

snoopy 的内部则通过封装 execv, execve 函数实现记录命令的目的. 即在执行程序之前, 通过 preload 机制, 预先加载封装好的 execv 和 execve 函数, 记录执行的命令, 则实际执行真实的命令。

execve-wrapper.c源文件包含了这两个函数的封装:
/*
* SNOOPY COMMAND LOGGER
*
* Copyright (c) 2000 Marius Aamodt Eriksen <marius@linux.com>
* Copyright (c) 2000 Mike Baker <mbm@linux.com>
* Copyright (c) 2010-2015 Bostjan Skufca <bostjan@a2o.si>
*
* Part hacked on flight KL 0617, 30,000 ft or so over the Atlantic :)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/



/*
* Includes order: from local to global
*/
#include "execve-wrapper.h"

#include "snoopy.h"

#include "init-deinit.h"

#include "action/log-syscall-exec.h"
#include "inputdatastorage.h"

#include <dlfcn.h>
#include <stddef.h>



/*
* Helpers to find pointer to overloaded function
*/
#if defined(RTLD_NEXT)
#  define REAL_LIBC RTLD_NEXT
#else
#  define REAL_LIBC ((void *) -1L)
#endif

#define FN(ptr, type, name, args)   ptr = (type (*)args)dlsym (REAL_LIBC, name)



/*
* Function wrapper - execv()
*/
__attribute__((visibility("default"))) int execv (const char *filename, char *const argv[]) {
static int (*func)(const char *, char * const *);

FN(func, int, "execv", (const char *, char * const *));

char *envp[] = { NULL };
snoopy_entrypoint_execve_wrapper_init(filename, argv, envp);
snoopy_action_log_syscall_exec();
snoopy_entrypoint_execve_wrapper_exit();

return (*func) (filename, argv);
}



/*
* Function wrapper - execve()
*/
__attribute__((visibility("default"))) int execve (const char *filename, char *const argv[], char *const envp[])
{
static int (*func)(const char *, char * const *, char * const *);

FN(func, int, "execve", (const char *, char * const *, char * const *));

snoopy_entrypoint_execve_wrapper_init(filename, argv, envp);
snoopy_action_log_syscall_exec();
snoopy_entrypoint_execve_wrapper_exit();

return (*func) (filename, argv, envp);
}



void snoopy_entrypoint_execve_wrapper_init (const char *filename, char *const argv[], char *const envp[])
{
snoopy_init();

snoopy_inputdatastorage_store_filename(filename);
snoopy_inputdatastorage_store_argv(argv);
snoopy_inputdatastorage_store_envp(envp);
}



void snoopy_entrypoint_execve_wrapper_exit ()
{
snoopy_cleanup();
}

noopy_log_syscall_execv 和 snoopy_log_syscall_execve 函数则无论成功与否都不会影响后续程序的真实执行, 在 log.c 源文件中处理, 两个都通过调用 snoopy_log_syscall_exec 函数进行处理, 该函数则包括解析配置, 初始化, 过滤, 输出等功能。

我们可以通过strace看下df -h命令的执行过程。

$ strace df -h

execve("/usr/bin/df", ["df", "-h"], 0x7ffc65a061c8 /* 28 vars */) = 0
brk(NULL)                               = 0x55ab28420000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffc87261090) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fabadbeb000
access("/etc/ld.so.preload", R_OK)      = 0
openat(AT_FDCWD, "/etc/ld.so.preload", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=35, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 35, PROT_READ|PROT_WRITE, MAP_PRIVATE, 3, 0) = 0x7fabadc24000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libsnoopy.so", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0PG\0\0\0\0\0\0"..., 832) = 832
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=52064, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 58728, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fabadbdc000
mmap(0x7fabadbe0000, 20480, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x4000) = 0x7fabadbe0000
mmap(0x7fabadbe5000, 12288, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x9000) = 0x7fabadbe5000
mmap(0x7fabadbe8000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xb000) = 0x7fabadbe8000
mmap(0x7fabadbea000, 1384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fabadbea000
close(3)                                = 0
munmap(0x7fabadc24000, 35)              = 0
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=22915, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 22915, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fabadbd6000

从上面的日志可以看到,execve函数开始执行df -h程序,第四行开始访问/etc/ld.so.preload,进而加载加载/lib/x86_64-linux-gnu/libsnoopy.so,后续内容则为具体的执行信息,对应的日志会记录在/var/log/auth.log 文件中,如图

Dec 10 00:50:16 localhost snoopy[47776]: [login:ubuntu ssh:((undefined)) sid:47641 tty:/dev/pts/1 (0/root) uid:root(0)/root(0) cwd:/var/log]: strace df -h
Dec 10 00:50:16 localhost snoopy[47779]: [login:ubuntu ssh:((undefined)) sid:47641 tty:/dev/pts/1 (0/root) uid:root(0)/root(0) cwd:/var/log]: df -h

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, 可能会引起系统挂死, 详见:https://github.com/a2o/snoopy/blob/master/doc/FAQ.md的 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

总结

snoopy的原理其实也非常的简单,通过hook libc中的execve相关的库函数达到命令记录的功能,算是一种在用户态的hook方式。 snoopy 通过封装系统调用来实现记录执行的命令, 这就存在一定的风险, 比如降低系统性能, 和其它软件相冲突, 以及 hang 住系统等严重的问题, 但也带来了其它方面的好处, 在安全审计和故障排错的场景中尤为有用. 当然我们也可以按需开启 snoopy, 比如在排错的场景中, 排错前开启, 完成后再关闭即可。 不过已经运行的程序不受 preload 机制的影响, 毕竟 上述介绍的 exec 相关的函数仅用来执行新的程序, 未使用上述的两个系统调用则不会被 snoopy 处理。

漏洞悬赏计划:涂鸦智能安全响应中心(https://src.tuya.com)欢迎白帽子来探索。

参考

  • https://blog.arstercz.com/how-does-snoopy-log-every-executed-command/
  • https://blog.spoock.com/2019/12/21/snoopy/
  • https://blog.csdn.net/u010039418/article/details/85068712

- END -

# 入侵检测系统 # 入侵检测 # 命令执行 # 主机入侵检测 # 入侵检测与防御
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录