遍历进程主要可以用到三个小点:
1. 使用CreateToolhelp32Snapshot
函数创建系统快照
这个函数可以捕获系统当前状态的一个快照,包括所有运行中的进程、线程、模块等。为了遍历进程,需要指定TH32CS_SNAPPROCESS
标志。
他有两个参数,一个是要遍历的对象,另一个是ID信息。
HANDLE CreateToolhelp32Snapshot(
DWORD dwFlags,
DWORD th32ProcessID
);
dwFlags
(DWORD
): 指定快照中包含的内容。这是一个位字段的标志,可以组合使用。常用的标志包括:
-
TH32CS_SNAPPROCESS
: 包含系统中所有进程的快照。 -
TH32CS_SNAPTHREAD
: 包含系统中所有线程的快照。 -
TH32CS_SNAPMODULE
: 包含指定进程关联的所有模块(DLL和驱动程序)的快照。要使用此标志,th32ProcessID
参数必须是有效的进程标识符。 -
TH32CS_SNAPHEAPLIST
: 包含指定进程的所有堆的快照。同样,需要有效的进程标识符。 -
TH32CS_SNAPALL
: 包括所有以上提到的快照。 -
TH32CS_INHERIT
: 指示返回的快照句柄是可继承的。
th32ProcessID
(DWORD
): 指定要获取快照的进程的进程标识符(PID)。如果dwFlags
参数包括TH32CS_SNAPMODULE
或TH32CS_SNAPHEAPLIST
,则此参数必须是目标进程的PID。对于其他类型的快照,通常设置为0,表示获取所有进程的信息。
以下每一项在api的调用非常关键:
使用注意事项
- 使用完成后,应通过调用
CloseHandle
函数来关闭快照句柄,避免资源泄漏。- 快照是在特定时间点捕获的系统状态的静态视图。系统状态在快照之后可能会发生变化。
- 在遍历模块或堆时,需要确保目标进程的PID有效,否则快照将不会包含这些信息。
- 该函数可能会因权限不足而失败,特别是尝试访问系统进程或高权限进程的模块和堆信息时。
2.Process32FirstW
Process32FirstW
函数是 Windows API 的一部分,用于遍历系统中的进程。此函数与 CreateToolhelp32Snapshot
函数配合使用,CreateToolhelp32Snapshot
函数用于创建系统当前状态的快照,而 Process32FirstW
则用于获取快照中的第一个进程的信息。Process32FirstW
的 "W" 表示该函数用于处理宽字符(Unicode)字符串,与之对应的还有 Process32First
函数,用于处理ANSI字符串。
3. Process32NextW
Process32NextW
函数是 Windows API 的一部分,用于继续遍历 CreateToolhelp32Snapshot
函数创建的快照中的进程信息。在调用 Process32FirstW
获取快照中第一个进程的信息之后,Process32NextW
被用来遍历和获取快照中剩余进程的信息。同样地,Process32NextW
的 "W" 后缀表示该函数处理宽字符(Unicode)字符串。
在Windows操作系统中,使用
CreateToolhelp32Snapshot
、Process32FirstW
和Process32NextW
这一组API来遍历系统进程时,进程被遍历的顺序并没有明确的、文档化的保证。这意味着Windows并没有承诺会按照某种特定顺序(如进程ID排序、创建时间顺序等)来返回进程信息。实际上,进程的遍历顺序取决于操作系统的内部实现细节,这些细节可能会在不同版本的Windows中有所不同,甚至可能会在相同版本的Windows中因为更新或配置的改变而有所变化。
代码示例:
#include <windows.h>
#include <tlhelp32.h>
#include <iostream>
// 函数声明:遍历所有进程并打印信息
void EnumerateProcesses() {
// 创建系统快照
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
std::wcerr << L"CreateToolhelp32Snapshot failed." << std::endl;
return;
}
PROCESSENTRY32W pe32; //pe32中有一些信息可供使用者参考,在这加个关键判断
pe32.dwSize = sizeof(PROCESSENTRY32W);
// 获取快照中的第一个进程
if (!Process32FirstW(hSnapshot, &pe32)) {
std::wcerr << L"Process32First failed." << std::endl;
CloseHandle(hSnapshot);
return;
}
// 遍历所有进程
do {
// 输出进程的ID和名称(使用宽字符)
// 此处可以用ID打开此进程详细信息,OpenProcess
std::wcout << L"Process ID: " << pe32.th32ProcessID << L"\tProcess Name: " << pe32.szExeFile << std::endl;
} while (Process32NextW(hSnapshot, &pe32));
CloseHandle(hSnapshot);
}
int main() {
EnumerateProcesses();
return 0;
}
4. PROCESSENTRY32W
PROCESSENTRY32W pe32; 中PROCESSENTRY32W结构介绍:
typedef struct tagPROCESSENTRY32W {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID;
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID;
LONG pcPriClassBase;
DWORD dwFlags;
WCHAR szExeFile[MAX_PATH];
} PROCESSENTRY32W;
成员介绍
-
dwSize
: 结构体的大小,以字节为单位。在调用Process32FirstW
或Process32NextW
之前,必须设置为sizeof(PROCESSENTRY32W)
。 -
cntUsage
: 进程的引用计数(不再被广泛使用,通常为0)。 -
th32ProcessID
: 进程的唯一标识符,即PID。 -
th32DefaultHeapID
: 进程默认堆的标识符(在大多数现代Windows版本中不再使用,一般不必关注)。 -
th32ModuleID
: 进程的模块标识符(不再被广泛使用,通常为0)。 -
cntThreads
: 进程中的线程数。 -
th32ParentProcessID
: 创建此进程的父进程的PID。 -
pcPriClassBase
: 进程的基本优先级。 -
dwFlags
: 进程的标志(目前没有广泛使用的标志,通常为0)。 -
szExeFile
: 进程的可执行文件名,使用宽字符表示。MAX_PATH
通常定义为 260,这意味着路径名最多可以包含259个字符加上一个终止的空字符。
szExeFile就是进程exe的名称,例如UYT.exe这种。
th32ParentProcessID这个可以知道是哪个进程ID创建的该进程,当然是上个时刻的进程此刻可能已经消亡,因此需要保证父进程一直不死才有利用价值,需要斟酌。
5. OpenProcess
HANDLE OpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwProcessId
);
-
dwDesiredAccess
(DWORD
): 访问权限标志,指定打开对象时所需的访问权限。这些权限控制对进程的访问类型。常见的访问权限包括:-
PROCESS_ALL_ACCESS
: 请求对进程的所有可能的访问权限。 -
PROCESS_QUERY_INFORMATION
: 需要此权限来使用GetExitCodeProcess
和GetPriorityClass
。 -
PROCESS_VM_READ
: 使用此权限可读取进程的虚拟内存,例如,通过ReadProcessMemory
。 -
PROCESS_VM_WRITE
和PROCESS_VM_OPERATION
: 写入权限和进行内存操作所需的权限,如WriteProcessMemory
。 - 更多权限选项可在Microsoft官方文档中找到。
-
-
bInheritHandle
(BOOL
): 指示返回的句柄是否可以被进程的子进程继承。如果此参数为TRUE
,句柄是可继承的。 -
dwProcessId
(DWORD
): 要打开的进程的标识符(PID)。每个进程在系统中都有一个唯一的标识符,可以通过任务管理器、CreateToolhelp32Snapshot
函数等方法获得。
- 指定的访问权限应仅限于应用程序实际需要的,过多的权限可能会增加安全风险。
- 即使拥有足够的权限,某些进程(特别是系统进程或拥有更高权限的进程)可能仍然无法打开。
- 使用完毕后,应通过调用
CloseHandle
函数关闭句柄,以避免资源泄露。PROCESS_ALL_ACCESS
在不同版本的Windows中可能有不同的定义,因此在跨版本使用时应谨慎。
HANDLE hProcess = OpenProcess(1,2,3),拿到这个hProcess,我们可以对进程干很多事情。
hProcess
1. 查询信息
-
进程退出代码:使用
GetExitCodeProcess
函数,你可以获取进程的退出代码,前提是进程已结束。 -
进程优先级类:
GetPriorityClass
函数可以返回进程的优先级类别。 -
进程工作集信息:
GetProcessWorkingSetSize
函数用于获取进程的最小和最大工作集大小。 -
进程时间信息:
GetProcessTimes
函数提供了进程的创建时间、退出时间、内核模式时间和用户模式时间。 -
进程内存使用信息:
GetProcessMemoryInfo
函数(需包含头文件"psapi.h"
并链接Psapi.lib
库)可以获取进程的内存使用情况,如工作集大小、页面错误次数等。 -
进程令牌:
OpenProcessToken
函数可以打开进程的访问令牌,用于获取或修改进程的安全属性,如权限等。 -
进程I/O统计数据:
GetProcessIoCounters
函数提供了进程的I/O操作统计数据,包括读写操作的次数和字节数。 -
进程亲和力:
GetProcessAffinityMask
函数用于获取进程的处理器亲和力设置,即进程可以在哪些处理器上运行。
-
查询内存统计信息:
QueryWorkingSet
和QueryWorkingSetEx
函数可以用来查询进程的工作集中页面的信息。这可以帮助开发者理解哪些页面被载入到物理内存中。 -
进程的虚拟内存信息:
VirtualQueryEx
函数可以查询指定进程的虚拟地址空间的状态信息,包括区域的保护属性、物理内存占用等。 -
获取进程环境:虽然没有直接的API来获取另一个进程的完整环境块,但可以通过读取进程内存的方式(结合
ReadProcessMemory
和进程PEB结构)来间接获得。 -
获取进程的可执行文件路径:
QueryFullProcessImageName
函数可以获取进程的完整路径。这对于确定正在运行的程序非常有用。 -
进程性能信息:
GetPerformanceInfo
函数可以提供系统或进程的性能信息,如缓存的统计数据、物理和虚拟内存的使用情况等。 -
查询进程安全信息:
GetSecurityInfo
和GetKernelObjectSecurity
函数允许查询进程对象的安全描述符,包括所有者、DACL(访问控制列表)等信息。 -
修改进程DACL:结合
SetSecurityInfo
或SetKernelObjectSecurity
可以修改进程的安全描述符,调整其访问控制策略。 -
枚举进程句柄:
NtQuerySystemInformation
(未公开文档的函数,可通过动态链接)和NtQueryObject
(同样未公开)可以用来枚举进程打开的句柄和句柄指向的对象类型,虽然这需要一定的权限和稳定性考虑。 -
进程完整性级别:通过
GetTokenInformation
函数(和进程令牌一起使用)可以查询进程的完整性级别,这对于理解进程的安全上下文很重要。 -
获取和设置进程的DEP(数据执行防止)设置:
GetProcessDEPPolicy
和SetProcessDEPPolicy
函数用于查询和配置进程的DEP策略,增强安全性。 -
进程句柄计数:
GetProcessHandleCount
函数可以获取进程当前打开的句柄数量。
信息提供了对进程更深入的监控和管理能力。使用这些API时,需要确保你的程序拥有相应的权限来访问进程信息,尤其是对于那些可能会改变进程状态或安全设置的操作。开发时还应注意API的版本兼容性和适用性,因为某些API可能在不同版本的Windows上表现不同,或者需要特定的系统权限。
2. 修改进程状态
-
设置进程优先级:
SetPriorityClass
函数允许你改变进程的优先级类别。 -
调整工作集大小:
SetProcessWorkingSetSize
函数可以调整进程的工作集大小,这是进程可以使用的物理内存量的最小值和最大值。 -
修改亲和力:
SetProcessAffinityMask
函数用于设置进程的处理器亲和力,指定进程可以运行的CPU核心。 -
终止进程:
TerminateProcess
函数可以用于强制结束进程。 -
挂起和恢复进程:通过
NtSuspendProcess
和NtResumeProcess
函数(这些函数不直接在Windows API文档中公开,但通过动态调用可用)来挂起和恢复进程的执行。
-
注入DLL:
CreateRemoteThread
结合LoadLibrary
方法可以用来向目标进程注入DLL。这通常通过创建一个远程线程来执行LoadLibrary
函数,并将DLL路径作为参数传递给目标进程的地址空间中。 -
卸载DLL:类似地,可以通过创建一个远程线程来调用
FreeLibrary
,以从目标进程中卸载之前注入的DLL。
-
修改内存保护:
VirtualProtectEx
函数用于改变进程虚拟地址空间中一页或多页的内存保护属性。这在修改代码段或需要特殊内存保护的情况下非常有用。 -
分配和释放内存:
VirtualAllocEx
和VirtualFreeEx
函数可以在目标进程的地址空间内分配或释放内存区域。这对于注入代码或数据到进程中特别有用。 -
调整权限:如果拥有对进程的访问令牌句柄,可以使用
AdjustTokenPrivileges
函数来启用或禁用指定的权限。这对于执行需要高权限的操作(比如关机)是必需的。 -
更改进程安全性:使用
SetSecurityInfo
或SetKernelObjectSecurity
函数可以修改进程对象的安全描述符,包括更改其DACL来控制对进程的访问。 -
创建进程:
CreateProcess
函数可以用于创建新的进程。虽然这不是直接修改一个现有进程的状态,但你可以通过创建进程时的继承选项和启动参数来间接影响进程行为。 -
进程组和作业:通过将进程添加到作业对象(使用
AssignProcessToJobObject
函数),可以对一组进程施加限制或策略,如CPU时间限制、工作集大小限制等。这可以间接修改进程的运行状态。
-
调试进程:使用
DebugActiveProcess
函数可以附加到一个正在运行的进程上作为其调试器。这允许监视和控制目标进程的执行,包括在发生异常时接收通知。 -
生成异常:
RaiseException
函数可以在当前进程中引发异常。如果有调试器附加到进程,这可能会改变进程的执行流程。 -
设置IO优先级:通过
NtSetInformationProcess
函数(未公开文档的函数,可通过动态链接)可以设置进程的IO优先级。这对于控制进程对IO资源的使用非常有用。
3. 访问进程虚拟内存
-
读取内存:
ReadProcessMemory
函数允许从打开的进程中的指定内存地址读取数据。 -
写入内存:
WriteProcessMemory
函数可以修改打开的进程中的指定内存地址处的数据。 -
查询内存信息:
VirtualQueryEx
函数用于查询进程的虚拟地址空间中的信息。
-
分配内存:
-
VirtualAllocEx
:在目标进程的虚拟地址空间内分配内存。这对于在进程中注入数据或代码非常有用。
-
-
释放内存:
-
VirtualFreeEx
:用于释放或解除目标进程虚拟地址空间内的内存区域。与VirtualAllocEx
配合使用,可以管理进程内存的生命周期。
-
-
修改内存保护:
-
VirtualProtectEx
:改变目标进程中一页或多页内存区域的保护属性。这在修改执行代码或需要调整内存访问权限时非常重要。
-
-
内存映射和模块信息:
-
EnumProcessModules
和EnumProcessModulesEx
:这些函数用于列出进程加载的所有模块(DLL文件和可执行文件)。可以用于获取模块基址,进而读取或修改模块内的数据。 -
GetModuleInformation
:获取指定模块的信息,如其大小、入口点和模块基址。
-
-
详细内存状态和统计:
-
GetProcessMemoryInfo
:提供了进程的内存使用信息,包括页面错误次数、工作集大小等。需要包含"psapi.h"
头文件并链接Psapi.lib
。 -
GlobalMemoryStatusEx
:虽然不是特定于单一进程,但此函数提供了系统级别的内存使用情况,包括总体物理和虚拟内存使用量。
-
-
内存优化:
-
EmptyWorkingSet
:将指定进程的工作集(即常驻内存集)清空,强制页面文件出所有物理内存页。这可以在某些情况下用来减少进程的物理内存需求,但可能会增加页面错误和性能开销。
-
-
内存页面优先级:
-
SetProcessWorkingSetSize
:除了调整工作集大小外,这个函数还可以影响进程的页面优先级,间接影响其内存页是否被交换出物理内存。
-
- 查询和修改内存:
-
NtQueryVirtualMemory
:虽然未公开文档,但通过动态调用可以使用,用于获取关于进程虚拟内存的更详细信息,如内存区域的状态、类型和访问权限。 -
NtWriteVirtualMemory
和NtReadVirtualMemory
:同样是未公开的函数,通过动态调用提供更底层的内存读写能力,可能在某些特殊情况下被用于调试或逆向工程。
4. 处理和线程
-
创建线程:
CreateRemoteThread
函数允许在打开的进程中创建一个新线程。 -
调整令牌权限:使用进程令牌句柄(通过
OpenProcessToken
获取)配合AdjustTokenPrivileges
函数可以修改进程令牌的权限,这对于执行需要提升的操作非常重要。 -
挂起和恢复线程:
-
SuspendThread
和ResumeThread
:这两个函数可以挂起和恢复单个线程的执行。这对于调试、单步执行或在修改线程上下文之前暂停线程执行非常有用。
-
-
设置线程优先级:
-
SetThreadPriority
: 可以调整线程的执行优先级。这对于控制应用程序中不同线程的执行相对重要性非常有用。
-
-
获取和设置线程上下文:
-
GetThreadContext
和SetThreadContext
:这些函数允许获取和设置线程的寄存器等上下文信息,通常用于调试器中。
-
-
结束线程:
-
TerminateThread
:强制结束指定线程。使用时需要谨慎,因为它不会保证线程资源的正确清理。
-
-
获取安全令牌信息:
-
GetTokenInformation
:可以获取进程令牌的详细信息,包括但不限于权限、用户账号信息和完整性级别。
-
-
创建进程与令牌:
-
CreateProcessAsUser
:使用指定用户令牌创建新进程。这对于实现在不同用户上下文中执行操作非常有用。
-
-
令牌复制和修改:
-
DuplicateTokenEx
:创建现有令牌的新副本,并可以设置新令牌的访问级别。这在需要修改令牌权限以执行特定操作时非常有用。
-
-
高级进程创建:
-
CreateProcessWithTokenW
和CreateProcessWithLogonW
:这些函数允许以特定用户身份创建新进程,提供更细粒度的控制,例如指定日志记录用户或使用特定的令牌。
-
-
线程本地存储(TLS):
-
TlsAlloc
,TlsGetValue
,TlsSetValue
,TlsFree
:这组函数用于管理线程本地存储,允许线程存储和检索对于每个线程都是唯一的数据。
-
-
进程和线程同步:
- 同步对象如互斥体(
Mutex
)、信号量(Semaphore
)、事件(Event
)和临界区(CriticalSection
)等可以在进程和线程之间同步操作。
- 同步对象如互斥体(
-
进程间通信(IPC):
- 管道(
Pipes
)、邮槽(Mailslots
)、共享内存和远程过程调用(RPC
)等机制允许在不同进程之间交换数据。
- 管道(