上回讲到如何编写简单的内核模块,那么内核模块可以用来做什么呢?一个例子就是可以hook系统调用,替换为自己的接口函数;还有就是设备驱动的开发。
本文将就hook系统底层调用技术展开讨论。下图是应用层的一个系统调用在linux系统中的调用流程:
在应用层调用getpid系统调用,为glibc中封装,此调用会引发int 80中断,调用切换到内核空间,然后根据system_call数组及其_NR_getpid下标找到系统调用sys_getpid接口的地址,之后将会返回系统调用结果到应用层。
system call table是个什么东西呢,如下图所示:
其实,system call tale就是一个数组,数组中各元素都是系统调用的首地址,比如exit系统调用在数组中是下标为__NR_exit的成员。linux系统提供了300多个系统调用,下图展示了各系统调用名称,数组下标值,参数所存的寄存器及源代码所在文件信息,来源网页见文末链接。
要想hook系统调用,第一步就是要获取系统调用表了,下面我们就聊聊如何获取系统调用表地址的几种方法:
//when the old way can not get table_addr,try this.
//kallsyms_lookup_name need be export, need CONFIG_KALLSYMS when compile kernel
void* sys_table_addr = (void*) kallsyms_lookup_name("sys_call_table");
用这种方法需要kallsyms_lookup_name为导出函数,即在内核编译时需要带上CONFIG_KALLSYMS宏。
首先要获取系统版本:
bool get_kernel_version(char *kernel_version, int len)
{
if(kernel_version == NULL || len <= 0)
return false;
memset(kernel_version, 0, len);
struct file *proc_version = filp_open("/proc/version", O_RDONLY, 0);
if(proc_version == NULL)
return false;
mm_segment_t oldfs;
oldfs = get_fs();
set_fs(KERNEL_DS);
char tmp_version[80] = {0};
int ret = vfs_read(proc_version, tmp_version, 78, &(proc_version->f_pos));
set_fs(oldfs);
if(ret < 0) goto out;
printk("current os_version:%s.\n", tmp_version);
char *tmp_version_ptr = tmp_version;
if(NULL == strsep(&tmp_version_ptr, " ") || NULL == strsep(&tmp_version_ptr, " "))
goto out;
// get the string between the 2th and 3th white space
char *tmp_buf = strsep(&tmp_version_ptr, " ");
if(tmp_buf == NULL) goto out;
if(strlen(tmp_buf) > len-1){
printk("[err] %s. kernerl version too long(%s).\n", __FUNCTION__, tmp_buf);
goto out;
}
printk("%s. kernel version: %s.\n", __FUNCTION__, tmp_buf);
strcpy(kernel_version, tmp_buf);
out:
filp_close(proc_version, 0);
return (kernel_version[0]==0);
}
因为vfs_read的声明:
extern ssize_t vfs_read(struct file *, char __user *, size_t, loff_t *);
要求传入的buf为__user用户态地址,内核模块中的地址都是在内核地址空间中,所以要通过set_fs (KERNEL_DS)来避开vfs_read中的地址空间检测来读取。
获取 sys_call_table 地址:
static unsigned long get_func_addr_from_system_map(char *kern_ver, char *func_name)
{
char system_map_entry[MAX_ENTRY_LEN] = {0};
unsigned long func_addr = NULL;
mm_segment_t oldfs = get_fs();
if(kern_ver == NULL || kern_ver[0] == 0 || func_name == NULL || func_name[0] == 0)
return NULL;
size_t len = strlen(kern_ver)+strlen("/boot/System.map-")+1;
char *filename = kzalloc(len, GFP_KERNEL);
if(filename == NULL) return NULL;
sprintf(filename, "/boot/System.map-%s", kern_ver);
struct file *system_map = filp_open(filename, O_RDONLY, 0);
if(system_map == NULL){
printk("%s. open %s failed.\n", __FUNCTION__, filename);
goto out1;
}
int i = 0;
set_fs(KERNEL_DS);
while(vfs_read(system_map, system_map_entry+i, 1, &system_map->f_pos) == 1){
if(system_map_entry[i] == '\n'){
i = 0;
//ffff000008a40688 D sys_call_table
char *system_map_entry_ptr = system_map_entry;
char *tmp_addr = strsep(&system_map_entry_ptr, " ");
if(tmp_addr == NULL || NULL == strsep(&system_map_entry_ptr, " ") || system_map_entry_ptr == NULL)
goto con1;
char *tmp_func_name = system_map_entry_ptr; //sys_call_table
tmp_func_name[strlen(tmp_func_name)-1] = 0; // repalce /n with /0 at the end of the string
if(strcmp(tmp_func_name, func_name) == 0){
kstrtoul(tmp_addr, 16, (unsigned long*)&func_addr);
printk("%s. %s retrieved, tmp_addr:%s, func_addr:%p.\n", __FUNCTION__, func_name, tmp_addr, func_addr);
break;
}
con1:
memset(system_map_entry, 0, MAX_ENTRY_LEN);
continue;
}
else if(i == MAX_ENTRY_LEN){
//more than max_entry_len in this line, drop the rest of the line
printk("wrn. more than max_entry_len in this line, drop the rest of the line.\n");
i = 0;
while(vfs_read(system_map, system_map_entry+i, 1, &system_map->f_pos) == 1){
if(system_map_entry[i] == '\n')
break;
}
memset(system_map_entry, 0, MAX_ENTRY_LEN);
continue;
}
i++;
}
out:
filp_close(system_map, 0);
set_fs(oldfs);
out1:
kfree(filename);
return func_addr;
}
最后,可以这样获取:
char kernel_version[64] = {0};
get_kernel_version(kernel_version, 64);
unsigned long *sys_call_table = (unsigned long*)get_func_addr_from_system_map(kernel_version, "sys_call_table");
static void* aquire_sys_call_table()
{
unsigned long int offset = 0;
unsigned long int end = VMALLOC_START < ULLONG_MAX ? VMALLOC_START : ULLONG_MAX;
void *table_addr = NULL;
void** tmp_table = NULL;
*(void**)&offset = PAGE_OFFSET;
while (offset < end)
{
tmp_table = (void**)offset;
if (tmp_table[__NR_close] == (void*)sys_close)
{
#ifdef ARM64
continue;
#else
table_addr = (void*)tmp_table;
#endif
break;
}
offset += sizeof(void *);
}
return table_addr;
}
注:PAGE_OFFSET为linux系统虚拟地址空间中内核空间与用户空间的分界地址,对于不同的体系结构会有所不同。如在32位系统中3G-4G 属于内核使用的内存空间。内核程序可以访问从PAGE_OFFSET 之后的内存,访问所有的信息。
以上就是获取系统调用表的几种方法,当然还有其它方法。获取之后就可以将系统调用替换成自己定义的接口:
//wran the write protect
sys_call_table[__NR_write] = (write_t)my_hook_write;
至于后续怎么做呢,且待以后慢慢分解。
参考资料:
https://syscalls.kernelgrok.com/
https://gitlab.tnichols.org/tyler/syscall_table_hooks/blob/master/src/hooks.c
感兴趣的话可以关注我的微信公众号【大胖聊编程】,我的公众号中有更多文章分享,也可以在公众号中联系到我,加好友一起交流学习。