转自:http://blog.csdn.net/gaa_ra/article/details/6989406
好吧,我胡汉三肥来了..(确实肥了- -),几个月没发什么文章.其实.我也只是一个新丁~欢迎大家交流,高手勿喷,谢谢合作^_^
对于SSDT,不知道的同学请自己百度,判断出SSDT被HOOK之后,如何恢复成原始服务地址?主要方法有两个,简单来说,一个Ring0,一个Ring3的方法(可能分类不够准确),我今天除了介绍方法之外,主要还是讲讲里面的原理吧,原理神马的才是最吸引人的对吧.
首先科普一下基础,SSDT,就是System Service Descriptor Table,在内核中导出符号为KeServiceDescriptorTable.对于Ring3下的一些API,最终会对应于Ntdll.dll里一个Ntxxx的函数,例如CreateFile,最终调用到Ntdll.dll里的NtCreateFile这个函数,NtCreateFile最终将系统服务号放入EAX,然后CALL系统的7FFE0300处存放的一个地址(后面细说),进入到内核当中,从Ring3->Ring0,最终在Ring0当中通过传入的EAX得到对应的同名系统服务的内核地址.这样就完成了一次系统服务的调用.下面的图是ntdll!NtCreateFile的反汇编片段:
可以看到,NtCreateFile传入的服务号是25h,对于7FFE0300h处,实际上是UserShareData!SystemCallStub,这是一个函数指针,里面存放的值可能是KiIntSystemCall或者是KiFastSystemCall的地址,在我的Xp下,存放的是KiFastSystemCall的地址,之后的流程是:KiFastSystemCall->sysenter->KiFastCallEntry->查找SSDT,call过去.有兴趣的同学可以在WRK当中搜索相关的函数去走一下流程.
说了那么多不相关的话- -,好吧,这个SSDT的表这么理解(如果你硬是要纠结于细节的话,是KeServiceDescriptorTable->Base这个表),就是一个存放着函数指针的表,而这些函数指针指向的就是系统重要的服务例程.这个表的重要性很高,如果说有恶意的程序对这个表进行了HOOK,修改了服务例程地址为自己提供的假服务例程,那么就可以干一些坏事了.我们可以很容易的得到SSDT的具体内容,如何知道SSDT有没有被Hook,一个简单的方法就是判断SSDT中的函数指针是否是在内核(ntoskrnl.exe或ntkrnlpa.exe)的内存镜像地址范围内,这个只是单纯的检测,具体恢复需要知道原始的服务例程的原始地址.我一开始以自己很傻很天真的思路想,内核获取函数地址不就是用MmGetSystemRoutineAddress么,那么这样就可以得到原始地址了嘛,结果实现了一下发现,你妹的MmGetSystemRoutineAddress只对导出的函数(输出目录当中的函数有用),我当时就凌乱了..痛苦的把脸转向一边...
对于KeServiceDescriptorTable是在KiInitSystem中初始化,代码如下:
具体是通过KiServiceTable初始化的,KiServiceTable的具体内容通过反汇编内核,我这里是ntkrnlpa.exe,如下:
可以看到,KiServiceTable是一个数组,具体内容已经硬编码到内核当中,好吧,聪明的同学已经懂了,硬盘上的内核文件有KiServiceTable的一个副本.当内核文件加载到内存的时候,我们在实际Hook的过程中修改的只是内存当中的数据,就是内存中的值,文件上的值是没有变化的,说点题外话,驱动的加载基址和EXE的加载不太相同,驱动是整个文件加载入内存当中,所以是允许驱动加载后删除驱动文件的.下面我就讲一讲得到内核中KiServiceTable文件偏移的两种方法,具体的方法当然已经被各个前辈说烂了,但是很少有涉及原理的,好吧,原理说白了其实也就是PE文件结构的解析,我会着重讲一讲这部分,把这部分(尽力)讲清楚,也算是自己学习的总结吧,欢迎兄弟们交流^_^
方法一,直接在Ring0里面完成内存映像和磁盘偏移的换算.在Ring0当中,你可以随意的读高过0x80000000-1的地址(同学们主要要减一),但是由于内核的exe文件(实质上是驱动)也能够在Ring3下打开,所以在Ring3下也是可以获取到KiServiceTable的文件偏移,但是Ring3下无法读高过2G的内存,所以Ring3下用的并不是内存映射与磁盘偏移的换算,而是另外的一种方法,后面再说哈.
下面我以ntkrnlpa.exe进行讲解(ntoskrnl.exe都是一样,你们#define ntkrnlpa.exe ntoskrnl.exe就可以了^^).大体的流程如下:
1.获取ntkrnlpa.exe的内存加载基址
2.获取KeServiceDescriptorTable>Base的RVA(就是VA - ImageBase),VA,虚拟地址,RVA相对虚拟地址,ImageBase镜像基址(这里是内存加载基址),不了解概念的的同学请百度或google,KeServiceDescriptorTable>Base也就是KiServiceTable,我们现在有的就是KiServiceTable的RVA
3.通过KiServiceTable的RVA判断KiServiceTable位于哪个Section(区块)当中,区块也可以叫做段,什么代码段(.text),数据段(.data)
4.知道在哪个Section之后计算对于Section起始处的偏移,之后就可以顺利换算为磁盘文件偏移
看看下面几个图,又要自己画图,伤不起啊
Section在第一个图里面的体现就是代码段,重要数据段,数据段等等,从第一个图可以看到内存映像和磁盘文件PE文件布局的一点不同,主要体现在从Section开始的偏移计算的不同,因此需要这样的一个换算,内存映像和磁盘文件在Section部分的对齐方式是不一样的,磁盘文件Section以0x200进行对齐,而内存中试0x1000对齐.当我们获取了内核加载基址之后,就可以对其进行PE文件解析,具体用到的结构在ntimage.h文件当中都有定义.具体的代码实现如下
ULONG
GetRawOffset(
IN PVOID ImageBase,
IN ULONG TableRVA,
OUT PULONG FileImageBase
)
{
PIMAGE_DOS_HEADER imageDosHeader = NULL;
PIMAGE_NT_HEADERS imageNtHeader = NULL;
PIMAGE_FILE_HEADER imageFileHeader = NULL;
PIMAGE_SECTION_HEADER imageSectionHeader = NULL;
PIMAGE_OPTIONAL_HEADER imageOptionalHeader = NULL;
ULONG sectionNum = 0;
ULONG rawOffset = 0;
ULONG i;
PAGED_CODE();
imageDosHeader = (PIMAGE_DOS_HEADER)ImageBase;
imageNtHeader = (PIMAGE_NT_HEADERS)(imageDosHeader->e_lfanew + (ULONG)ImageBase);
imageFileHeader = &imageNtHeader->FileHeader;
imageOptionalHeader = &imageNtHeader->OptionalHeader;
imageSectionHeader = (PIMAGE_SECTION_HEADER)((PUCHAR)imageNtHeader + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) + imageFileHeader->SizeOfOptionalHeader);
*FileImageBase = imageOptionalHeader->ImageBase;
sectionNum = imageFileHeader->NumberOfSections;
#ifdef _RING0HOOK_DEBUG
KdPrint(("Number of Section: %d\n", sectionNum));
KdPrint(("imageSectionHeader->Name: %s\n", imageSectionHeader->Name));
#endif
for (i = 0; i < sectionNum; i++, imageSectionHeader++)
{
if (TableRVA > imageSectionHeader->VirtualAddress &&
TableRVA < imageSectionHeader->VirtualAddress + imageSectionHeader->SizeOfRawData)
{
rawOffset = TableRVA - imageSectionHeader->VirtualAddress + imageSectionHeader->PointerToRawData;
return rawOffset;
}
}
return 0;
}
注意到,有一个FileImageBase,这个是在IMAGE_OPTIONAL_HEADER里面的ImageBase成员,这个记录的是PE文件的默认加载基址,在后面计算系统服务例程真实地址时,需要使用这个值进行修正,举个例子帮助理解,假设NtCreateFile这个服务例程机器码在ntkrnlpa.exe内存映像中的RVA是0x12345开始的,那么KiServiceTable中,另外,我的FileImageBase值是0x00400000,那么NtCreateFile这个函数硬编码的地址是0x00412345( = 0x00400000 + 0x12345),如果此时我们的内核加载基址是0x80000000,那么这个服务例程在内存当中的地址就是0x80012345,因此,我们最后获取的KiServiceTable进行修正的时候,我们要把一个服务例程的硬编码地址减去FileImageBase然后再加上内核的加载基址,才能得到内核当中系统服务的原始地址.基本代码里面可以看到PE文件解析的过程,也就是从PE各种头部里面获取到相应的信息.而imageSectionHeader->PointerToRawData是区块在磁盘文件当中相对于文件起始的偏移.有了大概的思路,对于细节还不了解的同学可以参考一下加密与解密第三版的PE文件一章,会感觉清晰很多的.我再重复一次,PE头部的结构体定义都在ntimage.h文件当中可以找到,你们懂的.
对于第二种方法,就是我说的在Ring0下得到KiServiceTable的,我自己没有实现,参考资料大家可以看这篇文章 http://blog.csdn.net/iiprogram/article/details/677576 ,如果我没有猜错的话,这个空间是Combojiang大牛的空间,此牛在pediy上有一个rootkit专题,那是相当的nice啊,口水,同学们你们又懂的.文章里面的代码编译的时候可能会有点小小问题,同学们自己改呗,嘿嘿.
主要思路是把内核用LoadLibraryEx在Ring3中再次加载到内存当中,但是使用DONT_RESOLVE_DLL_REFERENCES参数,让系统不要进行重定位工作.之后读取IMAGE_BASE_RELOCATION进行判断,找到KeServiceDescriptorTable的位置,从而定位到KiServiceTable.对于一个dll,我们前面讲过,PE文件的可选头部当中有一个ImageBase的成员,这个记录的是PE文件在加载入内存的时候理想的基址,但是,有时候这个需求不一定能够得到满足,那么此时就需要进行重定位工作,比如一些数据的偏移,我编译的时候已经硬编码进去了,现在你加载的基址跟我编译的时候使用的基址不一样,就要对他们进行修改.对于这个问题,Ms在PE结构中引入了基址重定位目录,虽然他的具体内容在.reloc区块当中,但是在可选头部的数据目录也有相关内容,具体结构如下
//
// Based relocation format.
//
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// TYPE_OFFSET TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
//
// 高4位和低12位,注意高高低低原则
//
typedef struct _TYPE_OFFSET{
WORD offset:12;
WORD type:4;
} TYPE_OFFSET, *PTYPE_OFFSET;
//
// Based relocation types.
//
#define IMAGE_REL_BASED_ABSOLUTE 0
#define IMAGE_REL_BASED_HIGH 1
#define IMAGE_REL_BASED_LOW 2
#define IMAGE_REL_BASED_HIGHLOW 3
#define IMAGE_REL_BASED_HIGHADJ 4
#define IMAGE_REL_BASED_MIPS_JMPADDR 5
#define IMAGE_REL_BASED_MIPS_JMPADDR16 9
#define IMAGE_REL_BASED_IA64_IMM64 9
#define IMAGE_REL_BASED_DIR64 10
在IMAGE_BASE_RELOCATION结构后面,TYPE_OFFSET被注释掉了,这指的是,.reloc区块除了开头的8字节,接下去的都是TYPE_OFFSET结构.这个结构通过offset指明了内存映像中需要重定位的数据地址,而type指明了重定位的类型,我们这里用到的类型是IMAGE_REL_BASED_HIGHLOW,表明指向的整个地址都需要重定位.我们前面说过,KeServiceDescriptorTable->Base是通过KiServiceTable进行初始化的,KeServiceDescriptorTable是内核的一个导出变量,那么对于这样的一个变量,当加载基址改变时,也是要进行重定向操作的.看个图
圈了红圈的就是机器码,可以看到KeServiceDescriptorTable(也是KeServiceDescriptorTable->Base,第一个数据成员)的硬编码地址为0x004846E0,这个是以ImageBase为加载地址的硬编码,现在我们现在所知道的情况:1,KeServiceDescriptorTable需要被重定向;2.KeServiceDescriptorTable的RVA可以得到(他是通过ImageBase硬编码,减去ImageBase就得到了KeServiceDescriptorTable的RVA);3.我们可以获取KeServiceDescriptorTable的RVA.4,KiServiceTable位于KeServiceDescriptorTable地址之后(如上图).
另外还有一点要说明的是,在.reloc区块中,可能存在有多个IMAGE_BASE_RELOCATION结构,因为基址重定位数据的组织方式是类似页分割的方法,所以可能有多个重定位块串接,一个块的大小是4KB,简单的计算,(4096-8)/2 = 2044,一个重定位块可以重定位2044个数据(理论值).在处理的时候我们需要一个一个重定位块的过去定位KeServiceDescriptorTable.
一点题外话,KeServiceDescriptorTable的获取,在内核当中可以还可以用MmGetSystemRoutineAddress获得,而当再Ring3下单作dll加载之后,可以用GetProcAddress获得,在内核的导出目录当中可以看到KeServiceDescriptor.
综合起来,我们得到的思路是,把内核文件单作dll加载进内存,设置DONT_RESOLVE_DLL_REFERENCES参数,保证系统不帮我们进行重定向处理,之后获取到KeServiceDescriptorTable的RVA,之后我们读取.reloc区块,手动处理重定向数据,直到找到对KeServiceDescriptorTable的重定向处理(通过跟我们得到的RVA比较,如果相等就是对KeServiceDescriptorTable的重定向处理),按照上图,我们得到KeServiceDescriptorTable的地址(我指的是需要重定向的数据的地址)是0x005C9DCE,此时我们判断0x005C9DCC(即0x005C9DCE - 2)是否为0x05c7(注意高高低低原则),如果是我们可以确定,我们现在是在mov ds:_KeServiceDescriptorTable, offset _KiServiceTable的指令当中,0x005C9DD2(即0x005C9DCE + 4)就是我们需要定位的KiServiceTable地址,至此KiServiceTable地址已经获取到.之后的处理与Ring0相似,也需要利用ImageBase进行修正.对于Ring3下获取内核加载基址可以使用为文档化的函数NtQuerySystemInformation,具体参考前面连接的代码(重复一下 http://blog.csdn.net/iiprogram/article/details/677576 ),看了这部分再回头看代码,应该会理解的比较快的^_^
说了这么久,可能有一些地方我没讲清楚,因为涉及到偏移啊地址转换啊有时候确实挺混乱的,有问题可以说出来大家研究研究,希望这篇文章能够对你有所帮助了,实现代码一搜一大堆,而像我这么蛋疼试图讲清楚原理的可能比较少吧,哈~
循惯例,末尾的吐槽反省: 唉,哥之前做了一件比较愚蠢的事,十分后悔,但是,有些事情发生了就发生了,再怎么念念不舍也没办法回到过去.只能坦然了,确实,每个人都有每个人的生活啊亲-___- .To someone: sorry, and thank you.