动态替换Linux核心函数的原理和实现

转载: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)。另外在对目标计算机上的函数进行替换之前,最好先在其它具有相同硬件平台和操作系统核心的节点上先做通试验,因为自己写的替换函数往往会存在一些问题而无法一次就通,以免造成不必要的麻烦。

时间: 2024-11-04 14:27:38

动态替换Linux核心函数的原理和实现的相关文章

jquery 手册核心函数整理.doc

jQuery 核心函数 //前者为查找对象,后者为前者查找的范围 $(argunment) jQuery([selector,[context]]) 参数 $(selector,[context]) selector 被查找的字符串 context  待查找的元素集.文档或 jQuery 对象 Eg ----  $('p','[class=two]').css('color','red') $('p','[class=one],[class=two]').css('color','red') e

Linux数据包路由原理、Iptables/netfilter入门学习

相关学习资料 https://www.frozentux.net/iptables-tutorial/cn/iptables-tutorial-cn-1.1.19.html http://zh.wikipedia.org/wiki/Netfilter http://www.netfilter.org/projects/iptables/ http://linux.vbird.org/linux_server/0250simple_firewall.php http://linux.vbird.o

Linux 系统调用函数

转载: 以下是Linux系统调用的一个列表,包含了大部分常用系统调用和由系统调用派生出的的函数.这可能是你在互联网上所能看到的唯一一篇中文注释的Linux系统调用列表,即使是简单的字母序英文列表,能做到这么完全也是很罕见的. 按照惯例,这个列表以man pages第2节,即系统调用节为蓝本.按照笔者的理解,对其作了大致的分类,同时也作了一些小小的修改,删去了几个仅供内核使用,不允许用户调用的系统调用,对个别本人稍觉不妥的地方作了一些小的修改,并对所有列出的系统调用附上简要注释. 其中有一些函数的

Linux核心技能与应用

第1章 课程介绍[欢迎来学习,有任何问题请在问答区进行提问]本章主要讲解为什么学习Linux,Linux的应用场景以及本门课程与专栏< Linux命令行与Shell脚本编程大全>的区别. 第2章 Linux的安装与配置本章主要带领带大家在VirtualBox虚拟机中安装CentOS,配置虚拟机中的 CentOS,主流云服务器的介绍.云服务器中安装CentOS以及安装CentOS遇到的常见问题. 第3章 Linux基础知识和命令本章主要讲解Linux的两种模式,图形和终端模式,Linux的基础命

linux中mmap系统调用原理分析与实现

参考文章:http://blog.csdn.net/shaoguangleo/article/details/5822110 linux中mmap系统调用原理分析与实现 1.mmap系统调用(功能)      void* mmap ( void * addr , size_t len , int prot , int flags ,int fd , off_t offset )      内存映射函数mmap, 负责把文件内容映射到进程的虚拟内存空间, 通过对这段内存的读取和修改,来实现对文件的

spring hibernate实现动态替换表名(分表)

1.概述 其实最简单的办法就是使用原生sql,如 session.createSQLQuery("sql"),或者使用jdbcTemplate.但是项目中已经使用了hql的方式查询,修改起来又累,风险又大!所以,必须找到一种比较好的解决方案,实在不行再改写吧!经过3天的时间的研究,终于找到一种不错的方法,下面讲述之. 2.步骤 2.1 新建hibernate interceptor类 /** * Created by hdwang on 2017/8/7. * * hibernate拦

【转载】Select函数实现原理分析

Select函数实现原理分析 <原文> select需要驱动程序的支持,驱动程序实现fops内的poll函数.select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行. 下面我们分两个过程来分析select: 1. select的睡眠过程 支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的BLOCK(阻塞)或NONBLO

JQuery 核心函数 基础研究与提高

前言 jquery对于一个程序员来说,或多或少都听过.相信很多人在项目中也都用过.现在也有很多开源的库都是依赖于jQuery,因此熟悉jQuery还是很有必要的.使用熟练的大神可以简单看看,对于小白来说还是纯纯的干货.熟悉jQuery还是先从核心函数入手比较好,后面其他的功能都是在此核心函数的基础上扩展的. jQuery 核心函数 jQuery(expression, [context]) jQuery(html, [ownerDocument]) jQuery(html, props) jQu

linux stat函数详解

linux stat函数详解 表头文件: #include <sys/stat.h> #include <unistd.h> 定义函数: int stat(const char *file_name, struct stat *buf); 函数说明: 通过文件名filename获取文件信息,并保存在buf所指的结构体stat中 返回值: 执行成功则返回0,失败返回-1,错误代码存于errno 错误代码: ENOENT 参数file_name指定的文件不存在 ENOTDIR 路径中的