进程实例身份、父子关系与会话归属
2026/7/31 13:57:33 网站建设 项目流程

进程实例身份、父子关系与会话归属

欢迎参考开源项目实现:KswordARK

https://github.com/KSwordDEV/KSword

目标、对象关系与四步流程

本篇要得到的结果是:在一次只读采样中,为当前每个进程建立可区分的实例记录,并输出创建时间能够验证的父子候选关系。每条记录保留 PID、父 PID、创建时间、会话 ID、快照名称、可读取的主映像路径和父边状态。PID(Process Identifier,进程标识符)是内核为存活进程分配的数字。进程退出后,系统可以把同一数字分配给后来的进程,因此 PID 只描述某个时段的标识槽位。创建时间是进程对象建立时记录的 100ns 时间值,PID 与创建时间组合后才能描述一次进程实例。

要完成这个结果,分为四步:

  1. 创建进程快照,收集 PID、父 PID 和快照名称。
  2. 按最小查询权限读取创建时间、会话 ID 和主映像路径。
  3. (PID, 创建时间)区分实例,再按父 PID 与时间先后建立候选父子边。
  4. 输出已验证关系和无法验证的原因,并保留会话 ID 与主映像路径等辅助证据。

对象之间的顺序为:进程快照 -> 进程对象查询 -> 实例键(PID, 创建时间)-> 父子候选边 -> 会话和映像等辅助证据。

进程快照负责固定采样时刻的原始 PID 列表。进程对象查询负责补齐创建时间、会话 ID 和主映像路径。实例键负责区分同一 PID 的不同生命周期。父子候选边负责表达这次采样能够验证到的创建关系。

一、第一步:理解进程对象并采集原始快照

进程对象由内核在创建过程建立,直到最后一个对象引用释放后销毁。线程对象属于进程的执行单元。进程可以有零个或多个线程。映像是由 Windows 映像加载器映射到进程地址空间的 PE 文件,主映像路径描述该映像来源。进程名称只是快照中的显示文本,主映像路径才是文件系统关联的候选位置。

父 PID 来自创建时的继承关系。创建子进程的调用方通常成为父进程,父进程可在子进程仍存活时退出。父 PID 也可能在父进程退出后被复用,因此“子记录中的父 PID”等于“当前快照某记录的 PID”只表示数字相同。将关系标为候选需要同时检查父、子创建时间:父实例创建时间必须早于或等于子实例创建时间。

进程树由许多候选边组成。服务管理器、计划任务、远程桌面、应用容器、调试器和启动器都可能影响创建链的表现。树形显示是一种观察方式,不能替代系统审计事件或实际CreateProcess调用记录。

1.1 先区分进程与其它关联对象

Session(会话)是 Windows 为交互用户、远程桌面连接和系统服务划分的运行范围。Session 0 常容纳服务和系统进程,交互用户会话通常拥有其它会话编号。Session ID 由进程对象保存,用于解释桌面和用户隔离。相同会话中的两个进程没有天然父子关系,跨会话进程也可能存在创建关系。

访问令牌(access token)是内核安全对象,保存用户 SID、组 SID、权限、完整性级别、会话标识和限制信息。进程主令牌决定默认安全上下文,线程可以临时使用模拟令牌。读取令牌需要先以OpenProcessToken获得一个真实 token HANDLE,再用GetTokenInformation查询指定信息类,最后调用CloseHandle

Job 对象是内核的资源与生命周期管理容器。一个 Job 可以包含多个进程,并施加 CPU、内存、终止和子进程限制。进程属于同一个 Job 说明它们受同一容器策略管理,却不说明它们有父子关系。IsProcessInJob可以查询一个进程是否属于指定 Job 或任意 Job。

服务是 Service Control Manager(SCM,服务控制管理器)数据库中的配置对象。服务配置中的二进制路径和运行账户可启动一个服务进程。服务进程的父 PID 常反映服务宿主和启动时序,服务对象与进程对象之间仍需通过服务状态、PID 和创建时间关联。将某个svchost.exe名称直接归属于特定服务会遗漏宿主组和运行时变化。

