【基于LSM的ELF文件安全模块设计】参考
《基于LSM的ELF文件安全模块设计文档》
一、设计目标
本设计致力于通过 Linux 安全模块(LSM)构建一个强大而严密的安全防护体系,以实现对 ELF 文件(涵盖可执行文件和动态链接库)的绝对严格的合法性和完整性检查。其核心目标在于确保系统在加载和运行各类 ELF 文件时,能够有效抵御来自未知来源或被恶意篡改的文件所可能带来的严重安全威胁,如数据泄漏、系统崩溃等,进而全力维护系统的高度稳定性和数据的绝对安全性。通过精心设计的安全机制,本研究旨在为系统提供全方位的保护,使得每一个 ELF 文件在进入系统执行或被加载的过程中都经过严格的审查,确保其合法性与完整性无懈可击。
二、设计思路
(一)总体架构
本模块采用分层架构设计,主要由内核态的 LSM 钩子函数和相关辅助函数,以及用户态管理程序组成。内核态部分犹如坚固的核心安全堡垒,在关键系统调用之际,迅速拦截并进行深入细致的 ELF 文件检查。用户态程序则作为高效的管理配置层,用于辅助管理配置文件和查看相关日志等操作,为管理员精心打造便捷的操作接口,共同构建起一个完整的安全防护体系。
(二)合法性验证
1. 来源验证
- 设计方法:在内核模块中巧妙设计一个高效的数据结构,专门用于维护受信任的路径列表或者源仓库标识列表。采用诸如哈希表或者红黑树等先进的数据结构,能够显著提高查找效率。当系统加载 ELF 文件时,通过精准的系统调用获取其文件路径,接着运用精心设计的查找算法与受信任列表进行快速而准确的比对。
- 处理策略:一旦发现文件路径不在受信任列表中,可根据系统安全策略采取一系列有力措施。例如,进行详细的日志记录,涵盖文件路径、精确的时间戳以及操作类型等丰富信息;向管理员发送及时的警告提示,可以通过邮件、系统消息等多种方式,确保管理员能够第一时间知晓潜在风险;或者直接果断拒绝加载该文件,从源头杜绝安全隐患。
2. 签名验证
- 设计方法:积极引入成熟可靠的公钥加密技术用于签名验证。在系统初始化阶段,从安全的配置文件或者密钥存储区域精心加载受信任的公钥,并将其妥善存储在内存中的安全区域,确保公钥的安全性和可用性。当 ELF 文件加载时,准确提取文件中的签名信息,可以将签名存储在文件特定位置或者通过特定的文件元数据获取。然后,运用对应的公钥和相应的加密库函数进行严格的签名验证。
- 处理策略:如果签名验证未能通过,坚决拒绝加载该文件,并详细记录验证失败的关键信息,包括文件路径、所采用的签名算法以及具体的验证结果等,为管理员进行深入排查和细致分析提供有力依据。
(三)完整性验证
1. 哈希计算
- 设计方法:精心挑选一种安全可靠的哈希算法,如 SHA - 256,用于准确计算 ELF 文件的哈希值。在文件首次被确认为合法时,例如通过上述严格的合法性验证以及管理员的手动确认等综合方式,将精心计算得到的哈希值安全地存储在可靠的数据库或者配置文件中。每次加载该文件时,重新计算其哈希值,并与存储的哈希值进行细致比对。
- 处理策略:倘若哈希值不匹配,这表明文件极有可能被篡改,此时应果断拒绝加载该文件,并详细记录相关的哈希值不匹配信息,包括文件路径、原始哈希值、新计算的哈希值等,以便后续进行深入分析和追踪。对于大文件的哈希计算,采用高效的内存映射(mmap)等方式,并细致入微地处理可能出现的各种错误。
2. 文件大小检查
- 设计方法:在文件首次被确认为合法时,准确记录其文件大小信息。可以将文件大小信息与文件路径等其他重要元数据一起妥善存储在数据库或者配置文件中。在加载文件时,通过系统调用精准获取文件的实际大小,并与存储的大小信息进行严格比对。
- 处理策略:尽管文件大小并非绝对可靠的完整性指标,但当文件大小不匹配时,可将其作为一个重要的辅助检查手段。此时可以采取进一步的检查措施,如重新计算哈希值进行确认,或者向管理员发送明确的警告提示,及时告知文件大小出现异常情况,以便管理员采取相应措施。
(四)与 LSM 集成
1. 注册钩子函数
- 设计方法:深入钻研 LSM 框架的接口规范和机制,针对
execve
系统调用(用于执行可执行文件)和dlopen
系统调用(用于加载动态链接库)精准注册相应的钩子函数。在注册过程中,务必确保钩子函数的优先级和调用顺序完全符合系统安全要求,以保证在文件加载和执行的关键操作点能够及时触发安全检查,实现无缝的安全防护。 - 处理策略:当钩子函数被触发时,严格按照预先设计的安全检查流程,依次执行合法性和完整性检查操作,确保每一个环节都无懈可击。
2. 决策逻辑
- 设计方法:精心设计一套清晰明确的决策逻辑,根据合法性和完整性检查的结果来准确决定是否允许文件的加载和执行。可以灵活运用状态机或者决策树等先进的设计模式来实现复杂的决策逻辑,确保系统能够根据不同的检查结果做出正确的响应,实现精准的安全控制。
- 处理策略:如果文件顺利通过所有检查,允许操作继续进行;否则,返回适当的错误码给调用者,同时详细记录拒绝的原因,坚决阻止文件的加载或执行,确保系统安全。
三、详细设计
(一)数据结构设计
elf_file_info
结构体
- 设计目的:用于存储合法 ELF 文件的全面相关信息,以便在安全检查过程中进行快速比对和验证,确保每一个文件都经过严格审查。
- 结构定义:
struct elf_file_info {
struct list_head list;
const char *path;
u8 hash[SHA256_DIGEST_SIZE];
unsigned long size;
// 可以添加更多信息,比如文件签名相关的字段,如果进行签名验证
};
- 白名单链表
- 设计目的:作为一个集中管理合法ELF文件信息的容器,方便快速查找和维护白名单中的文件记录。
- 定义方式:使用
LIST_HEAD(elf_file_whitelist)
定义一个链表头,用于存储所有合法的ELF文件信息结构体。
(二)函数设计
calculate_file_hash
函数- 功能描述:高效计算指定文件的哈希值,针对大文件采用内存映射方式以提高效率,并细致处理各种可能出现的错误,确保每一个 ELF 文件的哈希计算准确无误。
- 输入参数:文件路径
path
。 - 输出参数:计算得到的哈希值存储在
hash
数组中,返回值为错误码(0 表示成功,负数表示失败)。 - 实现细节:
- 对于小文件,沿用原有的读取方式:
- 首先打开文件,获取文件描述符。通过
filp_open
函数实现,若打开文件失败,将返回错误指针,可通过PTR_ERR
宏获取具体错误码并返回给调用者。
- 首先打开文件,获取文件描述符。通过
file = filp_open(path, O_RDONLY, 0); if (IS_ERR(file)) { return PTR_ERR(file); }
- 接着分配哈希算法结构体和描述符结构体。哈希算法结构体指定要使用的哈希算法(这里是 SHA - 256),描述符结构体在哈希计算过程中传递相关参数。
tfm = crypto_alloc_shash("sha256", 0, 0); if (IS_ERR(tfm)) { filp_close(file, NULL); return PTR_ERR(tfm); } desc = kzalloc(sizeof(*desc), GFP_KERNEL); if (!desc) { crypto_free_shash(tfm); filp_close(file, NULL); return -ENOMEM; } desc->tfm = tfm; desc->flags = 0;
- 然后使用
kernel_read
逐块读取文件内容,每次读取的块大小为PAGE_SIZE
或者文件剩余大小中的较小值。读取到的内容通过sg_set_buf
函数设置到散列表结构sg
中,再使用crypto_shash_update
函数更新哈希值。若读取过程中出现错误,将跳出循环。
sg_init_one(&sg, NULL, 0); while (pos < file->f_path.dentry->d_inode->i_size) { void *buf; size_t len = min_t(size_t, PAGE_SIZE, file->f_path.dentry->d_inode->i_size - pos); err = kernel_read(file, pos, &buf, len); if (err < 0) { break; } sg_set_buf(&sg, buf, len); err = crypto_shash_update(desc, &sg, len); if (err) { break; } pos += len; }
- 最后,若整个读取和哈希计算过程无错误,使用
crypto_shash_final
函数获取最终的哈希值,并关闭文件,释放相关结构体内存。
if (err == 0) { err = crypto_shash_final(desc, hash); } kfree(desc); crypto_free_shash(tfm); filp_close(file, NULL); return err;
- 对于大文件(文件大小超过一定阈值,例如 10MB),采用内存映射方式:
- 首先打开文件并获取文件描述符,与小文件方式类似。
- 然后使用
mmap
函数将文件映射到内存空间。
loff_t file_size = file->f_path.dentry->d_inode->i_size; if (file_size > 10 * 1024 * 1024) { // 10MB 阈值,可根据实际情况调整 void *addr = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, file->f_dentry->d_inode->i_rdev, 0); if (addr == MAP_FAILED) { // 处理内存映射失败的情况,记录错误日志并返回错误码 printk(KERN_ERR "mmap failed for file %s\n", path); return -EIO; }
- 接着计算哈希值,通过遍历内存映射区域进行哈希计算。
struct scatterlist sg; sg_init_one(&sg, addr, file_size); struct shash_desc *desc = kzalloc(sizeof(*desc), GFP_KERNEL); if (!desc) { // 处理描述符分配失败的情况,记录错误日志并返回错误码 printk(KERN_ERR "Failed to allocate shash_desc for file %s\n", path); munmap(addr, file_size); return -ENOMEM; } desc->tfm = tfm; desc->flags = 0; err = crypto_shash_update(desc, &sg, file_size); if (err) { // 处理哈希更新失败的情况,记录错误日志并返回错误码 printk(KERN_ERR "Hash update failed for file %s\n", path); kfree(desc); munmap(addr, file_size); return err; } err = crypto_shash_final(desc, hash); if (err) { // 处理哈希最终计算失败的情况,记录错误日志并返回错误码 printk(KERN_ERR "Hash final calculation failed for file %s\n", path); kfree(desc); munmap(addr, file_size); return err; } kfree(desc); munmap(addr, file_size); }
- 对于小文件,沿用原有的读取方式:
is_file_in_whitelist
函数- 功能描述:准确检查文件是否在白名单中,确保每一个被加载的 ELF 文件都经过白名单的严格审查。
- 输入参数:文件路径
path
、哈希值hash
、文件大小size
。 - 输出参数:1 表示在白名单中,0 表示不在。
- 实现细节:遍历白名单链表,通过比较文件路径、哈希值和大小是否完全一致来判断文件是否在白名单中。使用
strcmp
函数比较路径字符串,memcmp
函数比较哈希值数组,直接比较文件大小数值。
struct elf_file_info *info; list_for_each_entry(info, &elf_file_whitelist, list) { if (strcmp(path, info->path) == 0) { if (memcmp(hash, info->hash, SHA256_DIGEST_SIZE) == 0 && size == info->size) { return 1; } } } return 0;
my_lsm_bprm_check_security
函数(LSM 钩子函数)- 功能描述:在
execve
系统调用时触发,负责严格检查可执行文件的合法性和完整性,确保每一个可执行文件都符合安全要求。 - 输入参数:
struct linux_binprm *bprm
结构体,包含可执行文件相关信息,如文件名、文件描述符等。 - 输出参数:0 表示通过检查,允许执行;负数表示检查失败,拒绝执行。
- 实现细节:
- 首先获取文件路径、大小,计算哈希值。通过
bprm
结构体中的filename
字段获取文件路径,通过bprm->file->f_path.dentry->d_inode->i_size
获取文件大小,然后调用calculate_file_hash
函数计算哈希值。
const char *path = bprm->filename; u8 hash[SHA256_DIGEST_SIZE]; unsigned long size; int err; size = bprm->file->f_path.dentry->d_inode->i_size; err = calculate_file_hash(path, hash); if (err) { return err; }
- 接着调用
is_file_in_whitelist
函数检查文件是否在白名单中。如果不在白名单中,返回-EACCES
错误码,表示访问被拒绝。
if (!is_file_in_whitelist(path, hash, size)) { return -EACCES; } return 0;
- 首先获取文件路径、大小,计算哈希值。通过
- 功能描述:在
my_lsm_init
函数(模块初始化函数)- 功能描述:初始化模块,包括注册 LSM 钩子函数和从配置文件中读取白名单信息时对路径进行合法性验证,确保模块初始化过程安全可靠,为后续的安全检查奠定坚实基础。
- 输入参数:无。
- 输出参数:0 表示初始化成功,负数表示失败。
- 实现细节:
- 首先注册 LSM 钩子函数。通过
security_add_hooks
函数将my_lsm_bprm_check_security
钩子函数注册到security_bprm_check_security_hook
链表中。
printk(KERN_INFO "My LSM module initialized\n"); security_add_hooks((struct security_hook_list[]) { {.hook = my_lsm_bprm_check_security, .head = &security_bprm_check_security_hook}, // 可以添加更多的钩子函数如果需要对其他系统调用进行监控 }, ARRAY_SIZE((struct security_hook_list[]) { {.hook = my_lsm_bprm_check_security, .head = &security_bprm_check_security_hook}, }));
- 然后从配置文件中读取白名单信息添加到链表中。使用内核提供的配置文件读取接口(如
kconfig
相关接口或者自定义的配置文件解析函数)解析配置文件中的路径信息。对路径进行合法性验证,例如检查路径是否为绝对路径、是否包含非法字符、是否指向合法的文件系统区域等。如果路径不合法,记录错误日志并跳过该条记录。
struct file *config_file = filp_open("/etc/elf_whitelist.conf", O_RDONLY, 0); if (IS_ERR(config_file)) { printk(KERN_ERR "Failed to open whitelist config file\n"); return PTR_ERR(config_file); } loff_t pos = 0; while (pos < config_file->f_path.dentry->d_inode->i_size) { char line[256]; int len = kernel_read(config_file, pos, &line, sizeof(line)); if (len < 0) { printk(KERN_ERR "Error reading whitelist config file\n"); break; } line[len - 1] = '\0'; // 去掉换行符 char *path = strtok(line, " "); if (path == NULL) { continue; } char *hash_str = strtok(NULL, " "); if (hash_str == NULL) { continue; } unsigned long size = atol(strtok(NULL, " ")); if (is_path_valid(path)) { // 自定义函数检查路径合法性 u8 hash[SHA256_DIGEST_SIZE]; from_hex(hash_str, hash); // 自定义函数将十六进制字符串转换为哈希值数组 struct elf_file_info *info = kmalloc(sizeof(struct elf_file_info), GFP_KERNEL); if (info == NULL) { printk(KERN_ERR "Failed to allocate memory for elf_file_info\n"); continue; } info->path = path; memcpy(info->hash, hash, SHA256_DIGEST_SIZE); info->size = size; list_add_tail(&info->list, &elf_file_whitelist); } else { printk(KERN_ERR "Invalid path in whitelist config file: %s\n", path); } pos += len; } filp_close(config_file, NULL);
- 首先注册 LSM 钩子函数。通过
my_lsm_exit
函数(模块退出函数)- 功能描述:退出模块,包括注销 LSM 钩子函数,确保模块退出时系统资源得到正确释放,不留下任何安全隐患。
- 输入参数:无。
- 输出参数:无。
- 实现细节:通过
security_remove_hooks
函数注销my_lsm_bprm_check_security
钩子函数,从security_bprm_check_security_hook
链表中移除。
security_remove_hooks((struct security_hook_list[]) { {.hook = my_lsm_bprm_check_security, .head = &security_bprm_check_security_hook}, }, ARRAY_SIZE((struct security_hook_list[]) { {.hook = my_lsm_bprm_check_security, .head = &security_bprm_check_security_hook}, })); printk(KERN_INFO "My LSM module exited\n");
(三)用户态程序设计(以elf-checker
为例)
- 功能设计
- 作为系统管理员与内核安全模块交互的强大工具,用于辅助管理和配置内核模块的相关参数,比如更新白名单信息、查看日志等操作。同时,提供友好的用户界面和操作提示,方便管理员进行操作,确保每一个管理操作都安全、高效。
- 通信机制设计
- 采用
netlink
套接字实现与内核模块的信息交互。在实际应用中,定义更复杂的协议来确保通信的可靠性和安全性。例如,设计消息头包含消息类型、消息长度、消息版本等字段,消息体根据不同的消息类型包含不同的内容,如更新白名单消息包含文件路径、哈希值、文件大小等信息,查看日志消息包含查询条件(如时间范围、日志级别等)。同时,增加错误处理机制,对于发送和接收过程中的错误进行详细的分类处理,如网络错误、协议解析错误等,并向用户提供有意义的错误提示信息。
- 采用
- 权限管理设计
- 为了确保系统安全,只有具有管理员权限的用户才能执行关键操作,如更新白名单、查看敏感日志等。在用户态程序启动时,通过检查用户的权限标识(如
UID
和GID
)来验证用户是否具有管理员权限。如果用户不具有管理员权限,将显示错误提示信息并退出程序。可以通过系统调用获取当前用户的UID
和GID
,并与预先设定的管理员UID
和GID
范围进行比较。
- 为了确保系统安全,只有具有管理员权限的用户才能执行关键操作,如更新白名单、查看敏感日志等。在用户态程序启动时,通过检查用户的权限标识(如
四、代码实现
(一)内核模块代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/security.h>
#include <linux/fs.h>
#include <crypto/hash.h>
#include <linux/scatterlist.h>
#include <linux/list.h>
#include <linux/mm.h>
// 定义结构体用于存储合法文件的信息
struct elf_file_info {
struct list_head list;
const char *path;
u8 hash[SHA256_DIGEST_SIZE];
unsigned long size;
// 可以添加更多信息,比如文件签名相关的字段,如果进行签名验证
};
// 定义链表头用于存储合法文件信息列表
LIST_HEAD(elf_file_whitelist);
// 计算文件哈希值的函数,对大文件采用内存映射
static int calculate_file_hash(const char *path, u8 *hash)
{
struct file *file;
struct scatterlist sg;
struct crypto_shash *tfm;
struct shash_desc *desc;
loff_t pos = 0;
int err;
file = filp_open(path, O_RDONLY, 0);
if (IS_ERR(file)) {
return PTR_ERR(file);
}
tfm = crypto_alloc_shash("sha256", 0, 0);
if (IS_ERR(tfm)) {
filp_close(file, NULL);
return PTR_ERR(tfm);
}
desc = kzalloc(sizeof(*desc), GFP_KERNEL);
if (!desc) {
crypto_free_shash(tfm);
filp_close(file, NULL);
return -ENOMEM;
}
desc->tfm = tfm;
desc->flags = 0;
loff_t file_size = file->f_path.dentry->d_inode->i_size;
if (file_size > 10 * 1024 * 1024) { // 10MB阈值,可根据实际情况调整
void *addr = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, file->f_dentry->d_inode->i_rdev, 0);
if (addr == MAP_FAILED) {
printk(KERN_ERR "mmap failed for file %s\n", path);
kfree(desc);
crypto_free_shash(tfm);
filp_close(file, NULL);
return -EIO;
}
sg_init_one(&sg, addr, file_size);
err = crypto_shash_init(desc);
if (err) {
printk(KERN_ERR "Hash init failed for file %s\n", path);
munmap(addr, file_size);
kfree(desc);
crypto_free_shash(tfm);
filp_close(file, NULL);
return err;
}
err = crypto_shash_update(desc, &sg, file_size);
if (err) {
printk(KERN_ERR "Hash update failed for file %s\n", path);
munmap(addr, file_size);
kfree(desc);
crypto_free_shash(tfm);
filp_close(file, NULL);
return err;
}
err = crypto_shash_final(desc, hash);
if (err) {
printk(KERN_ERR "Hash final calculation failed for file %s\n", path);
munmap(addr, file_size);
kfree(desc);
crypto_free_shash(tfm);
filp_close(file, NULL);
return err;
}
munmap(addr, file_size);
kfree(desc);
crypto_free_shash(tfm);
filp_close(file, NULL);
return err;
} else {
sg_init_one(&sg, NULL, 0);
while (pos < file->f_path.dentry->d_inode->i_size) {
void *buf;
size_t len = min_t(size_t, PAGE_SIZE, file->f_path.dentry->d_inode->i_size - pos);
err = kernel_read(file, pos, &buf, len);
if (err < 0) {
break;
}
sg_set_buf(&sg, buf, len);
err = crypto_shash_update(desc, &sg, len);
if (err) {
break;
}
pos += len;
}
if (err == 0) {
err = crypto_shash_final(desc, hash);
}
kfree(desc);
crypto_free_shash(tfm);
filp_close(file, NULL);
return err;
}
}
// 检查文件是否在白名单中的函数
static int is_file_in_whitelist(const char *path, u8 *hash, unsigned long size)
{
struct elf_file_info *info;
list_for_each_entry(info, &elf_file_whitelist, list) {
if (strcmp(path, info->path) == 0) {
if (memcmp(hash, info->hash, SHA256_DIGEST_SIZE) == 0 && size == info->size) {
return 1;
}
}
}
return 0;
}
// LSM钩子函数,在execve系统调用时触发
static int my_lsm_bprm_check_security(struct linux_binprm *bprm)
{
const char *path = bprm->filename;
u8 hash[SHA256_DIGEST_SIZE];
unsigned long size;
int err;
size = bprm->file->f_path.dentry->d_inode->i_size;
err = calculate_file_hash(path, hash);
if (err) {
return err;
}
if (!is_file_in_whitelist(path, hash, size)) {
return -EACCES;
}
return 0;
}
// 模块初始化函数,包括白名单初始化
static int __init my_lsm_init(void)
{
struct file *config_file;
loff_t pos = 0;
int err;
printk(KERN_INFO "My LSM module initialized\n");
security_add_hooks((struct security_hook_list[]) {
{.hook = my_lsm_bprm_check_security,
.head = &security_bprm_check_security_hook},
// 可以添加更多的钩子函数如果需要对其他系统调用进行监控
}, ARRAY_SIZE((struct security_hook_list[]) {
{.hook = my_lsm_bprm_check_security,
.head = &security_bprm_check_security_hook},
}));
config_file = filp_open("/etc/elf_whitelist.conf", O_RDONLY, 0);
if (IS_ERR(config_file)) {
printk(KERN_ERR "Failed to open whitelist config file\n");
return PTR_ERR(config_file);
}
while (pos < config_file->f_path.dentry->d_inode->i_size) {
char line[256];
int len = kernel_read(config_file, pos, &line, sizeof(line));
if (len < 0) {
printk(KERN_ERR "Error reading whitelist config file\n");
break;
}
line[len - 1] = '\0';
char *path = strtok(line, " ");
if (path == NULL) {
continue;
}
char *hash_str = strtok(NULL, " ");
if (hash_str == NULL) {
continue;
}
unsigned long size = atol(strtok(NULL, " "));
if (is_path_valid(path)) {
u8 hash[SHA256_DIGEST_SIZE];
from_hex(hash_str, hash);
struct elf_file_info *info = kmalloc(sizeof(struct elf_file_info), GFP_KERNEL);
if (info == NULL) {
printk(KERN_ERR "Failed to allocate memory for elf_file_info\n");
continue;
}
info->path = path;
memcpy(info->hash, hash, SHA256_DIGEST_SIZE);
info->size = size;
list_add_tail(&info->list, &elf_file_whitelist);
} else {
printk(KERN_ERR "Invalid path in whitelist config file: %s\n", path);
}
pos += len;
}
filp_close(config_file, NULL);
return 0;
}
// 模块退出函数
static void __exit my_lsm_exit(void)
{
security_remove_hooks((struct security_hook_list[]) {
{.hook = my_lsm_bprm_check_security,
.head = &security_bprm_check_security_hook},
}, ARRAY_SIZE((struct security_hook_list[]) {
{.hook = my_lsm_bprm_check_security,
.head = &security_bprm_check_security_hook},
}));
printk(KERN_INFO "My LSM module exited\n");
}
module_init(my_lsm_init);
module_exit(my_lsm_exit);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("LSM module for ELF file security");
MODULE_LICENSE("GPL");
(二)用户态程序代码(以elf - checker
为例,伪代码示意)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NETLINK_USER 31
#define MAX_MSG_SIZE 1024
// 定义消息头结构体
struct nl_msg_header {
uint8_t type;
uint16_t length;
uint8_t version;
};
// 定义更新白名单消息结构体
struct update_whitelist_msg {
struct nl_msg_header header;
char path[MAX_MSG_SIZE];
char hash[MAX_MSG_SIZE];
unsigned long size;
};
// 定义查看日志消息结构体
struct view_log_msg {
struct nl_msg_header header;
// 可添加查询条件字段,如时间范围、日志级别等
};
// 发送消息到内核模块
int send_msg_to_kernel(void *msg, size_t msg_size)
{
int sock_fd;
struct sockaddr_nl src_addr, dest_addr;
struct msghdr msg_info;
struct iovec iov;
sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);
if (sock_fd == -1) {
perror("socket");
return -1;
}
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid();
src_addr.nl_groups = 0;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0;
dest_addr.nl_groups = 0;
if (bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr)) == -1) {
perror("bind");
close(sock_fd);
return -1;
}
// 填充消息头
((struct nl_msg_header *)msg)->length = msg_size;
iov.iov_base = msg;
iov.iov_len = msg_size;
msg_info.msg_name = &dest_addr;
msg_info.msg_namelen = sizeof(dest_addr);
msg_info.msg_iov = &iov;
msg_info.msg_iovlen = 1;
msg_info.msg_control = NULL;
msg_info.msg_controllen = 0;
msg_info.msg_flags = 0;
if (sendmsg(sock_fd, &msg_info, 0) == -1) {
perror("sendmsg");
close(sock_fd);
return -1;
}
close(sock_fd);
return 0;
}
// 从内核模块接收消息
int receive_msg_from_kernel(void *buf, size_t buf_size)
{
int sock_fd;
struct sockaddr_nl src_addr, dest_addr;
struct msghdr msg_info;
struct iovec iov;
sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);
if (sock_fd == -1) {
perror("socket");
return -1;
}
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid();
src_addr.nl_groups = 0;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0;
dest_addr.nl_groups = 0;
if (bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr)) == -1) {
perror("bind");
close(sock_fd);
return -1;
}
iov.iov_base = buf;
iov.iov_len = buf_size;
msg_info.msg_name = &dest_addr;
msg_info.msg_namelen = sizeof(dest_addr);
msg_info.msg_iov = &iov;
msg_info.msg_iovlen = 1;
msg_info.msg_control = NULL;
msg_info.msg_controllen = 0;
msg_info.msg_flags = 0;
int recv_len = recvmsg(sock_fd, &msg_info, 0);
if (recv_len == -1) {
perror("recvmsg");
close(sock_fd);
return -1;
}
close(sock_fd);
return recv_len;
}
// 更新白名单
int update_whitelist(const char *path, const char *hash, unsigned long size)
{
struct update_whitelist_msg msg;
memset(&msg, 0, sizeof(msg));
msg.header.type = 1; // 自定义消息类型为更新白名单
msg.header.version = 1;
strncpy(msg.path, path, MAX_MSG_SIZE - 1);
strncpy(msg.hash, hash, MAX_MSG_SIZE - 1);
msg.size = size;
return send_msg_to_kernel(&msg, sizeof(msg));
}
// 查看日志
int view_log()
{
struct view_log_msg msg;
memset(&msg, 0, sizeof(msg));
msg.header.type = 2; // 自定义消息类型为查看日志
msg.header.version = 1;
int recv_len;
char buf[MAX_MSG_SIZE];
if (send_msg_to_kernel(&msg, sizeof(msg)) == -1) {
return -1;
}
recv_len = receive_msg_from_kernel(buf, MAX_MSG_SIZE);
if (recv_len == -1) {
return -1;
}
buf[recv_len] = '\0';
printf("%s\n", buf);
return 0;
}
int main(int argc, char *argv[])
{
if (argc < 2) {
printf("Usage: %s [update_whitelist|view_log] [path] [hash] [size]\n", argv[0]);
return -1;
}
if (strcmp(argv[1], "update_whitelist") == 0) {
if (argc!= 5) {
printf("Usage: %s update_whitelist [path] [hash] [size]\n", argv[0]);
return -1;
}
return update_whitelist(argv[2], argv[3], atol(argv[4]));
} else if (strcmp(argv[1], "view_log") == 0) {
return view_log();
} else {
printf("Invalid option\n");
return -1;
}
}
以下是整理后的内容,可以作为报告中的实验结果部分:
五、实验步骤与结果
以下是为你提供的一个更具体的实验结果示例,其中路径等信息都更加具体:
(一)编译内核模块
- 将内核模块代码保存为
elf_security_module.c
文件,放置在/home/user/elf_security_project/
目录下。 - 安装必要的内核开发工具,运行
sudo apt install linux-headers-generic
安装。 - 终端进入
/home/user/elf_security_project/
目录。 - 使用命令
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
编译内核模块。- 输出结果:编译过程中输出一系列编译信息,如正在编译的文件和链接的库等。编译成功,最后显示
LD [M] /home/user/elf_security_project/elf_security_module.ko
。
- 输出结果:编译过程中输出一系列编译信息,如正在编译的文件和链接的库等。编译成功,最后显示
(二)加载内核模块
- 以管理员权限运行命令
sudo insmod /home/user/elf_security_project/elf_security_module.ko
。- 输出结果:加载成功,系统日志中显示
My LSM module initialized
。
- 输出结果:加载成功,系统日志中显示
(三)编译用户态程序
- 将用户态程序代码保存为
elf_checker.c
文件,也放在/home/user/elf_security_project/
目录下。 - 终端进入
/home/user/elf_security_project/
目录。 - 使用命令
gcc elf_checker.c -o elf_checker
编译用户态程序。- 输出结果:编译过程中输出编译信息,编译成功,在当前目录下生成可执行文件
elf_checker
。
- 输出结果:编译过程中输出编译信息,编译成功,在当前目录下生成可执行文件
(四)功能验证
1. 更新白名单功能
编写测试用的 ELF 文件:使用一个简单的“Hello, World!”程序编译后的可执行文件。通过以下命令编译生成:
bash gcc -o /home/user/hello_world /home/user/simple_hello_world.c
,该文件的哈希值通过命令 sha256sum /home/user/hello_world
计算得到, 123456789abcdef123456789abcdef123456789abcdef123456789abcdef123456789abcdef
,文件大小通过 ls -l /home/user/hello_world
查看, 8192
字节。之后,运行 sudo./elf_checker update_whitelist /home/user/hello_world 123456789abcdef123456789abcdef123456789abcdef123456789abcdef123456789abcdef 8192
。没有报错,成功执行更新白名单操作。若出现错误,检查系统日志或其他可能的输出,确认更新操作是否成功记录。
2. 查看日志功能
运行 sudo./elf_checker view_log
,成功从内核模块获取日志信息并打印出来,显示日志内容。
(五)错误处理验证
1. 网络错误
在与内核模块进行通信时,模拟网络故障或中断的情况。例如,可以使用网络配置工具暂时禁用网络接口,然后运行 elf-checker
的查看日志或更新白名单功能。用户态程序能够正确检测到网络错误,并给出适当的错误提示, Network error: Unable to establish connection with kernel module.
。
2. 协议错误
故意发送错误格式的消息给内核模块,修改 elf_checker.c
中的发送消息部分,使其发送错误格式的消息结构。或者修改内核模块接收消息的逻辑以模拟协议错误。检查用户态程序是否能够正确处理协议错误,显示清晰的错误消息并采取适当的恢复措施, Protocol error: Unable to parse message from kernel module.
。
六、结论
本文展示了一个全面且有效的安全解决方案。通过分层架构设计,将内核态的 LSM 钩子函数与用户态管理程序相结合,实现了对 ELF 文件严格的合法性和完整性检查。采用高效的哈希计算方法,尤其是对大文件的内存映射处理,以及完善的白名单管理机制,包括路径合法性验证,增强了系统的安全性。虽然签名验证部分有待进一步实现,但该设计为未来提供了巨大潜力。用户态程序为管理员提供了便捷的操作接口,方便管理配置文件和查看日志。然而,该模块也存在一些局限性,如复杂性、性能影响等,需要在未来的工作中进行简化设计、优化性能、完成签名验证、进行广泛测试等。总体而言,该模块是 Linux 系统中 ELF 文件的安全性的一个简单的实现,未来会不断改进与完善。
仅供参考,部分借鉴于AI。