背景
监控进程创建是系统管理和安全监控中的一个关键方面。本文将介绍四种常见的进程监控技术:So preload、Netlink Connector、Audit 和 Syscall hook的实现方式和适用场景。主要参考这篇文章,改文章已经归纳得很好了:https://www.freebuf.com/column/208928.html,
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: %s\n", 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 event\n"); } // 关闭套接字,虽然由于无限循环,实际上这行代码不会被执行 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
对应的地址改为我们安装的内核模块中的函数地址即可。
上述具体的实现细节可参考 YSRC 的这篇关于驭龙 HIDS 如何实现进程监控的文章:https://mp.weixin.qq.com/s/ntE5FNM8UaXQFC5l4iKUUw,这里贴出文章里的一张图方便大家对整个流程有个直观地了解:
代码示例
修改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: %s\n", 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 hooked\n"); 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 restored\n"); } 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 是一个不错的选择。
参考:
https://blog.spoock.com/2019/01/13/auditing-with-osquery/
https://www.freebuf.com/column/208928.html
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)