Issue 0x02, Phile #0x05 of 0x0A
|=---------------------------------------------------------------------------=|
|=-------------------=[ 高级Linux kernel inline hook技术 ]=------------------=|
|=---------------------------------------------------------------------------=|
|=---------------------------------------------------------------------------=|
|=--------------------------=[ By wzt ]=----------------------------=|
|=------------------------=[ <wzt_at_xsec.org> ]=-------------------------=|
|=---------------------------------------------------------------------------=|
一. 简述
目前流行和成熟的kernel inline hook技术就是修改内核函数的opcode,通过写入jmp或
push ret等指令跳转到新的内核函数中,从而达到修改或过滤的功能。这些技术的共同点
就是都会覆盖原有的指令,这样很容易在函数中通过查找jmp,push ret等指令来查出来,
因此这种inline hook方式不够隐蔽。本文将使用一种高级inline hook技术来实现更隐蔽的
inline hoo技术。
二. 更改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>
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;
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) {
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;
DbgPrint("*** hook engine: offset: 0x%08x\n", offset);
orig = offset + (unsigned int)p + 5;
DbgPrint("*** hook engine: original func: 0x%08x\n", orig);
if (orig == old_func) {
DbgPrint("*** hook engine: found old func at"
" 0x%08x\n",
old_func);
DbgPrint("%d\n", i);
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来查找函数地址。
四. 实例
下面是hook sys_read的部分代码实现,读者可以根据思路来补充完整。
/*
My hook engine v0.20
by wzt <wzt@xsec.org>
tested on amd64 as5, x86 as4,5
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/spinlock.h>
#include <linux/smp_lock.h>
#include <linux/fs.h>
#include <linux/file.h>
#include <linux/dirent.h>
#include <linux/string.h>
#include <linux/unistd.h>
#include <linux/socket.h>
#include <linux/net.h>
#include <linux/tty.h>
#include <linux/tty_driver.h>
#include <net/sock.h>
#include <asm/uaccess.h>
#include <asm/unistd.h>
#include <asm/siginfo.h>
#include "hook.h"
ssize_t (*orig_vfs_read)(struct file *file, char __user *buf, size_t count,
loff_t *pos);
unsigned int system_call_addr = 0;
unsigned int sys_call_table_addr = 0;
unsigned int sys_read_addr = 0;
int hook_vfs_read_flag = 1;
unsigned int get_sct_addr(void)
{
int i = 0, ret = 0;
for (; i < 500; i++) {
if ((*(unsigned char*)(system_call_addr + i) == 0xff)
&& (*(unsigned char *)(system_call_addr + i + 1) == 0x14)
&& (*(unsigned char *)(system_call_addr + i + 2) == 0x85)) {
ret = *(unsigned int *)(system_call_addr + i + 3);
break;
}
}
return ret;
}
ssize_t new_vfs_read(struct file *file, char __user *buf, size_t count,
loff_t *pos)
{
ssize_t ret;
ret = (*orig_vfs_read)(file, buf, count, pos);
if (ret > 0) {
DbgPrint("hello, world.\n");
}
return ret;
}
unsigned int patch_kernel_func(unsigned int handler, unsigned int old_func,
unsigned int new_func)
{
unsigned char *p = (unsigned char *)handler;
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) {
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;
DbgPrint("*** hook engine: offset: 0x%08x\n", offset);
orig = offset + (unsigned int)p + 5;
DbgPrint("*** hook engine: original func: 0x%08x\n", orig);
if (orig == old_func) {
DbgPrint("*** hook engine: found old func at"
" 0x%08x\n",
old_func);
DbgPrint("%d\n", i);
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;
}
static int hook_init(void)
{
struct descriptor_idt *pIdt80;
__asm__ volatile ("sidt %0": "=m" (idt48));
pIdt80 = (struct descriptor_idt *)(idt48.base + 8*0x80);
system_call_addr = (pIdt80->offset_high << 16 | pIdt80->offset_low);
if (!system_call_addr) {
DbgPrint("oh, shit! can't find system_call address.\n");
return 0;
}
DbgPrint(KERN_ALERT "system_call addr : 0x%8x\n",system_call_addr);
sys_call_table_addr = get_sct_addr();
if (!sys_call_table_addr) {
DbgPrint("oh, shit! can't find sys_call_table address.\n");
return 0;
}
DbgPrint(KERN_ALERT "sys_call_table addr : 0x%8x\n",sys_call_table_addr);
sys_call_table = (void **)sys_call_table_addr;
sys_read_addr = (unsigned int)sys_call_table[__NR_read];
DbgPrint("sys_read addr: 0x%08x\n", sys_read_addr);
lock_kernel();
CLEAR_CR0
if (sys_read_addr) {
orig_vfs_read = (ssize_t (*)())patch_kernel_func(sys_read_addr,
(unsigned int)vfs_read, (unsigned int)new_vfs_read);
if ((unsigned int)orig_vfs_read == 0)
hook_vfs_read_flag = 0;
}
SET_CR0
unlock_kernel();
DbgPrint("orig_vfs_read: 0x%08x\n", (unsigned int)orig_vfs_read);
DbgPrint("install hook ok.\n");
return 0;
}
static void hook_exit(void)
{
lock_kernel();
CLEAR_CR0
if (hook_vfs_read_flag)
patch_kernel_func(sys_read_addr, (unsigned int)new_vfs_read,
(unsigned int)vfs_read);
SET_CR0
unlock_kernel();
DbgPrint("uninstall hook ok.\n");
}
module_init(hook_init);
module_exit(hook_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wzt");
-EOF-
############################################################################
linux 中 inline hook 简单分析(do_exit)
author: jonathan
本文档的CopyRight归jonathan所有,可自由转载,转载时请保持文档的完整性。
/*----------------------------------------------------------------------------------------------------------------------------*/
Hook多年不搞了,总认为是上不了台面的技术。但是由于产品的需要没有办法,还是要弄一弄。
本文中重点描述一下Linux的函数Hook中注意的关键点。
1 概念
Hook两个字描述:劫持
对于目标是对象,就是对象HOOK;对于目标是函数,就叫Inline Hook;对于目标是IAT,就叫IAT Hook.
2 方法
对于函数HOOK,要能够跳转到劫持函数中,再跳转原函数。如何跳转,使用什么指令?其实方法很多,最多就是使用jmp,因为简单。如果使用call也可以,还需要自己维护call产生的栈问题。
当然,插入HOOK点理论上可以在函数任何位置,但是要保障插入HOOK点前后指令的完整性。现在disassembler库也很多,都不是难题。
2.1 jmp方法
jmp dst_address_offset
此命令占5个地址; dst_address_offset = 目的地址 - HOOK点开始位置 - jmp指令占用地址(5)
2.2 call方法
call dst_address_offset
此处注意call的分解:push 返回地址;jmp 目标地址。因此在HOOK函数返回时,注意平衡堆栈,特别时使用jmp方式返回
投机的方式:找到一个以有的call处,修改call处的跳转地址即可。
3 注意事项
3.1 内存要可写,可检查CR0寄存器中的WP位
3.2 处理好堆栈平衡
3.3 原子操作,特别是多cpu情况
4 Windows平台 HOOK
文章数不胜数,略。
5 Linux下函数Inline Hook
这里以do_exit为例。do_exit函数是导出函数,所以可以直接获取函数地址;对于非导出函数,则需要相关函数查找地址。现在假设do_exit未导出。
为了找到非导出函数地址,需要在内存中找特征码。但是对于要搜寻的函数空间也有两点要求:
函数地址可知,否则陷入鸡生蛋问题;
该函数要调用寻找的未导出函数地址。
对于do_exit,我们首先应该想到是sys_exit函数。
5.1 查找do_exit函数地址
(gdb) disass sys_exit
Dump of assembler code for function sys_exit:
0xc042ef4c <sys_exit+0>: push %ebp
0xc042ef4d <sys_exit+1>: mov %esp,%ebp
0xc042ef4f <sys_exit+3>: mov 0x8(%ebp),%eax
0xc042ef52 <sys_exit+6>: shl $0x8,%eax
0xc042ef55 <sys_exit+9>: and $0xffff,%eax
0xc042ef5a <sys_exit+14>: call 0xc042e79a <do_exit>
End of assembler dump.
(gdb) x/2 0xc042ef5a
0xc042ef5a <sys_exit+14>: 0xfff83be8 0xc08555ff
(gdb) disass do_exit
Dump of assembler code for function do_exit:
0xc042e79a <do_exit+0>: push %ebp
0xc042e79b <do_exit+1>: mov %esp,%ebp
0xc042e79d <do_exit+3>: push %edi
0xc042e79e <do_exit+4>: push %esi
0xc042e79f <do_exit+5>: push %ebx /*到此为止*, 可以看出do_exit不错,指令基本可以满足要求/
0xc042e7a0 <do_exit+6>: mov %eax,%ebx
0xc042e7a2 <do_exit+8>: sub $0x38,%esp
0xc042e7a5 <get_current+0>: mov %fs:0xc0858000,%edi
...
具体就不用多说了,看看2中原理,对照一下上面红色字体部分就明白了。
5.2 替换do_exit
static unsigned char g_original_do_exit[5] = { 0 };
static unsigned char g_stub_do_exit[5] = { 0xe9, 0, 0, 0, 0};
static unsigned long g_do_exit_address = 0;
static int hook_do_exit(unsigned char* do_exit_address)
{
int ret = -1;
unsigned long offset = 0;
// g_do_exit_address = (unsigned long)(do_exit_address + 3);
g_do_exit_address = (unsigned long)(do_exit_address );
memcpy(g_original_do_exit, (unsigned char *)g_do_exit_address, 5);
offset = (unsigned long)my_do_exit - g_do_exit_address - 5;
*((unsigned long *)(g_stub_do_exit + 1)) = offset;
lock_kernel();
CLEAR_CR0;
memcpy((unsigned char*)g_do_exit_address, g_stub_do_exit, 5);
SET_CR0;
unlock_kernel();
return ret;
}
static void unhook_do_exit(void)
{
lock_kernel();
CLEAR_CR0;
memcpy((unsigned char*)g_do_exit_address, g_original_do_exit, 5);
SET_CR0;
unlock_kernel();
}
5.3 my_do_exit处理
static long my_do_exit(int error_code)
{
#if 1 /* 从do_exit头开始hook */
asm("pushl %%ebp\n\t"
"movl %%esp,%%ebp\n\t"
"pushl %%edi\n\t"
"pushl %%esi\n\t"
"pushl %%ebx\n\t" /* 为何先pushl?其后面语句也是push ebx?你自己来思考了,这里有小弯 */
"movl %0, %%ebx\n\t" /* 为何选择 ebx而不是eax ,需要你自己来思考 */
"addl $5, %%ebx\n\t"
"jmp *%%ebx\n\t"
::"m"(g_do_exit_address)
);
#else /* 从do_exit + 3的位置hook */
asm("pushl %%edi\n\t"
"pushl %%esi\n\t"
"pushl %%ebx\n\t"
"movl %%eax, %%ebx\n\t"
"movl %0, %%eax\n\t" /* 为什么选择是eax , 您要思考一下 */
"addl $5, %%eax\n\t"
"jmp *%%eax\n\t"::"m"(g_do_exit_address)
);
#endif
return 0;
}
5.4 注意事项
应用层程序退出一般从sys_exit不到信息的,但是一定能够从do_exit获取到信息。
卸载do_exit的hook就崩溃了,原因我没有继续查找,留给您了。