查看: 377|回复: 3

NtQuerySystemInformation进程枚举:缓冲区增长与记

[复制链接]
发表于 昨天 10:00 | 显示全部楼层 |阅读模式
在Windows系统编程中,通过Native API枚举当前进程是许多系统工具和诊断程序的基础操作。NtQuerySystemInformation配合SystemProcessInformation信息类,可以获取当前可见进程的快照,但直接使用该API存在缓冲区大小不足、记录偏移验证缺失、字符串边界检查疏漏等隐患,轻则读取错误数据,重则导致内存越界崩溃。本文基于实际项目KswordARK的实现经验,梳理一套可靠的缓冲区遍历与记录验证流程,确保快照数据可安全、准确地解析。

进程实例的唯一性由PID和创建时间共同决定的。PID在进程退出后可被新进程复用,仅靠PID无法区分不同实例。创建时间(CreateTime)是进程对象建立时写入的100纳秒计数,在同一实例生命周期内恒定不变。因此,使用(PID, CreateTime)二元组作为进程实例的主键,能够准确标识同一进程在不同快照之间的对应关系,避免PID复用带来的混淆。父PID(InheritedFromUniqueProcessId)仅记录创建时的父进程标识,不能推断当前父进程是否存在。会话ID(SessionId)帮助区分服务、控制台和远程桌面会话,但同样不能替代创建时间进行实例验证。

Native API与Win32错误模型不同。NtQuerySystemInformation返回NTSTATUS状态码,成功时STATUS_SUCCESS(0x00000000),失败时包含STATUS_INFO_LENGTH_MISMATCH(0xC0000004L)、STATUS_BUFFER_TOO_SMALL(0xC0000023L)等。调用方应直接检查NTSTATUS,不要依赖GetLastError。函数地址通过GetModuleHandleW("ntdll.dll")获取借用模块句柄,再通过GetProcAddress取得NtQuerySystemInformation的函数指针。借用句柄无需也绝不应该调用FreeLibrary,因为ntdll在整个进程生命周期内始终加载。

缓冲区分配与增长策略。首次分配一块初始容量(如256KB),循环调用NtQuerySystemInformation,直至返回STATUS_SUCCESS。当返回STATUS_INFO_LENGTH_MISMATCH、STATUS_BUFFER_TOO_SMALL或STATUS_BUFFER_OVERFLOW时,需要扩展缓冲区。扩展规则:优先使用ReturnLength输出的建议大小(再加上64KB余量),若无ReturnLength或小于当前大小,则采用倍增策略(当前大小的两倍)。设置上限(如64MB)和最大重试次数(如8次),超出后终止。每次重试前清空旧缓冲区并重新分配,避免残留数据干扰。成功调用后,以容器实际大小和ReturnLength中的较小值作为有效字节范围。

记录遍历与结构验证。成功获取的缓冲区是连续字节,其中包含一个或多个SYSTEM_PROCESS_INFORMATION记录链。每条记录以固定前缀开头,后随NumberOfThreads个线程记录。遍历时维护当前偏移量,每次读取前检查剩余字节是否足以容纳前缀大小。记录内的NextEntryOffset表示从当前记录起点到下一条记录起点的字节偏移量。当NextEntryOffset为0时表示本记录为最后一项。非零值必须满足:大于等于前缀大小,且小于等于剩余字节数。验证通过后,偏移累加NextEntryOffset进入下一条记录。

UNICODE_STRING的边界检查。进程映像名称(ImageName)以UNICODE_STRING结构存储,包含Length(实际字节数)、MaximumLength(缓冲区容量)、Buffer(字符指针)。Buffer指针必须位于缓冲区有效范围内(起始地址≥缓冲区首地址,结束地址≤缓冲区末地址),Length不能超过剩余字节且必须是wchar_t大小的整数倍。任意条件不满足时,应将名称状态标记为异常,仅保留PID、创建时间等前缀字段。切勿将UNICODE_STRING当作以NUL结尾的字符串直接使用std::wstring构造,必须根据Length的长度复制到独立的wstring中。

线程记录与时间字段的处理。进程记录中的NumberOfThreads字段表示当前记录后跟随的线程记录数量。若只需要进程信息,可通过NextEntryOffset跳过整个线程数组。若需要读取线程,必须先验证NumberOfThreads * sizeof(线程结构)不超过本记录的NextEntryOffset,且不会造成整数溢出。CreateTime、UserTime、KernelTime均为LARGE_INTEGER形式的64位100纳秒计数,UserTime和KernelTime适合在同一进程的不同采样时刻做差值,用于计算CPU使用率。系统空闲进程等特殊进程的时间或句柄字段可能出现零值,显示时应保留原始数值并标注来源,避免误判为读取失败。

