1、 目的在調(diào)試核心模塊的過程中發(fā)現(xiàn),當(dāng)運(yùn)行了一段時間后內(nèi)核提供的函數(shù)在執(zhí)行的過程中表現(xiàn)出與預(yù)期不一致的狀態(tài),這種狀態(tài)有可能是核心模塊調(diào)用該函數(shù)時傳入的參數(shù)出現(xiàn)了異常造成的,也可能是Linux核心受插入模塊的影響,造成了其內(nèi)部狀態(tài)的不一致。此時需要有一種機(jī)制可以跟蹤察看被質(zhì)疑的函數(shù)的執(zhí)行流程。但是由于當(dāng)前的核心處于運(yùn)行狀態(tài),一貫被廣泛使用的在目標(biāo)函數(shù)中增加打印語句等方法需要重新編譯和啟動內(nèi)核,將會破壞難得的現(xiàn)場,因此不適用于這種場合,只有能夠動態(tài)替換動態(tài)運(yùn)行的內(nèi)核函數(shù)的機(jī)制才能起到真正的作用。 2、 基本原理Linux操作系統(tǒng)在執(zhí)行程序(內(nèi)核也可以被看作正在運(yùn)行的大程序)時,需要兩個最為基本的前提條件:(1)存放參數(shù)、返回地址及局部變量的堆棧(stack);(2)可執(zhí)行程序二進(jìn)制代碼。在調(diào)用某一個函數(shù)執(zhí)行之前,需要在堆棧中為該函數(shù)準(zhǔn)備好傳入的參數(shù)、函數(shù)執(zhí)行完之后的返回地址,然后設(shè)置處理器的程序計數(shù)器(eip,指向處理器即將執(zhí)行的下一個條指令)為被調(diào)用函數(shù)的第一條執(zhí)行代碼的地址,這樣下一個處理器周期將跳轉(zhuǎn)到被調(diào)用函數(shù)處執(zhí)行。下圖所示為調(diào)用執(zhí)行函數(shù)func(parameter1, parameter2, ... parametern)時的場景,該函數(shù)可執(zhí)行代碼在內(nèi)核空間中的地址為func_addr: 動態(tài)替換內(nèi)核涵數(shù)的目的或者想要達(dá)到的效果就是改變內(nèi)核原有的執(zhí)行流程,跳轉(zhuǎn)到由我們自己定制的函數(shù)流程上。從上述函數(shù)調(diào)用的原理圖可以看出,有三個地方可以作為函數(shù)替換的著手點(diǎn): (1) 修改堆棧
(2) 修改程序計數(shù)器的內(nèi)容
(3) 修改原函數(shù)代碼
指令集中能夠跳轉(zhuǎn)程序執(zhí)行流程的指令有兩個:call和jmp。 call是函數(shù)調(diào)用指令,由前面的論述知道,在call執(zhí)行之前,需要先在堆棧中設(shè)置好該函數(shù)執(zhí)行所需要的參數(shù),在此,由于進(jìn)入原函數(shù)之前已經(jīng)設(shè)置了參數(shù),所以我們必須將這些參數(shù)拷貝到堆棧頂部。這種拷貝過程涉及的堆棧地址與參數(shù)個數(shù)相關(guān),因此對不同的函數(shù)都需要重新計算,比較容易出錯。 jmp是直接進(jìn)行正常的跳轉(zhuǎn)(類似c語言中的goto語句),可以繼續(xù)使用原函數(shù)準(zhǔn)備好的參數(shù)及返回地址信息,無需重新拷貝堆棧的內(nèi)容,因此相對而言比較安全,實(shí)現(xiàn)起來也更為方便。 下圖是動態(tài)函數(shù)替換的一個場景示意圖。replace_func是func函數(shù)的替換函數(shù),其地址為new_address。 整個替換過程由一個核心模塊來完成。該核心模塊在初始化時,用跳轉(zhuǎn)指令碼替換原函數(shù)func開始部分的指令代碼,使得這部分代碼變成一個條轉(zhuǎn)到函數(shù)replace_func的指令。同時為了最后能夠恢復(fù)原函數(shù)func,必須將原函數(shù)被替換部分的指令碼保存下來,這樣在我們達(dá)到預(yù)期的目的之后卸載模塊時,可用保存的指令碼重新覆蓋回原地址即可,這樣,當(dāng)后續(xù)內(nèi)核再次執(zhí)行函數(shù)func時,就又能夠繼續(xù)執(zhí)行該函數(shù)原來的執(zhí)行代碼,不會破壞內(nèi)核的狀態(tài)。 3、 函數(shù)替換的實(shí)例在此,提供針對i386 32位平臺,版本為2.4.18 Linux環(huán)境下用上述描述的這種機(jī)制動態(tài)替換內(nèi)核函數(shù),比如vmtruncate、fget等函數(shù)的例子 3.1. 前提條件在使用這種方法時,有兩個必須注意的前提條件: (1) 原函數(shù)正在被替換的時刻,也就是插入替換核心模塊時,沒有被其它進(jìn)程所使用,否則其結(jié)果有可能造成內(nèi)核狀態(tài)不一致的現(xiàn)象。 (2) 替換函數(shù)和原函數(shù)具有相同的參數(shù)列表,且對應(yīng)次序上的參數(shù)類型相同,參數(shù)個數(shù)相同,同時函數(shù)具有相同的返回值。一般來說,我們替換核心函數(shù)的目的并不是改變它的功能而是要跟蹤該函數(shù)的執(zhí)行流程是否出現(xiàn)異常,各變量和參數(shù)是否具有預(yù)期的值,因此,替換函數(shù)和原函數(shù)具有相同的功能。 3.2. 替換過程整個替換流程的實(shí)現(xiàn)分為如下幾個步驟: (1) 替換指令碼:
(2) 用替換函數(shù)的地址覆蓋第一條指令中的后面8個0,并保留原來的指令碼:
(3) 恢復(fù)過程用保留的指令碼覆蓋原函數(shù)代碼:
3.3. 替換vmtruncate函數(shù)下面給出的是替換內(nèi)核函數(shù)vmtruncate的詳細(xì)內(nèi)核模塊實(shí)現(xiàn)代碼: #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函數(shù)的地址0xc0125d70可到system.map文件中查找*/ #define CODESIZE 7 /*替換代碼的長度 */ static char orig_code[7]; /*保存原vmtruncate函數(shù)被覆蓋部分的執(zhí)行碼 */ static char code[7] = "\xb8\x00\x00\x00\x00" "\xff\xe0"; /* 替換碼 */ /* 如果該函數(shù)沒有export出來,則需要自己實(shí)現(xiàn),供vmtruncate調(diào)用 */ 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的替換函數(shù) */ int _vmtruncate(struct inode * inode, loff_t offset) { unsigned long pgoff; struct address_space *mapping = inode->i_mapping; unsigned long limit; /* 在該函數(shù)中我們增加了許多判斷參數(shù)的打印信息 */ 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; } /* 核心中內(nèi)存拷貝的函數(shù),用于拷貝替換代碼 */ 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; /* 賦替換函數(shù)地址 */ _memcpy (orig_code, orig_vmtruncate, CODESIZE); _memcpy (orig_vmtruncate, code, CODESIZE); return 0; } void cleanup_module (void) { /* 卸載該核心模塊時,恢復(fù)原來的vmtruncate函數(shù) */ _memcpy (orig_vmtruncate, orig_code, CODESIZE); } 3.4. 替換fget函數(shù)下面是替換fget函數(shù)的實(shí)現(xiàn)代碼: #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函數(shù)的地址 */ #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; } /* 如果該函數(shù)沒有export出來,則需要自己實(shí)現(xiàn) */ 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的函數(shù) */ 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) { /* 在此,我們打印出所關(guān)心的變量的信息,以供查詢 */ 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) { /* 卸載模塊,恢復(fù)原函數(shù) */ _memcpy (orig_fget, orig_fget_code, CODESIZE); } 4、 該方法的局限性在替換前需要定制自己的替換函數(shù),同時必須能夠查到被替換函數(shù)在該運(yùn)行核心中的地址(通過System.map或/proc/ksyms)。另外在對目標(biāo)計算機(jī)上的函數(shù)進(jìn)行替換之前,最好先在其它具有相同硬件平臺和操作系統(tǒng)核心的節(jié)點(diǎn)上先做通試驗,因為自己寫的替換函數(shù)往往會存在一些問題而無法一次就通,以免造成不必要的麻煩。 |
|