文章目录
- 前言
- 一、什么是hook技术
- 二、Linux hook种类
- 三、系统调用表hook
- 3.1 查看删除文件用到系统调用
- 3.2 获取系统调用函数
- 3.3 编写hook函数
- 3.4 替换hook函数
- 3.5 测试
- 参考资料
前言
hook技术在Linux系统安全领域有着广泛的应用,例如通过hook技术可以劫持删除文件的系统调用,从而使用户无法删除特定文件,也可以实现对系统网络,文件,进程等的监控。
一、什么是hook技术
hook技术即钩子函数,钩子的本质是一段用以处理系统消息的程序,。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息(如屏幕取词,监视日志,截获键盘/鼠标输入等),也可以不作处理而继续传递该消息,还可以强制结束消息的传递。也即这项技术就是提供了一个入口,能够针对不同的消息或者API在执行前,先执行你的操作,你的操作也称为[钩子函数]
二、Linux hook种类
三、系统调用表hook
相信大家对某些无法删除的流氓软件都有深刻的印象,下面我们通过一个系统调用表hook来实现该功能。
试验环境:centos,x86,3.10+内核
3.1 查看删除文件用到系统调用
使用strace跟踪文件删除系统调用
strace -o 1.txt rm test
查看输出结果
cat 1.txt
execve("/usr/bin/rm", ["rm", "test"], 0x7ffc8101c548 /* 24 vars */) = 0
brk(NULL) = 0xa0f000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0ae12a4000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=87988, ...}) = 0
mmap(NULL, 87988, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f0ae128e000
close(3) = 0
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`&\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2156592, ...}) = 0
mmap(NULL, 3985920, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f0ae0cb6000
mprotect(0x7f0ae0e7a000, 2093056, PROT_NONE) = 0
mmap(0x7f0ae1079000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c3000) = 0x7f0ae1079000
mmap(0x7f0ae107f000, 16896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f0ae107f000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0ae128d000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0ae128b000
arch_prctl(ARCH_SET_FS, 0x7f0ae128b740) = 0
access("/etc/sysconfig/strcasecmp-nonascii", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/sysconfig/strcasecmp-nonascii", F_OK) = -1 ENOENT (No such file or directory)
mprotect(0x7f0ae1079000, 16384, PROT_READ) = 0
mprotect(0x60d000, 4096, PROT_READ) = 0
mprotect(0x7f0ae12a5000, 4096, PROT_READ) = 0
munmap(0x7f0ae128e000, 87988) = 0
brk(NULL) = 0xa0f000
brk(0xa30000) = 0xa30000
brk(NULL) = 0xa30000
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=106172832, ...}) = 0
mmap(NULL, 106172832, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f0ada774000
close(3) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
newfstatat(AT_FDCWD, "test", {st_mode=S_IFREG|0644, st_size=0, ...}, AT_SYMLINK_NOFOLLOW) = 0
geteuid() = 0
unlinkat(AT_FDCWD, "test", 0) = 0
lseek(0, 0, SEEK_CUR) = -1 ESPIPE (Illegal seek)
close(0) = 0
close(1) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++
从上述内容中我们不难看出用户使用rm
命令删除文件时,最终是调用了unlinkat
实现的删除操作。
3.2 获取系统调用函数
打开内核源码在线阅读网站,搜索sys_unlinkat
(系统调用函数一般是sys_*
的格式,在搜索时需要添加前缀)
在include/linux/syscalls.h
文件中找到函数的定义(主要是看一下参数列表)。
下面我们在系统调用表中找到这个函数的地址,获取它的函数指针。
unsigned long (*orig_unlinkat)(int dfd, const char __user * pathname, int flag);
unsigned long *sys_call_table = NULL;
sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table");
if(sys_call_table == NULL) {
printk("%s: can not find sys_call_table address\n", __func__);
return -1;
}
printk("%s: sys_call_addr = 0x%p\n", __func__, sys_call_table);
orig_unlinkat = sys_call_table[__NR_unlinkat];
printk("%s: _NR_unlinkat = 0x%p\n", __func__, orig_unlinkat);
3.3 编写hook函数
这里我们定义一个hook函数,当文件名称等于"test"返回错误,否则调用unlinkat函数。
这里有2点需要注意:
- hook函数参数列表必须和原函数完全一致;
- 在内核空间中无法直接访问用户空间地址的数据,如果要在hook函数中读取参数的值,需要借助
copy_from_user
先将数据拷贝到内核空间;
asmlinkage long hook_unlinkat(int dfd, const char __user * pathname, int flag)
{
char *filename;
long ret;
// Allocate memory to store the filename
filename = (char *)kmalloc(PATH_MAX, GFP_KERNEL);
if (!filename)
return -ENOMEM;
// Copy the pathname from userspace to kernelspace
if (copy_from_user(filename, pathname, PATH_MAX)) {
kfree(filename);
return -EFAULT;
}
// Check if the filename is the one we want to block
if (strcmp(filename, TARGET_FILENAME) == 0) {
printk(KERN_INFO "Attempt to delete %s blocked\n", TARGET_FILENAME);
kfree(filename);
return -EPERM; // Permission denied
}
// Call the original unlink syscall
ret = orig_unlinkat(dfd, pathname, flag);
kfree(filename);
return ret;
}
3.4 替换hook函数
这里只需要将系统调用表中__NR_unlinkat
表项存储的地址替换为我们自己定义的hook函数地址即可。
替换后在用户在使用unlinkat
系统调用时会走到我们的hook函数中,经过hook函数处理之后才原来的系统调用函数。
需要注意的是,系统调用表位于内核代码段,在修改系统调用表之前需要先将页表属性修改为可写。
/* make the page writable */
int make_rw(unsigned long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level); //查找虚拟地址所在的页表地址
pte->pte |= _PAGE_RW;// 设置页表读写属性
return 0;
}
/* make the page write protected */
int make_ro(unsigned long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level);
pte->pte &= ~_PAGE_RW; //设置只读属性
return 0;
}
make_rw((unsigned long)sys_call_table); //修改页属性
sys_call_table[__NR_unlinkat] = (unsigned long *)hook_unlinkat; //设置新的系统调用地址
make_ro((unsigned long)sys_call_table);
3.5 测试
加载编译好的内核模块,测试删除test文件
可以看到在加载了我们编译的模块之后,已经无法删除test文件,这里只是一个示例,通过修改hook函数我们可以实现更多的功能。
参考资料
- hook原理介绍与简单实例
- Linux hook 机制
- Hooking linux内核函数(一):寻找完美解决方案