| fOk = TRUE;
}
LocalFree(pspi);
if ( fOk || (ns != STATUS_INFO_LENGTH_MISMATCH) )
{
if ( !fOk) SetLastError(RtlNtStatusToDosError(ns));
break;
}
return fOk;
}
列表1-6 EnumProcesses()函数的示例实现
枚举进程模块
一但你从EnumProcess()返回的进程列表中发现了你感兴趣的进程ID,你可能会想知道在此进程的虚拟地址空间中加载了哪些模块。psapi.dll提供了另一个API函数来完成此功能,叫做EnumProcessModules()。与EnumDeviceDrivers()和EnumProcesses()不同,这个函数需要四个参数(参见列表1-7)。不同于前两个返回系统全局列表的函数,EnumProcessModules()只取回指定进程的列表,因此,增加的那个参数唯一表示一个进程。然而,该函数需要一个进程句柄(HANDLE)来代替进程ID。为了通过进程ID获取其句柄(HANDLE),必须调用OpenProcess()函数。
BOOL WINAPI EnumProcessModule( HNADLE hProcess,
HMODULE* lphModule,
DWORD cb,
DWORD* lpcbNeeded);
PHMODULE WINAPI dbgProcessModules( HANDLE hProcess, PDWORD pdCount)
{
DWORD dSize;
DWORD dCount = 0;
PHMODULE phList = NULL;
if ( hProcess != NULL )
{
dSize = SIZE_MINIMUM * sizeof( HMODULE );
while ( (phList = dbgMemoryCreate(dSize)) != NULL )
{
if ( EnumProcessModules(hProcess,phList,dSize,&dCount))
{
if (dCount <= dSize)
{
dCount /= sizeof( HMODULE );
break;
}
}
else
{
dCount = 0;
}
phList = dbgMemoryDestroy(phList);
if ( !(dSize = dCount) ) break;
}
}
if ( pdCount != NULL) *pdCount = dCount;
return phList;
}
列表1-7 枚举进程模块
EnumProcessModules()返回指定进程所有模块的句柄的引用。在Windows 2000中,一个HMODULE只是简单的模块映像基址。在SDK头文件windef.h中,HMODULE被定义为HINSTANCE的别名,二者都是HANDLE类型。严格的来讲HMODULE并不是一个句柄。通常,句柄是系统管理的一个表的索引,可通过此表来查找对象属性。系统返回的所有句柄都有一个与特定对象相关的计数器,在一个对象的所有句柄没有返回系统时,该对象不能从内存中被移除。Win32 API提供了CloseHandle()函数用于关闭句柄。该函数与Native API NtClose()等价。有关HMODULEs最重要的事情是,这些“handles”不需要关闭。
另一件让人困惑的事是,事实上,模块句柄通常并不被保证是一直有效的。SDK的GetModuleHandle()函数文档提示到,在多线程程序中必须更加注意模块句柄,因为一个线程可以通过卸载HMODULE引用的模块而让另一个线程拥有的HMODULE无效。在多任务环境下,一个程序(如调试器)使用另一程序的模块句柄时也许注意这一点。这似乎使HMODULEs没有多大用处了,但是,在下面两种情况中,HMODULE的有效性会保持足够长的时间:
1.由LoadLibrary()或LoadLibraryEx()返回的HMODULE在进程调用FreeLibrary()之前都会一直有效,由于这些函数包含了模块引用计数,所以即使在多线程程序中,这也会阻止模块被意外卸载。
2.如果HMODULE指向的模块会永久的存在,那么它也会一直有效。例如,所有Windows 2000内核组件(不包括内核模式的驱动程序)总是被映射到每个进程的相同固定地址上,并且在进程生命期里一直在那里。
不幸地是,这些情况并不适用于EnumProcessModules()函数返回的模块句柄,至少通常不行。复制到调用者提供的缓冲区中的HMODULE,在获取进程快照那一刻其所表示映像基址是有效的。稍后,进程可能调用FreeLibrary()来释放一个或多个模块,并将其从内存中移除,此时它们的句柄将无效,随后进程很有可能立即调用LoadLibrary()加载了另一个DLL,而此新模块恰好映射到了前面释放的地址上。这看上去是不是很熟悉?是的,同样的问题也存在于EnumDeviceDrivers()的指针数组和EnumProcesses()函数的ID数组。不过,这些问题是可以避免的。psapid.dll通过调用未文档化的API函数来完成数据收集工作后,考虑这些数据的完整性,可返回一个完整的请求对象的快照,其中应包括所有感兴趣的属性信息。这样就没有必要在稍后调用另一个函数来获取附加的信息了。我的观点是,psapi.dll的设计过于简单,因为它忽略了数据的完整性,这也是我不会将此DLL作为一个专业调试工具的基础的原因。
与EnumDeviceDrivers()和EnunProcesses()函数相比EnumProcessModules()函数算是个好公民了,因为如果调用者提供的缓冲区不能放下全部的输出数据,它会准确地提示有多少字节没有复制。注意列表1-7没有包括一个循环,在那里缓冲区会不断增大直到足够的大。然而,仍然需要trial-and-error循环,因为在下一次调用时,EnumProcessModules报告的所需大小可能已经无效了(如果指定进程在两次调用之间又加载了新的模块)。因此,列表1-7中的代码将不断枚举模块直到EnumProcessModules()报告需要的缓冲区等于或小于实际可用大小,或者出现了错误。
我不想描述EnumProcessModules()的等价函数,因为该函数要比EnumDeviceDrivers和EnumProcesses稍微复杂些,它涉及几个未文档化的数据结构。基本上,它还是通过调用NtQuerySystemInformation()函数(当然,该函数也没有文档化)来获取目标进程环境块(PEB)的地址,通过该地址可获取一个模块信息链表。因为不管是PEB还是这个链表在调用进程的地址空间都是无法直接使用的,EnumProcessModules调用Win32 API ReadProcessMemory()(该函数有文档记载)来遍历目标进程的地址空间。顺便说一下,PEB结构的布局将在第7章讨论,在附录C中,可以找到该结构的定义。
调整进程特权
回忆一下稍早讨论过的有关EnumProcessModules所需的进程句柄。通常,你首先得到的是进程ID---可能是EnumProcesses返回的进程ID集中的一个。Win32 API OpenProcess()可通过进程ID来获取其句柄。这个函数期望一个访问标志符作为其第一个参数。假定进程ID存放在一个DWORD类型的变量dId中,你以最大访问权限来调用OpenProcess,如下:
OpenProcess(PROCESS_ALL_ACCESS,FALSE,dId)
以获取该进程的句柄,此时你会收到一个针对几个低ID进程的错误代码。这并不是bug---这是安全特性!这些进程都是保持系统活动的系统服务。一个普通用户进程不允许执行针对系统服务的所有操作。例如,允许所有进程都可以杀死系统中其余进程并不是个好主意。如果一个程序意外终止了一个系统服务,那么整个系统都将崩溃。因此,一个进程只有拥有确切的访问权限才会有适当的特权。
由于多种原因,调试器必须拥有大量的权限来完成他的工作。改变进程的特权可通过以下三个简单的基本步骤:
1.首先,必须打开进程的访问令牌(access token),使用advapi32.dll中的函数OpenProcessToken()。
2.如果上一步正确完成,接下来就是准备TOKEN_PRIVILEGES结构,该结构包含有关要请求的特权的信息。这个工作需要advapi32.dll中的另一个函数LookupPrivilegeValue()的帮助。特权通过名称来指定。SDK文档winnt.h定义了27中特权名称和其对应的符号名称。例如,调试权限的符号名称为:SE_DEBUG_NAME,该名称和字符串“SeDebugPrivilege”等效。
3.如果上一步正确完成,就可以使用进程的令牌句柄(Token Handle)来调用AdjustTokenPrivileges()函数以初始化TOKEN_PRIVILEGES结构。该函数也是advapi32.dll导出的。
如果 上一页 [1] [2] [3] [4] 下一页 |