天天看點

linux核心鈎子--khook

簡介

本文介紹github上的一個項目khook,一個可以在核心中增加鈎子函數的架構,支援x86。項目位址在這裡:https://github.com/milabs/khook

本文先簡單介紹鈎子函數,分析這個工具的用法,然後再分析代碼,探究實作原理

鈎子

假設在核心中有一個函數,我們想截斷他的執行流程,比如說對某檔案的讀操作。這樣就可以監控對這個檔案的讀操作。這就是鈎子。通過插入一個鈎子函數,可以截斷程式正常的執行流程,做自己的想做的操作,可以僅僅隻做一個監控,也可以徹底截斷函數的執行。

khook的用法

引入頭檔案

#include "khook/engine.c"      

在kbuild/makefile中加入,這是一個連結控制腳本,後面會具體說明這個腳本的内容

ldflags-y += -T$(src)/khook/engine.lds      

使用khook_init()和khook_cleanup()對挂鈎引擎進行初始化和登出

在核心中的函數有兩種

  • 一種是在某一個頭檔案中已經被包含了,也就是核心已經定義了函數聲明,這樣隻需要包含内内容的頭檔案就可以使用該函數
  • 另一種是沒有聲明,隻是.c檔案内部使用的函數

對于已知原型的函數,包含頭檔案後,使用下面的代碼就可以定義一個鈎子函數

#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結構

先看一個結構體,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,                    \
    }
/*
有兩種類型的函數
1、頭檔案中包含了函數原型,則在代碼中包含頭檔案就行了
2、寫在.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

然後看另一個結構體,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;
}      

這個函數,做了以下幾件事

1、配置設定所有STUB需要用到的記憶體

2、查找符号表,獲得所有需要鈎住的函數的位址。然後建立虛拟位址的映射

3、執行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的内容。

1、先是用khook_stub_template的内容填充stub,這就是stub.inc

2、第3步中stub是需要跳轉到自定義鈎子函數的,stub_fixup填充這個位址

3、儲存函數的前一部分内容,這一部分必須大于5個位元組

4、設定傳回到原函數的位址

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;
}