1.2 快照先提供哪些字段

Toolhelp 进程快照是 Windows 提供的一份进程列表副本。PROCESSENTRY32W中的th32ProcessID是 PID,th32ParentProcessID是父 PID,szExeFile是快照名称。快照 HANDLE 由调用方拥有,完成枚举后调用CloseHandledwSize必须在每次Process32FirstWProcess32NextW前填入结构字节数,API 以它判断调用方结构版本。

// 作用:创建系统进程快照。// 返回:有效 HANDLE 成功。INVALID_HANDLE_VALUE 失败,读取 GetLastError。成功后必须 CloseHandle。HANDLECreateToolhelp32Snapshot(DWORD dwFlags,// 输入:TH32CS_SNAPPROCESS 请求进程记录DWORD th32ProcessID// 输入:进程快照使用 0。线程/模块快照有不同含义);// 作用:读取快照中第一条进程记录。// 返回:非零成功。0 失败,读取 GetLastError。lppe 不能为 nullptr,dwSize 必须预设。BOOLProcess32FirstW(HANDLE hSnapshot,// 输入:CreateToolhelp32Snapshot 成功返回的快照 HANDLELPPROCESSENTRY32W lppe// 输入/输出:调用方结构,szExeFile 为 UTF-16 输出数组);// 作用:读取下一条进程记录。// 返回:非零成功。0 且 ERROR_NO_MORE_FILES 表示正常结束,其它错误表示枚举中断。BOOLProcess32NextW(HANDLE hSnapshot,// 输入:有效快照 HANDLELPPROCESSENTRY32W lppe// 输入/输出:每次调用前重新设置 dwSize 的结构);

快照创建完成后,进程仍可能退出或新建。Process32NextW正常结束时读取GetLastError()==ERROR_NO_MORE_FILES。其他错误说明本轮枚举停止。快照名称和父 PID 属于该快照时刻,后续实例查询失败时仍可保留原始快照字段,同时标注创建时间或会话不可读。

第一步完成后,已经有了同一时刻的 PID、父 PID 和名称,但这些字段无法区分 PID 复用前后的两个进程。接下来要读取进程对象中的创建时间和会话信息,使每条快照记录具备实例身份。

二、第二步:按最小权限补齐实例身份

2.1 正确做法:读取创建时间和会话 ID

OpenProcess向对象管理器请求进程对象的访问引用。PROCESS_QUERY_LIMITED_INFORMATION适合读取基础查询信息,访问检查仍由目标进程安全描述符决定。受保护进程、已退出进程和权限限制会导致打开失败。失败时保存错误码并继续处理其他快照项。成功 HANDLE 由调用方CloseHandle

// 作用:按 PID 打开进程对象。// 返回:有效 HANDLE 成功。nullptr 失败,读取 GetLastError。成功后 CloseHandle。HANDLEOpenProcess(DWORD dwDesiredAccess,// 输入:PROCESS_QUERY_LIMITED_INFORMATION 等访问掩码BOOL bInheritHandle,// 输入:FALSE 禁止子进程继承此调查 HANDLEDWORD dwProcessId// 输入:目标 PID。调用前后都可能退出或复用);// 作用:读取进程创建、退出、内核 CPU 与用户 CPU 时间。// 返回:非零成功。0 失败,读取 GetLastError。四个 FILETIME 输出指针均不能为 nullptr。BOOLGetProcessTimes(HANDLE hProcess,LPFILETIME lpCreationTime,LPFILETIME lpExitTime,LPFILETIME lpKernelTime,LPFILETIME lpUserTime);// 作用:读取 PID 所属 Windows 会话。// 返回:非零成功。0 失败,读取 GetLastError。pSessionId 不能为 nullptr。BOOLProcessIdToSessionId(DWORD dwProcessId,DWORD*pSessionId);

