常见bash监控方案

说明

命令监控是HIDS中绕过不去的一个话题。下面就对命令监控中的各种方案进行总结。
本篇文章只是将目前常见的方法汇总起来了,并没有提出其他新颖的方法。文章中大部分原理和实现都是借助其他文章的内容而来。

bash_history

默认情况下,所有通过bash执行的命令全部是保存在对应用户目录下的.bash_history文件下。那么作为简单的一个方案就是防止.bash_history被篡改,同时尽可能收集用户输入的命令防止遗漏。
常见的做法包括,加固.bash_history、优化.bashrc配置

加固文件属性

1
2
3
4
5
# chattr +a ~/.bash_profile
# chattr +a ~/.bash_login
# chattr +a ~/.profile
# chattr +a ~/.bash_logout
# chattr +a ~/.bashrc

通过为常见的bash的配置文件增加a属性,限制文件只能被添加数据,不能被删除或修改。

增强配置

/.bashrc中配置:

1
2
shopt -s histappend
readonly PROMPT_COMMAND="history -a"

  • histappend 选项一旦被设置,每次 session 结束时,Bash 会将该 session 的历史追加到历史文件中(通常是 ~/.bash_history),而不是覆盖它。这意味着来自不同终端窗口或会话的命令历史记录会被保留下来,而不会互相覆盖。
  • PROMPT_COMMAND 是一个环境变量,它设置了一个命令,该命令会在显示 Bash 提示符之前执行。
  • history -a 命令会将当前会话中新增的历史记录追加到历史文件中。这意味着每次命令提示符出现之前,新的命令都会被保存起来,这样即使是在不同的终端会话中,你也能立即访问到其他会话中输入的命令。
  • PROMPT_COMMAND 设置为 readonly 是为了防止这个变量在后续被修改。readonly 是一个内置命令,用于将变量或函数标记为只读,这样它们就不能在赋值语句中被重新赋值,也不能被 unset。

总结一下,这两行代码的作用是改进多个 Bash 会话之间的命令历史记录的管理。它们确保了所有会话的命令都被追加到历史文件中,并且在每个新命令提示符出现之前都会保存当前会话的命令历史记录。
参考:https://www.51cto.com/article/244661.html

cornerstone

实时记录
通过trap DEBUG可以很好的及时记录准备执行的命令,因为它会在命令执行前被调用。但有个小插曲,如果直接使用trap ‘AUDIT_DEBUG’ DEBUG来调用,会使环境变量$(记录上一条命令的最后一个参数)失效,因此需要加入”$”。

1
2
3
4
5
6
7
8
9
10
11
function AUDIT_DEBUG() {
...
local AUDIT_CMD="$(history 1)" #current history command
AUDIT_HISTLINE=$(/bin/echo $AUDIT_CMD |/bin/awk '{print $1}')
if ... #avoid logging unexecuted commands after 'ctrl-c', 'empty+enter', or after 'ctrl-d'
LOGGER
fi
}
...
declare -rx PROMPT_COMMAND="[ -z \"\$AUDIT_INCLUDED\" ] && source $AUDIT_FILE;"
trap 'AUDIT_DEBUG "$_"' DEBUG

LOGGER是最终记录命令的代码,使用的logger命令,具体级别配置等信息可以自行参考相关文章,按需使用。

1
/bin/logger -p local6.notice "$AUDIT_STR $PWD ${AUDIT_CMD}"

其他
记录未分配伪终端情况下的命令
上述依赖于history 1来记录的方式有一个弊端:在未分配伪终端(分辨方法:输入tty,如果回显not a tty,则是未分配伪终端的情况)的情况下,history不会有记录。因此,需要在配置文件中加入如下语句:

1
2
3
4
if [[ ! "$(tty)" =~ "/dev" ]]
then
set -o history
fi

