根据call/jmp操作数偏移检测内核是否被rootkit控制

拥有回忆 提交于 2020-04-13 13:36:15

【今日推荐】:为什么一到面试就懵逼!>>>

今天上午,我写了一个检测我自己的rootkit的代码,但是那个代码只能检测hook住函数开头ftrace stub的情况:
https://blog.csdn.net/dog250/article/details/105465553

手艺人不能就此罢休。我下午准备写一个x86_64指令解析器的,这样就是把内核的整个TEXT段作为一个状态机的输入,然后整个TEXT段在这个解析器里过一遍,就能把所有的call/jmp指令的操作数给过滤出来,然后判断这些操作数的偏移是不是越过了TEXT段的范围,比方说,如果callee的地址是一个内核模块地址范围的地址,那基本就可以说明内核函数call了一个 在别处的函数 ,在我看来,除了export出来的回调函数,这种情况并不多,详细检测这个callee地址,应该能看出其所以然来:

  • 是正常的export回调函数调用吗?
  • 是正常的kpatch打入的hotfix吗?
  • 是恶意注入的代码吗?

无奈下午一觉醒来就快六点了,风雨大作,电闪雷鸣,时间不够了,我也就只能写下面的简单POC了:

  • 该POC可以过滤两类hook:ftrace stub hook和ip_local_deliver里的中间hook。

代码如下:

#include <linux/module.h>
#include <linux/kallsyms.h>

#define TEXT_SIZE	0xff0000
static int __init checker_init(void)
{
	s32 offset;
	int i = 0;
	unsigned char *pos;
	unsigned int *code;
	unsigned long *lcode, target;
	char *__text;// = 0xffffffff81000000;

	__text = (void *)kallsyms_lookup_name("_text");

	for (i = 0; i < TEXT_SIZE;) {
		pos = &__text[i];
		code = (unsigned int *)&pos[5];
		lcode = (unsigned long *)pos;
		// 下面的if语句过滤ftrace函数开头的call hook
		if (*code == 0xe5894855 && *lcode != 0x8948550000441f0f && pos[0] == 0xe8) {
			offset = *(s32 *)&pos[1];
			target = (unsigned long)__text + i + offset;
			if (target > (unsigned long)__text + TEXT_SIZE) {
				printk("caller address: %llx  callee address[请详查]: %llx\n",
					(unsigned long)__text + i, target);
			}
			i += 9;
			continue;
		}

		// 下面的语句过滤函数中间的call hook。
		// 没办法,我只能这样过滤了,实际上正规的方法是扫描整个指令,在状态机中找call指令:
		// call的偏移如果越过内核TEXT段,基本就要详细check一下了!
		code = (unsigned int *)&pos[5];
		if (*code == 0x7501f883 && pos[0] == 0xe8 /*&& pos[5] == 0x90*/ &&
			((*(pos - 1) != 0xc1) &&
			 (*(pos - 1) != 0x83 && *(pos - 2) != 0x48) &&
			 (*(pos - 1) != 0x25 && *(pos - 2) != 0x04) &&
			 (*(pos - 1) != 0xe8) &&
			 (*(pos - 1) != 0x55) &&
			 (*(pos - 1) != 0x1d) &&
			 (*(pos - 1) != 0xe9) &&
			 (*(pos - 2) != 0x4c) &&
			 (*(pos - 2) != 0x0f) &&
			 (*(pos - 2) != 0x81) && // for set_mode
			 (*(pos - 3) != 0xe8) && // for xhci_dbg_cmd_ptrs
			 (*(pos - 3) != 0x4c) &&
			 //(*(pos - 5) != 0x48) && // for set_max_huge_pages
			 (*(pos -2) != 0x44 && *(pos - 1) != 0x89))) {
			offset = *(s32 *)&pos[1];
			target = (unsigned long)__text + i + offset + 5;
			if (target > (unsigned long)__text + TEXT_SIZE) {
				printk("[middle hook] caller address: %llx  callee address[请详查]: %llx\n",
					(unsigned long)__text + i, target);
			}
			i += 9;
			continue;
		}
		i ++;
	}

	return -1;
}

