进程实例身份、父子关系与会话归属欢迎参考开源项目实现KswordARKhttps://github.com/KSwordDEV/KSword目标、对象关系与四步流程本篇要得到的结果是在一次只读采样中为当前每个进程建立可区分的实例记录并输出创建时间能够验证的父子候选关系。每条记录保留 PID、父 PID、创建时间、会话 ID、快照名称、可读取的主映像路径和父边状态。PIDProcess Identifier进程标识符是内核为存活进程分配的数字。进程退出后系统可以把同一数字分配给后来的进程因此 PID 只描述某个时段的标识槽位。创建时间是进程对象建立时记录的 100ns 时间值PID 与创建时间组合后才能描述一次进程实例。要完成这个结果分为四步创建进程快照收集 PID、父 PID 和快照名称。按最小查询权限读取创建时间、会话 ID 和主映像路径。用(PID, 创建时间)区分实例再按父 PID 与时间先后建立候选父子边。输出已验证关系和无法验证的原因并保留会话 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 ManagerSCM服务控制管理器数据库中的配置对象。服务配置中的二进制路径和运行账户可启动一个服务进程。服务进程的父 PID 常反映服务宿主和启动时序服务对象与进程对象之间仍需通过服务状态、PID 和创建时间关联。将某个svchost.exe名称直接归属于特定服务会遗漏宿主组和运行时变化。1.2 快照先提供哪些字段Toolhelp 进程快照是 Windows 提供的一份进程列表副本。PROCESSENTRY32W中的th32ProcessID是 PIDth32ParentProcessID是父 PIDszExeFile是快照名称。快照 HANDLE 由调用方拥有完成枚举后调用CloseHandle。dwSize必须在每次Process32FirstW和Process32NextW前填入结构字节数API 以它判断调用方结构版本。// 作用创建系统进程快照。// 返回有效 HANDLE 成功。INVALID_HANDLE_VALUE 失败读取 GetLastError。成功后必须 CloseHandle。HANDLECreateToolhelp32Snapshot(DWORD dwFlags,// 输入TH32CS_SNAPPROCESS 请求进程记录DWORD th32ProcessID// 输入进程快照使用 0。线程/模块快照有不同含义);// 作用读取快照中第一条进程记录。// 返回非零成功。0 失败读取 GetLastError。lppe 不能为 nullptrdwSize 必须预设。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 正确做法读取创建时间和会话 IDOpenProcess向对象管理器请求进程对象的访问引用。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 计数。将dwHighDateTime和dwLowDateTime合成 64 位数值后比较先后避免按格式化日期字符串比较。创建时间、PID、快照时间和查询状态应一起保存。HANDLE processOpenProcess(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.LowPartcreated.dwLowDateTime;value.HighPartcreated.dwHighDateTime;instanceCreation100nsvalue.QuadPart;}CloseHandle(process);// 本分支拥有 process关闭后不得再传入查询 API。}DWORD session0;if(!ProcessIdToSessionId(pid,session)){DWORD errorGetLastError();// 立刻保存日志格式化可能覆盖错误槽。}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使用两阶段缓冲区第一次以TokenInformationnullptr查询所需字节数常见失败为ERROR_INSUFFICIENT_BUFFER。第二次分配精确字节数后读取。返回缓冲区里的 SID、组和权限指针属于该缓冲区复制前不能释放。Job 查询结果为 TRUE 表示归属FALSE 表示不归属。API 返回零才是查询失败。第二步完成后每条记录都已尝试补齐创建时间、会话 ID 和主映像路径并明确标记不可读取的字段。下一步需要把子记录中的父 PID 与同一批记录进行匹配再用创建时间排除明显不成立的关系。三、第三步用实例键建立父子候选边3.1 比较父、子实例的 PID 与创建时间对每个子实例在同一批实例记录中查找parentPid。父实例存在、父创建时间可读、子创建时间可读、父创建时间不晚于子创建时间时建立候选边。父 PID 缺失、父 PID 记录创建时间晚于子实例、任一时间查询失败时保存“父边不可验证”状态。服务、Job、令牌和会话字段作为边或节点属性附加不能取代时间顺序。同一会话可以有大量无关进程。同一 Job 可以包含多代进程。相同令牌用户可以启动多个无关程序。相同映像路径可对应多次启动实例。这些关系帮助解释进程来源和限制父子边仍需依赖 PID 与创建时间。第三步完成后创建时间顺序成立的记录已经形成候选父子边。其余记录不能被强行归类需要在输出中保留父进程已退出、PID 已复用或字段不可读等原因同时把其它对象关系作为单独证据。四、第四步输出证据状态并说明关联边界4.1 最小错误示范以下映射只保存数字 PIDPID 复用后会指向另一实例parentOf[childPid]parentPid;// 错误记录缺少父、子创建时间和快照时间。以下比较把 CPU 时间误当作创建时间GetProcessTimes(process,created,exited,kernel,user);instanceTimekernel;// 错误kernel 是累计 CPU 时间不能描述实例建立时刻。正确流程以created建立实例键关闭所有成功打开的 HANDLE保存ERROR_ACCESS_DENIED、ERROR_INVALID_PARAMETER、ERROR_NO_MORE_FILES等状态。进程实例关系描述的是采样证据运行时事件、服务配置和令牌内容需要各自的查询与时间关联。4.2 服务关联、令牌缓冲区和证据范围SCM 服务对象与进程对象通过运行状态在某个时间点关联。服务配置包含服务名、显示名、启动类型、账户、依赖项和二进制路径。服务状态查询可给出运行 PID。一个服务可在独立进程中运行也可由共享宿主承载。共享宿主中的多个服务可指向同一 PID。关联记录应保存服务名、查询时间、报告 PID、PID 的创建时间、服务账户和进程映像路径避免仅凭名称把服务配置绑定到错误实例。服务账户与进程令牌也属于不同层。服务配置声明期望使用的账户进程令牌记录实际可查询到的安全上下文。令牌可能因受限令牌、模拟、UAC 完整性级别和进程保护策略出现差异。TokenUser、TokenGroups、TokenPrivileges、TokenIntegrityLevel和TokenSessionId分别描述用户 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 bytes0;GetTokenInformation(token,TokenUser,nullptr,0,bytes);if(GetLastError()ERROR_INSUFFICIENT_BUFFERbytes!0){std::vectorBYTEraw(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