发明内容
为了弥补现有技术的不足,本发明提出了一种基于编译器的内核级return-oriented rootkits防御方法,以抵御最新内核级return-oriented rootkits的攻击,保护操作系统的安全。
为了实现上述目的,本发明的内核级return-oriented rootkits的防御方法,包括:
(1)基于编译器的指令转换步骤
(1.1)初始化返回索引ret_index为0;
(1.2)创建一个空的函数指针索引文件fpindex_file;
(1.3)在编译器后端的机器指令生成阶段,编译器取得操作系统中间表示IR指令集中的一条指令I;
(1.4)判断指令I的类型,如果I是直接call指令,则执行步骤(1.5);如果I是间接call指令,则执行步骤(1.6);如果I是间接jump指令,则执行步骤(1.7);如果I是ret指令,则执行步骤(1.8);否则执行步骤(1.9);
(1.5)对直接call指令I进行如下转换:
(1.5a)在指令I前插入指令“push $ret_index”,并将返回索引ret_index加1;
(1.5b)取得指令I的目标地址dst;
(1.5c)在指令I前插入指令“jmp dst”;
(1.5d)删除指令I;
(1.5e)跳转到步骤(1.10);
(1.6)对间接call指令I进行如下转换:
(1.6a)取得指令I的目标地址dst;
(1.6b)在指令I前插入指令“mov(dst),%reg”,其中reg表示寄存器;
(1.6c)在指令I前插入指令“cmp $functable_size,%reg”,其中functable_size表示函数指针表的最大容量;
(1.6d)在指令I前插入指令“jg err_handler”,其中err_handler为系统中定义的错误处理例程;
(1.6e)在指令I前插入指令“push $ret_index”,并将返回索引ret_index加1;
(1.6f)在指令I前插入指令“shl $0x3,%reg”;
(1.6g)在指令I前插入指令“jmp*FuncTable(%reg)”,其中FuncTable表示函数 指针表的基地址;
(1.6h)删除指令I;
(1.6i)跳转到步骤(1.10);
(1.7)对间接jump指令I进行如下转换:
(1.7a)取得指令I的目标地址dst;
(1.7b)在指令I前插入指令“mov(dst),%reg”;
(1.7c)在指令I前插入指令“cmp $functable_size,%reg”;
(1.7d)在指令I前插入指令“jg err_handler”;
(1.7e)在指令I前插入指令“shl $0x3,%reg”;
(1.7f)在指令I前插入指令“jmp*FuncTable(%reg)”;
(1.7g)删除指令I;
(1.7h)跳转到步骤(1.10);
(1.8)对ret指令I进行如下转换:
(1.8a)在指令I前插入指令“pop%reg”;
(1.8b)在指令I前插入指令“cmp $rettable_size,%reg”,其中rettable_size表示返回地址表的最大容量;
(1.8c)在指令I前插入指令“jg err_handler”;
(1.8d)在指令I前插入指令“shl $0x3,%reg”;
(1.8e)在指令I前插入指令“jmp*RetTable(%reg)”,其中RetTable表示返回地址表的基地址;
(1.8f)删除指令I;
(1.8g)跳转到步骤(1.10);
(1.9)遍历指令I的所有操作数,如果某个操作数与操作系统内核中的任意一个函数相关联,则为该关联函数分配一个唯一的函数指针索引,用分配的函数指针索引替换该操作数,并将分配的函数指针索引与其关联函数的映射关系记录到函数指针索引文件fpindex_file中;
(1.10)如果操作系统中间表示IR指令集中还有未处理的指令,返回步骤(1.3),开始下一条指令的处理;否则结束;
(2)函数指针表和返回地址表的构造步骤
(2.1)在系统内存空间为函数指针表分配基地址FuncTable,并指定函数指针表的最大容量为functable_size;
(2.2)在系统内存空间为返回地址表分配基地址RetTable,并指定返回地址表的最大容量为rettable_size;
(2.3)创建一个空的临时文件temp_file,并将完成指令转换后的操作系统镜像文件中的内容输出到临时文件temp_file;
(2.4)从临时文件temp_file中依次取得函数指针索引文件fpindex_file中记录的所有函数的入口地址,并将这些入口地址按照为它们所分配的函数指针索引值依次填入函数指针表的相应位置;
(2.5)从临时文件temp_file中依次取得系统中所有有效的返回地址,并将这些返回地址依照它们所对应的返回索引的顺序依次填入返回地址表的相应位置;
(2.6)将函数指针表设置为只读;
(2.7)将返回地址表设置为只读。
本发明与现有的技术相比,具有如下的有益效果:
1)本发明基于编译器技术,在操作系统内核中识别、定位和转换与控制数据相关的所有指令来实现控制数据索引机制。该机制为每个控制数据分配一个索引,这个索引指向收集了系统中所有有效跳转地址的表格的某个单元;通过指令转换,在进行程序跳转时,不直接使用控制数据,而是利用它所对应的跳转表格索引查找跳转表格,获得真正有效的跳转地址来间接执行。通过对跳转表格的保护,就提供了对所有控制数据的保护。本发明使得return-oriented rootkits无法完成实施攻击的第二步,即它需要改写某个控制数据来改变系统原有的执行流程,以达到攻击的目的。与现有技术相比,本发明能够成功抵御所有内核级return-oriented rootkits的攻击,保护操作系统内核免受侵害。
2)本发明仅在需要进行间接跳转时增加一次内存访问操作,即查询函数指针表或返回地址表的操作。它具有性能高效的优点,基于benchmark的性能测试结果表明,本发明所带来的性能损失小于5%。
3)本发明提供了对操作系统内核中所有控制数据的保护,它实际上保护了整个操作系统的控制流程,所以不仅能够抵御内核级return-oriented rootkits的攻击,而且还可以击败所有针对操作系统控制流程的攻击,为操作系统安全提供强有力的保障。
具体实施方式
参照图1,本发明包括基于编译器的指令转换以及函数指针表和返回地址表的构造两部分。其中基于编译器的指令转换主要包括参数初始化和与控制数据相关指令的转换,指令转换完成后进行函数指针表和返回地址表的构造。
一.基于编译器的指令转换
参照图2,本部分的具体实现如下:
步骤1,初始化返回索引ret_index为0。
本发明为每个返回地址分配一个唯一的返回索引ret_index,且ret_index从0开始计数,每分配一次加1。
步骤2,创建一个空的函数指针索引文件fpindex_file。
本发明通过执行操作系统的创建文件命令,比如fileopen来生成一个空的函数指针索引文件fpindex_file;该文件在进行函数指针索引的分配时使用,它存储了所有分配的函数指针索引与其关联函数的映射关系。
步骤3,编译器遍历操作系统中间表示IR指令集中的每条指令,将与控制数据相关的指令进行转换。
本发明是基于这样一种观测而提出的:无论哪种类型的return-oriented rootkits,它们要想实施攻击,必须改变系统原有的执行流程,跳转到攻击者所挑选的首个指令片段才能进行。如果能够对所有可能改变系统执行流程的地方进行保护,就可以从根本上抵御return-oriented rootkits的攻击,为此,必须对系统中所有的控制数据进行保护。
控制数据,即control data,是指计算机程序在执行到某个时刻被加载入CPU程序计数器中的数据,它决定了程序跳转时的执行路径。由于控制数据的特殊性,它们经常被攻击者利用来改变程序原有的执行流程,比如通过缓冲区溢出的手段对它们进行改写,转而去执行攻击者新注入的恶意代码或按照他们精心选定的次序来“滥用”原有 的代码片段,达到恶意攻击的目的。
本发明的核心思想是设计并实现了一种控制数据索引机制。该机制为每个控制数据分配一个索引,这个索引指向收集了系统中所有有效跳转地址的表格的某个单元;通过基于编译器的指令转换,操作系统在进行程序跳转时,不直接使用控制数据,而是利用为它分配的索引去查找跳转表格获得真正有效的跳转地址来间接进行。系统通过对跳转表格的保护,比如将它们设置为只读,就提供了对所有控制数据的保护。本发明使得return-oriented rootkits无法完成实施攻击的第二步,即它需要改写系统中的某个控制数据来改变系统原有的执行流程,从而达到防御的目的。
操作系统中有两种常用类型的控制数据,即函数指针和返回地址。为了将函数指针转换成索引格式,首先必须准确定位它们。由于在操作系统的机器码中,每个函数指针最终都会被某个间接call或间接jump指令调用,因此,可以利用编译器在程序编译的中间表示IR阶段识别所有的间接call和间接jump指令。从而,可以在这些指令里定位并将函数指针转换成索引格式,称为函数指针索引,在进行函数调用时再借助于该索引将目标地址重定向到某个跳转表格,即函数指针表中对应的函数入口地址。另外,对于每个间接call或间接jump指令,它的目标地址,即函数指针的值通常会被某个先前的指令,比如mov或lea指令,预加载到某个寄存器或内存单元里。对于此类触及函数指针的预加载指令,同样必须进行定位和格式转换。经过分析得知,在代码的中间表示阶段,这种预加载指令的某个操作数必定会是系统中的某个函数标号。因此,可以扩充编译器来识别它们,并且将它们转换成函数指针索引格式,后续当某个间接call或间接jump指令执行时,它的原始目标地址自然可以通过查询函数指针表来获得。
针对返回地址,则不能采取与函数指针一样的处理方式,因为返回地址的值是在系统运行时动态生成并且是变化无常的。具体来说,遵照系统中函数调用的流程,当执行call指令调用一个函数时,无论是直接还是间接调用,总会将一个返回地址压入系统栈中;而后系统调用完成后,再由ret指令将它从栈中弹出。由于每个返回地址必定指向它所对应的call指令的紧挨着的下一条指令,从而可以预先计算出系统中所有有效的返回地址,并且将它们存入某个跳转表格即返回地址表中。然后,借助于编译器在IR代码阶段识别所有与返回地址相关的指令,即call和ret指令,将返回地址替换成索引,即返回索引的格式。由于每个call指令实际上对应了一个唯一的返回地 址,基于call指令所在的位置为它分配一个唯一的索引值,从而避免了索引分配时的冲突。并且,不管返回地址的值如何变化,它总是由call指令产生,无论直接或间接调用,而后被ret指令“消费”。所以,只要对所有的call和ret指令进行了转换,就可以保证所有的返回地址都得到了转换和保护。
将函数指针和返回地址综合考虑,本发明需要对系统中所有的直接call指令、间接call指令、间接jump指令、ret指令以及操作数与操作系统内核中任意一个函数相关联的指令进行转换,详细的转换步骤如下:
(3a)在编译器后端的机器指令生成阶段,编译器取得操作系统中间表示IR指令集中的一条指令I。编译器是一个或一组计算机程序,它将用某种编程语言编写的源代码转换成另一种计算机语言,即目标语言,通常是二进制形式的目标代码。编译器包括前端和后端两部分,前端负责完成源代码预处理、词法分析、语法分析、语义分析、中间代码生成任务,后端负责完成中间代码分析、优化和机器代码生成工作。本发明选择在编译器后端机器代码生成阶段进行相关指令的识别和转换工作,这是为了避免与编译器中此阶段之前的机器代码优化操作产生冲突;
(3b)判断指令I的类型,如果I是直接call指令,则执行步骤(3c);如果I是间接call指令,则执行步骤(3d);如果I是间接jump指令,则执行步骤(3e);如果I是ret指令,则执行步骤(3f);否则执行步骤(3g)。在直接call指令中,目的操作数是一个绝对的跳转地址或相对于当前指令地址的偏移量;而在间接call或间接jump指令中,目的操作数则是系统中某个寄存器或内存单元,在该寄存器或内存单元中存储了指令的跳转地址;
(3c)对直接call指令I进行如下转换:
(3c1)在指令I前插入指令“push $ret_index”,并将返回索引ret_index加1;
(3c2)取得指令I的目标地址dst;
(3c3)在指令I前插入指令“jmp dst”;
(3c4)删除指令I;
(3c5)跳转到步骤(3h);
(3d)对间接call指令I进行如下转换:
(3d1)取得指令I的目标地址dst;
(3d2)在指令I前插入指令“mov(dst),%reg”,其中reg表示寄存器;该mov 指令是按照AMD64硬件平台汇编指令的格式书写的,源操作数放在前面,目的操作数放在后面,其他硬件平台的汇编指令格式类似;
(3d3)在指令I前插入指令“cmp $functable_size,%reg”,其中functable_size表示函数指针表的最大容量,在构造函数指针表时分配;该cmp指令是按照AMD64硬件平台汇编指令的格式书写的,源操作数放在前面,目的操作数放在后面,其他硬件平台的汇编指令格式类似;
(3d4)在指令I前插入指令“jg err_handler”,其中err_handler为系统中定义的错误处理例程;
上述步骤(3d3)-(3d4)是为了对函数指针索引的取值范围进行限定。如果索引取值超出了合理的范围,即如果函数指针索引的值大于函数指针表的最大容量functable_size,系统将跳转到错误处理例程err_handler,这是为了防止针对索引取值范围的溢出攻击;
(3d5)在指令I前插入指令“push $ret_index”,并将返回索引ret_index加1;
(3d6)在指令I前插入指令“shl $0x3,%reg”,该shl指令是按照AMD64硬件平台汇编指令的格式书写的,源操作数放在前面,目的操作数放在后面,其他硬件平台的汇编指令格式类似;该shl指令的作用是通过将寄存器reg里的函数指针索引值左移3位,即乘以8而转换成函数指针表的偏移,因为在AMD64平台下函数指针表的每个单元为8字节长;
(3d7)在指令I前插入指令“jmp*FuncTable(%reg)”,其中FuncTable表示函数指针表的基地址,在构造函数指针表时分配;
(3d8)删除指令I;
(3d9)跳转到步骤(3h);
(3e)对间接jump指令I进行如下转换:
(3e1)取得指令I的目标地址dst;
(3e2)在指令I前插入指令“mov(dst),%reg”,该mov指令是按照AMD64硬件平台汇编指令的格式书写的,源操作数放在前面,目的操作数放在后面,其他硬件平台的汇编指令格式类似;
(3e3)在指令I前插入指令“cmp $functable_size,%reg”,其中functable_size表示函数指针表的最大容量,在构造函数指针表时分配;该cmp指令是按照AMD64硬 件平台汇编指令的格式书写的,源操作数放在前面,目的操作数放在后面,其他硬件平台的汇编指令格式类似;
(3e4)在指令I前插入指令“jg err_handler”,其中err_handler为系统中定义的错误处理例程;
上述步骤(3e3)-(3e4)是为了对函数指针索引的取值范围进行限定。如果索引取值超出了合理的范围,即如果函数指针索引的值大于函数指针表的最大容量functable_size,系统将跳转到错误处理例程err_handler,这是为了防止针对索引取值范围的溢出攻击;
(3e5)在指令I前插入指令“shl $0x3,%reg”,该shl指令是按照AMD64硬件平台汇编指令的格式书写的,源操作数放在前面,目的操作数放在后面,其他硬件平台的汇编指令格式类似;该shl指令的作用是通过将寄存器reg里的函数指针索引值左移3位,即乘以8而转换成函数指针表的偏移,因为在AMD64平台下函数指针表的每个单元为8字节长;
(3e6)在指令I前插入指令“jmp*FuncTable(%reg)”,其中FuncTable表示函数指针表的基地址,在构造函数指针表时分配;
(3e7)删除指令I;
(3e8)跳转到步骤(3h);
(3f)对ret指令I进行如下转换:
(3f1)在指令I前插入指令“pop%reg”;
(3f2)在指令I前插入指令“cmp $rettable_size,%reg”,其中rettable_size表示返回地址表的最大容量,在构造返回地址表时分配;该cmp指令是按照AMD64硬件平台汇编指令的格式书写的,源操作数放在前面,目的操作数放在后面,其他硬件平台的汇编指令格式类似;
(3f3)在指令I前插入指令“jg err_handler”,其中err_handler为系统中定义的错误处理例程;
上述步骤(3f2)-(3f3)是为了对返回索引的取值范围进行限定。如果索引取值超出了合理的范围,即如果返回索引的值大于返回地址表的最大容量rettable_size,系统将跳转到错误处理例程err_handler,这是为了防止针对索引取值范围的溢出攻击;
(3f4)在指令I前插入指令“shl $0x3,%reg”,该shl指令是按照AMD64硬件 平台汇编指令的格式书写的,源操作数放在前面,目的操作数放在后面,其他硬件平台的汇编指令格式类似;该shl指令的作用是通过将寄存器reg里的返回索引值左移3位,即乘以8而转换成返回地址表的偏移,因为在AMD64平台下返回地址表的每个单元为8字节长;
(3f5)在指令I前插入指令“jmp*RetTable(%reg)”,其中RetTable表示返回地址表的基地址,在构造返回地址表时分配;
(3f6)删除指令I;
(3f7)跳转到步骤(3h);
(3g)遍历指令I的所有操作数,如果某个操作数与操作系统内核中的任意一个函数相关联,则为该关联函数分配一个唯一的函数指针索引,并用分配的函数指针索引替换该操作数,具体执行步骤如下:
(3g1)读取指令I的操作数个数n;
(3g2)如果n大于0,则执行步骤(3g3),否则结束;
(3g3)取出指令I中一个未处理过的操作数Operand,如果操作数Operand与操作系统内核中的任意一个函数func相关联,则查询函数指针索引文件fpindex_file,查看该文件中是否已经存在关联函数func的映射条目;
(3g4)如果fpindex_file文件中已经存在关联函数func的映射条目,则将条目中的函数指针索引fpindex取出,用fpindex替换操作数Operand;否则将fpindex_file文件中最大的函数指针索引值加1,作为新的函数指针索引fpindex_new分配给关联函数func,用fpindex_new替换操作数Operand,并将新分配的函数指针索引fpindex_new和其关联函数func的映射关系作为一个新条目记录到函数指针索引文件fpindex_file中;
(3g5)将n减1,返回步骤(3g2);
(3h)如果操作系统中间表示IR指令集中还有未处理的指令,返回步骤(3a),开始下一条指令的处理;否则结束。
二.函数指针表和返回地址表的构造
由于系统中有两种常用类型的控制数据,即函数指针和返回地址,所以存在两个跳转表格,分别为函数指针表和返回地址表。
参照图1,本部分的具体实现如下:
步骤A,通过调用操作系统内存分配函数,在系统内存空间为函数指针表分配基地址FuncTable,并指定函数指针表的最大容量为functable_size。
步骤B,通过调用操作系统内存分配函数,在系统内存空间为返回地址表分配基地址RetTable,并指定返回地址表的最大容量为rettable_size。
步骤C,通过执行操作系统的创建文件命令,比如fileopen创建一个空的临时文件temp_file,并将完成指令转换后的操作系统镜像文件中的内容输出到临时文件temp_file。
步骤D,从临时文件temp_file中依次取得函数指针索引文件fpindex_file中记录的所有函数的入口地址,并将这些入口地址按照为它们所分配的函数指针索引值依次填入函数指针表的相应位置。
步骤E,从临时文件temp_file中依次取得系统中所有有效的返回地址并将它们按顺序填入返回地址表。
由于每个有效的返回地址都指向紧挨着某个call指令后面的那条指令,即每个call指令对应了一个唯一的返回地址,所以在临时文件temp_file中参照call指令转换时所分配的返回索引值,就可以将返回索引与返回地址进行对应,并将这些返回地址按照返回索引的顺序依次填入返回地址表的相应位置。
步骤F,将函数指针表设置为只读。
在函数指针表里,存放着所有可能通过函数指针进行间接调用的函数的入口地址,即函数指针在系统运行过程中的具体取值;通过将函数指针表设置为只读,使得攻击者无法通过改写系统中的函数指针控制数据来改变操作系统的执行流程,达到抵御攻击的目的。
步骤G,将返回地址表设置为只读。
在返回地址表里存放着系统中所有合法的函数返回地址;通过将返回地址表设置为只读,使得攻击者无法通过改写系统中的返回地址控制数据来改变操作系统的执行流程,达到抵御攻击的目的。
本发明的性能效果可以通过以下实验进一步说明:
1)实验条件
将本发明实现到LLVM(Low Level Virtual Machine)编译器中。LLVM编译器是美国UIUC大学的一个开源项目,它的设计目标是用来替换GCC编译器,具有结构 清晰、模块化设计、可扩展性强、方便灵活等特性,目前已经被苹果公司商业化,市场上流行的的iPhone、iPad和iPod苹果产品中的系统软件就是由该编译器编译出来的。本发明利用LLVM编译器来完成FreeBSD 8.0操作系统内核指令的转换以及函数指针表和返回地址表的构造,对FreeBSD 8.0操作系统提供保护。硬件平台选用Dell公司的Optiplex 740 PC机,它的CPU是AMD64 X2 5200+,内存为2GB。
2)实验内容
选用benchmark测试工具LMbench分别对FreeBSD 8.0原系统内核和应用了本发明的FreeBSD 8.0新系统内核运行不同任务或操作时的性能进行测试,以得出本发明所带来的性能损失。测试的任务或操作包括空调用、整型加运算、浮点型除运算、上下文切换、文件删除、文件读、本地拷贝和内存写操作,共测试10次,取平均值,测试的结果如图3所示。
3)结果分析
从图3可以看出,本发明的最大性能损失是在进行上下文切换操作时的4.78%,而基本算术运算的性能损失几乎为0。总体上,本发明具有性能高效的优点,基于benchmark的性能测试结果表明,它所带来的性能损失小于5%。