动态替换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)。另外在对目标计算机上的函数进行替换之前,最好先在其它具有相同硬件平台和操作系统核心的节点上先做通试验,因为自己写的替换函数往往会存在一些问题而无法一次就通,以免造成不必要的麻烦。
动态替换Linux核心函数的原理和实现的更多相关文章
- 动态修改 C 语言函数的实现
Objective-C 作为基于 Runtime 的语言,它有非常强大的动态特性,可以在运行期间自省.进行方法调剂.为类增加属性.修改消息转发链路,在代码运行期间通过 Runtime 几乎可以修改 O ...
- Linux数据包路由原理、Iptables/netfilter入门学习
相关学习资料 https://www.frozentux.net/iptables-tutorial/cn/iptables-tutorial-cn-1.1.19.html http://zh.wik ...
- 【Java核心】ClassLoader原理及其使用
又把博客的皮肤换了换,看着更加简洁舒心一些.前段的知识只是略懂,拿过来就能用,只是自己的审美和设计水平有限,实在难以弄出自己特别满意的东西,也算是小小的遗憾吧!言归正传,由于最近涉及到Java核心的东 ...
- linux select函数详解
linux select函数详解 在Linux中,我们可以使用select函数实现I/O端口的复用,传递给 select函数的参数会告诉内核: •我们所关心的文件描述符 •对每个描述符,我们所关心的状 ...
- bootparam - 介绍Linux核心的启动参数
描叙 Linux 核心在启动的时候可以接受指定的"命令行参数"或"启动参数".在通常情况下,由于核心有可能无法识别某些硬件,或可能将某些硬件识别为不正确的配置, ...
- 解密jQuery内核 DOM操作的核心函数domManip
domManip是什么 dom即Dom元素,Manip是Manipulate的缩写,连在一起就是Dom操作的意思. .domManip()是jQuery DOM操作的核心函数 对封装的节点操作做了参数 ...
- Java动态替换InetAddress中DNS的做法简单分析2
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.i ...
- 【OpenGL游戏开发之三】OpenGl核心函数库汇总
OpenGl核心函数库 glAccum 操作累加缓冲区 glAddSwapHintRectWIN 定义一组被SwapBuffers拷贝的三角形 glAlphaFunc允许设置alpha检测功能 glA ...
- 节点地址的函数list_entry()原理详解
本节中,我们继续讲解,在linux2.4内核下,如果通过一些列函数从路径名找到目标节点. 3.3.1)接下来查看chached_lookup()的代码(namei.c) [path_walk()> ...
随机推荐
- [剑指Offer] 48.不用加减乘除做加法
题目描述 写一个函数,求两个整数之和,要求在函数体内不得使用+.-.*./四则运算符号. [思路] 首先看十进制是如何做的: 5+7=12,三步走第一步:相加各位的值,不算进位,得到2.第二步:计算进 ...
- query 获取本身的HTML
<div class="test"><p>hello,你好!</p></div> <script> $(".t ...
- 反向传播算法 Backpropagation Algorithm
假设我们有一个固定样本集,它包含 个样例.我们可以用批量梯度下降法来求解神经网络.具体来讲,对于单个样例(x,y),其代价函数为:这是一个(二分之一的)方差代价函数.给定一个包含 个样例的数据集,我们 ...
- BZOJ4289 PA2012Tax(最短路)
一个暴力的做法是把边看成点,之间的边权为两边的较大权值,最短路即可.但这样显然会被菊花图之类的卡掉. 考虑优化建图.将边拆成两个有向边,同样化边为点.原图中同一条边在新图中的两个点之间连边权为原边权的 ...
- BZOJ5286:[HNOI/AHOI2018]转盘——题解
https://www.lydsy.com/JudgeOnline/problem.php?id=5286 https://www.luogu.org/problemnew/show/P4425 ht ...
- POJ.1006 Biorhythms (拓展欧几里得+中国剩余定理)
POJ.1006 Biorhythms (拓展欧几里得+中国剩余定理) 题意分析 不妨设日期为x,根据题意可以列出日期上的方程: 化简可得: 根据中国剩余定理求解即可. 代码总览 #include & ...
- Eclipse中 properties 文件中 中文乱码
在.properties文件写注释时,发现中文乱码了,由于之前在idea中有见设置.properties文件的编码类型,便找了找乱码原因 在中文操作系统中,Eclipse中的Java类型文件的编码的默 ...
- ACE中UDP通信
转载于:http://www.cnblogs.com/TianFang/archive/2006/12/07/585205.html udp是一种无连接的协议,提供无连接不可靠的服务. 在ace中,通 ...
- hdu 5616
Jam's balance Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others)Tot ...
- Educational Codeforces Round 61 (Rated for Div. 2) D,F题解
D. Stressful Training 题目链接:https://codeforces.com/contest/1132/problem/D 题意: 有n台电脑,每台电脑都有初始电量ai,也有一个 ...