背景
So preload
在 Linux 系统中,通过设置动态链接库预加载(LD_PRELOAD),可以在程序启动时优先加载指定的库。这使得用户可以覆盖库中已有的函数实现,从而实现对某些关键函数(如execve
)的监控。
编写一个demo示例,如hook.c
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
typedef int (*orig_execve_func)(const char *filename, char *const argv[], char *const envp[]);
static orig_execve_func orig_execve = NULL;
// 我们的自定义 execve 函数
int execve(const char *filename, char *const argv[], char *const envp[]) {
// 打印将要执行的程序名称
printf("Intercepted execve call: %sn", filename);
// 动态获取原始的 execve 函数指针
orig_execve = (orig_execve_func)dlsym(RTLD_NEXT, "execve");
// 调用原始的 execve 函数
return orig_execve(filename, argv, envp);
}
然后执行以下命令进行编译
gcc -shared -fPIC hook.c -o hook.so -ldl
编译完成之后,将上面生成的动态链接库注册成 preload
echo "/usr/lib/hook.so" | sudo tee -a /etc/ld.so.preload
在这个示例中,我们通过定义一个新的execve
函数,先打印出被执行的命令,然后调用原始的 execve
函数。通过设置 LD_PRELOAD
环境变量,我们的库会在程序启动时被加载,从而允许我们的 execve
覆盖标准库中的同名函数。
退出当前 shell 并重新登录(下面会讲原因),执行命令即可看到我们编写的代码已被执行:
优缺点
-
优点:轻量级,不需修改内核代码。
-
缺点:不能监控静态链接的程序;通过系统调用(如 int80h)可绕过。
使用条件
该方法没有什么条件限制,只需有 root 权限即可(做入侵监控程序 root 权限是必需的,后面的几种方法默认也都是在 root 权限下)。
适用场景
适用于需要监控动态链接应用程序的创建过程,不涉及内核修改,适用于权限受限的环境。
Netlink Connector
Netlink Connector 是一种特殊的基于 Netlink 协议的通信机制,它构建在 Linux 内核中,用于内核与用户空间应用之间的通信。Netlink 本身是一种灵活的IPC(进程间通信)机制,主要用于网络配置和管理,但其使用范围已经扩展到了各种系统事件的通知。Netlink Connector 则专门用于传递事件和消息,包括进程事件,如进程创建和终止。
Netlink Connector 使用标准的 Netlink 套接字和通信机制,但它专注于事件的传递。它允许内核组件注册事件源,并将这些事件广播给订阅这些事件的用户空间应用程序。
关键组件
-
连接器模块(Connector Module):
-
内核模块,负责管理事件的注册和广播。
-
它处理来自用户空间的订阅请求,并在相应的事件发生时向订阅者广播通知。
-
用户空间应用程序:
-
通过创建 Netlink 套接字并绑定到特定的 Netlink 协议(如 NETLINK_CONNECTOR)来与内核模块通信。
-
应用程序发送订阅请求到内核,并监听来自内核的事件通知。
实现步骤
-
内核端配置:
-
确保内核配置中启用了 Netlink Connector 支持(通常是 CONFIG_CONNECTOR 和 CONFIG_PROC_EVENTS)。
-
用户空间监听程序:
-
创建一个 Netlink 套接字。
-
绑定到 Netlink Connector 协议。
-
发送订阅消息给内核,表明它希望接收特定类型的事件(例如,进程创建和终止)。
-
监听套接字并处理接收到的事件通知。
代码示例
下面是一个简单的用户空间程序示例,通过 Netlink Connector 接口监听内核发出的进程事件:
// 引入必要的头文件
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/connector.h>
#include <linux/cn_proc.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
// 定义Netlink协议中使用的CONNECTOR类型
#define NETLINK_CONNECTOR 11
int main() {
// 套接字描述符
int sock;
// Netlink套接字地址结构
struct sockaddr_nl sa;
// 数据接收缓冲区
char buffer[8192];
// 接收函数的返回值
int ret;
// 创建一个Netlink套接字,使用NETLINK_CONNECTOR协议
sock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR);
if (sock == -1) {
perror("socket"); // 如果创建套接字失败,则打印错误信息
return 1; // 并退出程序
}
// 初始化sockaddr_nl结构体
memset(&sa, 0, sizeof(sa));
sa.nl_family = AF_NETLINK; // 设置地址类型为AF_NETLINK
sa.nl_groups = CN_IDX_PROC; // 加入到进程事件组,监听进程相关事件
sa.nl_pid = getpid(); // 设置Netlink套接字的端口号为当前进程ID
// 将套接字绑定到刚刚设置的地址
if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) == -1) {
perror("bind"); // 如果绑定失败,打印错误信息
close(sock); // 关闭套接字
return 1; // 并退出程序
}
// 循环接收消息
while (1) {
ret = recv(sock, buffer, sizeof(buffer), 0); // 从套接字接收数据
if (ret == -1) {
perror("recv"); // 如果接收失败,打印错误信息
close(sock); // 关闭套接字
return 1; // 并退出程序
}
// 打印接收到的消息提示
printf("Received process eventn");
}
// 关闭套接字,虽然由于无限循环,实际上这行代码不会被执行
close(sock);
return 0;
}
这段代码主要通过 Netlink Connector 来接收来自内核的进程事件通知。它首先创建了一个 Netlink 套接字,然后将其绑定到一个特定的地址,该地址订阅了进程事件。通过无限循环,程序不断接收并处理来自内核的消息。每当接收到一个消息时,程序会输出一条信息表示已接收到进程事件。这个简单的示例没有对接收到的数据进行解析,只是简单地通知已接收到数据。
优缺点
-
优点:实现简单,直接从内核获取信息。
-
缺点:获取的信息有限,可能需要额外步骤来完整获取进程信息。
适用场景
非常适合需要实时监控进程活动且对性能要求不是非常高的场合。
Audit
Linux Audit 系统是一个内核集成的审计系统,能够记录系统中发生的详细安全相关事件,包括系统调用、文件访问和网络活动。
具体架构如下 :
-
用户通过用户态的管理进程配置规则(例如图中的 go-audit ,也可替换为常用的 auditd ),并通过 Netlink 套接字通知给内核。
-
内核中的 kauditd 通过 Netlink 获取到规则并加载。
-
应用程序在调用系统调用和系统调用返回时都会经过 kauditd ,kauditd 会将这些事件记录下来并通过 Netlink 回传给用户态进程。
-
用户态进程解析事件日志并输出。
从上面的架构图可知,整个框架分为用户态和内核态两部分,内核空间的 kauditd 是不可变的,用户态的程序是可以定制的,目前最常用的用户态程序就是 auditd ,除此之外知名的 osquery在底层也是通过与 Audit 交互来获取进程事件。下面我们就简单介绍一下如何通过 auditd 来监控进程创建。
apt update && apt install auditd
systemctl start auditd && systemctl status auditd
sudo auditctl -a always,exit -F arch=b64 -S execve -k exec-monitor //创建一个对execve这个系统调用的监控
再通过 auditd 软件包中的 ausearch
来检索 auditd 产生的日志:
sudo ausearch -k exec-monitor
这里使用auditctl
工具来添加一个监控所有 execve
调用的审计规则。-k exec-monitor
是为这个规则设置的一个关键字,使得查找这个规则生成的日志更容易。通过 ausearch
工具,我们可以根据关键字 exec-monitor
搜索相关的审计日志。至于其他的使用方法可以通过 man auditd
和 man auditctl
来查看。
使用条件
内核开启 Audit
-
cat/boot/config-$(uname-r)|grep^CONFIG_AUDIT
优缺点
优点
-
组件完善,使用 auditd 软件包中的工具即可满足大部分需求,无需额外开发代码。
-
相比于 Netlink Connector ,获取的信息更为全面,不仅仅是 pid 。
缺点
-
性能消耗随着进程数量提升有所上升,需要通过添加白名单等配置来限制其资源占用。
关于性能消耗:
开启了osquery的审计功能之后,会在两个方面存在性能损耗:
-
当开启了内核中审计功能之后并且存在审计规则,那么内核每次都会比对审计规则和实际产生的审计事件。
-
这个
audit consumer
(在本例中是osquery
)将会从内核netlink socket
中接受数据,然后将数据解析为了其内部定义的表的格式(socket_events
和process_events
)并保存在早RocksDB中。最后一旦这个数据被查询,那么这个数据就会被写入到文件或者是通过日志插件发送。
每一次在内核中产生系统调用,就会产生相应的审计事件。如果这样的系统调用越多,那么内核产生这些系统调用的审计事件的工作量就越大,同时osquery解析这些审计事件并且保存在数据库中的工作量也越大。
Syscall hook
上面的 Netlink Connector 和 Audit 都是 Linux 本身提供的监控系统调用的方法,如果我们想拥有更大程度的可定制化,我们就需要通过安装内核模块的方式来对系统调用进行 hook 。
目前常用的 hook 方法是通过修改 sys_call_table
( Linux 系统调用表)来实现,具体原理就是系统在执行系统调用时是通过系统调用号在 sys_call_table
中找到相应的函数进行调用,所以只要将 sys_call_table
中 execve
对应的地址改为我们安装的内核模块中的函数地址即可。
这里贴出文章里的一张图方便大家对整个流程有个直观地了解:
代码示例
修改sys_call_table
来挂钩 execve
系统调用。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/syscalls.h>
#include <linux/kallsyms.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
void **sys_call_table;
asmlinkage int (*original_execve)(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp);
asmlinkage int hook_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp) {
printk(KERN_INFO "Execve hooked: %sn", filename);
return original_execve(filename, argv, envp);
}
static int __init hook_init(void) {
// Find the system call table address
sys_call_table = (void **)kallsyms_lookup_name("sys_call_table");
// Store the original pointer of execve
original_execve = sys_call_table[__NR_execve];
// Change the page protection settings so we can write to the table
write_cr0(read_cr0() & (~0x10000));
sys_call_table[__NR_execve] = hook_execve;
write_cr0(read_cr0() | 0x10000);
printk(KERN_INFO "Module loaded: execve hookedn");
return 0;
}
static void __exit hook_exit(void) {
// Restore the original execve function in the syscall table
write_cr0(read_cr0() & (~0x10000));
sys_call_table[__NR_execve] = original_execve;
write_cr0(read_cr0() | 0x10000);
printk(KERN_INFO "Module unloaded: execve restoredn");
}
module_init(hook_init);
module_exit(hook_exit);
这个代码通过修改系统调用表来挂钩 execve
系统调用。模块初始化时,我们查找系统调用表的地址,并替换掉 execve
系统调用的入口点,使其指向我们自定义的函数 hook_execve
。当任何进程尝试执行 execve
时,都会先调用 hook_execve
,在这里我们仅打印出被执行文件的名称。
编译内核模块
创建一个makefile
obj-m += syscall_hook.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
加载模块
sudo insmod syscall_hook.ko
查看消息
dmesg
使用条件
-
可以安装内核模块。
-
需针对不同 Linux 发行版和内核版本进行定制。
优缺点
优点
-
高定制化,从系统调用层面获取完整信息。
缺点
-
开发难度大。
-
兼容性差,需针对不同发行版和内核版本进行定制和测试。
总结
So preload :Hook 库函数,不与内核交互,轻量但易被绕过。
Netlink Connector :从内核获取数据,监控系统调用,轻量,仅能直接获取 pid ,其他信息需要通过读取 proc/<pid>/来补全。
Audit :从内核获取数据,监控系统调用,功能多,不只监控进程创建,获取的信息相对全面。
Syscall hook :从内核获取数据,监控系统调用,最接近实际系统调用,定制度高,兼容性差。
单纯地看监控进程创建这方面,更推荐使用 Netlink Connector 的方式,这种方式在保证从内核获取数据的前提下又足够轻量,方便进行定制化开发。如果是想要进行全方面的监控包括进程、网络和文件,Audit 是一个不错的选择。
原文始发于微信公众号(德斯克安全小课堂):对内核监控方式评测
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论