查看: 144|回复: 3

跨位数进程信息读取:Win32 API实现映像路径与命令行解析

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在Windows系统管理和安全审计工具的开发中,经常需要根据进程PID获取目标进程的主映像路径、命令行以及环境变量。这些信息看似简单,实际处理时却存在不少陷阱,尤其是面对32位(WOW64)与64位进程混跑的环境。本文从Win32/Native API调用的角度,梳理一条最小权限、按长度读取、区分位数的完整实现路径。

整体流程可以概括为:输入PID,以最小权限OpenProcess打开进程;通过IsWow64Process2确认目标机器类型;用QueryFullProcessImageNameW读取主映像路径;用NtQueryInformationProcess的ProcessCommandLineInformation按字节长度读取命令行;最后用GetEnvironmentStringsW读取当前进程环境块,并明确远程环境块的读取边界。这条路径涉及的关键结构是PEB(进程环境块)和RTL_USER_PROCESS_PARAMETERS。前者是进程初始化时在用户态建立的结构,后者保存命令行、环境块、当前目录、标准句柄等启动参数,由PEB中的指针引用。理解这些结构对跨位数读取尤其重要,因为32位和64位进程的指针宽度和结构偏移完全不同。

第一步:以最小权限打开进程

PID只是数字标识,后续API需要的是进程句柄。OpenProcess接受访问权限、继承标志和PID三个参数。读取映像路径和机器类型只需要PROCESS_QUERY_LIMITED_INFORMATION,而要读取远程命令行内容则必须额外请求PROCESS_VM_READ。正确做法是只请求必要的权限,关闭继承标志,并在同一作用域内释放句柄。目标进程已退出、受保护或安全描述符拒绝访问时,OpenProcess会失败,错误码应记录后停止后续操作。
  1. HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ,
  2.                          FALSE, targetPid);
  3. if (process == nullptr) {
  4.     const DWORD error = GetLastError();
  5.     // 记录 error,终止查询
  6. } else {
  7.     // 执行读取操作
  8.     CloseHandle(process);
  9. }
复制代码

第二步:确认机器类型并读取映像路径

IsWow64Process2返回两个USHORT值,分别表示目标进程的仿真机器类型和系统原生机器类型。若pProcessMachine为IMAGE_FILE_MACHINE_UNKNOWN,说明目标进程与原生架构一致;如果是IMAGE_FILE_MACHINE_I386等值,则说明目标进程运行在WOW64仿真层下。该函数只提供架构证据,不提供PEB偏移。

读取映像路径使用QueryFullProcessImageNameW。该API可以返回Win32路径(如C:\Windows\System32\notepad.exe)或原生NT路径(如\Device\HarddiskVolume...)。其长度参数以wchar_t字符数计算,成功返回的字符数不含结尾NUL。调用时需要先分配缓冲区,把容量写入lpdwSize,再调用。缓冲区不足时返回ERROR_INSUFFICIENT_BUFFER,此时应该扩大缓冲区重试,而不是直接放弃。
  1. std::vector<wchar_t> path(260, L'\0');
  2. for (;;) {
  3.     DWORD chars = static_cast<DWORD>(path.size());
  4.     if (QueryFullProcessImageNameW(process, 0, path.data(), &chars)) {
  5.         std::wstring image(path.data(), chars);
  6.         break;
  7.     }
  8.     const DWORD error = GetLastError();
  9.     if (error != ERROR_INSUFFICIENT_BUFFER || path.size() >= 32768) {
  10.         break;
  11.     }
  12.     path.resize(path.size() * 2, L'\0');
  13. }
复制代码

第三步:按字节长度读取命令行

命令行存储在RTL_USER_PROCESS_PARAMETERS中,由UNICODE_STRING结构描述。UNICODE_STRING的Length字段是字节数且不含NUL,MaximumLength是缓冲区总容量,Buffer指向第一个UTF-16代码单元。由于不同Windows版本中Buffer指向的位置可能不同——有的指向返回缓冲区内的数据,有的直接指向远程进程地址——必须分别验证。

NtQueryInformationProcess是ntdll导出Native API,返回NTSTATUS状态码。使用进程信息类ProcessCommandLineInformation(值为60)时,可以先以空缓冲区调用获取所需字节数,再分配缓冲区并读取。系统返回的UNICODE_STRING可能直接包含命令行数据,也可能需要进一步用ReadProcessMemory从远程地址复制。
  1. ULONG bytes = 0;
  2. NTSTATUS status = NtQueryInformationProcess(process, (PROCESSINFOCLASS)60,
  3.                                             nullptr, 0, &bytes);
  4. if ((status == STATUS_INFO_LENGTH_MISMATCH || status == STATUS_BUFFER_TOO_SMALL) &&
  5.     bytes >= sizeof(UNICODE_STRING) && bytes <= 1024 * 1024) {
  6.     std::vector<BYTE> raw(bytes);
  7.     status = NtQueryInformationProcess(process, (PROCESSINFOCLASS)60,
  8.                                        raw.data(), (ULONG)raw.size(), &bytes);
  9.     if (status == STATUS_SUCCESS && bytes >= sizeof(UNICODE_STRING)) {
  10.         const auto* value = reinterpret_cast<const UNICODE_STRING*>(raw.data());
  11.         // 按 value->Length 读取 value->Buffer,可能需 ReadProcessMemory
  12.     }
  13. }
复制代码

使用ReadProcessMemory读取远程地址时,nSize是字节数,完成后必须验证lpNumberOfBytesRead等于请求的Length。部分复制或目标进程退出会产生不完整文本,绝不能使用。

第四步:读取当前进程环境块及远程边界