set -o history就是开启当前环境的history功能。通过适当的条件判断后,也可以在shell脚本执行的环境下开启history来记录在脚本内执行的命令。但这么做会改变脚本执行时的环境,可能会造成一些意料之外的影响。
如在命令行下直接调用sh。sh是bash符合POSIX标准的一种特殊模式,通常不会调用bashrc等文件。但是除了非交互且非登录式的sh以外,sh会加载$ENV所指向的文件。因此需要声明ENV变量指向我们的配置文件。

1
declare -rx ENV="/etc/secaudit_bash"

无论是bash还是sh,调用时均可以通过–norc来指定不加载配置文件。因此需要在PROMPT_COMMAND中“手动”加载配置文件。

1
declare -rx PROMPT_COMMAND="[ -z \"\$AUDIT_INCLUDED\" ] && source $AUDIT_FILE;

参考:
https://github.com/momosecurity/cornerstone
https://mp.weixin.qq.com/s/suRCuK0ctC6F9v2dOg5Wcg

仅仅只是通过.bash_history文件来收集用户执行,还是存在被绕过的可能。常见的方式比如用户自行上传编译好的bash二进制文件,或者是直接通过代码执行命令,比如java中的exec。所以这种方式还是会很容易被绕过。
但是想对后面的想法,这种方式也是对系统侵入性最小的方法,仅仅只需要修改bashrc等配置文件就可以了。

ld_preload

ld_preload介绍

其实系统中大部分命令执行,最终无外乎都是通过最终在用户态中调用execvefork等命令完成的。那么如果可以在用户态中监控这些调用,相当间接监控了用户执行的命令,当然也包括了各种通过java或者是golang执行的命令。
Linux操作系统的动态链接库在加载过程中,动态链接器会先读取LD_PRELOAD环境变量和默认配置文件/etc/ld.so.preload,并将读取到的动态链接库文件进行预加载。即使程序不依赖这些动态链接库,LD_PRELOAD环境变量和/etc/ld.so.preload配置文件中指定的动态链接库依然会被加载,因为它们的优先级比LD_LIBRARY_PATH环境变量所定义的链接库查找路径的文件优先级要高,所以能够提前于用户调用的动态库载入。
简单来说LD_PRELOAD的加载是最优先级的我们可以用他来做一些有趣的操作(骚操作).

一般情况下,ld-linux.so加载动态链接库的顺序为:

LD_PRELOAD > LD_LIBRARY_PATH > /etc/ld.so.cache > /lib > /usr/lib

参考:https://ivanzz1001.github.io/records/post/linux/2018/04/08/linux-ld-preload

之前文章 snoopy记录命令原理分析 就是利用LD_PRELOAD监控在用户态下执行的execvexecve函数。这种方式向比较前面的直接记录.bash_history更加难以绕过和通用。除了可以记录使用的bash命令之外,所有在用户态的执行命令都可以记录,意味着各种语言执行的命令都可以记录到。

snoopy实现原理

在snoopy.c中一共定义了三个函数.分别是execv,execve,snoopy_log. execv和execve都是libc中的库函数,最终回去调用sys_execve和sys_execve两个系统调用.在系统中所有执行的命令最终都会调用execve()这个一系列的系统调用函数,由于覆盖了libc中的库函数,所以理所当然就能够记录到所有的命令了.
通过 FN(func,int,"execv",(const char *, char **const)); 来执行实际的函数,然后调用snoopy_log()来记录命令.
在snoopy_log()中,通过

  1. strncat(logString, argv[i], min(SNOOPY_MAX_ARG_LENGTH, argLength));得到execve所有的参数,拼接起来就是命令
  2. syslog(LOG_INFO, "[uid:%d sid:%d cwd:%s tty:%s]: %s", getuid(), getsid(0), cwd, ttyPath, logString);写入对应的系统文件.在ubuntu系统中,对应的系统文件就是/var/log/auth.log

参考: https://blog.spoock.com/2019/12/21/snoopy/

snoopy特点

noopy 的缺点也比较明显, 主要包含以下几点:

  1. 仅支持 execv, execve 相关系统调用的操作;
  2. 不设置规则可能产生的日志过多, 对日志搜集系统造成很大的负担;
  3. 暂不支持过滤敏感信息规则;