module_init(checker_init);
MODULE_LICENSE("GPL");

来吧,演示一下吧。

如果空加载这个模块,不会有任何输出,但是我注入了两个恶意的rootkit:

  1. 隐藏进程和CPU利用率。
  2. 统计iptables DROP的数量(不算恶意…)。

关于以上第二个,参见:
https://blog.csdn.net/dog250/article/details/105206753

为了便于验证和归档,我再次给出代码:

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/kallsyms.h>
#include <linux/cpu.h>

char *stub;
char *addr = NULL;

// 传入ip_local_deliver的地址
static unsigned long laddr = 0xffffffffa0267000;
module_param(laddr, ulong, 0644);

// 计数INPUT链上的被DROP的数据包的数量
static unsigned int counter = 0;
module_param(counter, int, 0444);

void test_stub1(void) __attribute__ ((aligned (1024)));
void test_stub2(void) __attribute__ ((aligned (1024)));
void test_stub1(void)
{
	printk("yes\n");
}
void test_stub2(void)
{
	printk("yes yes\n");
}

#define FTRACE_SIZE   	5
#define POKE_OFFSET		173
#define POKE_LENGTH		5
#define COND_LENGTH		5
#define COUNTE_LENGTH	8

static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
static struct mutex *_text_mutex;

static unsigned int pos, target;
static int __init hotfix_init(void)
{
	unsigned char e8_call[POKE_LENGTH];
	unsigned char incl[COUNTE_LENGTH];
	unsigned char cond[COND_LENGTH];
	s32 offset, i;
	u32 low32 = (unsigned int)(((unsigned long)&counter) & 0xffffffff);

	laddr = (void *)kallsyms_lookup_name("ip_local_deliver");
	_text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
	_text_mutex = (void *)kallsyms_lookup_name("text_mutex");
	if (!laddr || !_text_poke_smp || !_text_mutex) {
		printk("not found\n");
		return -1;
	}
	addr = (void *)laddr;


	stub = (void *)test_stub1;

	// 两个函数的call地址偏移
	offset = (s32)((long)stub - (long)addr - FTRACE_SIZE);
	// 两个函数指令相对偏移
	pos = (unsigned int)((long)stub - (long)addr);

	_text_poke_smp(&stub[0], &addr[POKE_OFFSET], POKE_LENGTH);

	// 调节校准call nf_hook_slow的相对地址偏移
	target = *((unsigned int *)&addr[POKE_OFFSET + 1]);
	target -= pos;
	target += POKE_OFFSET;
	_text_poke_smp(&stub[1], &target, sizeof(target));

	// 填充条件判断:只有返回DROP才会被计数
	cond[0] = 0x83; // cmp $0x1, %eax
	cond[1] = 0xf8;
	cond[2] = 0x01;
	cond[3] = 0x74; // jz $ret
	cond[4] = 0x07; // skip "incl $counter"
	_text_poke_smp(&stub[POKE_LENGTH], &cond, COND_LENGTH);

	// 插入的指令中需要save/restore寄存器,但这里简单,略过
	incl[0] = 0xff; // incl $counter
	incl[1] = 0x04;
	incl[2] = 0x25;
	(*(u32 *)(&incl[3])) = low32;
	incl[7] = 0xc3; // retq
	_text_poke_smp(&stub[POKE_LENGTH + COND_LENGTH], &incl, 8);

	// call比jmp方便,可以自动帮忙return,不然还要自己jmp回来,但是代价是push/pop
	e8_call[0] = 0xe8;
	(*(s32 *)(&e8_call[1])) = offset - POKE_OFFSET;
	for (i = 5; i < POKE_LENGTH; i++) {
		e8_call[i] = 0x90; // nop 占位符
	}
	get_online_cpus();
	mutex_lock(_text_mutex);
	_text_poke_smp(&addr[POKE_OFFSET], e8_call, POKE_LENGTH);
	mutex_unlock(_text_mutex);
	put_online_cpus();

	return 0;
}

