Linux Rootkit 系列四:对于系统调用挂钩方法的补充

本文发布时间: 2019-Mar-21
前言本篇是《Linux Rootkit系列一:LKM的基础编写及隐藏 》的后续,不好意思隔这么久才来填坑。我看见有位好心的同学novice帮我填了一部分内容:《Linux Rootkit 系列二:基于修改 sys_call_table 的系统调用挂钩》《Linux Rootkit系列三:实例详解 Rootkit 必备的基本功能》,在此表示感谢。我将会把系列文章继续写下去,由于本系列文章novice同学也在写,所以我俩的顺序可能有点乱,不过他写过的内容我不会在重复,可能以后我俩会进行沟通,合理安排内容,争取可以合写好这个系列。本篇文章按照之前文章所说的,来介绍linux rootkit中的系统调用挂钩技术。1.背景本次环境依然是linux 2.6系列内核,ubuntu10.04。本篇文章及上篇文章的示例代码:Github链接。通常,通过rootkit来实现对系统控制的主要途径之一就是通过对系统调用进行挂钩(Hook)来实现的,这是因为系统调用本身的重要性质决定的。系统调用提供用户程序与操作系统之间的接口,由操作系统内核来提供,运行在核心态,这意味着一旦掌握系统调用,我们就可以掌握操作系统的权限和功能,来实现用户态所无法完成的事情。而掌握系统调用通常是通过对系统调用挂钩来进行的,也就是通过实现一个自己的系统调用例程来替换操作系统中的系统调用例程。当然我们也可以通过其他的办法来实现rootkit对系统的控制,比如对内存直接进行操作,或者修改已有的内核模块,这些我们后面会讨论,今天的主题还是系统调用。往日的岁月总是美好的,在曾经的linux2.4系列内核中,我们可以轻易的获取系统调用表(sys_call_table),并对其进行修改,指向我们自己实现的系统调用历程,从而实现挂钩。(说句题外话,FreeBSD6.0当中的系统调用挂钩方法也是类似的,具体可以参考Joseph Kong的《Designing BSD Rookit》一书,在我学习rootkit过程中该书给我了很大的指导与启发。)extern void *sys_call_table[];......sys_call_table[__NR_syscall] = (our_sys_call_func);但是linux2.6版本以后,sys_call_table[]不再是全局变量了,无法通过简单的”extern” 就可以得到它了。当然,天无绝人之路,我们可以曲线救国。在novice同学文章中,他选择了使用暴力搜索内存空间法来获取sys_call_table,简洁有效。不过我们这次将介绍另外两种不同的办法来搞定sys_call_table。2.通过system.map获取系统调用表我介绍的第一种方法,也是我认为获取系统调用表的最最简单的方法,是通过system.map来定位系统调用表在内存中的位置。什么是是system.map?在我们利用它前,首先需要了解它。System.map,顾名思义,系统的映射,但具体映射的是什么东西?其实是内核符号及其所在内存地址两者的映射。通过地址,我们可以找到符号,也就是找到变量及函数;通过符号,我们也可以得知其所在地址。更多请参考该网站。在这里,我们需要通过符号来获取地址,我们已知的符号是sys_call_table,系统调用表,我们在system.map里便可以找到系统调用表所对应,也就是所在的内存地址。说了这么多,该如何具体操作呢。System.map位于/boot目录下,我们可以通过cat命令进行查看。(不同内核版本system.map的后缀不同,需要注意),由于内容太多,我们只展现部分内容。然后我们来找找系统调用表的内存地址究竟是多少。(注意每个机器的地址会不同,具体以自己的机器为准。我在github中提供的示例代码中sys_call_table的地址是我机器上的地址,如果要使用示例代码,需要根据本地情况修改sys_call_table的内存地址)OK,接下来的事情也就简单了,我先上代码,这里我们挂钩的是sys_mkdir系统调用:asmlinkage long (*real_mkdir)(const char __user *pathname,umode_t mode);asmlinkage long fake_mkdir(const char __user *pathname, umode_t mode){ printk("Arciryas:mkdir-%s ", pathname); return (*real_mkdir)(pathname, mode);}real_mkdir = (void *)sys_call_table[__NR_mkdir];sys_call_table[__NR_mkdir] = fake_mkdir;相信大家基本都能看懂,我这里解释下那个”__NR_mkdir”是怎么回事,参见该网址。这里的”__NR_xxxx”是unistd.h中定义的宏,代表着系统调用号,而系统调用号对应着系统调用表的相应的入口,具体参考 该网址。当然不能这样就完了,内存可不是你想修改就修改的,就像之前novice同学提到的,我们需要关闭写保护,因为之前提过我就不再赘述了。我直接上代码:static int lkm_init(void){ write_cr0(read_cr0() & (~0x10000)); real_mkdir = (void *)sys_call_table[__NR_mkdir]; sys_call_table[__NR_mkdir] = fake_mkdir; write_cr0(read_cr0() | 0x10000); printk("Arciryas:module loaded "); return 0;}static void lkm_exit(void){ write_cr0(read_cr0() & (~0x10000)); sys_call_table[__NR_mkdir] = real_mkdir; write_cr0(read_cr0() | 0x10000); printk("Arciryas:module removed ");}ok,我们的任务成功告一段落,makefile文件和上篇文章一样,我们现在就来make,insmod,然后创建一个新文件夹,看看我们的lkm有没有正常运作,如图:看来一切都在我们掌握之中!3.通过IDT(中断描述符表)获取系统调用表为什么我们要介绍不同的几种获取sys_call_table的办法?一招鲜没法吃遍天,就像novice在该系列第二篇文章中所说,暴力搜索内存空间法来获取sys_call_table存在被欺骗的可能,而我们刚才介绍的通过system.map的方法也有缺陷,要知道system.map对于内核来说并非必不可少的,如果没有system.map,我们该怎么做?于是接下来我们将要补充的是第三种方法:通过IDT(中断描述符表)获取系统调用表。首先,我们还是需要理解下什么是IDT,以及为什么通过IDT可以得到sys_call_table。中断描述符表(Interrupt Descriptor Table,IDT),其作用是将每个异常或中断向量分别与它们的处理过程联系起来,每一个向量在表中有相应的中断或异常处理程序的入口地址。当系统发生中断时,内核根据异常或中断向量来在IDT中选择对应的处理程序的入口地址,进而对中断或异常进行处理。然后是重点:linux中的系统调用,也是通过一个特殊的中断——0×80号中断来实现的(补充一句,linux的系统调用还可以通过sysenter方法进入,所以这里介绍的通过0×80中断获取sys_call_table方法也是有局限的):1.用户进程在执行系统调用前,先把系统调用名(实际上是系统调用号)、输入参数等放到寄存器上(EBX,ECX等寄存器)2.然后发出int 0×80指令,即触发128号中断3.系统暂停用户进程,根据128号中断找到中断服务程序system_call4.128号中断的中断服务程序system_call紧接着执行。在进行必要的处理后,统一调用 call sys_call_table(,eax,4)来调用sys_call_table表中的系统调用服务,eax存放的即时系统调用号;执行完毕后它又会把输出结果放到寄存器中。5.系统恢复用户进程,进程从寄存器中取到自己想要的东西,然后继续执行。ok,在这个过程中,我们发现了sys_call_table的出现。在具体操作中,我们应该如何来通过IDT来得到系统调用表呢,以下是我们所需要完成的程序的核心思路:1.利用sidt 指令,得到IDT2.在IDT中找到0×80号中断的中断服务程序的地址system_call3.从0×80号中断的中断服务程序system_call的地址开始搜索硬编码 ÿ?,这块硬编码的后面紧接着就是系统调用表的地址了,因为x86 call指令的二进制格式为ÿ?,而中断服务程序调用系统调用的语句是call sys_call_table(,eax,4)IDT和系统调用的关联,以及通过IDT获取sys_call_table的思路已经介绍完毕,接下来我们用代码来进一步说明:struct{unsigned short size;unsigned int addr;}__attribute__((packed)) idtr;struct{unsigned short offset_1; /*offset bits 0..15*/unsigned short selector; /*a code segment selector in GDT or LDT*/unsigned char zero; /*unused, set to 0*/unsigned char type_attr; /*type and attributes*/unsigned short offset_2; /*offset bits 16..31*/}__attribute__((packed)) idt;这两个结构体代表着IDTR和IDT表项,IDTR是中断描述符表寄存器(Interrupt Descriptor Table Register),用来定位IDT的位置,因为IDT表可以驻留在线性地址空间的任何地方,所以处理器专门有寄存器来储存IDT的位置,也就是IDTR寄存器。我们通过sidt指令加载IDTR寄存器的内容,然后储存到我们自己的这个结构体中,然后通过其找到IDT的位置所在,将IDT存到我们所设的结构体中,便于操作。unsigned long *find_sys_call_table(void){unsigned int sys_call_off;char *p;int i;unsigned int ret;asm("sidt %0":"=m"(idtr));printk("Arciryas:idt table-0x%x ", idtr.addr);memcpy(&idt, idtr.addr+8*0x80, sizeof(idt));sys_call_off = ((idt.offset_216) | idt.offset_1);p = sys_call_off;for(i=0; i100; i++){if(p[i]=='ÿ' && p[i+1]=='' && p[i+2]=='?')ret = *(unsigned int *)(p+i+3);}printk("Arciryas:sys_call_table-0x%x ", ret);return (unsigned long**)ret;}这是我们这次获取系统调用操作的核心代码,我将详细说明:asm("sidt %0":"=m"(idtr));这是使用内联汇编的办法调用sidt这一汇编指令,然后将加载出的中断描述符表寄存器中的内容存入我们之前准备好的的idtr结构体。memcpy(&idt, idtr.addr+8*0x80, sizeof(idt));这条语句的目的是获取0×80中断所对应的IDT中的表项。中断描述符表共256项,每项8字节,每项代表一种中断类型。所以我们要从IDR起始地址后的8*0×80位置拷贝一个IDT表项大小的数据,也就是0×80中断所对应的IDT中的表项,到我们之前准备好的结构体中。sys_call_off = ((idt.offset_216) | idt.offset_1);这条语句获取的是128号中断的中断服务程序system_call的地址,idt.offset_1和idt.offset_2代表什么参考我之前的注释。for(i=0; i100; i++){if(p[i]=='ÿ' && p[i+1]=='' && p[i+2]=='?')ret = *(unsigned int *)(p+i+3);}最后一击,搜索ÿ?,得到sys_call_table地址。具体挂钩操作在之前介绍system.map方法的内容中有叙述,在此不再赘述。代码完成了,看看实验结果如何:显然我们成功达成了目标。4.结语技多不压身,多掌握几种不同的系统调用挂钩法不但有助于开拓我们的视野,还可以让我们在不同的情况下选择更合适的办法来hook。当然系统调用的挂钩法远远不止我介绍的两种和novice同学所介绍的,其他还包括模拟callsys_call_table(,eax,4)方法,dump_stack法(本质和system.map方法接近),栈结构获取法等等。这些有兴趣的同学可以自行研究,本系列文章就不再进行讲述了,请大家期待接下来的文章!


(以上内容不代表本站观点。)
---------------------------------
本网站以及域名有仲裁协议。
本網站以及域名有仲裁協議。

2024-Mar-04 02:08pm
栏目列表