2009 年第 12 期 前置知识:VC 关键词:编程、PE 结构、驱动开发 SSDT 与SSDT Shadow 完全解析(一) 文/图wofeiwo[党壮] && lrlr[武云龙] 为了避免应用程序访问操作系统或者修改操作系统,Windows 使用两种处理器访问模 式:用户模式和内核模式.用户程序代码运行在用户模式(低2G 内存) ,操作系统代码(主 要是系统服务和设备驱动程序)运行在内核模式(高2G 内存) .用户模式只能访问低 2G 的 用户虚拟内存和部分 CPU 指令,而内核模式可以访问所有 4G 虚拟内存和所有 CPU 指令;用 户模式的进程独享自己的虚拟内存, 进程与进程之间互不干扰, 而内核模式共享高 2G 空间, 因此当我们自己写的驱动存在 BUG 时,就可能导致整个操作系统崩溃. 下面以 ReadFile 为例来说明用户 API 是如何调用内核 API 的. 1)kenel32.dll 中的 ReadFile 调用 ntdll.dll 中的 NtReadFile; 2)ntdll.dll 中的 NtReadFile 提供函数服务号; 3)系统函数调度程序 KiSystemService 根据服务号,索引到 ntoskrnl.exe 的NtReadFile 地址.整个流程如图 1 所示. 图12009 年第 12 期 系统服务调度表 SSDT 及SSDT Shadow 系统服务:由操作系统提供的一组函数(内核函数) ,API 可以间接或者直接调用系统 服务.操作系统以动态链接库(DLL)的形式提供 API. SSDT:系统服务调度表(System Service Dispatch Table) ,该表可以基于系统服务 编号进行索引,来定位函数内存地址. SSPT:系统服务参数表(System Service Parameter Table) ,指定系统服务函数的 参数字节数. 系统有 2 个SSDT 表, 一个是 KeServiceDescriptorTable(ntoskrnl.exe 导出), 一个是 KeServieDescriptorTableShadow(ntoskrnl.exe 未导出).两者的区别是,KeServiceDescriptorTable 仅有ntoskrnel.exe 中的函数一项,KeServieDescriptorTableShadow 包含了 ntoskrnel.exe 以及 win32k.sys 中包含的函数. 一般的 ntdll.dll 中的 Native API 的函数地址由 KeServiceDescriptorTable 分派, gdi.dll、user.dll 的内核 API 调用服务地址由 KeServieDescriptorTableShadow 分派. 1)KeServiceDescriptorTable 是内核导出的一张表,该表含有一个指针指向 SSDT 中 包含 Ntoskrnl.exe 实现的核心服务, 还包含一个指针指向 SSPT. KeServiceDescriptorTable 结构如下: typedef struct _ServiceDescriptorEntry { unsigned int *ServiceTableBase; //SSDT 基址 unsigned int *ServiceCounterTableBase; //SSDT 中服务被调用次数的计数器 unsigned int NumberOfServices; //SSDT 服务个数 unsigned char *ParamTableBase; //SSPT 基址 }SSDT, *PSSDT; 下面是在 windbg 中进行的实验结果: lkd> dd KeServiceDescriptorTable //导出表 80563520 804e58b0 00000000 0000011c 805120cc 80563530 00000000 00000000 00000000 00000000 80563540 00000000 00000000 00000000 00000000 80563550 00000000 00000000 00000000 00000000 80563560 00000002 00002710 bf80c339 00000000 80563570 baecda80 f753c4a0 8a09655c 807120c0 80563580 00000000 00000000 ffea8ad6 ffffffff 80563590 52841216 01ca0418 00000000 00000000 lkd> dd 804e58b0 //SSDT 基址 804e58b0 80591bfb 80585358 805e1f35 805dbc4a 804e58c0 805e1fbc 80640ce4 80642e75 80642ebe 804e58d0 805835aa 80650be3 806404a3 805e1787 804e58e0 806387ba 80586fa3 805e08e8 8062f462 804e58f0 805d9781 80571edd 805e8258 805e939e 804e5900 804e5ec4 80650bcf 805cd537 804ed822 lkd> dd 805120cc //SSPT 基址 2009 年第 12 期805120cc 2c2c2018 44402c40 1818080c 0c040408 805120dc 08081810 0808040c 080c0404 2004040c 805120ec 140c1008 0c102c0c 10201c0c 20141038 805120fc 141c2424 34102010 080c0814 04040404 8051210c 0428080c 1808181c 1808180c 040c080c 8051211c 100c0010 10080828 0c08041c 00081004 8051212c 0c080408 10040828 0c0c0404 28240428 8051213c 0c0c0c30 0c0c0c18 0c10300c 0c0c0c10 2)KeServiceDescriptorTableShadow 是内核未导出的另一张表,包含 Ntoskrnel.exe 和win32k.sys 服务函数.某些网游通过挂钩按键相关函数(NtUserSendInput)防止模拟按 键、(NtUserFindWindowEx ) 防止搜索 窗口、Anti_Virus 通过挂钩窗口相关的函数(NtUserPostMessage 、 NtUserQueryWindow ) 来防止被关闭.KeServiceDescriptorTableShadow 实际上是SSDT 结构数组,也就是KeServiceDescriptorTableShadow 是一组系统描述表.Windows XP SP3 下组数是 4.在Windows XP 系统下,KeServiceDescriptorTableShadow 表位于 KeServiceDescriptorTable 表上方,偏移 0x40 处.下面是 windbg 中进行的实验结果: lkd> dd KeServiceDescriptorTableShadow 805634e0 804e58b0 00000000 0000011c 805120cc //SSDT 表?Ntoskrnel.exe 805634f0 bf99a000 00000000 0000029b bf99ad10 //SSDT Shdow 表?Win32k.sys 80563500 00000000 00000000 00000000 00000000 80563510 00000000 00000000 00000000 00000000 80563520 804e58b0 00000000 0000011c 805120cc //KeServiceDescriptorTable 表80563530 00000000 00000000 00000000 00000000 80563540 00000000 00000000 00000000 00000000 80563550 00000000 00000000 00000000 00000000 由于 KeServiceDescriptorTableShadow 表属于未导出,因此我们需要定位地址.定位 未导出函数和结构的思想就是利用已导出函数和结构, 暴力搜索内存空间. 方法一般有四种, 一是依据KeServiceDescriptorTable 的地址和两者之间的偏移;二是搜索KeAddSystemServiceTable 导出函数;三是 搜索线程的 ServiceTable 指向;四是 MJ 提出的搜 索有效内存地址. SSDT HOOK SSDT 是根据函数服务号索引地址的,函数地址:KeServiceDescriptorTable->ServiceTableBase[ 服务号](或者(ULONG)KeServiceDescriptorTable->ServiceTableBase+服务号*4) ,因此最终我们都是通过 SSDT 依据函数服务号来索引函数地址的. SSDT 中有些函数是 Ntoskrnl.exe 导出的函数,有些是未导出函数,可以用 LoadPE 查看Ntoskrnl.exe 的导出表查找导出函数. 2009 年第 12 期 导出函数服务号可以直接利用*(PULONG)((ULONG)Zw 函数+1)即可取得服务号,未导 出函数服务号则通过解析 ntdll.dll 的导出表 EAT (因为 SSDT 表中的函数均在 ntdll 中导出) , 详细代码如下: //得到 ntoskrnl.exe SSDT 导出函数服务号 ULONG GetServiceId( PCWSTR FunctionName ) //PCWSTR 常量指针,指向 16 位UNICODE { UNICODE_STRING UnicodeFunctionName; ULONG address; ULONG ServiceId; RtlInitUnicodeString( &UnicodeFunctionName, FunctionName ); //MmGetSystemRoutineAddress 函数是从 Ntoskrnl.exe 和HAL 中查找导出函数地址 address = (ULONG)MmGetSystemRoutineAddress( &UnicodeFunctionName ); //打印导出函数地址 KdPrint(("[GetServiceId] address:0x%x\n",address)); ServiceId = *(PSHORT)(address + 1); //打印导出函数服务号 KdPrint(("[GetServiceId] ServiceId:0x%x\n",ServiceId)); return ServiceId; } 函数利用方法为: //只能获取导出函数服务号 GetServiceId(L"ZwCreateFile") *函数名:GetFunctionId *参数: [IN] PUNICODE_STRING DllName, [IN] char* FunctionName FunctionName *功能描述:解析 ntdll 的导出表 EAT 获得 SSDT 函数服务号 *返回值:ULONG ServiceId *原理: R0 下没有 LoadLibrary 函数,利用 ZwCreateFile 打开文件,利用 ZwCreateSection 创建区段,利用 ZwMapViewOfSection 映射区段到当前进程的虚拟内存, 定位PE Header 地址,定位第一个数据目录,到EAT 导出表,搜索函数名,定位函数地址 ULONG GetFunctionId( PUNICODE_STRING DllName, char* FunctionName ) { NTSTATUS ntstatus; HANDLE hFile = NULL, hSection = NULL ; OBJECT_ATTRIBUTES object_attributes; 2009 年第 12 期IO_STATUS_BLOCK io_status = {0}; PVOID baseaddress = NULL; SIZE_T size = 0; //模块基址 PVOID ModuleAddress = NULL; //偏移量 ULONG dwOffset = 0; PIMAGE_DOS_HEADER dos = NULL; PIMAGE_NT_HEADERS nt = NULL; PIMAGE_DATA_DIRECTORY expdir = NULL; PIMAGE_EXPORT_DIRECTORY exports = NULL; ULONG addr; PULONG functions; PSHORT ordinals; PULONG names; ULONG max_name; ULONG max_func; ULONG i; ULONG pFunctionAddress; ULONG Size; //函数返回的服务号 ULONG ServiceId; //初始化 OBJECT_ATTRIBUTES 结构 InitializeObjectAttributes( &object_attributes, DllName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); //打开文件 ntstatus = ZwCreateFile( &hFile, FILE_EXECUTE | SYNCHRONIZE, &object_attributes, &io_status, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN, FILE_NON_DIRECTORY_FILE | FILE_RANDOM_ACCESS | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); 2009 年第 12 期if( !NT_SUCCESS( ntstatus )) { KdPrint(("[GetFunctionAddress] error0\n")); KdPrint(("[GetFunctionAddress] ntstatus = 0x%x\n", ntstatus)); return 0; } //创建区段 InitializeObjectAttributes( &object_attributes, NULL, OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE, NULL, NULL); ntstatus = ZwCreateSection( &hSection, SECTION_ALL_ACCESS, &object_attributes,0, PAGE_EXECUTE, SEC_IMAGE, hFile); if( !NT_SUCCESS( ntstatus )) { KdPrint(("[GetFunctionAddress] error1\n")); KdPrint(("[GetFunctionAddress] ntstatus = 0x%x\n", ntstatus)); return 0; } //映射区段到进程虚拟空间 ntstatus = ZwMapViewOfSection( hSection, NtCurrentProcess(), //ntddk.h 定义的宏用来获取当前进程句柄 &baseaddress, 0,1000,0,&size,(SECTION_INHERIT)1,MEM_TOP_DOWN,PAGE_READWRITE); if( !NT_SUCCESS( ntstatus )) { KdPrint(("[GetFunctionAddress] error2\n")); KdPrint(("[GetFunctionAddress] ntstatus = 0x%x\n", ntstatus)); return 0; } ZwClose( hFile ); //得到模块基址 dwOffset = ( ULONG )baseaddress; //验证基址 KdPrint(("[GetFunctionAddress] BaseAddress:0x%x\n", dwOffset)); //DOS 头部 2009 年第 12 期dos =(PIMAGE_DOS_HEADER) baseaddress; //PE 文件头 nt =(PIMAGE_NT_HEADERS)((ULONG) baseaddress + dos->e_lfanew); //数据目录 expdir = (PIMAGE_DATA_DIRECTORY)(nt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT); addr = expdir->VirtualAddress; //数据块起始 RVA Size = expdir->Size;//数据块长度 //导出表 exports =(PIMAGE_EXPORT_DIRECTORY)((ULONG) baseaddress + addr); //导出表的三个数组指针 functions =(PULONG)((ULONG) baseaddress + exports->AddressOfFunctions); ordinals =(PSHORT)((ULONG) baseaddress + exports->AddressOfNameOrdinals); names =(PULONG)((ULONG) baseaddress + exports->AddressOfNames); max_name =exports->NumberOfNames; max_func =exports->NumberOfFunctions; for (i = 0; i < max_name; i++) { ULONG ord = ordinals[i]; if(i >= max_name || ord >= max_func) { return 0; } if (functions[ord] < addr || functions[ord] >= addr + Size) { if (strcmp((PCHAR) baseaddress + names[i], FunctionName) == 0) { pFunctionAddress =(ULONG)((ULONG) baseaddress + functions[ord]); break; } } } KdPrint(("[GetFunctionAddress] %s:0x%x\n",FunctionName, pFunctionAddress)); ServiceId = *(PSHORT)(pFunctionAddress + 1); //打印导出函数服务号 KdPrint(("[GetServiceId] ServiceId:0x%x\n",ServiceId)); //卸载区段,释放内存 ZwUnmapViewOfSection( NtCurrentProcess(), baseaddress); ZwClose( hSection); return ServiceId; } 函数利用如下: 2009 年第 12 期//所有 SSDT 表函数均可以获取服务号,因为 SSDT 所有函数均为 ntdll.dll 导出函数 UNICODE_STRING dllName ; ULONG ZwCreateProcessEx_ServiceId; //忽略盘符的影响 RtlInitUnicodeString(&dllName, L"\\SystemRoot\\system32\\ntdll.dll"); ZwCreateProcessEx_ServiceId = GetFunctionId( &dllName, "ZwCreateProcessEx" ); SSDT hook 的具体代码,黑防以前已经多次提到,详细代码就不分析了,只进行一下总 结:一是由于 SSDT 内存只读,通过修改 CR0 寄存器 WP 位绕过内存写保护机制,然后进 行HOOK;二是利用 MS 映射 MDL 方式绕过内存写保护机制,利用 HOOK 宏. SSDT HOOK 检测与恢复 恢复原理:PE 文件被加载到内存后,PE 文件里面的数据是可以被修改的,但是磁盘文 件上的数据不会变, 因此可以利用读取磁盘文件的原始数据来恢复被修改的数据. 这里涉及 到大部分 PE 的知识,详细的就不多加介绍了,可以参考《加密与解密第三版》 . 这部分需要获取的主要数据有:服务号:R3 下解析 ntdll.dll 导出表;函数名:R3 下解 析ntdll.dll 导出表;当前地址:传入服务号给驱动,驱动返回当前地址;模块名:传入当前 地址给驱动,驱动返回模块名;原始地址:获取 SSDT 表在磁盘文件中的偏移,重定位获得 地址. 最后我实现的效果如图 2 所示,部分核心代码如下: 图2 2009 年第 12 期 *函数名:FindModuleByAddress *功能描述:根据函数地址查找所属模块 原理:利用 ZwQuerySystemInformation 传入 SystemModuleInformation(11)得到系统 模块列表;得到每个模块的起始和结束地址;比对地址,在哪个范围就属于哪个模块;得到 模块名 void FindModuleByAddress( ULONG Address, PVOID buffer) { NTSTATUS status; ULONG size; ULONG i; ULONG minAddress; ULONG maxAddress; PSYSMODULELIST List; //得到需要申请的空间大小 ZwQuerySystemInformation( SystemModuleInformation ,&size,0,&size); KdPrint(("[FindModuleByAddress] size:0x%x\n",size)); List=(PSYSMODULELIST)ExAllocatePool(NonPagedPool,size); if(List==NULL) { KdPrint(("[FindModuleByAddress] malloc memory failed\n")); ExFreePool( List ); return ; } status=ZwQuerySystemInformation(SystemModuleInformation,List,size,0); if(!NT_SUCCESS(status)) { KdPrint(("[FindModuleByAddress] query failed\n")); //打印错误 KdPrint(("[FindModuleByAddress] status: 0x%x\n",status)); ExFreePool( List ); return ; } //得到了模块链表,判断模块名 for( i=0; i
ulCount; i++) { //得到模块的范围 minAddress = (ULONG)List->smi[i].Base; maxAddress = minAddress + List->smi[i].Size; //判断地址 if( Address >= minAddress && Address <= maxAddress ) { memcpy(buffer, List->smi[i].ImageName,sizeof(List->smi[i].ImageName)); KdPrint(("[FindModuleByAddress] modulename: %s\n",buffer)); //释放内存 ExFreePool(List); 2009 年第 12 期break; } } } *函数名:GetOriFunctionAddress *功能描述:得到原始 SSDT 表中函数地址 原理:找到内核文件,获取基址 BaseAddress;根据内核文件查找 SSDT 表的文件偏移 SSDTFileOffset = SSDTRVA-(节RVA-节Offset) ;读取函数的文件偏移 FunctionFileOffset; VA=BaseAddress+FunctionFileOffset-00400000=800d8000 + FunctionFileOffset;根据 RVA 查 找所在的文件偏移:FileOffset = Rva- (节Rva - 节Offset);找到区块表 ULONG FindFileOffsetByRva( ULONG ModuleAddress,ULONG Rva) { PIMAGE_DOS_HEADER dos; PIMAGE_FILE_HEADER file; PIMAGE_SECTION_HEADER section; //区块数目 ULONG number; ULONG i; ULONG minAddress; ULONG maxAddress; ULONG SeFileOffset; ULONG FileOffset; dos = (PIMAGE_DOS_HEADER)ModuleAddress; file = (PIMAGE_FILE_HEADER)( ModuleAddress + dos->e_lfanew + 4 ); //得到区块数量 number = file->NumberOfSections; KdPrint(("[FindFileOffsetByRva] number :0x%x\n",number)); //得到第一个区块地址 section = (PIMAGE_SECTION_HEADER)(ModuleAddress + dos->e_lfanew + 4 + sizeof(IMAGE_FILE_HEADER) + file->SizeOfOptionalHeader); for( i=0;i minAddress && Rva < maxAddress) { KdPrint(("[FindFileOffsetByRva] minAddress :0x%x\n",minAddress)); 2009 年第 12 期KdPrint(("[FindFileOffsetByRva] SeFileOffset :0x%x\n",SeFileOffset)); FileOffset = Rva - ( minAddress - SeFileOffset); KdPrint(("[FindFileOffsetByRva] FileOffset :0x%x\n",FileOffset)); break ; } } return FileOffset; } //路径解析出子进程名 void GetModuleName( char *ProcessPath, char *ProcessName) { ULONG n = strlen( ProcessPath) - 1; ULONG i = n; KdPrint(("%d",n)); while( ProcessPath[i] { i = i-1; } strncpy( ProcessName, ProcessPath+i+1,n-i); } *根据传入的服务号得到原始地址 ULONG FindOriAddress( ULONG index ) { //根据传入的 index 得到函数 VA 地址 //重定位函数地址 //BaseAddress - 0x00400000 + *(PULONG)(FileOffset+(index*4)) //ZwQuerySystemInformation 得到内核文件基地址 //得到 SSDT 表的地址 //得到 SSDT RVA 查找 SSDT RVA 所在的节 NTSTATUS status; ULONG size; ULONG BaseAddress; ULONG SsdtRva; ULONG FileOffset = 0; PSYSMODULELIST list; char Name[32]={0}; char PathName[256] = "\\SystemRoot\\system32\\"; ANSI_STRING name; UNICODE_STRING modulename; OBJECT_ATTRIBUTES object_attributes; IO_STATUS_BLOCK io_status = {0}; HANDLE hFile; 2009 年第 12 期//读取的位置 ULONG location; LARGE_INTEGER offset; ULONG address; //得到需要申请的内存大小 ZwQuerySystemInformation( SystemModuleInformation,&size,0,&size ); //申请内存 list = (PSYSMODULELIST) ExAllocatePool( NonPagedPool,size ); //验证是否申请成功 if( list == NULL) { //申请失败 KdPrint(("[FindOriAddress] malloc memory failed\n")); ExFreePool(list); return 0; } status = ZwQuerySystemInformation( SystemModuleInformation,list,size,0); if( !NT_SUCCESS( status )) { //获取信息失败 KdPrint(("[FindOriAddress] query failed\n")); KdPrint(("[FindOriAddress] status:0x%x\n",status)); ExFreePool(list); return 0; } //得到模块基址,第一个模块为内核文件 BaseAddress = (ULONG )list->smi[0].Base; KdPrint(("[FindOriAddress] BaseAddress:0x%x\n",BaseAddress)); //分离出内核文件名 GetModuleName(list->smi[0].ImageName,Name); KdPrint(("[FindOriAddress] processname:%s\n",Name)); strcat(PathName,Name); RtlInitAnsiString(&name,PathName); RtlAnsiStringToUnicodeString(&modulename,&name,TRUE); KdPrint(("[FindOriAddress] modulename: %wZ\n",&modulename)); ExFreePool(list); //经验证地址正确,得到 SSDT 表的 Rva SsdtRva = (ULONG)KeServiceDescriptorTable->ServiceTableBase - BaseAddress; //验证 KdPrint(("[FindOriAddress] SsdtRva:0x%x\n",SsdtRva)); //根据 RVA 查找文件偏移,得到文件偏移了 FileOffset= FindFileOffsetByRva( BaseAddress,SsdtRva); KdPrint(("[FindOriAddress] FileOffset:0x%x\n",FileOffset)); //读取的位置 location = FileOffset + index * 4; offset.QuadPart =location; KdPrint(("[FindOriAddress] location:0x%x\n",location)); //利用 ZwReadFile 读取文件,初始化 OBJECT_ATTRIBUTES 结构 InitializeObjectAttributes( &object_attributes, &modulename, 2009 年第 12 期OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); //打开文件 status = ZwCreateFile( &hFile, FILE_EXECUTE | SYNCHRONIZE, &object_attributes, &io_status, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN, FILE_NON_DIRECTORY_FILE | FILE_RANDOM_ACCESS | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); if( !NT_SUCCESS( status )) { KdPrint(("[FindOriAddress] open error\n")); KdPrint(("[FindOriAddress] status = 0x%x\n", status)); return 0; } status = ZwReadFile( hFile, NULL, NULL, NULL, NULL, &address, sizeof(ULONG), &offset, NULL); if( !NT_SUCCESS( status )) { KdPrint(("[FindOriAddress] read error\n")); KdPrint(("[FindOriAddress] status = 0x%x\n", status)); return 0; } KdPrint(("[FindOriAddress] address:0x%x\n",address)); //重定位 address = BaseAddress - 0x00400000 + address; KdPrint(("[FindOriAddress] Oriaddress:0x%x\n",address)); //释放动态分配的内存 RtlFreeUnicodeString(&modulename); return address; } 至于 SSDT Shadow HOOK 和SSDT Shadow 检测与恢复, 还请大家继续关注本文的后续 文章.