通过trace stack检测内核函数是否被hook

Rootkit需要及时发现是否有程序抓它,而侦测程序本身也需要时刻警惕Rootkit的注入,左右互搏。

侦测程序发现Rootkit的手段是非常多的,前面我介绍过通过内核text段互相调用的地址范围来静态扫描的方式:
https://blog.csdn.net/dog250/article/details/105474909
本文我将介绍一种动态trace stack的方式来捕捉内核函数的调用异常。

以一个中性程序为例来讲吧。无关褒贬善恶。

上个月,我实现了一个功能,统计被iptables在INPUT链上DROP掉的数据包的数量:
https://blog.csdn.net/dog250/article/details/105206753
具体细节请看那篇文章吧。

我的意思是,如何查出ip_local_deliver的中间被hook了呢?

这其实很难,但也不是没有办法。

假设在不考虑性能损耗的情况下,我为内核的每一个函数头部添加一个jmp stub,在该stub中dump当前的stack,那么只要该stack的RBP下面附近有非内核text段地址区间范围的地址,那就需要详查,排除掉回调函数,剩下的就是非法的。

内核text段地址区间大致就是:

ffffffff81000000 T _text
...
ffffffff81649abb T _etext

我们摆出DROP统计里的例子,看看代码被hook成了什么样子:

# 这是原始的调用
0xffffffff81561eb5 <ip_local_deliver+165>:      movq   $0xffffffff81561ad0,-0x18(%rbp)
0xffffffff81561ebd <ip_local_deliver+173>:      callq  0xffffffff815586a0 <nf_hook_slow>
0xffffffff81561ec2 <ip_local_deliver+178>:      cmp    $0x1,%eax

# 这是被hook后的调用
0xffffffff81561eb5 <ip_local_deliver+165>:      movq   $0xffffffff81561ad0,-0x18(%rbp)
0xffffffff81561ebd <ip_local_deliver+173>:      callq  0xffffffffa00ef000 <test_stub1>
0xffffffff81561ec2 <ip_local_deliver+178>:      cmp    $0x1,%eax

crash> dis test_stub1
0xffffffffa00ef000 <test_stub1>:        callq  0xffffffff815586a0 <nf_hook_slow>
0xffffffffa00ef005 <test_stub1+5>:      cmp    $0x1,%eax
0xffffffffa00ef008 <test_stub1+8>:      je     0xffffffffa00ef011 <test_stub1+17>
0xffffffffa00ef00a <test_stub1+10>:     incl   0xffffffffa00f1280
0xffffffffa00ef011 <test_stub1+17>:     retq
0xffffffffa00ef012 <test_stub1+18>:     callq  0xffffffff8162e40d <printk>
0xffffffffa00ef017 <test_stub1+23>:     pop    %rbp
0xffffffffa00ef018 <test_stub1+24>:     retq

OK,我们确认下如果在nf_hook_slow打印stack的话,应该就能发现test_stub1,毕竟是它调用的nf_hook_slow。其栈中应该会有 0xffffffffa00ef005 这个地址。

让我们试试看。

为了编码方便省去编译的过程,我依然使用guru模式的stap脚本:

%{
     
#include 
#include 
%}

%{
     
unsigned char *_self;
%}

function init_self()
%{
     
	_self = (void *)kallsyms_lookup_name("nf_hook_slow");
%}

