首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
编程技术
发新帖
0
0
[原创]进程实例身份、父子关系与会话归属
发表于: 2026-7-30 14:47
293
[原创]进程实例身份、父子关系与会话归属
WangWei_CM.
2026-7-30 14:47
293
# 进程实例身份、父子关系与会话归属 欢迎参考开源项目实现:KswordARK <mark class="encrypted">291K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6w2f1%4N6G2M7X3c8p5c8g2k6Q4x3V1k6w2f1%4N6G2M7X3b7`.</mark>  ## 目标、对象关系与四步流程 本篇要得到的结果是:在一次只读采样中,为当前每个进程建立可区分的实例记录,并输出创建时间能够验证的父子候选关系。每条记录保留 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 由调用方拥有,完成枚举后调用 `CloseHandle`。`dwSize` 必须在每次 `Process32FirstW` 和 `Process32NextW` 前填入结构字节数,API 以它判断调用方结构版本。 ```cpp // 作用:创建系统进程快照。 // 返回:有效 HANDLE 成功。INVALID_HANDLE_VALUE 失败,读取 GetLastError。成功后必须 CloseHandle。 HANDLE CreateToolhelp32Snapshot( DWORD dwFlags, // 输入:TH32CS_SNAPPROCESS 请求进程记录 DWORD th32ProcessID // 输入:进程快照使用 0。线程/模块快照有不同含义 ); // 作用:读取快照中第一条进程记录。 // 返回:非零成功。0 失败,读取 GetLastError。lppe 不能为 nullptr,dwSize 必须预设。 BOOL Process32FirstW( HANDLE hSnapshot, // 输入:CreateToolhelp32Snapshot 成功返回的快照 HANDLE LPPROCESSENTRY32W lppe // 输入/输出:调用方结构,szExeFile 为 UTF-16 输出数组 ); // 作用:读取下一条进程记录。 // 返回:非零成功。0 且 ERROR_NO_MORE_FILES 表示正常结束,其它错误表示枚举中断。 BOOL Process32NextW( HANDLE hSnapshot, // 输入:有效快照 HANDLE LPPROCESSENTRY32W lppe // 输入/输出:每次调用前重新设置 dwSize 的结构 ); ``` 快照创建完成后,进程仍可能退出或新建。`Process32NextW` 正常结束时读取 `GetLastError()==ERROR_NO_MORE_FILES`。其他错误说明本轮枚举停止。快照名称和父 PID 属于该快照时刻,后续实例查询失败时仍可保留原始快照字段,同时标注创建时间或会话不可读。 第一步完成后,已经有了同一时刻的 PID、父 PID 和名称,但这些字段无法区分 PID 复用前后的两个进程。接下来要读取进程对象中的创建时间和会话信息,使每条快照记录具备实例身份。 ## 二、第二步:按最小权限补齐实例身份 ### 2.1 正确做法:读取创建时间和会话 ID `OpenProcess` 向对象管理器请求进程对象的访问引用。`PROCESS_QUERY_LIMITED_INFORMATION` 适合读取基础查询信息,访问检查仍由目标进程安全描述符决定。受保护进程、已退出进程和权限限制会导致打开失败。失败时保存错误码并继续处理其他快照项。成功 HANDLE 由调用方 `CloseHandle`。 ```cpp // 作用:按 PID 打开进程对象。 // 返回:有效 HANDLE 成功。nullptr 失败,读取 GetLastError。成功后 CloseHandle。 HANDLE OpenProcess( DWORD dwDesiredAccess, // 输入:PROCESS_QUERY_LIMITED_INFORMATION 等访问掩码 BOOL bInheritHandle, // 输入:FALSE 禁止子进程继承此调查 HANDLE DWORD dwProcessId // 输入:目标 PID。调用前后都可能退出或复用 ); // 作用:读取进程创建、退出、内核 CPU 与用户 CPU 时间。 // 返回:非零成功。0 失败,读取 GetLastError。四个 FILETIME 输出指针均不能为 nullptr。 BOOL GetProcessTimes( HANDLE hProcess, LPFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime ); // 作用:读取 PID 所属 Windows 会话。 // 返回:非零成功。0 失败,读取 GetLastError。pSessionId 不能为 nullptr。 BOOL ProcessIdToSessionId(DWORD dwProcessId, DWORD* pSessionId); ``` `FILETIME` 由两个 32 位部分组成,表示自 1601-01-01 起的 100ns 计数。将 `dwHighDateTime` 和 `dwLowDateTime` 合成 64 位数值后比较先后,避免按格式化日期字符串比较。创建时间、PID、快照时间和查询状态应一起保存。 ```cpp 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`,调用方扩容后重试。得到路径只说明当前进程主映像的文件名,文件存在性、签名和加载模块列表属于后续查询。 ```cpp // 作用:读取进程主映像完整路径。 // 返回:非零成功。0 失败,读取 GetLastError。输出缓冲区由调用方拥有。 BOOL QueryFullProcessImageNameW( HANDLE hProcess, // 输入:有查询权限的进程 HANDLE DWORD dwFlags, // 输入:0 返回 Win32 路径。PROCESS_NAME_NATIVE 返回 NT 路径 LPWSTR lpExeName, // 输出:UTF-16 缓冲区。不能为 nullptr PDWORD lpdwSize // 输入容量、输出实际字符数。单位 wchar_t,不能为 nullptr ); // 作用:打开进程的主访问令牌。 // 返回:非零成功。0 失败,读取 GetLastError。成功 token HANDLE 由调用方 CloseHandle。 BOOL OpenProcessToken( HANDLE ProcessHandle, // 输入:进程 HANDLE,须有 PROCESS_QUERY_LIMITED_INFORMATION DWORD DesiredAccess, // 输入:TOKEN_QUERY 等令牌访问掩码 PHANDLE TokenHandle // 输出:成功时 token HANDLE。不能为 nullptr ); // 作用:判断进程是否属于指定 Job 或任意 Job。 // 返回:非零表示调用成功。0 失败,读取 GetLastError。Result 不能为 nullptr。 BOOL IsProcessInJob( HANDLE ProcessHandle, // 输入:可查询进程 HANDLE HANDLE JobHandle, // 输入:nullptr 查询是否属于任意 Job。非空为指定 Job HANDLE PBOOL 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 复用后会指向另一实例: ```cpp parentOf[childPid] = parentPid; // 错误:记录缺少父、子创建时间和快照时间。 ``` 以下比较把 CPU 时间误当作创建时间: ```cpp GetProcessTimes(process, &created, &exited, &kernel, &user); instanceTime = kernel; // 错误: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、组、权限、完整性和会话。查询每一类时都要记录信息类、缓冲区长度和访问失败原因。 ```cpp // 作用:读取访问令牌中的指定信息类。 // 返回:非零成功。0 失败,读取 GetLastError。长度查询常返回 ERROR_INSUFFICIENT_BUFFER。 BOOL GetTokenInformation( HANDLE TokenHandle, // 输入:OpenProcessToken 成功返回且尚未关闭的 token HANDLE TOKEN_INFORMATION_CLASS TokenInformationClass, // 输入:TokenUser、TokenGroups 等查询类别 LPVOID TokenInformation, // 输出:调用方缓冲区。测量阶段可为 nullptr DWORD TokenInformationLength, // 输入:缓冲区容量,单位字节。测量阶段为 0 PDWORD ReturnLength // 输出:实际或所需字节数。不能为 nullptr ); ``` ```cpp 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、会话和映像路径构成进程实例的多维证据图,任何单一字段都不足以回答完整的来源问题。每条关联记录还应保存采样开始与结束时间,便于识别查询期间的实例变化。 完整可运行程序在附件 
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
上传的附件:
029-进程实例身份父子关系与会话归属.zip
(158.98kb,2次下载)
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
WangWei_CM.
30
发帖
1
回帖
40
RANK
关注
私信
他的文章
[原创] 进程模块枚举、映像身份与线程启动地址关联
214
[原创]Windows 进程 CPU、内存、I/O 与调度指标采样
980
[原创]跨位数进程的映像、命令行与环境块读取
534
[原创]进程实例身份、父子关系与会话归属
293
[原创]Windows 进程枚举NtQuerySystemInformation(SystemProcessInformation
634
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
huangyalei
Fzzz
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部