在实际的使用中, snoopy 记录方式可以很详细的记录所有的命令操作信息, 帮助我们定位很多疑难问题. 不过我们也需要通过过滤规则来避免产生过多的信息, snoopy 的过滤规则可以满足以下需求:

  1. 忽略 cron, daemon 产生的记录;
  2. 忽略监控用户(比如 nagios, zabbix, promethus 等) 产生的记录;

比如以下配置, 即可忽略 crond, my-daemon 守护进程, 忽略 zabbix 用户:

1
2
# zabbix uid 为 992
filter_chain = exclude_uid:992;exclude_spawns_of:crond,my-daemon

备注: 过滤规则在 (filtering.c - snoopy_filtering_check_chain) 函数实现, 由 log.c - snoopy_log_syscall_exec 函数调用, 过滤规则为事后行为, 即在打印日志的时候判断是否满足过滤规则, 并非事前行为.
另外, 我们在 snoopy 的基础上增加了 exclude_comm 过滤规则, 我们可以忽略记录指定的命令, 比如以下:

1
filter_chain = exclude_uid:992;exclude_comm:mysql,mongo,redis-cli

exclude_comm 指定忽略以 mysql, mongo 和 redis-cli 工具执行的命令, 很多管理员或者脚本在使用这些工具的时候常常会加上用户密码信息, 这在明文环境中是很危险的行为, exclude_comm 规则简单的避免了常用工具泄漏敏感信息的隐患.
参考:https://blog.arstercz.com/how-to-audit-linux-system-operation/

Audit

audit介绍

有关audit的介绍,之前在分析osquery中的实现时,也对audit进行了简要的介绍。

在这张图片中,go-audit的角色等同于auditd以及osquery.当软件运行时,它会产生系统调用。当audit启动之后,内核中的audit程序(即图中中的kauditd)就会判断这些系统调用是否在已经设置的审计规则当中。如果存在,那么这条事件就会被发送至audit netlink socket.然后像auditd/osquery/go-audit就会监听并接受audit netlink socket中的事件。
参考:https://blog.spoock.com/2019/01/13/auditing-with-osquery/

Linux中的audit系统是一个强大的安全监控和跟踪框架,它允许系统管理员和安全专家记录系统上的事件,这些事件可能与安全相关,如文件访问、系统调用、网络活动、用户登录等。audit系统由两个主要组件组成:auditd守护进程和一系列的用户空间工具。
auditdaudit系统的核心,是一个后台运行的守护进程,负责监听内核发出的审计事件,然后将这些事件记录到磁盘上的日志文件中。auditd守护进程的配置文件通常位于/etc/audit/auditd.conf,在这个文件中可以设置日志文件的位置、如何处理日志文件满的情况、日志保留策略等。
auditctl:用于向audit系统添加、删除或更新审计规则,也可以用来控制auditd守护进程的运行。
auditd 整体上为分离的架构, auditctl 可以控制 kauditd 生成记录的策略, kauditd 生成的记录事件会发送到 auditd 守护程序, audisp 可以消费 auditd 的记录到其它地方. 其主要的几个工具包含如下:

  • auditd, audit 守护程序, audit 相关配置的加载, 日志落盘等都通过 auditd 完成
  • auditctl,用来控制 kernel audit 相关的规则, 可以及时生效, 过滤通常使用 auditctl 实时修改
  • audisp,和 auditd 守护程序通信, 将收到的记录信息发送到别处, 比如发到 syslog 中
  • augenrules, ausearch, autrace, aureport, audit 提供的一些辅助分析的工具

在实际的使用中, 我们不建议对常见的一些系统调用进行监控, 比如connect, accept, execve 等都是日志高产的行为, 应该在需要定位问题的时候开启. 当然如果过滤策略设置的足够详细, 比如忽略了指定用户, crond 进程等, 就可以比较放心的监控这些系统调用.
参考: https://blog.arstercz.com/how-to-audit-linux-system-operation

auditbeat