static void __exit hotfix_exit(void)
{
	target -= POKE_OFFSET;
	target += pos;
	_text_poke_smp(&stub[1], &target, sizeof(target));
	get_online_cpus();
	mutex_lock(_text_mutex);
	_text_poke_smp(&addr[POKE_OFFSET], &stub[0], POKE_LENGTH);
	mutex_unlock(_text_mutex);
	put_online_cpus();
}

module_init(hotfix_init);
module_exit(hotfix_exit);
MODULE_LICENSE("GPL");

注入这两个之后,再次加载checker:

[root@localhost test]# insmod ./check.ko
insmod: ERROR: could not insert module ./check.ko: Operation not permitted
[root@localhost test]# dmesg
[34980.244454] caller address: ffffffff810b4f10  callee address[请详查]: ffffffffa011a000
[34980.244456] caller address: ffffffff810b4fb0  callee address[请详查]: ffffffffa011a000
[34980.251519] [middle hook] caller address: ffffffff81561ebd  callee address[请详查]: ffffffffa0252000
[34980.251666] caller address: ffffffff8158

我们用crash命令查一下:

...
crash> dis ffffffffa0123000 10
dis: WARNING: ffffffffa0123000: no associated kernel symbol found
   0xffffffffa0123000:  nopl   0x0(%rax,%rax,1)
   0xffffffffa0123005:  push   %rbp
   0xffffffffa0123006:  cmp    $0x1,%rsi
   0xffffffffa012300a:  mov    %rsp,%rbp
   0xffffffffa012300d:  je     0xffffffffa0123017
   0xffffffffa012300f:  cmpw   $0x4d2,0xe(%rsi)
   0xffffffffa0123015:  je     0xffffffffa0123020
   0xffffffffa0123017:  pop    %rbp
   0xffffffffa0123018:  retq
   0xffffffffa0123019:  nopl   0x0(%rax)
crash> dis ffffffffa0252000 10
0xffffffffa0252000 <test_stub1>:        callq  0xffffffff815586a0 <nf_hook_slow>
0xffffffffa0252005 <test_stub1+5>:      cmp    $0x1,%eax
0xffffffffa0252008 <test_stub1+8>:      je     0xffffffffa0252011 <test_stub1+17>
0xffffffffa025200a <test_stub1+10>:     incl   0xffffffffa0254280
0xffffffffa0252011 <test_stub1+17>:     retq
0xffffffffa0252012 <test_stub1+18>:     callq  0xffffffff8162e40d <printk>
0xffffffffa0252017 <test_stub1+23>:     pop    %rbp
0xffffffffa0252018 <test_stub1+24>:     retq
0xffffffffa0252019 <test_stub1+25>:     nop
0xffffffffa025201a <test_stub1+26>:     nop
crash>

一把就揪出来了!

当然了,如果有时间,我会写一个完整的x86_64指令解析状态机,这样就可以搜集所有的jmp/call目标了,逐一检查这些目标,看看哪些是可疑的,最终揪出真凶。

除了jmp/call的目标,内核数据结构的回调函数也是检测目标之一,比如系统调用表的内容,肯定要在TEXT段中,比如很多inet回调函数,也必须处在TEXT中。

我之所以没有通过读取/proc/kallsyms,那是怕它被hook掉啊!而且通过kallsyms_lookup_name得到的_text是不是也是可信的,也是有问题的,kallsyms_lookup_name本身被hook掉怎么办?

然而,这些问题并不大,我们心里要有个数,基本上,内核代码的位置就是在0xffffffff81xxxxxx这些地址上的,并且内核函数的布局是很连续紧凑的,这些特点我们心里都有数,如果非要揪着这些细节说这个方法不严谨,那就杠精嫌疑了,没意思。

其实,本来也没什么意思。


浙江温州皮鞋湿,下雨进水不会胖。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!