本文介紹github上的一個項目khook,一個能夠在內核中增長鉤子函數的框架,支持x86。項目地址在這裏:https://github.com/milabs/khooknode
本文先簡單介紹鉤子函數,分析這個工具的用法,而後再分析代碼,探究實現原理linux
假設在內核中有一個函數,咱們想截斷他的執行流程,好比說對某文件的讀操做。這樣就能夠監控對這個文件的讀操做。這就是鉤子。經過插入一個鉤子函數,能夠截斷程序正常的執行流程,作本身的想作的操做,能夠僅僅只作一個監控,也能夠完全截斷函數的執行。git
引入頭文件github
#include "khook/engine.c"
在kbuild/makefile中加入,這是一個連接控制腳本,後面會具體說明這個腳本的內容數組
ldflags-y += -T$(src)/khook/engine.lds
使用khook_init()和khook_cleanup()對掛鉤引擎進行初始化和註銷app
在內核中的函數有兩種框架
對於已知原型的函數,包含頭文件後,使用下面的代碼就能夠定義一個鉤子函數函數
#include <linux/fs.h> // has inode_permission() proto KHOOK(inode_permission); static int khook_inode_permission(struct inode *inode, int mask) { int ret = 0; ret = KHOOK_ORIGIN(inode_permission, inode, mask); printk("%s(%p, %08x) = %d\n", __func__, inode, mask, ret); return ret; }
對於原型未知的函數,則須要使用下面的方式(這裏的頭文件不是函數原型所在的文件,是參數所用結構體定義的位置)工具
#include <linux/binfmts.h> // has no load_elf_binary() proto KHOOK_EXT(int, load_elf_binary, struct linux_binprm *); static int khook_load_elf_binary(struct linux_binprm *bprm) { int ret = 0; ret = KHOOK_ORIGIN(load_elf_binary, bprm); printk("%s(%p) = %d\n", __func__, bprm, ret); return ret; }
能夠函數,假設原函數名字爲fun,則自定義的fun的鉤子函數名字必須爲khook_fun,而後根據函數類型不一樣使用不一樣鉤子定義方式源碼分析
先上做者github上的兩張圖
未加入鉤子以前的正常執行流程
CALLER | ... | CALL X -(1)---> X | ... <----. | ... ` RET | ` RET -. `--------(2)-'
加入鉤子以後的執行流程
CALLER | ... | CALL X -(1)---> X | ... <----. | JUMP -(2)----> STUB.hook ` RET | | ??? | INCR use_count | | ... <----. | CALL handler -(3)------> HOOK.fn | | ... | | DECR use_count <----. | ... | ` RET -. | ` RET -. | | CALL origin -(4)------> STUB.orig | | | | | | ... <----. | N bytes of X | | | | | ` RET -. | ` JMP X + N -. `------------|----|-------(8)-' '-------(7)-' | | | `-------------------------------------------|----------------------(5)-' `-(6)--------------------------------------------'
好,分析第二張圖,X的第一條指令被替換成JUMP的跳轉指令,另外,還能夠知道多了3個部分STUB.hook、HOOK.fn、STUB.orig,他們的含義分別是
STUB.hook:框架自定義的鉤子函數模板,有4部分,除了引用的維護,還有3一條跳轉,8一條返回。3是跳轉到HOOK.fn
HOOK.fn:這是使用者自定義的鉤子函數,在上面的例子中,這個函數被定義成khook_inode_permission、khook_load_elf_binary。這裏的4就是KHOOK_ORIGIN,鉤子替換下來的原函數地址,通常來講,自定義的鉤子函數最後也會調用原函數,用來保證正常的執行流程不會出錯
STUB.orig:框架自定義的鉤子函數模板,因爲X的第一條指令被替換成JUMP的跳轉指令,要正常執行X,則須要先執行被替換的幾個字節,而後回到X,也就是圖中的過程5
因此說,總體的思路就是,替換掉須要鉤掉的函數的前幾個字節,替換成一個跳轉指令,讓X開始執行的時候跳轉到框架自定義的STUB代碼部分,STUB再調用用戶自定義的鉤子函數。而後又會執行原先被跳轉指令覆蓋的指令,最後回到被鉤掉的函數的正常執行邏輯
先看一個結構體,khook,表示一個鉤子,比較難理解的就是addr_map,由於咱們須要對函數的內容進行從新,須要將這個函數的內容映射到一個能夠訪問的虛擬地址,addr_map就是這個虛擬地址,後面覆蓋爲jump就須要向這個地址寫
/* 表明一個內核鉤子 fn:鉤子函數 name:符號名字 addr:符號地址 addr_map:符號地址被映射的虛擬地址 orig:原函數 */ typedef struct { void *fn; // handler fn address struct { const char *name; // target symbol name char *addr; // target symbol addr (see khook_lookup_name) char *addr_map; // writable mapping of target symbol } target; void *orig; // original fn call wrapper } khook_t;
先從用戶定義鉤子函數的入口開始分析,也就是KHOOK和KHOOK_EXT
/* 格式規定 假設原函數名字爲fun 則自定義的fun的鉤子函數名字必須爲khook_fun */ #define KHOOK_(t) \ static inline typeof(t) khook_##t; /* forward decl */ \ khook_t \ __attribute__((unused)) \ __attribute__((aligned(1))) \ __attribute__((section(".data.khook"))) \ KHOOK_##t = { \ .fn = khook_##t, \ .target.name = #t, \ } /* 有兩種類型的函數 一、頭文件中包含了函數原型,則在代碼中包含頭文件就好了 二、寫在.c文件,可是.h文件中沒有定義,則須要經過KHOOK_EXT來定義鉤子函數 */ #define KHOOK(t) \ KHOOK_(t) #define KHOOK_EXT(r, t, ...) \ extern r t(__VA_ARGS__); \ KHOOK_(t)
__attribute__((unused)表示可能不會用到
__attribute__((aligned(1)))表示一字節對齊
__attribute__((section(".data.khook")))表示這個結構須要被分配到.data.khook節中
能夠明白KHOOK就是作了一個格式規定,而後保證這個結構被分配到.data.khook節中
KHOOK_EXT則是加入一個函數聲明,這樣未聲明的函數就能夠被使用了
在上面的鉤子函數中,還用到了一個宏,含義根據khook就能夠明白
/* 傳入原函數的名字和參數,KHOOK_ORIGIN就能夠當作原函數來執行 */ #define KHOOK_ORIGIN(t, ...) \ ((typeof(t) *)KHOOK_##t.orig)(__VA_ARGS__)
關注一個問題,使用說明中,有一個條件,加入一個連接腳本
ldflags-y += -T$(src)/khook/engine.lds
這裏看看這個連接腳本
SECTIONS { .data : { KHOOK_tbl = . ; *(.data.khook) KHOOK_tbl_end = . ; } }
engine.c中看到全部的鉤子都被分配到.data.khook節中
下面這個腳本的含義是將全部.data.khook的內容都放在.data節之中
.這個字符表示的是當前定位器符號的位置,因此KHOOK_tbl指向的是.data.khook開頭,KHOOK_tbl_end指向的是KHOOK_tbl_end的結尾
如下腳本將輸出文件的text section定位在0×10000, data section定位在0×8000000:
SECTIONS { . = 0×10000; .text : { *(.text) } . = 0×8000000; .data : { *(.data) } .bss : { *(.bss) } }
解釋一下上述的例子:
. = 0×10000 : 把定位器符號置爲0×10000 (若不指定, 則該符號的初始值爲0).
.text : { *(.text) } : 將全部(*符號表明任意輸入文件)輸入文件的.text section合併成一個.text section, 該section的地址由定位器符號的值指定, 即0×10000.
. = 0×8000000 :把定位器符號置爲0×8000000
.data : { *(.data) } : 將全部輸入文件的.data section合併成一個.data section, 該section的地址被置爲0×8000000.
.bss : { *(.bss) } : 將全部輸入文件的.bss section合併成一個.bss section,該section的地址被置爲0×8000000+.data section的大小.
鏈接器每讀完一個section描述後, 將定位器符號的值*增長*該section的大小. 注意: 此處沒有考慮對齊約束.
綜上所述,這個連接腳本定義了兩個變量表示鉤子表的起始和結束地址,KHOOK_tbl和KHOOK_tbl_end
而後看另外一個結構體,STUB
typedef struct { #pragma pack(push, 1) union { unsigned char _0x00_[ 0x10 ]; atomic_t use_count; }; union { unsigned char _0x10_[ 0x20 ]; unsigned char orig[0]; }; union { unsigned char _0x30_[ 0x40 ]; unsigned char hook[0]; }; #pragma pack(pop) unsigned nbytes; } __attribute__((aligned(32))) khook_stub_t;
根據上一節介紹的原理能夠知道,一個鉤子函數必定會有一個STUB
而這個STUB會被初始化爲stub.inc或stub32.inc。也就是stub的模板。
用到了兩個內核中操做指令的函數,兩個函數的功能是獲取某個地址的指令,用struct insn表示,和獲取這個指令的長度
/** 下面是內核關於這兩個函數的說明 insn_init() - initialize struct insn @insn: &struct insn to be initialized @kaddr: address (in kernel memory) of instruction (or copy thereof) @x86_64: !0 for 64-bit kernel or 64-bit app insn_get_length() - Get the length of instruction @insn: &struct insn containing instruction If necessary, first collects the instruction up to and including the immediates bytes. */ static struct { typeof(insn_init) *init; typeof(insn_get_length) *get_length; } khook_arch_lde; //尋找到這兩個函數的地址 static inline int khook_arch_lde_init(void) { khook_arch_lde.init = khook_lookup_name("insn_init"); if (!khook_arch_lde.init) return -EINVAL; khook_arch_lde.get_length = khook_lookup_name("insn_get_length"); if (!khook_arch_lde.get_length) return -EINVAL; return 0; } //獲取地址p的指令的長度,先調用insn_init得到insn結構,而後調用get_length獲得指令長度,結果存放在insn的length字段 static inline int khook_arch_lde_get_length(const void *p) { struct insn insn; int x86_64 = 0; #ifdef CONFIG_X86_64 x86_64 = 1; #endif #if defined MAX_INSN_SIZE && (MAX_INSN_SIZE == 15) /* 3.19.7+ */ khook_arch_lde.init(&insn, p, MAX_INSN_SIZE, x86_64); #else khook_arch_lde.init(&insn, p, x86_64); #endif khook_arch_lde.get_length(&insn); return insn.length; }
內核中有一個全局的符號表kallsyms,能夠經過/proc/kallsyms來查詢,也能夠經過system.map來獲取內核編譯時期造成的靜態符號表。
在內核中,一樣可使用函數kallsyms_on_each_symbol來查詢符號表,這個函數被封裝成了下面兩個部分
//查詢符號表的函數 static int khook_lookup_cb(long data[], const char *name, void *module, long addr) { int i = 0; while (!module && (((const char *)data[0]))[i] == name[i]) { if (!name[i++]) return !!(data[1] = addr); } return 0; } /* 利用kallsyms_on_each_symbol能夠查詢符號表,只須要傳入查詢函數就能夠了 data[0]表示要查詢的地址 data[1]表示結果 */ static void *khook_lookup_name(const char *name) { long data[2] = { (long)name, 0 }; kallsyms_on_each_symbol((void *)khook_lookup_cb, data); return (void *)data[1]; }
前面說到,因爲是須要符號符號執行的內存,因此須要給這個符號執行的地址分配一個虛擬地址,這個操做封裝在下面這個函數中
//爲符號所在的物理內存創建一個虛擬地址的映射 static void *khook_map_writable(void *addr, size_t len) { struct page *pages[2] = { 0 }; // len << PAGE_SIZE long page_offset = offset_in_page(addr); int i, nb_pages = DIV_ROUND_UP(page_offset + len, PAGE_SIZE); addr = (void *)((long)addr & PAGE_MASK); for (i = 0; i < nb_pages; i++, addr += PAGE_SIZE) { if ((pages[i] = is_vmalloc_addr(addr) ? vmalloc_to_page(addr) : virt_to_page(addr)) == NULL) return NULL; } addr = vmap(pages, nb_pages, VM_MAP, PAGE_KERNEL); return addr ? addr + page_offset : NULL; }
要使用框架,先要調用khook_init函數,它定義在engine.c中
int khook_init(void) { void *(*malloc)(long size) = NULL; //爲全部鉤子的stub分配內存 malloc = khook_lookup_name("module_alloc"); if (!malloc || KHOOK_ARCH_INIT()) return -EINVAL; khook_stub_tbl = malloc(KHOOK_STUB_TBL_SIZE); if (!khook_stub_tbl) return -ENOMEM; memset(khook_stub_tbl, 0, KHOOK_STUB_TBL_SIZE); //從kallsyms尋找到每一個鉤子的地址 khook_resolve(); //創建映射 khook_map(); //中止全部機器,執行khook_sm_init_hooks stop_machine(khook_sm_init_hooks, NULL, NULL); khook_unmap(0); return 0; }
這個函數,作了如下幾件事
一、分配全部STUB須要用到的內存
二、查找符號表,得到全部須要鉤住的函數的地址。而後創建虛擬地址的映射
三、執行khook_sm_init_hook,創建好STUB和khook的關聯,保證他們的跳轉邏輯
查找符號的地址函數很簡單,看下面
//對KHOOK_tbl中每個鉤子都得到他們在內核中的地址 static void khook_resolve(void) { khook_t *p; KHOOK_FOREACH_HOOK(p) { p->target.addr = khook_lookup_name(p->target.name); } }
一樣創建映射的函數
//爲鉤子創建好虛擬地址的映射 static void khook_map(void) { khook_t *p; KHOOK_FOREACH_HOOK(p) { if (!p->target.addr) continue; p->target.addr_map = khook_map_writable(p->target.addr, 32); khook_debug("target %s@%p -> %p\n", p->target.name, p->target.addr, p->target.addr_map); } }
最重要的就是第3步
static int khook_sm_init_hooks(void *arg) { khook_t *p; KHOOK_FOREACH_HOOK(p) { if (!p->target.addr_map) continue; khook_arch_sm_init_one(p); } return 0; }
核心實如今下面的函數
static inline void khook_arch_sm_init_one(khook_t *hook) { khook_stub_t *stub = KHOOK_STUB(hook); //E9是相對跳轉。FF是絕對跳轉。 if (hook->target.addr[0] == (char)0xE9 || hook->target.addr[0] == (char)0xCC) return; BUILD_BUG_ON(sizeof(khook_stub_template) > offsetof(khook_stub_t, nbytes)); memcpy(stub, khook_stub_template, sizeof(khook_stub_template)); //設置第3步 stub_fixup(stub->hook, hook->fn); //一條相對跳轉指令爲5,因此必須保存下至少5個字節的指令 while (stub->nbytes < 5) stub->nbytes += khook_arch_lde_get_length(hook->target.addr + stub->nbytes); memcpy(stub->orig, hook->target.addr, stub->nbytes); //設置第5步 x86_put_jmp(stub->orig + stub->nbytes, stub->orig + stub->nbytes, hook->target.addr + stub->nbytes); //設置第2步 x86_put_jmp(hook->target.addr_map, hook->target.addr, stub->hook); hook->orig = stub->orig; // the only link from hook to stub }
能夠看到這就是設置stub的內容。
一、先是用khook_stub_template的內容填充stub,這就是stub.inc
二、第3步中stub是須要跳轉到自定義鉤子函數的,stub_fixup填充這個地址
三、保存函數的前一部份內容,這一部分必須大於5個字節
四、設置返回到原函數的地址
五、用跳轉指令覆蓋原函數的內容
而後用到的幾個輔助函數在這裏
// place a jump at addr @a from addr @f to addr @t static inline void x86_put_jmp(void *a, void *f, void *t) { *((char *)(a + 0)) = 0xE9; *(( int *)(a + 1)) = (long)(t - (f + 5)); } //這個數組的內容寫在stub.inc或是stub32.inc中,表示一個stub的模板 static const char khook_stub_template[] = { # include KHOOK_STUB_FILE_NAME }; //看stub32.inc中,後部有幾個連續的0xca,從這以後再寫入value,鉤子函數地址 static inline void stub_fixup(void *stub, const void *value) { while (*(int *)stub != 0xcacacaca) stub++; *(long *)stub = (long)value; }