|
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导出的。
如果OpenProcessToken()调用成功,要记得关闭其返回的令牌句柄(Token Handle)。w2k_dbg.dll包含一个dbgPrivilegeSet()函数,该函数合并了这几个步骤,下面的列表1-8列出了该函数和w2k_dbg.dll中的另一个函数:dbgPrivilegeDebug()。此函数是dbgPrivilegeSet()的一个外包函数,为了便于设定调试特权。顺便说一下,Windows NT Server资源工具集中的kill.exe也使用了同样的技巧。Kill.exe需要调试特权来剔除内存中饿死的服务(starved services)。这是NT Server管理员不可缺少的一个工具,这对于重起一个挂掉的系统服务十分有用,而且可以避免不必要的系统重启。对于使用IIS(Internet Information Server)的人,在他们的紧急工具箱中可能都有这个工具,以便重起偶尔挂掉的inerinfo.exe。
BOOL WINAPI dbgPrivilegeSet(PWORD pwName)
{
HANDLE hToken;
TOKEN_PRIVILEGES tp;
BOOL fOk = FALSE;
if ( (pwName != NULL) &&
OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES,
&hToken) )
{
if ( LookupPrivilegeValue(NULL,pwName,&tp,Privileges->Luid) )
{
tp.Privileges->Attributes = SE_PRIVILEGE_ENABLED;
tp.PrivilegeCount = 1;
fOk = AdjustTokenPrivileges(hToken,FALSE,&tp,0,NULL,NULL)
&& (GetLastError() == ERROR_SUCCESS);
}
CloseHandle(hToken);
}
return fOk;
}
//------------------------------------------------------------------------------------
BOOL WINAPI dbgPrivilegeDebug(void)
{
return dbgPrivilegeSet(SE_DEBGU_NAME);
}
列表1-8 Requesting a Privilege for a Process 上一页 [1] [2] [3] |