一、引言
进程管理器在系统维护过程中起着十分重要的作用。当发现某个程序停止响应,或是怀疑有病毒程序在运行时,自然就会想到利用进程管理器查看内存中运行着的程序及进程列表,强制结束无响应的程序或病毒程序。虽然Windows任务管理器的功能较多,但缺少诸如枚举进程引用模块、获取进程的路径或命令行、进程端口扫描、驱动及服务列表等重要功能,于是网友们开发出现了不少好的进程管理软件,以弥补Windows任务管理器功能的不足。笔者自己也动手开发了一个进程管理器,实用效果较好,现奉献给广大读者。
二、进程管理器功能
笔者自编的进程管理器可运行在WindowsXP/2000环境下,以进程管理功能为主,兼具模块(DLL)管理、服务管理、启动项管理、驱动查询、端口扫描、窗口扫描等实用功能,最适合用于查杀木马病毒。实例程序的各功能简介如下。
1.进程管理
枚举进程、向远程进程空间注入DLL、结束进程、设置进程优先级、设置进程运行权限、隐藏进程、复制进程路径或命令行、查询进程相关信息(进程ID、用户及账号、路径或命令行、内存占用、优先级等),如图1所 示。
图1 进程管理界面
2.模块管理
枚举进程引用的DLL、从远程进程空间卸载DLL、注册/注销DLL、删除DLL文件、打开DLL所在文件夹、复制DLL文件名、查询DLL相关信息(入口地址、文件版本、宿主进程等),如图2所示。
图2 模块管理界面
3.服务管理
枚举服务(包括Win32应用服务、系统内核服务)、设置服务启动方式、启动服务、停止服务、删除服务、删除服务设备、复制服务设备名,如图3所示。
图3 Win32应用服务管理界面
4.设备查询
查询内核驱动设备,界面如图4所示。
5.启动选项
可查询、修改、添加、删除注册表中以下启动项设置: Run、RunOnce、RunOnceEX、RunServices、RunServicesOnce、Explorer Run、Windows、Winlogon、Session Manager、Command Processor、ShellExcuteHooks、Browser Help Objects,还可启用/禁用注册表编辑器,运行界面如图5所示。
图5 启动项管理界面
6.窗口管理
扫描窗口、鼠标捕捉窗口、显示/隐藏窗口、启用/禁用窗口、关闭窗口、获取密码、向窗口发送消息,如图6所示。
图6 窗口管理界面
7.端口扫描
扫描进程使用的端口及IP地址,只适用WindowsXP系统,运行界面如图7所示。
图7扫描界面
8.其他功能
支持系统托盘、右键快捷菜单、文件拖放、列表框排序、列表框单元格颜色设置、列表框宽度设置(自动设置、各列等宽)、热键Ctrl+Shift+A唤出等。以醒目红色标识大多数的可疑模块,提醒用户注意。
下面就结合这个实例程序,介绍其主要功能的实现方法。为既便于理解,又节省篇幅,在下文中只列出核心源代码片断,必要时以伪代码表示。未尽细节请参阅MSDN或NTDDK。
三、枚举进程
1.枚举进程的四种工具
用来枚举进程的工具有四种,即Toolhelp32、PSAPI(Process Status API)、PEB(Process Environment Block)和PDH(Performance Data Helper),最常用的是Toolhelp32和PSAPI。
比较而言,PEB和PDH的能够要比Toolhelp32和PSAPI的功能强大得多,可以获得进程和模块的更多细节资料,但用法比较复杂。PDH还可以用来枚举远程NT系统的进程(需有远程系统的管理员权限),查询CPU使用率和内存使用率。可以调用注册表函数来访问PDH数据库。下面重点讲述Toolhelp32和PSAPI工具的用法。
ToolHelp32能够通过建立快照(Snapshot)来获取有关进程、线程、模块和堆的信息。 PSAPI可用于获取有关进程、驱动器、模块、内存和工作集的信息,若有远程系统的管理员权限,PSAPI还可以用来枚举远程NT系统的进程。虽然Toolhelp32和PSAPI两者提供的函数接口不同,但最终都是通过调用由ntdll.dll导出的未公开函数 NtQuerySystemInformation来实现进程枚举功能的。
Toolhelp32适用于Windows98/Me/XP/2000,但不适用于Windows NT。 PSAPI适用于WindowsXP/2000/NT,但不适用于Windows98/Me 。
2.使用Toolhelp32枚举进程
Toolhelp32的CreateToolhelp32Snapshot函数用于创建快照,其函数原型定义如下:
HANDLE WINAPI CreateToolhelp32Snapshot(DWORD dwFlags, DWORD th32ProcessID);
dwFlags指定快照的类型,有以下几种:TH32CS_SNAPPROCESS(进程快照)、TH32CS_SNAPTHREAD(线程快照)、TH32CS_SNAPMODULE(模块快照)、TH32CS_SNAPHEAPLIST(堆快照)、TH32CS_SNAPALL(组合快照)。
th32ProcessID为进程的标示符(ID),仅当指定dwFlags为TH32CS_S NAPHEAPLIST或TH32CS_SNAPMODULE时才有效,通常设置为0。
当进程快照创建成功后,就可以利用Toolhelp32的Process32First及Process32Next函数枚举所有的进程。
PROCESSENTRY32 p32;
p32.dwSize = sizeof(PROCESSENTRY32);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(Process32First(handle, &p32))
{
……
while(Process32Next(handle, &p32))
……
}
hSnapshot 为CreateToolhelp32Snapshot返回的进程快照表的句柄,lppe为指向进程控制块的LPPROCESSENTRY32结构类型变量的指针,用于存储进程控制信息。LPPROCESSENTRY32结构的定义如下:
typedef struct tagPROCESSENTRY32 {
DWORD dwSize; // 结构的大小
DWORD cntUsage; // 进程的引用计数
DWORD th32ProcessID; // 进程的标示符
DWORD th32DefaultHeapID; // 进程默认堆的标示符
DWORD th32ModuleID; // 进程模块的标示符
DWORD cntThreads; // 进程所含的线程数
DWORD th32ParentProcessID; // 进程的父进程的标示符
LONG pcPriClassBase; // 进程的优先级
DWORD dwFlags; // 保留未用
char szExeFile[MAX_PATH]; // 进程对应的可执行文件及路径名
} PROCESSENTRY32;
typedef PROCESSENTRY32 * PPROCESSENTRY32;
typedef PROCESSENTRY32 * LPPROCESSENTRY32;
Process32First用于枚举快照表中第一个进程。当Process32First返回TRUE时,就可重复调用Process32Next函数枚举其余的进程。每当成功枚举到一个进程,其控制信息就被复制到PROCESSENTRY32结构类型变量中。
在PROCESSENTRY32中,th32ProcessID成员变量为进程的标识符(ID),它可以被传给OpenProcess以获得该进程的句柄,pcPriClassBase为进程的优先级。它分为以下几个级别: 0-暂缺,4-低,5至7-低于标准,8-标准,9至12-高于标准,13-高,24-实时。系统允许用户改变进程的优先级。th32ParentProcessID为父进程的标识符,据此可以构造进程树。
3.使用PSAPI枚举进程
WindowsXP/2000/NT环境下,创建进程快照表可使用PSAPI提供的一组函数,这些函数由psapi.dll导出。编译时需要psapi.h和psapi.lib这两个文件,可在Platform SDK中可以找到,也可以从网上单独下载。
使用PSAPI创建进程列表的第一步是调用EnumProcesses, 其函数原型定义:
BOOL EnumProcesses(DWORD *lpidProcess, DWORD cbSize, DWORD *cbNeeded);
lpidProcess为指向存放返回进程列表的数组指针,cbSize为预先定义的该数组的大小,cbNeeded用于存放实际返回的数组大小,使用算式cbNeeded/sizeof(DWORD)可以得到返回的进程数目。
由于EnumProcesses不会在cbNeeded中返回一个大于cbSize参数传递值,为确保EnumProcesses函数调用成功,若出现返回的cbNeeded=cbSize值的现象,就必须增大cbSize的值,并重复调用EnumProcesses一直到cbNeeded<cbSize为止,才能枚举到所有的进程。为简单起见,可为cbSize预置一个足够大的值如1024,绝大多数情况下都能够保证EnumProcesses调用成功。
四、关联信息
虽然进程控制块以提供了较多的信息,但缺少一些诸如进程路径名、命令行、用户名、账号、可视窗口等,需要通过其他途径来获取。
1.路径名
许多病毒制造者为迷惑用户,有意将病毒文件名伪装成系统文件名,常见的如smss.exe、crss.exe、winlogong.exe、sertvices.exe、lsass.exe、svchost.exe等,使一般用户难以辨别出哪些是系统进程,哪些是病毒进程。若能获取进程对应的可执行文件名及路径名,就容易区分出病毒进程,因为同一目下的每个文件名都具有唯一性,与系统文件同名的病毒文件是不可能与系统文件同时存放在同一目录下的。
当使用Toolhelp32枚举进程时,Process32First及Process32Next返回的进程信息存放PROCESSENTRY32结构类型变量中。在Windows9X/Me环境下,此结构的szExeFile成员变量存放的是进程对应的可执行文件名及其路径全名,而在WindowsXP/2000/NT环境下,szExeFile成员变量存放的仅是进程对应的可执行文件名,但不包括可执行文件所在路径名,此时要想获得路径名就必须调用PSAPI的 GetModuleFileNameEx函数,代码如下:
char szProcessName[MAX_PATH] = {0};
HMODULE hMod;
DWORD cbNeeded;
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, ProcessID);
if (EnumProcessModules(hProcess, &hMod, sizeof(hMod), &cbNeeded))
GetModuleFileNameEx(hProcess, hMod, szProcessName, sizeof(szProcessName));
CloseHandle(hProcess);
ProcessID 为目标进程的标识符。EnumProcessModules将返回目标进程引用第一个模块(即进程自身)的句柄,并将其保存到变量hMod中,GetModuleFileNameEx返回的进程可执行文件名及路径名。
如果将hMod定义为一个足够大的数组,那么EnumProcessModules将返回目标进程引用的所有模块。如果用GetModuleBaseName来替代GetModuleFileNameEx,则仅能够获得进程的可执行文件名。
有个别系统进程的路径名比较特殊,类似于“\SystemRoot\System32\”或“\??\C:WINDOWS\system32\”,这是kernel32.dll导出函数Bug造成的,其实都表示系统目录。
2.命令行
枚举到进程并且知道了进程对应的可执行文件及路径后,如能获取启动进程的命令行将是十分有用的,由此可以了解进程启动时附加了哪些参数,可以帮助有经验的用户快速查找病毒进程。
在WindowsXP/2000/NT环境下,许多进程(特别是在后台运行服务类的进程)是通过svchost.exe来加载的。而Windows自带的任务管理器,包括网上流行的多个进程管理器,因不具备进程命令行参数显示功能,都只会显示多个svchost.exe进程,但无法帮助用户区分每个svchost.exe进程所加载的模块,对于rundll32.exe进程也有类似的情况。
如何获得进程的命令行呢?这个问题对于DOS程序的编写者来说比较简单,但对MFC应用程序编写者来说就没有那么简单了。虽然有现成的GetCommandLine函数可供调用,但它只能返回当前进程的命令行,对其他进程无效,而要获取所有进程的命令行,这就需要调用另外一个未公开的函数NtQueryInformationProcess,设法从进程环境块PEB中读取命令行。
第一步,调用OpenProcess打开进程,注意其dwDesiredAccess参数必须指定为PROCESS_QUERY_INFORMATION|PROCESS_VM_READ;
第二步,调用NtQueryInformationProcess用于得到目标进程的进程环境块PEB,其函数原型定义如下:
NTSTATUS NtQueryInformationProcess(IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL);
NtQueryInformationProcess函数由ntdll.dll导出,需调用GetProcAddress获得其地址。ProcessInformationClass是一个可变的枚举类型,在此需要取值为0,即可复制进程基本信息块到自定义的PROCESS_BASIC_INFORMATION结构类型变量中,其成员变量PebBaseAddress指明了进程环境快PEB的首地址。
第三步,调用ReadProcessMemory从PebBaseAddress指明的首地址开始复制进程环境块信息到PEB结构类型变量中,其成员变量ProcessParameters指明了进程命令行首地址;
第四步,调用ReadProcessMemory从ProcessParameters指明的首址开始复制进程命令行到PROCESS_PARAMETERS结构类型变量中,其成员变量CommandLine.Buffer和CommandLine.Length分别指明了进程命令行首址和长度;
第五步,调用ReadProcessMemory从CommandLine.Buffer指明的首址开始复制命令行到一个LPWSTR(Unicode字符)类型指针指向的缓冲区中;
第六步,调用WideCharToMultiByte将返回到缓冲区的命令行字符串由Unicode格式转换为ANSI格式。
PEB Peb;
PROCESS_PARAMETERS ProcParam;
PROCESS_BASIC_INFORMATION pbi;
char cmdline[MAX_PATH]={0}, buf[MAX_PATH]={0};
LPVOID lpBuf;
DWORD dwRead, dwLen;
typedef LONG (WINAPI *PROCNTQIP)(HANDLE,UINT,PVOID,ULONG,PULONG);
PROCNTQIP lpNtQIP;
lpNtQIP = (PROCNTQIP)GetProcAddress(
GetModuleHandle("ntdll"),"NtQueryInformationProcess");
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION|
PROCESS_VM_READ, FALSE, ProcessID);
LONG status = lpNtQIP( hProcess, 0, (PVOID)&pbi,
sizeof(PROCESS_BASIC_INFORMATION), NULL);
ReadProcessMemory( hProcess, pbi.PebBaseAddress, &Peb, sizeof(PEB), &dwRead);
ReadProcessMemory( hProcess, Peb.ProcessParameters, &ProcParam,
sizeof(PROCESS_PARAMETERS), &dwRead);
lpBuf = ProcParam.CommandLine.Buffer;
dwLen = ProcParam.CommandLine.Length;
ReadProcessMemory( hProcess, lpBuf, (LPWSTR)buf, dwLen, &dwRead );
WideCharToMultiByte( CP_ACP, 0, (LPWSTR)cmdline, -1, buf, sizeof( buf ), NULL, NULL );
CloseHandle (hProcess);
buf中存放的是Unicode格式命令行字符串,cmdline中存放的是ANSI格式命令行字符串。cmdline和buf两个缓冲区的大小至少要等于dwLen。
这里说明一下Unicode格式字符串集转和ANSI格式的字符串的区别。假设有一个ANSI格式字符串“ABC”,其对应的 Unicode格式字符串则为‘A’,0, ‘B’,0, ‘C’,0。由此可见,后者是将前者的每个字节扩展为双字节,且双字节的高位一律为0。
上面用到的PROCESS_BASIC_INFORMATION、PEB和PROCESS_PARAMETERS三个自定义结构是由NTDDK提供的。
3.用户名及账号
作为系统管理员,有时可能需要了解每个进程的拥有者,即其用户名、账号和密码。
要想获取进程的用户名和账号,应首先调用OpenProcessToken打开进程的令牌(Token),打开方式设为TOKEN_QUERY,然后将TOKEN_INFORMATION_CLASS设为 TokenUser调用GetTokenInformation枚举进程的令牌信息块SID,通常称为安全标识(Security Identifier),最后调用LookupAccountSid从SID中检索用户名和账号。
char buf[256], szUser[128], szComputer[128];
HANDLE hProcess, hToken;
DWORD cbUser, cbComputer;
SID_NAME_USE snu;
if(ProcessID <= 0x0c)
显示用户名为"SYSTEM";
Else
显示用户名为"SERVICE ";
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, 0, ProcessID);
OpenProcessToken(hProcess, TOKEN_QUERY, &hToken);
GetTokenInformation(hToken, TokenUser, &buf, 256, &cbUser);
cbComputer=sizeof(szComputer);
LookupAccountSid(NULL, (DWORD *)(*(DWORD *)buf), szUser,
&cbUser, szComputer, &cbComputer, &snu);
CloseHandle(hToken);
CloseHandle(hProcess);
代码中ProcessID 为目标进程的标识符。标识符小于0x0c的进程为系统核心进程,在Windows任务管理器中显示其用户名为“SYSTEM”。GetTokenInformation 的第二个参数指明要枚举的令牌信息块类型,在这里将其指定为TokenUser,buf缓冲区用于接收返回的令牌信息块SID。SID的首个双字为一指针,此指针指向一个地址,此地址内又存放着块内用户名及账号字符串的地址。需要注意的是,在调用LookupAccountSid前,必须为变量cbComputer赋值,其大小为预先分配的szComputer缓冲区的大小,并且以 (DWORD *)(*(DWORD *)buf)方式来指定作为输入参数的令牌信息块SID内用户名及账号字符串的首址。返回的用户名和账号分别到存放变量szUser和szComputer中。
在WindowsXP/2000/NT中,所有用户的登录密码都存放在Winlogon.exe进程的地址空间内,可以通过调用NtQuerySystemInformation枚举系统信息块获得。考虑到安全性,本文对如何获得用户密码的细节不作介绍。
4.可视窗口
查找进程的可视窗口,查看窗口标题,有助于了解进程的功能。
既然进程是一个正在运行着的可执行程序的实例,那么它就可能就会有可视窗口,也可能没有可视窗口(如多数后台服务类进程)。一个进程若有可视窗口的话,则其可视窗口可能是一个,也可能是多个,典型的如Explorer.exe和Iexplore.exe两个进程,各自都有多个可视窗口。
给定一个目标进程,从桌面窗口开始枚举所有顶层窗口,逐个判别是否具有WS_VISIBLE特性,若有的话再调用GetWindowThreadProcessId,判别其返回的窗口的进程标识符是否与给定的目标进程标识符相同,如相同则找到了目标进程的一个可视窗口,接着再判别其余的顶层窗口是否为目标进程的可视窗口。重复上述步骤,即可找到所有目标进程的可视窗口。
DWORD pID;
hWndNext=::GetWindow(::GetDesktopWindow(), GW_CHILD);
while(hWndNext)
{
if(GetWindowLong(hWnd, GWL_STYLE) & WS_VISIBLE)
GetWindowThreadProcessId(hWnd, &pID);
if(pID==ProcessID && ::GetParent(hWnd) == NULL)
//记录找到的目标进程的一个可视窗口;
}
hWndNext=::GetWindow(hWndNext, GW_HWNDNEXT);
如果去掉匹配目标进程标识符ProcessID这一限制条件,用上述方法可以找到系统所有可视窗口,即在Windows任务管理器“应用程序”栏内看到的内容。
5.打开的文档
通过调用DeviceIoControl可以很容易地查询到Windows98/ME进程打开的文档,但不能查询WindowsXP/2000进程打开的文档,只能查询其磁盘设备。
要想查询WindowsXP/2000进程打开的文档,可靠的方法是创建一个管道专门用来接收应用程序输出,通过重定向输出得到进程打开的所有文档列表。由于具体实现方法十分复杂,笔者在这里只提供思路,供有兴趣的读者自己研究。
五、枚举进程引用的模块
与枚举进程一样,枚举进程引用模块所使用的工具也是Toolhelp32、PSAPI、PEB和PDH,常用的也是Toolhelp32和PSAPI。
1.Toolhelp32枚举模块
在前面“使用Toolhelp32枚举进程”一节已经提到CreateToolhelp32Snapshot也可以创建进程引用的模块快照,只需将第一个参数dwFlags设为TH32CS_SNAPMODULE,将第二个参数th32ProcessID设为目标进程的标识符即可,实现的核心代码如下:
MODULEENTRY32 m32;
m32.dwSize = sizeof(MODULEENTRY32);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, ProcessID);
if(Module32First (handle, &m32))
{
……
While(Module32Next(handle, &m32))
……
}
当模块快照创建成功后,就可以利用的Module32First及Module32Next函数枚举目标进程引用的所有模块。
Module32First用于枚举快照表中第一个进程。当Module32First返回TRUE时,就可重复调用Module32Next函数枚举其余的进程。每当成功枚举到一个进程,其控制信息就被复制到MODULEENTRY32结构类型变量中。MODULEENTRY32结构定义如下: typedef struct tagMODULEENTRY32 {
DWORD dwSize;
DWORD th32ModuleID; // 模块标识符
DWORD th32ProcessID; // 引用此模块进程的标识符
DWORD GlblcntUsage; // 模块的引用计数(全局)
DWORD ProccntUsage; // 模块的引用计数(局部)
BYTE * modBaseAddr; // 模块基地址
DWORD modBaseSize; // 模块大小
HMODULE hModule; // 模块句柄
char szModule[MAX_MODULE_NAME32 + 1]; // 模块名
char szExePath[MAX_PATH]; // 模块路径
} MODULEENTRY32;
typedef MODULEENTRY32 * PMODULEENTRY32;
typedef MODULEENTRY32 * LPMODULEENTRY32;
hModule是一个十分重要的成员变量,有些专杀注入型木马病毒的软件正是由此获得病毒模块句柄的。
2.PSAPI枚举模块
在前面“获取进程的路径名”一节里,已经提到使用EnumProcessModules 来枚举目标进程引用的模块,其第二个参数lphModule为指向存放返回模块句柄列表的数组指针,cbSize为预先定义的该数组的大小, lpcbNeeded用于存放实际返回的数组大小,使用算式*lpcbNeeded/sizeof(HMODULE)可以得到返回的模块句柄数目,代码如下:
HANDLE hProcess;
DWORD cbNeeded, cbSize=1024;
HMODULE hMods[cbSize];
MODULEINFO mf;
char szModName[MAX_PATH] = {" 无 "};
unsigned int i;
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, ProcessID);
if(EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded))
for (i = 0; i<(cbNeeded/sizeof(HMODULE)); i++)
{
GetModuleInformation(hProcess, hMods[i], &mf, sizeof(MODULEINFO));
GetModuleFileNameEx(hProcess, hMods[i], szModName, sizeof(szModName));
}
CloseHandle(hProcess);
ProcessID为目标进程标识符,返回的模块信息存放在MODULEINFO结构类型变量中,GetModuleFileNameEx返回的模块文件名及路径名存储在szModName中。
与调用EnumProcesses的情形很类似,可为cbSize预置一个足够大的值如1024,绝大多数情况下都能够保证EnumProcessModules调用成功。
MODULEINFO结构定义如下:
typedef struct _MODULEINFO {
LPVOID lpBaseOfDll;
DWORD SizeOfImage;
LPVOID EntryPoint;
} MODULEINFO, *LPMODULEINFO;
成员变量lpBaseOfDll为模块的句柄,SizeOfImage为模块的大小,EntryPoint为模块代码段在目标进程地址空间内的起始地址,并不是DllMain函数入口地址。
EnumProcessModules返回的第一个模块总是进程自身。
|