FILETIME由两个 32 位部分组成,表示自 1601-01-01 起的 100ns 计数。将dwHighDateTimedwLowDateTime合成 64 位数值后比较先后,避免按格式化日期字符串比较。创建时间、PID、快照时间和查询状态应一起保存。

HANDLE process=OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,FALSE,pid);if(process!=nullptr){FILETIME created{},exited{},kernel{},user{};if(GetProcessTimes(process,&created,&exited,&kernel,&user)){ULARGE_INTEGER value{};value.LowPart=created.dwLowDateTime;value.HighPart=created.dwHighDateTime;instanceCreation100ns=value.QuadPart;}CloseHandle(process);// 本分支拥有 process,关闭后不得再传入查询 API。}DWORD session=0;if(!ProcessIdToSessionId(pid,&session)){DWORD error=GetLastError();// 立刻保存,日志格式化可能覆盖错误槽。}

2.2 读取主映像路径并区分独立关联

主映像路径通过QueryFullProcessImageNameW读取。lpdwSize输入时是wchar_t字符容量,成功返回时是实际字符数且不含结尾 NUL。路径缓冲区不足时 API 返回零并设置ERROR_INSUFFICIENT_BUFFER,调用方扩容后重试。得到路径只说明当前进程主映像的文件名,文件存在性、签名和加载模块列表属于后续查询。

// 作用:读取进程主映像完整路径。// 返回:非零成功。0 失败,读取 GetLastError。输出缓冲区由调用方拥有。BOOLQueryFullProcessImageNameW(HANDLE hProcess,// 输入:有查询权限的进程 HANDLEDWORD dwFlags,// 输入:0 返回 Win32 路径。PROCESS_NAME_NATIVE 返回 NT 路径LPWSTR lpExeName,// 输出:UTF-16 缓冲区。不能为 nullptrPDWORD lpdwSize// 输入容量、输出实际字符数。单位 wchar_t,不能为 nullptr);// 作用:打开进程的主访问令牌。// 返回:非零成功。0 失败,读取 GetLastError。成功 token HANDLE 由调用方 CloseHandle。BOOLOpenProcessToken(HANDLE ProcessHandle,// 输入:进程 HANDLE,须有 PROCESS_QUERY_LIMITED_INFORMATIONDWORD DesiredAccess,// 输入:TOKEN_QUERY 等令牌访问掩码PHANDLE TokenHandle// 输出:成功时 token HANDLE。不能为 nullptr);// 作用:判断进程是否属于指定 Job 或任意 Job。// 返回:非零表示调用成功。0 失败,读取 GetLastError。Result 不能为 nullptr。BOOLIsProcessInJob(HANDLE ProcessHandle,// 输入:可查询进程 HANDLEHANDLE JobHandle,// 输入:nullptr 查询是否属于任意 Job。非空为指定 Job HANDLEPBOOL Result// 输出:TRUE/FALSE 归属结果);

令牌查询时,TOKEN_QUERY允许读取令牌信息。GetTokenInformation使用两阶段缓冲区:第一次以TokenInformation=nullptr查询所需字节数,常见失败为ERROR_INSUFFICIENT_BUFFER。第二次分配精确字节数后读取。返回缓冲区里的 SID、组和权限指针属于该缓冲区,复制前不能释放。Job 查询结果为 TRUE 表示归属,FALSE 表示不归属。API 返回零才是查询失败。

第二步完成后,每条记录都已尝试补齐创建时间、会话 ID 和主映像路径,并明确标记不可读取的字段。下一步需要把子记录中的父 PID 与同一批记录进行匹配,再用创建时间排除明显不成立的关系。

三、第三步:用实例键建立父子候选边

3.1 比较父、子实例的 PID 与创建时间

对每个子实例,在同一批实例记录中查找parentPid。父实例存在、父创建时间可读、子创建时间可读、父创建时间不晚于子创建时间时,建立候选边。父 PID 缺失、父 PID 记录创建时间晚于子实例、任一时间查询失败时,保存“父边不可验证”状态。服务、Job、令牌和会话字段作为边或节点属性附加,不能取代时间顺序。

同一会话可以有大量无关进程。同一 Job 可以包含多代进程。相同令牌用户可以启动多个无关程序。相同映像路径可对应多次启动实例。这些关系帮助解释进程来源和限制,父子边仍需依赖 PID 与创建时间。

第三步完成后,创建时间顺序成立的记录已经形成候选父子边。其余记录不能被强行归类,需要在输出中保留父进程已退出、PID 已复用或字段不可读等原因,同时把其它对象关系作为单独证据。

四、第四步:输出证据状态并说明关联边界

4.1 最小错误示范

以下映射只保存数字 PID,PID 复用后会指向另一实例:

parentOf[childPid]=parentPid;// 错误:记录缺少父、子创建时间和快照时间。

以下比较把 CPU 时间误当作创建时间:

GetProcessTimes(process,&created,&exited,&kernel,&user);instanceTime=kernel;// 错误:kernel 是累计 CPU 时间,不能描述实例建立时刻。

正确流程以created建立实例键,关闭所有成功打开的 HANDLE,保存ERROR_ACCESS_DENIEDERROR_INVALID_PARAMETERERROR_NO_MORE_FILES等状态。进程实例关系描述的是采样证据,运行时事件、服务配置和令牌内容需要各自的查询与时间关联。

4.2 服务关联、令牌缓冲区和证据范围

SCM 服务对象与进程对象通过运行状态在某个时间点关联。服务配置包含服务名、显示名、启动类型、账户、依赖项和二进制路径。服务状态查询可给出运行 PID。一个服务可在独立进程中运行,也可由共享宿主承载。共享宿主中的多个服务可指向同一 PID。关联记录应保存服务名、查询时间、报告 PID、PID 的创建时间、服务账户和进程映像路径,避免仅凭名称把服务配置绑定到错误实例。

服务账户与进程令牌也属于不同层。服务配置声明期望使用的账户,进程令牌记录实际可查询到的安全上下文。令牌可能因受限令牌、模拟、UAC 完整性级别和进程保护策略出现差异。TokenUserTokenGroupsTokenPrivilegesTokenIntegrityLevelTokenSessionId分别描述用户 SID、组、权限、完整性和会话。查询每一类时都要记录信息类、缓冲区长度和访问失败原因。

// 作用:读取访问令牌中的指定信息类。// 返回:非零成功。0 失败,读取 GetLastError。长度查询常返回 ERROR_INSUFFICIENT_BUFFER。BOOLGetTokenInformation(HANDLE TokenHandle,// 输入:OpenProcessToken 成功返回且尚未关闭的 token HANDLETOKEN_INFORMATION_CLASS TokenInformationClass,// 输入:TokenUser、TokenGroups 等查询类别LPVOID TokenInformation,// 输出:调用方缓冲区。测量阶段可为 nullptrDWORD TokenInformationLength,// 输入:缓冲区容量,单位字节。测量阶段为 0PDWORD ReturnLength// 输出:实际或所需字节数。不能为 nullptr);
DWORD bytes=0;GetTokenInformation(token,TokenUser,nullptr,0,&bytes);if(GetLastError()==ERROR_INSUFFICIENT_BUFFER&&bytes!=0){std::vector<BYTE>raw(bytes);if(GetTokenInformation(token,TokenUser,raw.data(),bytes,&bytes)){// TOKEN_USER 与其中 SID 指针只在 raw 存活期间有效。保存时复制 SID 内容。}}CloseHandle(token);

Job 对象的限制也需要和父子关系分开。Job 可在父进程退出后继续管理成员,也可配置“关闭 Job 时终止成员”等策略。成员进程之间可能没有直接创建边。服务、令牌、Job、会话和映像路径构成进程实例的多维证据图,任何单一字段都不足以回答完整的来源问题。每条关联记录还应保存采样开始与结束时间,便于识别查询期间的实例变化。

完整可运行程序在附件

https://wangweicm.lanzouu.com/i7mvA3zkmggd

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询