動態替換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核心函數的原理和實現