auditbeat是elastic公司开发用于接收和处理audit产生的审计信息,然后发送到ES等相关技术栈中。无需触碰 auditd 即可在 Elastic Stack 中监控用户的行为和系统进程,分析用户事件数据。Auditbeat 与 Linux 审计框架直接通信,收集与 auditd 相同的数据,并实时发送这些事件消息到 Elastic Stack。如果您比较怀旧,也可以(在新的内核中)让 auditd 与 Auditbeat 一起运行。
您可以使用既有审计规则。轻而易举地采集数据,而无需重写规则。是谁在什么时间做了什么事情?Auditbeat 会记住所有这些原始的系统调用数据,以及相关联的路径,方便您了解所需的上下文信息。
与 auditd 不同的是,Auditbeat 会组合相关消息到一个事件里面。它同时也解析这些消息并对其进行标准化处理,如将数字 ID 转换为名字,然后将结构化的数据发送到 Elasticsearch。同时使用每个 Beat 都有的处理器 (processor) 特性,您可以很轻松地对数据进行过滤和修改。
您可以使用既有审计规则。轻而易举地采集数据,而无需重写规则。是谁在什么时间做了什么事情?Auditbeat 会记住所有这些原始的系统调用数据,以及相关联的路径,方便您了解所需的上下文信息。
参考:https://www.elastic.co/cn/beats/auditbeat

部署和使用
关闭auditd

1
sudo systemctl stop  auditd.service

安装

1
2
3
4
curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-8.11.4-amd64.deb
sudo dpkg -i auditbeat-8.11.4-amd64.deb
sudo systemctl start auditbeat
sudo systemctl status auditbeat

添加规则

1
2
3
$ sudo auditctl -a always,exit -F arch=b64 -S execve -k execve_auditbeat
$ sudo auditctl -l
-a always,exit -F arch=b64 -S execve -F key=execve_auditbeat

持久化规则

1
echo '-a always,exit -F arch=b64 -S execve -k execve_auditbeat' | sudo tee -a /etc/audit/rules.d/execve.rules

auditbeat目录下也存在一个默认规则示例:/etc/auditbeat/audit.rules.d/sample-rules.conf.disabled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## If you are on a 64 bit platform, everything should be running
## in 64 bit mode. This rule will detect any use of the 32 bit syscalls
## because this might be a sign of someone exploiting a hole in the 32
## bit API.
-a always,exit -F arch=b32 -S all -F key=32bit-abi

## Executions.
-a always,exit -F arch=b64 -S execve,execveat -k exec

## Identity changes.
-w /etc/group -p wa -k identity
-w /etc/passwd -p wa -k identity
-w /etc/gshadow -p wa -k identity

## Unauthorized access attempts.
-a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -k access
-a always,exit -F arch=b64 -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -k access

输出到ES
配置规则: /etc/auditbeat/auditbeat.yml

1
2
3
4
5
6
7
8
9
10
11
output.elasticsearch:
# Array of hosts to connect to.
hosts: ["localhost:9200"]

# Protocol - either `http` (default) or `https`.
#protocol: "https"

# Authentication credentials - either API key or username/password.
#api_key: "id:api_key"
#username: "elastic"
#password: "changeme"

配置完成之后,通过sudo systemctl restart auditbeat重启生效。查看规则是否生效:

1
2
sudo auditbeat show auditd-rules
-a always,exit -F arch=b64 -S execve -F key=execve_auditbeat

参考:https://www.elastic.co/guide/en/beats/auditbeat/current/auditbeat-installation-configuration.html

通过ES就可以查看到由auditbeat执行的所有信息,下面就是展示了auditbeat发送有关进程的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
"process": {
"pid": 1825704,
"parent": {
"pid": 1823989
},
"title": "whoami",
"name": "whoami",
"executable": "/usr/bin/whoami",
"working_directory": "/etc/auditbeat",
"args": [
"whoami"
]
}

Bash

