標籤:使用 os strong 檔案 資料 io
Windows下如何枚舉所有進程
Posted on 13:37:00 by 曉月 and filed under Coding, Windows, Windows Mobile
要編寫一個類似於 Windows 工作管理員的軟體,首先遇到的問題是如何?枚舉所有進程。暫且不考慮進入核心態去查隱藏進程一類的,下面提供幾種方法。請注意每種方法的使用局限,比如使用這些 API 所需要的作業系統是什麼(尤其是是否能在 Windows Mobile 下使用)。
本文參考使用者態枚舉進程的幾種方法,原文對於每一種方法都給出了完整的代碼,被我照抄下來。還有一篇:如何用 Win32 APIs 枚舉應用程式視窗和進程。基於我現學現賣的本質,對我演繹的部分請抱著批判的眼光來看,另外代碼也沒有充分驗證。
使用 ToolHelp API
ToolHelp API 的功能就是為了擷取當前運行程式的資訊,從而編寫適合自己需要的工具(@MSDN)。它支援的平台比較廣泛,可以在 Windows CE 下使用。在 Windows Mobile SDK 的 Samples 裡面有一個 PViewCE 的範例程式,就是用這個來查看進程和線程資訊的。
使用方法就是先用 CreateToolhelp32Snapshot 將當前系統的進程、線程、DLL、堆的資訊儲存到一個緩衝區,這就是一個系統快照。如果你只是對進程資訊感興趣,那麼只要包含 TH32CS_SNAPPROCESS 標誌即可。
然後調用一次 Process32First 函數,從快照中擷取第一個進程,然後重複調用 Process32Next,直到函數返回 FALSE 為止。這樣將遍曆快照中進程列表。這兩個函數都帶兩個參數,它們分別是快照控制代碼和一個 PROCESSENTRY32 結構。調用完 Process32First 或 Process32Next 之後,PROCESSENTRY32 中將包含系統中某個進程的關鍵資訊。其中進程 ID 就儲存在此結構的 th32ProcessID。此 ID 傳給 OpenProcess API 可以獲得該進程的控制代碼。對應的可執行檔名及其存放路徑存放在 szExeFile 結構成員中。在該結構中還可以找到其它一些有用的資訊。
需要注意的是:在調用 Process32First() 之前,要將 PROCESSENTRY32 結構的 dwSize 成員設定成 sizeof(PROCESSENTRY32)。 然後再用 Process32First、Process32Next 來枚舉進程。使用結束後要調用 CloseHandle 來釋放儲存的系統快照。
以下為參考代碼:
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
void useToolHelp()
{
HANDLE procSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(procSnap == INVALID_HANDLE_VALUE)
{
printf("CreateToolhelp32Snapshot failed, %d ",GetLastError());
return;
}
//
PROCESSENTRY32 procEntry = { 0 };
procEntry.dwSize = sizeof(PROCESSENTRY32);
BOOL bRet = Process32First(procSnap,&procEntry);
while(bRet)
{
wprintf(L"PID: %d (%s) ", procEntry.th32ProcessID, procEntry.szExeFile);
bRet = Process32Next(procSnap, &procEntry);
}
CloseHandle(procSnap);
}
void main()
{
useToolHelp();
getchar();
}
使用 Processing Status API
在 Windows SDK 中可以找到 PSAPI,通過 PSAPI 可以擷取進程列表和裝置驅動列表。通過 EnumProcesses、EnumProcessModules、GetModuleFileNameEx 和 GetModuleBaseName 來實現。
首先使用 EnumProcesses 來枚舉所有進程,它有三個參數:DWORD 類型的數組指標 lpidProcess;該數組的大小尺寸 cb;以及一個指向 DWORD 的指標 cbNeeded,它接收返回資料的長度。DWORD 數組用於儲存當前啟動並執行進程 IDs。cbNeeded 返回數組所用的記憶體大小。下面算式可以得出返回了多少進程:nReturned = cbNeeded / sizeof(DWORD)。
注意:雖然文檔將返回的 DWORD 命名為“cbNeeded”,實際上是沒有辦法知道到底要傳多大的數組的。EnumProcesses 根本不會在 cbNeeded 中返回一個大於 cb 參數傳遞的數組值。所以,唯一確保 EnumProcesses 函數成功的方法是分配一個 DWORD 數組,並且,如果返回的 cbNeeded 等於 cb,分配一個較大的數組,並不停地嘗試直到 cbNeeded 小於 cb 。
下面是參考代碼:
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include "psapi.h"
#pragma comment(lib,"psapi.lib")
void PrintProcessNameAndID(DWORD processID)
{
TCHAR szProcessName[MAX_PATH] = _T("<unknown>");
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS/* | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ*/,FALSE,processID);
//Process name.
if(NULL!=hProcess)
{
HMODULE hMod;
DWORD cbNeeded;
if(EnumProcessModules(hProcess,&hMod,sizeof(hMod), &cbNeeded))
{
GetModuleBaseName(hProcess,hMod,szProcessName,sizeof(szProcessName)/sizeof(TCHAR));
}
}
wprintf(_T("PID: %d (%s) "),processID,szProcessName);
CloseHandle(hProcess);
}
void main( )
{
DWORD aProcesses[1024], cbNeeded, cProcesses;
unsigned int i;
if(!EnumProcesses(aProcesses,sizeof(aProcesses),&cbNeeded))
return;
cProcesses = cbNeeded/sizeof(DWORD);
for(i=0;i<cProcesses;i++)
PrintProcessNameAndID(aProcesses[i]);
getchar();
}
注意到,此方法由於需要進行 OpenProcess 操作,所以需要一定的許可權,當許可權不夠時,有些進程將不能被開啟。下面給出提升許可權的相關代碼:
void RaisePrivilege()
{
HANDLE hToken;
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if(OpenProcessToken(GetCurrentProcess(),TOKEN_ALL_ACCESS,&hToken))
{
if(LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&tp.Privileges[0].Luid))
{
AdjustTokenPrivileges(hToken,FALSE,&tp,NULL,NULL,0);
}
}
if(hToken)
CloseHandle(hToken);
}
使用 Native API
在 使用Native API 探測本機系統資訊 中我介紹了 Native API 中的 NtQuerySystemInformation(ZwQuerySystemInformation)。當設定查詢的資訊類型為 SystemProcessesAndThreadsInformation 時(第5號功能),可以用來枚舉所有進程和線程。
提醒:這個函數屬於 Undocumented API,並且不建議使用,因為不同系統的結構和常量有所不同。下面列出 Windows XP 下可以用的相關結構和常量:
typedef DWORD (WINAPI *ZWQUERYSYSTEMINFORMATION)(DWORD, PVOID, DWORD, PDWORD);
typedef struct _SYSTEM_PROCESS_INFORMATION
{
DWORD NextEntryDelta;
DWORD ThreadCount;
DWORD Reserved1[6];
FILETIME ftCreateTime;
FILETIME ftUserTime;
FILETIME ftKernelTime;
UNICODE_STRING ProcessName;
DWORD BasePriority;
DWORD ProcessId;
DWORD InheritedFromProcessId;
DWORD HandleCount;
DWORD Reserved2[2];
DWORD VmCounters;
DWORD dCommitCharge;
PVOID ThreadInfos[1];
}SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
#define SystemProcessesAndThreadsInformation 5
然後動態載入 ntdll.dll,獲得函數的地址。便可以進行進程的枚舉相關代碼如下:
#include <windows.h>
#include <ntsecapi.h>
#include <stdio.h>
typedef DWORD (WINAPI *ZWQUERYSYSTEMINFORMATION)(DWORD, PVOID, DWORD, PDWORD);
typedef struct _SYSTEM_PROCESS_INFORMATION
{
DWORD NextEntryDelta;
DWORD ThreadCount;
DWORD Reserved1[6];
FILETIME ftCreateTime;
FILETIME ftUserTime;
FILETIME ftKernelTime;
UNICODE_STRING ProcessName;
DWORD BasePriority;
DWORD ProcessId;
DWORD InheritedFromProcessId;
DWORD HandleCount;
DWORD Reserved2[2];
DWORD VmCounters;
DWORD dCommitCharge;
PVOID ThreadInfos[1];
}SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
#define SystemProcessesAndThreadsInformation 5
void main()
{
HMODULE hNtDll = GetModuleHandle(L"ntdll.dll");
if(!hNtDll)
return;
ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)GetProcAddress(hNtDll,"ZwQuerySystemInformation");
ULONG cbBuffer = 0x10000;
LPVOID pBuffer = NULL;
pBuffer = malloc(cbBuffer);
if(pBuffer == NULL)
return;
ZwQuerySystemInformation(SystemProcessesAndThreadsInformation,pBuffer,cbBuffer,NULL);
PSYSTEM_PROCESS_INFORMATION pInfo = (PSYSTEM_PROCESS_INFORMATION)pBuffer;
for(;;)
{
wprintf(L"PID: %d (%ls) ",pInfo->ProcessId,pInfo->ProcessName.Buffer);
if(pInfo->NextEntryDelta == 0)
break;
pInfo = (PSYSTEM_PROCESS_INFORMATION)(((PUCHAR)pInfo) + pInfo->NextEntryDelta);
}
free(pBuffer);
getchar();
}
對這個方法有問題的,可以參考我之前的那篇介紹 Native API 的文章。
同樣使用 ZwQuerySystemInformation 函數,查詢類型如果設定為 SystemHandleInformation(第16號功能)也可以達到目的。它能擷取系統中所有控制代碼,再加上進程 ID 的判斷就可以枚舉所有進程了。
#include <windows.h>
#include <ntsecapi.h>
#include <ntstatus.h>
#include <stdio.h>
typedef NTSTATUS (WINAPI *ZWQUERYSYSTEMINFORMATION)(DWORD, PVOID, DWORD, PDWORD);
typedef struct _SYSTEM_HANDLE_INFORMATION
{
ULONG ProcessId;
UCHAR ObjectTypeNumber;
UCHAR Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
}SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;
typedef struct _SYSTEM_HANDLE_INFORMATION_EX
{
ULONG NumberOfHandles;
SYSTEM_HANDLE_INFORMATION Information[1];
}SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX;
#define SystemHandleInformation 0x10 //16
void main()
{
HMODULE hNtDll = LoadLibrary(L"ntdll.dll");
if(!hNtDll)
return;
ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)GetProcAddress(hNtDll,"ZwQuerySystemInformation");
ULONG cbBuffer = 0x4000;
LPVOID pBuffer = NULL;
NTSTATUS s;
do
{
pBuffer = malloc(cbBuffer);
if(pBuffer == NULL)
return;
memset(pBuffer,0,cbBuffer);
s = ZwQuerySystemInformation(SystemHandleInformation,pBuffer,cbBuffer,NULL);
if(s == STATUS_INFO_LENGTH_MISMATCH)
{
free(pBuffer);
cbBuffer = cbBuffer * 2;
}
}while(s == STATUS_INFO_LENGTH_MISMATCH);
PSYSTEM_HANDLE_INFORMATION_EX pInfo = (PSYSTEM_HANDLE_INFORMATION_EX)pBuffer;
ULONG OldPID = 0;
for(DWORD i = 0;i<pInfo->NumberOfHandles;i++)
{
if(OldPID != pInfo->Information[i].ProcessId)
{
OldPID = pInfo->Information[i].ProcessId;
wprintf(L"PID: %d ",OldPID);
}
}
free(pBuffer);
FreeLibrary(hNtDll);
getchar();
}
原文中提到,在進行進程“隱藏”工作的時候,此處的控制代碼是一件容易被忽略的地方,因此需要注意隱藏由程式開啟的相關控制代碼。由於系統中控制代碼計數經常變換,所以沒有什麼必要修改其中的 NumberOfHandles 域,因為如果修改此處的值,則需要不停對控制代碼的變化進行維護,開銷比較大。在使用者態下的進程枚舉已經變得不可靠,因為一個核心級的 Rootkit 很容易就能夠更改這些函數的返回結果。所以進程的可靠枚舉應在核心態中實現,可以通過編寫驅動來實現。
有關16位程式
根據參考的第二篇文章:在 Windows 95,Windows 98 和 Windows ME 中,ToolHelp32 對待16位程式一視同仁,它們與 Win32 程式一樣有自己的進程 IDs。但是在 Windows NT,Windows 2000 或 Windows XP 中情況並不是這樣。在這些作業系統中,16位程式運行在所謂的 VDM 當中(也就是DOS機)。
為了在 Windows NT,Windows 2000 和 Windows XP 中枚舉16位程式,必須使用一個名為 VDMEnumTaskWOWEx 的函數。它的聲明包含在 Windows SDK 中的 VDMDBG.h 中,並且需要在項目中連結 VDMDBG.lib 檔案。
微軟的網上協助裡面有一篇介紹的文章:如何在 Windows NT、 Windows 2000 和 Windows XP 上使用 VDMDBG 函數。