http://nokyo.blogbus.com/logs/37850401.html
第三部分:从进程句柄获取信息
在第二部分我们使用了一个前提:可以通过进程句柄得到PID等信息。
事实上这是可行的,这一部分我们就进行介绍。我这里使用的是炉子大虾的《API HOOK实现ring3的进程保护》一文中提到的方法。
炉子那篇文章里讲的很详细,这里只说下如何从进程句柄中获取信息吧,在NTDLL中有个函数可以帮助我们:Zw(Nt)QueryInformationProcess,下面是我在《The Undocumented Functions——Microsoft Windows NT/2000》一书中查到的原型声明:
NTSYSAPI NTSTATUS NTAPI NtQueryInformationProcess(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength );
第一个参数输入进程句柄,第三个参数输出该句柄的相关信息,至于是什么信息,就要看第二个参数的说明了。
第二个参数是个enum结构,很长,我数了一下,足有39个变量,所以这里就不列出来浪费版面了,我们需要关心的是第一个,即ProcessBasicInformation,与它相关的结构体是PROCESS_BASIC_INFORMATION,如下所示:
typedef struct _PROCESS_BASIC_INFORMATION { NTSTATUS ExitStatus; PPEB PebBaseAddress; ULONG_PTR AffinityMask; KPRIORITY BasePriority; ULONG_PTR UniqueProcessId; ULONG_PTR InheritedFromUniqueProcessId; } PROCESS_BASIC_INFORMATION;
这个结构的UniqueProcessId域可以给出我们想要的进程句柄信息。不过有时候我们还想知道进程名,因为通过进程名可以更加直观地获得信息。
我仔细看了一下PROCESSINFOCLASS这个枚举结构,发现第28个子域应该是代表进程路径信息:ProcessImageFileName,但遗憾的是我找不到与之相对应的结构体定义(家里没法上网,又不会调试,只好先放下以后再说)。
现在我们只好通过其他方法来得到通过句柄进程名信息,我开始的思路是首先调用函数ObReferenceObjectByHandle通过句柄获取到一个指针,然后调用ObQueryNameString获取有关信息,可惜没测试成功,后来改用ZwQueryObject还是没测试成功,自己不会调试又没人可以给指点下,只好作罢。
既然我们不会从句柄直接得到进程名,但不要忘了现在我们已经可以从句柄得到进程的PID,通过PID再得到进程名还不简单?
在ring3通过PID得到进程名还真简单,我们随便选一种方法先遍历一下,然后比对一下PID,就能得到进程名了。不对,是连进程的完整路径都可以得到。不过在ring0我还不会得到完整路径,而且我也不想使用那种麻烦的方法来获取进程名。
偶然在某篇文章(真忘了是哪篇-_-)见有人提到一个函数PsLookupProcessByProcessId,到《The Undocumented Functions》中查到它的原型声明如下所示:
NTSYSAPI NTSTATUS NTAPI PsLookupProcessByProcessId(
IN ULONG ProcessId,
OUT PEPROCESS *pEProcess );
可以看到它输出了大名鼎鼎的PEPROCESS结构,这个结构中正好包含有进程名的信息(可惜不是完整路径),就凑合着用吧。这个结构在相关偏移量在不同的系统上是不同的,这里我参考了北极星2003大虾的某篇文章,专门写了一个函数首先判断系统,然后再动态返回相应的偏移量。
辛辛苦苦忙活了半天,总算可以通过句柄获得进程的PID和进程名信息了,如下面的代码所示:
ULONG lRet; PVOID pBuffer; PROCESS_BASIC_INFORMATION *pbi; PEPROCESS EProcess; PUCHAR ImageFilePath; ULONG dwNameOffset; pBuffer = ExAllocatePool(PagedPool, sizeof(PROCESS_BASIC_INFORMATION)); ZwQueryInformationProcess(ProcessHandle, ProcessBasicInformation, pBuffer,sizeof(PROCESS_BASIC_INFORMATION), &lRet); pbi = (struct _PROCESS_BASIC_INFORMATION *)pBuffer; PsLookupProcessByProcessId(pbi->UniqueProcessId, &EProcess); dwNameOffset = GetPlantformDependentInfo(); ImageFilePath = (PUCHAR)((LPTSTR)EProcess + dwNameOffset); KdPrint(("[nokyo] 目标进程:%d -> %s", pbi->UniqueProcessId, ImageFilePath));
什么,要看演示效果?笨呐,图2不就是演示效果么!!!
第四部分:动态获取函数索引
文章写到了这里还不算完,在第一部分我们使用了硬编码来确定ZwCreateThread的索引号,但硬编码的情况我们必须尽量避免,否则就很难保证通用性。
动态获取Zw*函数的索引号其实并不难,网上有关恢复SSDT的代码铺天盖地,里面都少不了这一个功能。这里我是从李马的SSDTDump中提取出了关于获取函数索引的代码(控制台的,不需要驱动,它不能在驱动里面直接用,需要自己修改),主要代码如下所示:
void EnumSSDT(IN HMODULE hNtDll) { DWORD dwOffset = (DWORD)hNtDll; PIMAGE_EXPORT_DIRECTORY pExpDir = NULL; int nNameCnt = 0; LPDWORD pNameArray = NULL; int i = 0; // 到PE头部 dwOffset += ((PIMAGE_DOS_HEADER)hNtDll)->e_lfanew + sizeof(DWORD); // 到第一个数据目录 dwOffset += sizeof(IMAGE_FILE_HEADER) + sizeof(IMAGE_OPTIONAL_HEADER) -IMAGE_NUMBEROF_DIRECTORY_ENTRIES * sizeof(IMAGE_DATA_DIRECTORY); // 到导出表位置 dwOffset = (DWORD)hNtDll + ((PIMAGE_DATA_DIRECTORY)dwOffset)->VirtualAddress; pExpDir = (PIMAGE_EXPORT_DIRECTORY)dwOffset; nNameCnt = pExpDir->NumberOfNames; // 到函数名RVA数组 pNameArray = (LPDWORD)((DWORD)hNtDll + pExpDir->AddressOfNames); // 初始化系统模块链表 PSYSMODULELIST pList = CreateModuleList(hNtDll); // 循环查找函数名 for (i = 0; i < nNameCnt; ++i) { PCSTR pszName = (PCSTR)( pNameArray[i] + (DWORD)hNtDll ); if ('N' == pszName[0] && 't' == pszName[1]) { // 找到了函数,则定位至查找表 LPWORD pOrdNameArray = (LPWORD)( (DWORD)hNtDll + pExpDir->AddressOfNameOrdinals ); // 定位至总表 LPDWORD pFuncArray = (LPDWORD)( (DWORD)hNtDll + pExpDir->AddressOfFunctions ); LPCVOID pFunc = (LPCVOID)( (DWORD)hNtDll +pFuncArray[pOrdNameArray[i]] ); // 解析函数,获取服务名 SSDTENTRY entry; CopyMemory( &entry, pFunc, sizeof( SSDTENTRY ) ); if ( MOV == entry.byMov ) { // 可在这里判断函数名以取得索引号 printf( "0x%04X\t%s\r\n", entry.dwIndex, pszName ); } } } DestroyModuleList( pList ); }
对函数代码有不太理解的地方可以详细参考《城里城外看SSDT》一文,这段代码我们同样可以将其写在驱动程序中以动态获取函数的索引号。
上述代码的演示结果如图3所示:
到这里本文就算结束了,代码写的很简陋,关于如何向应用层程序发送通知和信息的方法就不再赘述了,关于DeviceIoControl的示例到处都是。本文还遗留了一个问题,就是如何得到目标DLL的路径,也就是CreateRemoteThread函数的第五个参数lpParameter,这个问题就交给各位读者解决了。
补充:
其实这篇文章的代码并不完整,具体点就是在第二部分过滤的不够详细,因为多数进程在启动的时候都需要其他进程创建主线程的,大多数是由explorer.exe等系统进程创建的,因此我们还需要通过判断目标进程是否为空,这个可以参考炉子的文章。
但炉子的文章中使用的是硬编码,而动态获取某个值偏移量的方法我不会,所以我使用的是判断目标进程的父进程方法,虽然不太保险,但总算解决了。