function filter:long(iphdr:long, stat:long)
{
     
	hooknum = 0;
	protocol = @cast(iphdr, "iphdr")->protocol
	if (stat) {
     
		hooknum = @cast(stat, "nf_hook_state")->hook
	}
	return (hooknum == 1 && protocol == %{
      IPPROTO_ICMP %})
}
function ip_hdr:long(skb:long)
%{
     
	struct iphdr *iph = ip_hdr((struct sk_buff *)STAP_ARG_skb);
	STAP_RETVALUE = (long)iph;
%}
function _backtrace ()
%{
     
	unsigned long rbp;
	unsigned long *prbp;
	int i, prn = 0;
	asm ("mov %%rbp, %0;\n":"=m"(rbp)::);

	prbp = (unsigned long *)rbp;

	for (i = 0; i < 20; i++) {
     
		// 由于stap机制本身就堆积了很多调用,所以我们skip掉它们,仅从nf_hook_slow下面开始。
		if (prbp[i] == (unsigned long)_self)
			prn = 1;
		if (prn == 1)
			STAP_PRINTF("0x%lx \n", prbp[i]);
	}
%}

probe begin {
     
	init_self();
}

probe kernel.function("nf_hook_slow")
{
     
	iph = ip_hdr(pointer_arg(1));
	stat = pointer_arg(2);
	if (filter(iph, stat)) {
     
		_backtrace();
		println();
		println();
	}
}

实验现在开始。

增加一条iptables规则并运行stap脚本:

[root@localhost test]# iptables -A INPUT -p icmp -j DROP
[root@localhost test]# stap -g ./sdump.stp

然后ping一下本机,看输出:

0xffffffff815586a0
0xffff88003fd83c80
0xffffffffa00ef005
0xffff88003fd83c70
0xffffffff8112945e
0xffff88003c92d000
0xffff8800361af410
0xffff88003d4f0000

哈哈,找到 0xffffffffa00ef005 了吧! 眯着眼都能发现不对劲,明显不是81开头的…这个地址明显不在内核的代码段区间里,而是在模块的映射区间里:

0xffffffffa0000000 ~ 0xffffffffff000000

这一看就知道是在模块里分配内存干的事情。

然而,我发现了这个问题,如此明显就被抓到也比较尴尬,那么,我们如果把代码藏到内核代码段本身呢?请看下面的文章:
https://blog.csdn.net/dog250/article/details/105496996

内核里有大把的地方供你藏污纳垢,直接找nop区域就好,以下是我找到的一个地方,并把代码复制了过去:

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

现在加载这个藏污纳垢版的模块,再次用上述stap脚本检测:

0xffffffff815586a0
0xffff88003fd83c80
0xffffffff810001d5
0xffff88003fd83c70
0xffffffff8112945e
0xffff88003ae96f00
0xffff88003bc71210
0xffff88003d4f0000

注意这个 0xffffffff810001d5 地址,这就是_stext里的了。如果不仔细看这个地址的内容,很容易将它当成正常地址从而放过。

停!有问题!

如果stap被封堵怎么办?试试perf probe:

perf probe --del 'nf_hook_slow*' && perf probe 'nf_hook_slow caller=$stack0'&& ping 127.0.0.1

然后打印结果:

[root@localhost ~]# perf record -e probe:nf_hook_slow -aR sleep 3 && perf script
[ perf record: Woken up 1 times to write data ]
...
            ping  3376 [003]  5361.373481: probe:nf_hook_slow: (ffffffff815586a0) caller=ffffffff810001d5

我们可以看到caller, ffffffff810001d5, 依然抓个正着!

当然了,你会质疑,我怎么知道去probe nf_hook_slow这个函数呢?这难道不是事后诸葛亮吗?

是的,这个质疑完全合理,我事前并不知道到底要probe哪个函数,我这里只是演示。

如果我们有一个 可能被hook后调用的函数列表 ,也就是嫌疑函数列表,那么这事儿就简单了一半。事实上我们有这个列表。比方说系统调用函数。

很多时候,我们都是hook住原始系统调用,进入我们自己的逻辑,然后再调用原始的系统调用函数:

int my_sys_write(...)
{
     
	do_something(...);
	return orig_sys_write(...);
}

很显然,my_sys_write肯定不在内核text段的范围内,我们使用上述的方法,就可以成功捕获这个hook行为。

好了,要出发了,今天就先到这里。


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

你可能感兴趣的:(rootkit)