转载:https://www.ibm.com/developerworks/cn/linux/l-knldebug/
动态替换Linux核心函数的原理和实现
在调试Linux核心模块时,有时需要能够实时获取内部某个路径上的某些函数的执行状态,例如查看传入的变量是否是期望的值,以便判断整个执行流程是否依然正常。由于系统运行时的动态性,使得在执行之前无法预先知道在执行路径的什么地方可能出现问题,因此只能在整个路径上增加许多不必要的信息查询点,造成有用的状态信息被淹没,而且这种增加信息输出的方式(一般是在核心中通过printk语句打印)需要重新编译内核,重新引导,造成了时间上浪费。此时就需要有一种能够方便地实时截取执行路径上怀疑点的方法,本文描述了一种动态替换linux核心函数的基本实现原理。
1、 目的
在调试核心模块的过程中发现,当运行了一段时间后内核提供的函数在执行的过程中表现出与预期不一致的状态,这种状态有可能是核心模块调用该函数时传入的参数出现了异常造成的,也可能是Linux核心受插入模块的影响,造成了其内部状态的不一致。此时需要有一种机制可以跟踪察看被质疑的函数的执行流程。但是由于当前的核心处于运行状态,一贯被广泛使用的在目标函数中增加打印语句等方法需要重新编译和启动内核,将会破坏难得的现场,因此不适用于这种场合,只有能够动态替换动态运行的内核函数的机制才能起到真正的作用。
2、 基本原理
Linux操作系统在执行程序(内核也可以被看作正在运行的大程序)时,需要两个最为基本的前提条件:(1)存放参数、返回地址及局部变量的堆栈(stack);(2)可执行程序二进制代码。在调用某一个函数执行之前,需要在堆栈中为该函数准备好传入的参数、函数执行完之后的返回地址,然后设置处理器的程序计数器(eip,指向处理器即将执行的下一个条指令)为被调用函数的第一条执行代码的地址,这样下一个处理器周期将跳转到被调用函数处执行。下图所示为调用执行函数func(parameter1, parameter2, ... parametern)时的场景,该函数可执行代码在内核空间中的地址为func_addr:
动态替换内核涵数的目的或者想要达到的效果就是改变内核原有的执行流程,跳转到由我们自己定制的函数流程上。从上述函数调用的原理图可以看出,有三个地方可以作为函数替换的着手点:
(1) 修改堆栈
但是,这种方式只能修改函数执行的参数和返回地址,达不到改变执行流程的目的;
(2) 修改程序计数器的内容
在操作系统内部无法直接给eip赋值,没有提供这样的指令码;
(3) 修改原函数代码
当调用某个函数执行时,eip将指向被调用函数代码的起始地址,将根据该函数的第一条指令决定eip的下一个指向的值。因此我们可以在保留现有的堆栈内容不变的情况下,修改原函数代码的首部,使得它将eip的内容跳转到我们提供的替代函数代码上。
指令集中能够跳转程序执行流程的指令有两个:call和jmp。
call是函数调用指令,由前面的论述知道,在call执行之前,需要先在堆栈中设置好该函数执行所需要的参数,在此,由于进入原函数之前已经设置了参数,所以我们必须将这些参数拷贝到堆栈顶部。这种拷贝过程涉及的堆栈地址与参数个数相关,因此对不同的函数都需要重新计算,比较容易出错。
jmp是直接进行正常的跳转(类似c语言中的goto语句),可以继续使用原函数准备好的参数及返回地址信息,无需重新拷贝堆栈的内容,因此相对而言比较安全,实现起来也更为方便。
下图是动态函数替换的一个场景示意图。replace_func是func函数的替换函数,其地址为new_address。
整个替换过程由一个核心模块来完成。该核心模块在初始化时,用跳转指令码替换原函数func开始部分的指令代码,使得这部分代码变成一个条转到函数replace_func的指令。同时为了最后能够恢复原函数func,必须将原函数被替换部分的指令码保存下来,这样在我们达到预期的目的之后卸载模块时,可用保存的指令码重新覆盖回原地址即可,这样,当后续内核再次执行函数func时,就又能够继续执行该函数原来的执行代码,不会破坏内核的状态。
3、 函数替换的实例
在此,提供针对i386 32位平台,版本为2.4.18 Linux环境下用上述描述的这种机制动态替换内核函数,比如vmtruncate、fget等函数的例子
3.1. 前提条件
在使用这种方法时,有两个必须注意的前提条件:
(1) 原函数正在被替换的时刻,也就是插入替换核心模块时,没有被其它进程所使用,否则其结果有可能造成内核状态不一致的现象。
(2) 替换函数和原函数具有相同的参数列表,且对应次序上的参数类型相同,参数个数相同,同时函数具有相同的返回值。一般来说,我们替换核心函数的目的并不是改变它的功能而是要跟踪该函数的执行流程是否出现异常,各变量和参数是否具有预期的值,因此,替换函数和原函数具有相同的功能。
3.2. 替换过程
整个替换流程的实现分为如下几个步骤:
(1) 替换指令码:
b8 00 00 00 00 /*movl $0, $eax;这里的$0将被具体替换函数的地址所取代*/
ff e0 /*jmp *$eax ;跳转函数*/
将上述7个指令码存放在一个字符数组中:
replace_code[7]
(2) 用替换函数的地址覆盖第一条指令中的后面8个0,并保留原来的指令码:
memcpy (orig_code, func, 7); /* 保留原函数的指令码 */
*((long*)&replace_code[1])= (long) replace_func; /* 赋替换函数的地址 */
memcpy (func, replace_code, 7); /* 用新的指令码替换原函数指令码 */
(3) 恢复过程用保留的指令码覆盖原函数代码:
memcpy (func, orig_code, 7)
3.3. 替换vmtruncate函数
下面给出的是替换内核函数vmtruncate的详细内核模块实现代码:
#ifndef __KERNEL__ #define __KERNEL__ #endif #ifndef MODULE #define MODULE #endif #include <linux/kernel.h> #include <linux/config.h> #include <linux/module.h> #include <asm/string.h> #include <asm/unistd.h> #include <linux/fs.h> #include <linux/sched.h> #include <linux/mm.h> #include <linux/pagemap.h> #include <asm/smplock.h> int (*orig_vmtruncate) (struct inode * inode, loff_t offset) = (int(*) (struct inode *inode, loff_t offset))0xc0125d70; /* 原vmtruncate函数的地址0xc0125d70可到system.map文件中查找*/ #define CODESIZE 7 /*替换代码的长度 */ static char orig_code[7]; /*保存原vmtruncate函数被覆盖部分的执行码 */ static char code[7] = "\xb8\x00\x00\x00\x00" "\xff\xe0"; /* 替换码 */ /* 如果该函数没有export出来,则需要自己实现,供vmtruncate调用 */ static void _vmtruncate_list(struct vm_area_struct *mpnt, unsigned long pgoff) { do { struct mm_struct *mm = mpnt->vm_mm; unsigned long start = mpnt->vm_start; unsigned long end = mpnt->vm_end; unsigned long len = end - start; unsigned long diff; if (mpnt->vm_pgoff >= pgoff) { zap_page_range(mm, start, len); continue; } len = len >> PAGE_SHIFT; diff = pgoff - mpnt->vm_pgoff; if (diff >= len) continue; start += diff << PAGE_SHIFT; len = (len - diff) << PAGE_SHIFT; zap_page_range(mm, start, len); } while ((mpnt = mpnt->vm_next_share) != NULL); } /* vmtruncate的替换函数 */ int _vmtruncate(struct inode * inode, loff_t offset) { unsigned long pgoff; struct address_space *mapping = inode->i_mapping; unsigned long limit; /* 在该函数中我们增加了许多判断参数的打印信息 */ printk (KERN_ALERT "Enter into my vmtruncate, pid: %d\n", current->pid); printk (KERN_ALERT "inode->i_ino: %d, inode->i_size: %d, pid: %d\n", inode->i_ino, inode->i_size, current->pid); printk (KERN_ALERT "offset: %ld, pid: %d\n", offset, current->pid); printk (KERN_ALERT "Do nothing, pid: %d\n", current->pid); return 0; if (inode->i_size < offset) goto do_expand; inode->i_size = offset; spin_lock(&mapping->i_shared_lock); if (!mapping->i_mmap && !mapping->i_mmap_shared) goto out_unlock; pgoff = (offset + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT; printk (KERN_ALERT "Begin to truncate mmap list, pid: %d\n", current->pid); if (mapping->i_mmap != NULL) _vmtruncate_list(mapping->i_mmap, pgoff); if (mapping->i_mmap_shared != NULL) _vmtruncate_list(mapping->i_mmap_shared, pgoff); out_unlock: printk (KERN_ALERT "Before to truncate inode pages, pid:%d\n", current->pid); spin_unlock(&mapping->i_shared_lock); truncate_inode_pages(mapping, offset); goto out_truncate; do_expand: limit = current->rlim[RLIMIT_FSIZE].rlim_cur; if (limit != RLIM_INFINITY && offset > limit) goto out_sig; if (offset > inode->i_sb->s_maxbytes) goto out; inode->i_size = offset; out_truncate: printk (KERN_ALERT "Come to out_truncate, pid: %d\n", current->pid); if (inode->i_op && inode->i_op->truncate) { lock_kernel(); inode->i_op->truncate(inode); unlock_kernel(); } printk (KERN_ALERT "Leave, pid: %d\n", current->pid); return 0; out_sig: send_sig(SIGXFSZ, current, 0); out: return -EFBIG; } /* 核心中内存拷贝的函数,用于拷贝替换代码 */ void* _memcpy (void *dest, const void *src, int size) { const char *p = src; char *q = dest; int i; for (i=0; i<size; i++) *q++ = *p++; return dest; } int init_module (void) { *(long *)&code[1] = (long)_vmtruncate; /* 赋替换函数地址 */ _memcpy (orig_code, orig_vmtruncate, CODESIZE); _memcpy (orig_vmtruncate, code, CODESIZE); return 0; } void cleanup_module (void) { /* 卸载该核心模块时,恢复原来的vmtruncate函数 */ _memcpy (orig_vmtruncate, orig_code, CODESIZE); }
3.4. 替换fget函数
下面是替换fget函数的实现代码:
#ifndef __KERNEL__ #define __KERNEL__ #endif #ifndef MODULE #define MODULE #endif #include <linux/kernel.h> #include <linux/config.h> #include <linux/module.h> #include <asm/string.h> #include <asm/unistd.h> #include <linux/fs.h> #include <linux/sched.h> #include <asm/smplock.h> struct file * (*orig_fget) (unsigned int fd) = (struct file * (*)(unsigned int))0xc0138800; /*原fget函数的地址 */ #define CODESIZE 7 static char orig_fget_code[7]; static char fget_code[7] = "\xb8\x00\x00\x00\x00" "\xff\xe0"; void* _memcpy (void *dest, const void *src, int size) { const char *p = src; char *q = dest; int i; for (i=0; i<size; i++) *q++ = *p++; return dest; } /* 如果该函数没有export出来,则需要自己实现 */ static inline struct file * _fcheck (unsigned int fd) { struct file * file = NULL; struct files_struct *files = current->files; if (fd < files->max_fds) file = files->fd[fd]; return file; } /* 替换fget的函数 */ struct file* _fget (unsigned int fd) { struct file * file; struct files_struct *files = current->files; read_lock(&files->file_lock); file = _fcheck (fd); if (file) { struct dentry *dentry = file -> f_dentry; struct inode *inode; if (dentry && dentry->d_inode) { inode = dentry -> d_inode; if (inode->i_ino == 298553) { /* 在此,我们打印出所关心的变量的信息,以供查询 */ printk ("Enter into my fget for file: name: %s, ino: %d\n", dentry->d_name.name, inode->i_ino); } } get_file(file); } read_unlock (&files->file_lock); return file; } int init_module (void) { lock_kernel(); *(long *)&fget_code[1] = (long)_fget; _memcpy (orig_fget_code, orig_fget, CODESIZE); _memcpy (orig_fget, fget_code, CODESIZE); unlock_kernel(); return 0; } void cleanup_module (void) { /* 卸载模块,恢复原函数 */ _memcpy (orig_fget, orig_fget_code, CODESIZE); }
4、 该方法的局限性
在替换前需要定制自己的替换函数,同时必须能够查到被替换函数在该运行核心中的地址(通过System.map或/proc/ksyms)。另外在对目标计算机上的函数进行替换之前,最好先在其它具有相同硬件平台和操作系统核心的节点上先做通试验,因为自己写的替换函数往往会存在一些问题而无法一次就通,以免造成不必要的麻烦。