每个Windows进程都由一个Active进程EPROCESS表示,此结构体除了包含许多与此进程相关的属性,还包含部分指向其他结构体的指针。如每个进程都存在一个或多个线程,每个线程又通过一个可执行线程ETHREAD结构体表示。EPROCESS结构体及其大多数相关的数据结构都存储在系统地址空间,唯一例外的是进程环境块PEB。因为结构体PEB一部分组件是需要用户模式访问的,故而它是系统结构群中为数不多驻留在用户空间的对象。
Windows加载器、堆管理器以及子系统DLLs中使用的数据都驻留在PEB。
TEB(ThreadEnvironmentBlock,线程环境块)位于用户地址空间。进程中的每个线程都有自己的一个TEB。一个进程的所有TEB都以堆栈方式存放在从0x7FFDE000处开始的线性内存中,每4KB为一个完整的TEB,不过该内存区域是向下扩展的。在用户模式下,当前线程的TEB位于独立的4KB段,可通过CPU的FS寄存器来访问该段,一般存储在[FS:0]。在用户态下,WinDbg中可用命令$thread取得TEB地址。
结构体PEB的定义在头文件winternl.h中给出:
typedefstruct_PEB{ BYTE Reserved1[2]; BeingDebugged; Reserved2[1]; Reserved3[2]; Ldr; BYTE BYTE PVOID PPEB_LDR_DATA PRTL_USER_PROCESS_PARAMETERSProcessParameters; BYTE Reserved4[104]; Reserved5[52]; PVOID PPS_POST_PROCESS_INIT_ROUTINEPostProcessInitRoutine; BYTE Reserved6[128]; Reserved7[1]; SessionId; PVOID ULONG }PEB,*PPEB;
从微软官方SDK文档给出的定义可知,大量的成员变量都隐藏了,无法知晓这些变量的具体含义,不过我们可以使用调试器打印出此结构体的详细信息,如图1所示。
由图1可知,PEB结构的偏移0xC处保存着指针Ldr,该指针指向PEB_LDR_DATA结构体,如图2所示。
该结构的后三个成员是指向LDR_MODULE链表结构中相应三条双向链表头的指针,分别是按照加载顺序、在内存中地址顺序和初始化顺序排列的模块信息结构的指针。双向链表指针结构LIST_ENTRY在双端队列中以一个成员节点形式存在,即这些结构体被嵌入在一个更大的结构体内,如图3所示。
为了更清楚的了解LDR_DATA_TABLE_ENTRY结构体,可以使用dt命令查看结构体成员,如图4所示。
从结构体定义可知,InMemoryOrderLinksLIST_ENTRY结构体相对于上述结构体地址偏移8字节位置。要注意的是,LIST_ENTRY结构体不同于自定义的双向链表,其相邻的节点并不是相邻结构体的首字节处,而是偏移一定位置。LDR_DATA_TABLE_ENTRY结构体中包含了进程地址空间中加载的所有模块元数据信息,如每个模块的基地址,且这些结构体链表中的第三个节点即是kernel32.dll库。下面我们用Windbg调试工具来验证这点。首先定位到TEB偏移0x30处,即FS:[0x30]地址处保存的一个指针,指向PEB,如图5所示。
然后再通过_PEB_LDR_DATA的InMemoryOrderModuleList成员获取_LIST_ENTRY结构,再由_LIST_ENTRY的Flink成员获取_LDR_DATA_TABLE_ENTRY结构,如图6所示。
通过上述命令即可列出偏移地址0x341ed0位置下的所有_LIST_ENTRY结构体中Flink的内存地址值。此时,我们可以进一步打印出第二个节点对应的大结构体_LDR_DATA_TABLE_ENTRY对应的元数据信息,如图7所示。更简单明了的确认方式,可直接输入如图8所示的命令。
从上图给出的kernel32.dll地址可知,前面给出的逻辑分析正确。即给定一个TEB地址,我们可以获取到PEB地址,而PEB结构中包含一个PEB_LDR_DATA结构体,此结构体中的InMemoryOrderModuleList成员对应的Flink成员又指向了_LDR_DATA_TABLE_ENTRY结构体,此结构体链表的第三个节点即存放了kernel32.dll库文件。整个调用过程可如图9所示进行描述。
有了上面的基础,通过下面的代码即可实现DLL库文件的遍历。
#includewindows.h #defineCONTAINING_RECORD(address,type,field)((type*)(/ (PCHAR)(address)-/ (ULONG_PTR)(((type*)0)-field))) /*其中结构体_PEB_LDR_DATA、_LDR_DATA_TABLE_ENTRY、_PEB自行定义*/ voidmain(void) { PLDR_DATA_TABLE_ENTRYpLdrDataEntry=NULL; PLIST_ENTRYpListEntryStart=NULL,pListEntryEnd=NULL; PPEB_LDR_DATApPebLdrData=NULL; PPEBpPeb=NULL; //任意加载一个DLL文件,获取当前进程的PEB结构体指针地址 LoadLibrary(blackbox.dll); __asm { //通过fs:[30h]获取当前进程的_PEB结构 moveax,dwordptrfs:[30h]; movpPeb,eax } //通过_PEB的Ldr成员获取_PEB_LDR_DATA结构 pPebLdrData=pPeb-Ldr; //通过_PEB_LDR_DATA的InMemoryOrderModuleList成员获取_LIST_ENTRY结构 pListEntryStart=pListEntryEnd=pPebLdrData-InMemoryOrderModuleList.Flink; //查找所有已载入到内存中的模块 while(pListEntryStart!=pListEntryEnd){ //通过_LIST_ENTRY的Flink成员获取_LDR_DATA_TABLE_ENTRY结构 pLdrDataEntry= (PLDR_DATA_TABLE_ENTRY)CONTAINING_RECORD(pListEntryStart,LDR_DATA_TAB LE_ENTRY,InMemoryOrderLinks); //输出_LDR_DATA_TABLE_ENTRY的BaseDllName或FullDllName成员信息 printf(%x%s/n,pLdrDataEntry-BaseDllName.DllBase,BaseDllName.Buffer); pListEntryStart=pListEntryStart-Flink; } }
通过分析PEB和TEB结构体,我们可以利用PEB结构体中的成员变量实现进程中特定DLL模块的隐藏,即从PEB的Ldr链中摘除指定的DLL模块即可。更多的Rootkit是通过上述方法获取到kernel32.dll基址,之后搜索Kernel32.dll的导出表地址,接下来就是hacker构造自己的程序导入表,实现恶意Rookit的加载等操作。