环境块是一串“名称=值\0”项,最后以额外NUL结束,例如Path=C:\Windows\0TEMP=C:\Temp\0\0。当前进程可以使用GetEnvironmentStringsW获取系统分配的本地块,完成后必须用FreeEnvironmentStringsW释放。遍历时,从块首开始,逐项移动到下一项首字符,直到遇到空项。
  1. LPWCH block = GetEnvironmentStringsW();
  2. if (block == nullptr) {
  3.     const DWORD error = GetLastError();
  4. } else {
  5.     for (LPWCH item = block; *item != L'\0';) {
  6.         std::wstring oneItem(item);
  7.         item += oneItem.size() + 1;
  8.     }
  9.     FreeEnvironmentStringsW(block);
  10. }
复制代码

这种遍历方式只适用于本地环境块。远程进程的环境块不能直接解引用指针,因为它位于目标地址空间。远程环境读取需要分层复制:先读目标PEB获取进程参数指针,再读匹配位数的RTL_USER_PROCESS_PARAMETERS前缀,取得环境地址和长度,然后分块复制到本地,在已复制范围内寻找连续两个NUL作为结束条件。关键约束是:32位目标用32位指针和结构偏移,64位目标用64位指针和结构偏移,WOW64目标可能同时涉及32位用户参数和64位宿主环境。把x64的PEB偏移直接套在32位目标上,会把命令行、环境地址和长度解释到错误位置,这是最常见的跨位数错误。

环境块读取时,注意单环境项可以包含如=C:=C:\...这样的驱动器当前目录形式,解析器不能因为等号前为空就丢弃它。环境块在目标进程运行期间可能被修改,分块读取中发现长度不一致时应记录部分读取并停止。

常见错误与边界

最容易混淆的单位问题是:UNICODE_STRING::Length和MaximumLength以字节为单位,QueryFullProcessImageNameW的容量参数以wchar_t字符数为单位,ReadProcessMemory的nSize以字节为单位。把字符数传给字节参数,或者反过来,会造成两倍分配、截断或越界读取。

另外,进程参数、令牌、会话和环境变量是不同层次的数据。进程参数是用户态启动文本,令牌是内核安全对象,会话来自进程对象或令牌信息。环境变量中的USERPROFILE等文本可能与令牌中的SID不一致,但两者可以相互印证,不能互相替代。

读取时序也是隐患。目标进程可能在长度查询和数据查询之间退出,命令行和环境数据可能在两次复制间变化。每次读取失败都应保存PID、进程创建时间、机器类型、远程地址、请求字节、实际字节和错误码。下一轮查询必须重新建立基线,不能复用上轮远程指针。

实践建议

对于仅需映像路径和位数判断的场景,只请求PROCESS_QUERY_LIMITED_INFORMATION就够了。只有在确定要读取远程命令行或环境块时,才增加PROCESS_VM_READ权限。这样做既符合最小权限原则,也能减少因权限不足导致的打开失败。写入代码时,善用RAII或defer保证句柄释放,避免错误路径泄漏。

映像路径、命令行和环境块三者来源不同,文本可以不同。命令行可由启动器构造,映像路径可能经过符号链接,环境变量可在进程运行后修改。调查记录应分别保存路径来源、路径形式、命令行原文、环境块解析状态、进程机器类型、会话、令牌查询状态和读取时间,保证证据链完整。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: 跨位数进程信息读取:Win32 API实现映像路径与命令行解析

楼主写得很实用,正好最近在做类似工具。有个疑问:环境变量部分貌似没有展开?远程进程环境块用 `GetEnvironmentStringsW` 只能读当前进程的吧?如果要跨位数读目标进程的环境变量,是不是得走 PEB 解析 `ProcessParameters->Environment`,并且注意 32/64 位结构偏移不同?另外 `QueryFullProcessImageNameW` 在权限不够时如果已拿到 `PROCESS_QUERY_LIMITED_INFORMATION` 一般够用,但有些系统进程可能还是打不开,楼主有碰到过这种场景吗?
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: 跨位数进程信息读取:Win32 API实现映像路径与命令行解析

感谢分享,讲得很清晰。特别是 `IsWow64Process2` 和 `NtQueryInformationProcess` 按字节长度读取命令行这两步,正好说中了跨位数读取时最容易踩的坑。 我想追问一下远程环境变量块的读取边界问题:您提到要“明确读取边界”,但环境块和 `UNICODE_STRING` 不一样,它没有显式的长度字段,通常只能靠连续读到两个空字符来判断结束。那在实际实现里,是用 `VirtualQueryEx` 先探测 `PEB->rocessParameters->Environment` 指向的内存区域大小来限定读取范围吗?还是有其他更稳妥的做法?希望能展开说说,谢谢。
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: 跨位数进程信息读取:Win32 API实现映像路径与命令行解析

感谢分享,写得很清楚。特别是按字节长度读取命令行那段,实际中确实容易踩坑——我之前直接在栈上固定缓冲区去接NtQueryInformationProcess的返回值,结果在Win10上偶尔拿到的是远程指针而非数据。后来也是先查长度再动态分配,再判断Buffer指向再决定要不要ReadProcessMemory。 另外补充一个点:想读取远程进程环境变量时,光有ProcessParameters还不够,环境块地址在RTL_USER_PROCESS_PARAMETERS的Environment字段,而且WOW64下和原生64位进程的PEB结构布局完全不同,需要用IsWow64Process2判断后分别处理。我甚至遇到过某些进程的环境块是UNICODE_STRING长度与实际数据不一致的情况,需要额外做个边界校验。 楼主有没有实际测过从32位进程读取64位进程命令行的场景?这里NtQueryInformationProcess返回的字节序或者UNICODE_STRING长度有没有坑?期待后续经验。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-8-3 12:17 , Processed in 0.022225 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部