前面说过的.bash_history的方式相当于是bash记录的本地执行日志。换种思路,可以直接通过修改bash源代码直接记录用户通过bash执行的命令然后将其发送到远程指定存储,比如ES等等。
如果需要通过修改bash源代码记录到所有的执行的命令,那么就需要深入分析bash源代码,了解到需要具体修改得是bash中的那个函数。
因为bash源代码中的整体逻辑比较复杂,涉及到的函数也很多,那么对应的实现方案就会有很多了。

bash_syslog_history

修改bash_syslog_history函数,将用户输入全部发送到
修改config-top.h文件为如下内容

1
2
3
4
5
6
7
/* Define if you want each line saved to the history list in bashhist.c:
bash_add_history() to be sent to syslog(). */
#define SYSLOG_HISTORY
#if defined (SYSLOG_HISTORY)
# define SYSLOG_FACILITY LOG_LOCAL1
# define SYSLOG_LEVEL LOG_DEBUG
#endif

默认情况喜爱日志记录在/var/log/message文件中。通过上面的配置调整为local1.debug指定的文件中。
修改bashhist.c中的bash_syslog_history函数实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void
bash_syslog_history (line)
const char *line;
{
char trunc[SYSLOG_MAXLEN];

if (strlen(line) < SYSLOG_MAXLEN)
syslog (SYSLOG_FACILITY|SYSLOG_LEVEL, "HISTORY: PID=%d UID=%d USER=%s CMD=%s", getpid(), current_user.uid, current_user.user_name, line);
else
{
strncpy (trunc, line, SYSLOG_MAXLEN);
trunc[SYSLOG_MAXLEN - 1] = '\0';
syslog (SYSLOG_FACILITY|SYSLOG_LEVEL, "HISTORY: PID=%d UID=%d USER=%s CMD(TRUNCATED)=%s", getpid(), current_user.uid, current_user.user_name, trunc);
}
}

  • 记录的命令包括:HISTORY: PID=%d UID=%d USER=%s CMD=%s", getpid(), current_user.uid, current_user.user_name, line,进程 ID(通过 getpid() 获取)、用户 ID(current_user.uid)、用户名(current_user.user_name)和命令行(line
  • 调用 syslog 函数将整个命令行记录到系统日志。

修改完成之后,需要重新编译后安装。成功安装完成之后,就需要配置rsyslog
配置文件:/etc/rsyslog.conf

1
local1.debug "/var/log/bash-log/%FROMHOST-IP%.log"

重启restart Rsyslog

1
sudo systemctl restart rsyslog

运行之后,得到的日志结果如下所示:

1
2
3
4
5
6
7
8
9
10
$ tail /var/log/bash-log/127.0.0.1.log
2018-07-19T23:23:37.568131-04:00 centos7 bash: HISTORY: PID=12511 UID=1000 USER=vagrant CMD=sudo -Es
2018-07-19T23:23:37.573825-04:00 centos7 sudo: vagrant : TTY=pts/0 ; PWD=/home/vagrant/rpmbuild/SOURCES ; USER=root ; COMMAND=/bin/bash
2018-07-19T23:23:37.589258-04:00 centos7 systemd-logind: Got message type=signal sender=org.freedesktop.DBus destination=n/a object=/org/freedesktop/DBus interface=org.freedesktop.DBus member=NameOwnerChanged cookie=4454 reply_cookie=0 error=n/a
2018-07-19T23:23:37.590633-04:00 centos7 dbus[588]: [system] Activating service name='org.freedesktop.problems' (using servicehelper)
2018-07-19T23:23:37.590806-04:00 centos7 dbus-daemon: dbus[588]: [system] Activating service name='org.freedesktop.problems' (using servicehelper)
2018-07-19T23:23:37.592160-04:00 centos7 dbus[588]: [system] Activated service 'org.freedesktop.problems' failed: Failed to execute program /lib64/dbus-1/dbus-daemon-launch-helper: Success
2018-07-19T23:23:37.592311-04:00 centos7 dbus-daemon: dbus[588]: [system] Activated service 'org.freedesktop.problems' failed: Failed to execute program /lib64/dbus-1/dbus-daemon-launch-helper: Success
2018-07-19T23:23:37.602174-04:00 centos7 systemd-logind: Got message type=signal sender=org.freedesktop.DBus destination=n/a object=/org/freedesktop/DBus interface=org.freedesktop.DBus member=NameOwnerChanged cookie=4455 reply_cookie=0 error=n/a
2018-07-19T23:23:38.520300-04:00 centos7 bash: HISTORY: PID=12585 UID=0 USER=root CMD=ls

