使用Intel XED检测Linux内核是否被rootkit控制

接着上文继续说:
https://blog.csdn.net/dog250/article/details/105474909

我一心一意想写一个指令解析器,我的目的是扫描出Linux内核text段中的所有的jmp和call指令,从而检测内核是否已经被篡改。基于以下事实:

  • 一般内核函数互相调用都跑不出内核的text段,从0xffffffff81000000开始的几兆空间,凡是跳转越界跑出这个空间的,都要详查,过滤掉正常的hotfix,export回调这些,剩下的就是篡改了。
  • 像bridge/bonding的rx_handler回调函数,是在模块里注册的回调指针,并非立即数出现在call指令中,即便rx_handler有问题,也只是模块自己的无心错(没人傻到用这种方式进行二进制注入)。

关于指令解析器,有现成的就不用自己写了,我用的是Intel XED,代码在:
https://github.com/intelxed/xed
照着readme操作就可以了,最后会得到一个叫做xed的可执行程序:

[root@localhost obj]# pwd
/root/test/xed/kits/xed-install-base-2020-03-25-lin-x86-64/examples/obj
[root@localhost obj]#

配合下面的stap脚本,我们先看下这个指令解析的效果:

// scan_text.stp
%{
#include <linux/module.h>
%}

function scan_text:long()
%{
#define MAX_TEXT_SIZE	0xff0000
	int i;
	unsigned int size;
	unsigned char *__text;
	unsigned char *__etext;

	// 内核代码的开始和结束
	__text = (void *)kallsyms_lookup_name("_text");
	__etext = (void *)kallsyms_lookup_name("_etext");
	__text ++;
	STAP_PRINTF("90 ");

	size = __etext - __text;
	// 将内核text段全部dump下来
	for (i = 0; i < size; i++) {
		STAP_PRINTF("%x ", __text[i]);
	}

	STAP_RETVALUE = 0;
%}

probe begin
{
	scan_text();
	exit(); // oneshot模式
}

我们跑一下试试看:

[root@localhost obj]# stap -g ./scanner.stp >./code.hex
[root@localhost obj]# cat code.hex |more
90 8d 2d f9 ff ff ff 48 81 ed 0 0 0 1 48 89 e8 25 ff ff 1f 0 85 c0 f 85 a7 1 0 0 48 8d 5 db ff ff ff
48 c1 e8 2e f 85 96 1 0 0 48 1 2d c2 8f ae 0 48 1 2d b3 df 94 0 48 1 2d b4 df 94 0 48 1 2d 85 ff 94 0
 48 8d 3d ae ff ff ff 48 8d 1d a7 7f ae 0 48 89 f8 48 c1 ...

好的,现在让我们上xed程序:

[root@localhost obj]# ./xed -ih ./code.hex -64 >./code.txt
[root@localhost obj]#
[root@localhost obj]# cat ./code.txt |head -n 10
XDIS 0: NOP       BASE       90                       nop
XDIS 1: MISC      BASE       8D2DF9FFFFFF             lea ebp, ptr [rip-0x7]
XDIS 7: BINARY    BASE       4881ED00000001           sub rbp, 0x1000000
XDIS e: DATAXFER  BASE       4889E8                   mov rax, rbp
XDIS 11: LOGICAL   BASE       25FFFF1F00               and eax, 0x1fffff
XDIS 16: LOGICAL   BASE       85C0                     test eax, eax
XDIS 18: COND_BR   BASE       0F85A7010000             jnz 0x1c5
XDIS 1e: MISC      BASE       488D05DBFFFFFF           lea rax, ptr [rip-0x25]
XDIS 25: SHIFT     BASE       48C1E82E                 shr rax, 0x2e
XDIS 29: COND_BR   BASE       0F8596010000             jnz 0x1c5
[root@localhost obj]#

这不就是内核代码吗?是的,这就是内核代码!

现在,让我们揪出里面所有的0xe8相对地址call指令:

[root@localhost obj]# cat ./code.txt |egrep '\sE8[0-9A-F]{8}\s' >./call.txt
[root@localhost obj]#
[root@localhost obj]# cat call.txt |head -n 20|tail -n 10
XDIS 21db: CALL      BASE       E8007D2F00               call 0x2f9ee0
XDIS 21fd: CALL      BASE       E85E153100               call 0x313760
XDIS 2226: CALL      BASE       E835250700               call 0x74760
XDIS 2254: CALL      BASE       E807153100               call 0x313760
XDIS 227f: CALL      BASE       E82C722F00               call 0x2f94b0
XDIS 2353: CALL      BASE       E818742F00               call 0x2f9770
XDIS 2387: CALL      BASE       E8D4722F00               call 0x2f9660
XDIS 23b1: CALL      BASE       E81A982F00               call 0x2fbbd0
XDIS 23c8: CALL      BASE       E840C06200               call 0x62e40d
XDIS 2408: CALL      BASE       E8C3972F00               call 0x2fbbd0

以最后一行为例,它在Linux内核的text段的偏移为0x2408,相对call的偏移是0x2fbbd0,加上_text基地址就是绝对地址了,一般而言,这个基地址就是0xffffffff81000000,所以该call指令的目标是0xffffffff812fbbd0,我们确认一下:

