Linux系统中获取系统调用表(system call table)地址的几种方法

上回讲到如何编写简单的内核模块,那么内核模块可以用来做什么呢?一个例子就是可以hook系统调用,替换为自己的接口函数;还有就是设备驱动的开发。

 

本文将就hook系统底层调用技术展开讨论。下图是应用层的一个系统调用在linux系统中的调用流程:

Linux系统中获取系统调用表(system call table)地址的几种方法_第1张图片

在应用层调用getpid系统调用,为glibc中封装,此调用会引发int 80中断,调用切换到内核空间,然后根据system_call数组及其_NR_getpid下标找到系统调用sys_getpid接口的地址,之后将会返回系统调用结果到应用层。

 

system call table是个什么东西呢,如下图所示:

 

Linux系统中获取系统调用表(system call table)地址的几种方法_第2张图片

其实,system call tale就是一个数组,数组中各元素都是系统调用的首地址,比如exit系统调用在数组中是下标为__NR_exit的成员。linux系统提供了300多个系统调用,下图展示了各系统调用名称,数组下标值,参数所存的寄存器及源代码所在文件信息,来源网页见文末链接。

 

Linux系统中获取系统调用表(system call table)地址的几种方法_第3张图片

要想hook系统调用,第一步就是要获取系统调用表了,下面我们就聊聊如何获取系统调用表地址的几种方法:

1、通过调用内核导出函数kallsyms_lookup_name获取:

  //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宏。

2、通过读取system.map文件获取:

首先要获取系统版本:

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");

3、从PAGE_OFFSET开始遍历查找__NR_close系统调用首地址:

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

感兴趣的话可以关注我的微信公众号【大胖聊编程】,我的公众号中有更多文章分享,也可以在公众号中联系到我,加好友一起交流学习。

你可能感兴趣的:(Linux,hook,内核,linux)