参考:https://unix.stackexchange.com/questions/457107/sending-bash-history-to-syslog

expand_word_list_internal

函数 expand_word_list_internal 是对一系列单词进行各种 shell 扩展,比如变量 ($HOME)、通配符 (*, ?)、大括号扩展 ({a,b})、波浪线扩展 (~)、算术扩展 ($((expression)))、命令替换 (command$(command)) 等。
expand_word_list_internal 函数的执行结果是,用户输入的原始命令行文本被转换成了 Bash 可以直接执行的一系列命令和参数。那么就可以利用expand_word_list_internal执行完成之后的list,转换为用户的输入字符串。仿照上面bash_syslog_history,将最终的命令输出到syslog中。
基本的实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* 辅助函数,将 WORD_LIST 转换为字符串 */
char *word_list_to_string(WORD_LIST *list) {
WORD_LIST *wl;
size_t needed_size = 1; // 初始为 '\0'
char *result, *p;

// 首先计算所需的字符串长度
for (wl = list; wl; wl = wl->next) {
needed_size += strlen(wl->word->word) + 1; // 加 1 为了空格或最后的 '\0'
}

// 分配内存
result = (char *)malloc(needed_size);
if (!result) {
return NULL;
}

// 拼接字符串
p = result;
for (wl = list; wl; wl = wl->next) {
strcpy(p, wl->word->word); // 复制单词
p += strlen(wl->word->word); // 移动指针
if (wl->next) {
*p++ = ' '; // 单词间添加空格
}
}
*p = '\0'; // 确保字符串以 '\0' 结尾

return result;
}

/* 在 expand_word_list_internal 函数的末尾添加以下代码 */
/* ... (expand_word_list_internal 的其余部分) ... */

// 扩展完成后,将 WORD_LIST 转换为字符串
char *expanded_str = word_list_to_string(new_list);
if (expanded_str) {
char trunc[SYSLOG_MAXLEN];
strncpy(trunc, expanded_str, SYSLOG_MAXLEN - 1);
trunc[SYSLOG_MAXLEN - 1] = '\0'; // 确保字符串以 '\0' 结尾
syslog(SYSLOG_FACILITY|SYSLOG_LEVEL, "EXPANDED: PID=%d UID=%d CMD=%s", getpid(), current_user.uid, trunc);
free(expanded_str); // 释放字符串占用的内存
}

首先定义了一个辅助函数 word_list_to_string,它接收一个 WORD_LIST 并将其转换成一个字符串。然后,在 expand_word_list_internal 函数的末尾调用这个辅助函数,并将结果字符串记录到 syslog 中。
之后的syslog配置就和bash_syslog_history中说明的配置是一样的,就不在作介绍了。
通过修改bash源代码记录日志的好处是在于,可以记录到用户执行的命令的完整信息,包括参数等,可以自行设置数据格式以及需要的额外信息。但是这种方式也存在一些明显的缺陷:

  1. 容易被绕过, 用户可以使用 csh, zsh 等,只需要不使用修改后的bash就可以绕过命令记录;
  2. 无法记录 shell 脚本内的操作,比如bash some.sh这种就无法记录到some.sh脚本具体内容,当然这种方式也是.bash_history方式的缺点;
  3. 可能需要不停的更新 bash 版本, 工作量大, 否则容易被发行版替换;
  4. bash几乎是所有系统中的基础组件,所以对于开发人员的水平有较高的要求,后续的维护成本也是需要考虑的问题;