[root@localhost obj]# cat /proc/kallsyms |grep ffffffff812fbbd0
ffffffff812fbbd0 T sscanf

嗯,它超大概率是正常的内核函数之间的互相调用。

所以,我们只需要寻找相对偏移大于0xffffff的即可:

[root@localhost obj]# cat ./code.txt |egrep '\sE8[0-9A-F]{8}\s' |gawk --non-decimal-data '{if ($NF > 0xffffff)print $0}'
[root@localhost obj]#

没有,什么都没有…

当然什么都没有咯,因为我们的内核目前还是干净的!

现在,让我们注入昨天的那个hack ip_local_deliver的代码,再次尝试:

[root@localhost obj]# insmod ./drop.ko
[root@localhost obj]# stap -g ./scanner.stp >./code2.hex
[root@localhost obj]# ./xed -ih ./code2.hex -64 >./code2.txt
[root@localhost obj]# cat ./code2.txt |egrep '\sE8[0-9A-F]{8}\s' |gawk --non-decimal-data '{if ($NF > 0xffffff)print $0}'
XDIS 561ebd: CALL      BASE       E83EA1B41E               call 0x1f0ac000
[root@localhost obj]#

输出一条,我们需要严查它了,看看它到底是什么,call到了哪里:

crash> dis 0xffffffff81561ebd 5
0xffffffff81561ebd <ip_local_deliver+173>:      callq  0xffffffffa00ac000 <test_stub1>
0xffffffff81561ec2 <ip_local_deliver+178>:      cmp    $0x1,%eax
0xffffffff81561ec5 <ip_local_deliver+181>:      jne    0xffffffff81561e69 <ip_local_deliver+89>
0xffffffff81561ec7 <ip_local_deliver+183>:      jmp    0xffffffff81561e5f <ip_local_deliver+79>
0xffffffff81561ec9 <ip_local_deliver+185>:      nopl   0x0(%rax)
crash>

注意第一行那个 callq 0xffffffffa00ac000 <test_stub1> 成功揪出了真凶!!

注意!上面的实验结果不是一次的结果,所以数值对不上,以下图的计算为准:
在这里插入图片描述


自己和自己下棋还是很有趣的,左右互搏,道高一尺,魔高一丈。

谁说我必须把test_stub1放在0xffffffffa00ac000这么远的地方了,我把它塞进Linux内核的text段行不行?

谁说不行,当然行!问题是你要找到一片空隙,足够你塞入你的stub代码。

那就扫nop序列呗!

我还真就找到了:

[root@localhost obj]# cat code.hex |egrep -o '(90\s{1}){10}' |head -n 10
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
[root@localhost obj]# cat ./code.txt |egrep '\s90\s' |more
...
XDIS 1ce: NOP       BASE       90                       nop
XDIS 1cf: NOP       BASE       90                       nop
XDIS 1d0: NOP       BASE       90                       nop
...
XDIS 1e2: NOP       BASE       90                       nop
XDIS 1e3: NOP       BASE       90                       nop
...

就从1d0开始吧!换算成地址就是0xffffffff810001d0。来吧,动手,修改那个drop hook:

#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;
module_param(laddr, ulong, 0644);

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

#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);

	char *gap = (void *)0xffffffff810001d0;

	laddr = (unsigned long)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 *)gap;

	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);

	target = *((unsigned int *)&addr[POKE_OFFSET + 1]);
	target -= pos;
	target += POKE_OFFSET;
	_text_poke_smp(&stub[1], &target, sizeof(target));

	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);

	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);

	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");

加载该模块,再用xed检测,就什么都检测不出来了!我们用crash看看就是ip_local_deliver调用了哪里:

crash> dis ip_local_deliver+173
0xffffffff81561ebd <ip_local_deliver+173>:      callq  0xffffffff810001d0 <_stext+8>

我们可以看到 0xffffffff810001d0 这个地址看上去非常正常,它妥妥就在text范围内!我们看看它是什么:

crash> dis 0xffffffff810001d0 10
0xffffffff810001d0 <_stext+8>:  callq  0xffffffff815586a0 <nf_hook_slow>
0xffffffff810001d5 <_stext+13>: cmp    $0x1,%eax
0xffffffff810001d8 <_stext+16>: je     0xffffffff810001e1 <_stext+25>
0xffffffff810001da <_stext+18>: incl   0xffffffffa0239280
0xffffffff810001e1 <_stext+25>: retq
0xffffffff810001e2 <_stext+26>: nop
0xffffffff810001e3 <_stext+27>: nop
0xffffffff810001e4 <_stext+28>: nop
0xffffffff810001e5 <_stext+29>: nop
0xffffffff810001e6 <_stext+30>: nop
crash>

噢,我的天,它就是我们的stub函数,这下它藏匿到了一个更加不容易被检测到的地方了!

所以说咯,检测程序还需要更加智能一些。

本以为右手打赢了左手,没想到左手最后一击反扑成功!

如果你担心text即便用text poke也不能写,那就自己改页表项呗…


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

原创文章 1603 获赞 5261 访问量 1129万+

猜你喜欢

转载自blog.csdn.net/dog250/article/details/105496996