首页
社区
课程
招聘
[原创]进程实例身份、父子关系与会话归属
发表于: 3天前 164

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

3天前
164

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

欢迎参考开源项目实现:KswordARK 886K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6w2f1%4N6G2M7X3c8p5c8g2k6Q4x3V1k6w2f1%4N6G2M7X3b7`.
图片描述

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

本篇要得到的结果是:在一次只读采样中,为当前每个进程建立可区分的实例记录,并输出创建时间能够验证的父子候选关系。每条记录保留 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。
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

// 作用:按 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 计数。将 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。输出缓冲区由调用方拥有。
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 复用后会指向另一实例:

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。
BOOL GetTokenInformation(
    HANDLE TokenHandle,              // 输入:OpenProcessToken 成功返回且尚未关闭的 token HANDLE
    TOKEN_INFORMATION_CLASS TokenInformationClass, // 输入:TokenUser、TokenGroups 等查询类别
    LPVOID TokenInformation,         // 输出:调用方缓冲区。测量阶段可为 nullptr
    DWORD TokenInformationLength,    // 输入:缓冲区容量,单位字节。测量阶段为 0
    PDWORD 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、会话和映像路径构成进程实例的多维证据图,任何单一字段都不足以回答完整的来源问题。每条关联记录还应保存采样开始与结束时间,便于识别查询期间的实例变化。

完整可运行程序在附件
图片描述


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

上传的附件:
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回