Netlink 是一个套接字家族(socket family),它被用于内核与用户态进程以及用户态进程之间的 IPC 通信,我们常用的 ss命令就是通过 Netlink 与内核通信获取的信息。
Netlink Connector 是一种 Netlink ,它的 Netlink 协议号是 NETLINK_CONNECTOR,其代码位于 https://github.com/torvalds/linux/tree/master/drivers/connector 中,其中 connectors.c 和 cnqueue.c 是 Netlink Connector 的实现代码,而 cnproc.c 是一个应用实例,名为进程事件连接器,我们可以通过该连接器来实现对进程创建的监控。
图中的 ncp 为 Netlink Connector Process,即用户态我们需要开发的程序。

参考:https://sq.sf.163.com/blog/article/311384915510648832

好朋友之前也写过来相关的实现文章,参见: netlink监控系统进程.md
已经有基于netlin编写的进程监控程序,pmon ,配套的文章是 linux process monitoring
通过 gcc pmon.c-o pmon生成可执行程序,然后执行该程序即可看到效果:

使用netlink的优缺点都很明显。
优点,2.6+的内核就支持netlink的功能,所以基本上可以覆盖掉所有的系统,同时对系统侵入性低。缺点是只能获取到pid信息,其他信息需要通过/proc/<pid>获取,所以进程存活时间太短,可能就无法捕获到有效信息。

Execve

既然所有的命令执行,最终都是由系统调用execveexecveat系列函数完整,那么直接Hook这些系统调用就完全可以解决命令执行的问题。当然内核Hook的方式也有很多,这里就以作为传统的LKM的方式作为演示和说明。
有关LKM的实现原理,可以参见 lkm入门&netlink通信示例
LKM是loadable kernel module的简称, 即可加载的内核模块,是一段运行在内核空间的代码,可以动态加载,无需重新实现整个内核。
首先这个内核不同与微内核的模块,微内核的模块是一个个的daemon进程,工作于用户空间。Linux的模块只是一个内核的目标代码,内核通过执行运行时的连接,来把它整合到kernel中,所以说Linux的模块机制并没有改变Linux内核的单内核的本质。其模块也是工作于内核模式,享有内核的所有的特权。
在早期 smith_hook.c 中就是采用的LKM技术监控系统内核,主要的实现逻辑如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct kretprobe execve_kretprobe = {
.kp.symbol_name = P_GET_SYSCALL_NAME(execve),
.entry_handler = execve_entry_handler,
.maxactive = MAXACTIVE,
};

struct execve_data {
char *abs_path;
char *argv;
char *ssh_connection;
char *ld_preload;

int free_abs_path;
int free_ssh_connection;
int free_ld_preload;
int free_argv;
};

int execve_entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs) {
struct execve_data *data = kzalloc(sizeof(struct execve_data), GFP_ATOMIC);
if(unlikely(!data))
return 0;
if (share_mem_flag != -1) {
char **argv = (char **) p_get_arg2(regs);
char **env = (char **) p_get_arg3(regs);
get_execve_data(argv, env, data);
}
return 0;
}

作用是创建一个 kretprobe 实例,用于监视 execve 系统调用的返回,并在返回时执行特定的处理函数 execve_entry_handler
get_execve_data()中就是处理execve系统调用的所有的参数,最终得到execve_data结构体中所有的信息。
通过LKM技术监控常见的execve系统调用,虽然功能丰富因为是深入到系统内核,且和内核版本高度相关,在实际实施和应用过程中也会遇到很多的问题。这也就是为什么目前没有被大规模采用的原因。

  1. 有关execve的系列内和函数较多,包括execveexecveat等等。为了防止被绕过,需要全部Hook
  2. LKM技术Hook的内核函数的签名和内核版本高强度相关。所以如果涉及到的内核版本较多,就需要做大量的内核适配的工作
  3. LKM需要各个版本的内核函数之间的差异性非常了解,同时对C语言也需要有一定的开发经验,同时还要具备内核开发经验,同时具备以上条件对开发人员的开发水平就会有很高的要求
  4. LKM由于是深入到内核当中Hook技术,所以问题复现和排查都非常困难,LKM一旦出现了crush直接会导致机器重启重启,严重情况下会直接影响业务,所以后期维护的成本也非常高
    基于以上的原因,虽然LKM技术可以自定义开发获取到所有的信息,但是基于开发成本、维护成本,这种方案并没有大规模被采用。