快照的证据范围和限制。一次成功的NtQuerySystemInformation调用只提供查询时刻的进程状态快照。进程可能在查询期间、之前或之后被创建或退出,因此不同批次快照之间的条目、父PID、名称变化属于正常现象。Native快照不能反映隐藏进程或保护进程。作为回退方案,可使用CreateToolhelp32Snapshot和Process32NextW(Toolhelp API)获取另一类快照。两种快照的采样时刻和字段来源不同,比较时应记录来源标签("Native"或"Toolhelp"),并结合创建时间和采样时间分析差异,不能直接断言存在隐藏进程。

错误处理与资源管理。Native API快照过程不产生需要CloseHandle的真实句柄:ntdll模块句柄是借用引用,导出函数地址也是借用,进程快照字节由std::vector管理,离开作用域自动释放。在需要调用GetLastError的场景(如Toolhelp API),应先保存错误码,避免后续调用覆盖。对于偏移链校验失败的记录,应停止解析后续条目,因为失去可信起点。

完整实现代码可参考KswordARK项目(github.com/wangwei-cm/KswordARK),其中包含了上述缓冲区管理、记录遍历、边界验证和错误处理的完整逻辑。掌握这些细节后,开发者能够编写出安全、可靠的进程枚举工具,避免常见的内存错误和逻辑陷阱。
回复

使用道具 举报

发表于 昨天 10:05 | 显示全部楼层

Re: NtQuerySystemInformation进程枚举:缓冲区增长与记

非常感谢楼主的详细分享!最近刚好在做一个进程监控的小工具,也遇到了`NtQuerySystemInformation`缓冲区大小和UNICODE_STRING越界的问题,楼主的这篇总结非常及时。 特别是关于`(PID, CreateTime)`作为唯一主键的说明,以前只考虑了PID会复用,但没注意到创建时间的重要性。还有那些边界检查和偏移量验证的步骤,正是我踩坑的地方。文章中提到的用`ReturnLength`加余量以及倍增策略的缓冲区扩展方法,感觉比直接猜大小靠谱很多。 有个小问题想请教一下:在验证`UNICODE_STRING`的`Buffer`指针时,除了检查指针范围和对齐,是否还需要考虑`Buffer`指向的字符串可能没有终止符的情况?我目前是直接按`Length`拷贝到wstring,但有时发现某些进程的`ImageName`长度是0但指针非空,这种异常状态是否建议直接标记为invalid名称?期待楼主的进一步指点,谢谢!
回复 支持 反对

使用道具 举报

发表于 昨天 10:05 | 显示全部楼层

Re: NtQuerySystemInformation进程枚举:缓冲区增长与记

非常详细和专业的分享,感谢楼主把NtQuerySystemInformation的踩坑点和安全处理策略梳理得这么清晰。特别是关于PID+创建时间作为唯一主键、NextEntryOffset的边界校验、UNICODE_STRING的Length而非Buffer直接使用这些细节,在实际开发中确实容易被忽略。KswordARK项目的代码参考也很实用。 想请教一个点:您提到了快照不能反映隐藏进程,那么在实际项目中,当Native和Toolhelp两种快照出现差异时(比如Native比Toolhelp多出某个进程),除了记录来源标签外,有没有推荐的进一步验证手段来区分是正常的快照时刻差异,还是真正的隐藏/保护进程?还是说一般就回退到驱动层去验证了?
回复 支持 反对

使用道具 举报

发表于 昨天 10:05 | 显示全部楼层

Re: NtQuerySystemInformation进程枚举:缓冲区增长与记

感谢楼主的详细分享,这篇内容对理解NtQuerySystemInformation的正确使用方式非常有价值。特别是关于进程实例唯一性由(PID, 创建时间)共同决定、缓冲区增长策略中ReturnLength与倍增的权衡、以及UNICODE_STRING边界检查的注意事项,这些都是实际开发中容易踩坑的地方。KswordARK项目的开源实现也很有参考意义。 有一个小问题想请教:在缓冲区增长循环中,如果ReturnLength返回0但状态码是STATUS_BUFFER_TOO_SMALL,此时采用倍增策略是否可能陷入无限循环(比如缓冲区大小刚好达到某个阈值但系统依然返回失败)?您在项目中设置的上限64MB和8次重试是否基于经验值,还是考虑了某些特殊场景(如大量进程的服务器)?
回复 支持 反对

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

GMT+8, 2026-7-30 05:45 , Processed in 0.041468 second(s), 33 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部