eBPF

有关eBPF的历史背景和入门介绍,前面也写了很多文章说明和介绍。有兴趣可以参考 eBPF介绍eBPF代码入门 eBPF中常见的事件类型
本章节只是关注如何使用eBPF技术监控到Linux系统中的命令执行。如果使用eBPF监控系统的命令执行从用户态和内核态出发,各有一种方案:

  • 用户态:使用eBPF探测bash进程,获取执行命令
  • 内核态,使用eBPF探测execve,execveat等系统调用函数,获取执行命令

通过execve,execveat等系统调用函数的方案的具体实现和前面基于LKM技术基本上是一致的。本章节关注使用eBPF技术探测bash进程,获取命令执行。
可以直接探测bash进程中的readline()函数,readline()函数就是bash处理用户输入命令的函数。因为bash属于是用户态进程,所以采用eBPF中的uprobe技术。uprobe是”User Probe”的缩写,它利用了Linux内核中的ftrace(function trace)框架来实现。通过uprobe,可以在用户空间程序的指定函数入口或出口处插入探测点,当该函数被调用或返回时,可以触发事先定义的处理逻辑。kprobe是用于监控内核态的程序,uprobe就是用于监控用户态的程序。
内核态代码:

1
2
3
4
5
6
7
SEC("uprobe/readline")
int uprobe_readline(void *ctx)
{
bpf_printk("new bash command detected\n");
void *line = (void *) PT_REGS_RC(ctx);
return 0;
};

通过(void *) PT_REGS_RC(ctx)方式就可以获得用户输入的命令
用户态代码:

1
2
3
4
5
6
7
8
9
10
m := &manager.Manager{
Probes: []*manager.Probe{
{
Section: "uprobe/readline",
EbpfFuncName: "uprobe_readline",
AttachToFuncName: "readline",
BinaryPath: "/usr/bin/bash",
},
},
}

uprobe监控相比其他类型事件的监控存在一个很明显的差别,需要额外配置BinaryPath。原因是因为uprobe针对用户态程序的监控,所以需要指定用户态程序的路径。在本例中,我们针对的是bash进程,所以需要指定/usr/bin/bash
最终实现效果是:

参考: https://blog.spoock.com/2023/08/19/eBPF-Hook/
有关针对具体的介绍,可以参见文章 在 eBPF 中使用 uprobe 捕获 bash 的 readline 函数调用
使用eBPF开发优势:

  1. eBPF目前生态蓬勃发展,相关工具、技术发展都非常迅猛,网上也有大量的文章,资料丰富
  2. 相比较LKM开发,eBPF开发更容易上手。因为eBPF也有更加严格的校验,所以eBPF的程序也更加安全
  3. 目前内核对于eBPF的支持力度越来越大,每个内核版本中的eBPF的能力都不断增强,eBPF目前也在安全、观测领域应用也越来越广泛

使用eBPF开发劣势:

  1. eBPF 在 kernel-4.10 之后的支持才相对全面,所以适配性就会存在问题,低于此版本的内核无法使用eBPF
  2. 如果在代码中使用了高版本eBPF的功能特性,那么同样地在低版本内核下可能也无法成功运行
  3. 同样的,目前能够熟练应用eBPF编写代码的开发人员也也比较少,后期的迭代开发和维护成本也是需要考虑的问题。

参考

https://cloud.tencent.com/developer/article/1560417
https://www.cnblogs.com/cute/p/14785650.html
https://yongnights.github.io/2020/04/03/%E4%BD%BF%E7%94%A8%20Auditbeat%20%E6%A8%A1%E5%9D%97%E7%9B%91%E6%8E%A7%20shell%20%E5%91%BD%E4%BB%A4/
https://www.51cto.com/article/244661.html
https://github.com/momosecurity/cornerstone
*https://blog.51cto.com/koumm/1763145**