查看: 166|回复: 3

解析Session Manager的BootExecute/SetupE

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
Session Manager(会话管理器)是Windows在用户登录前启动的关键系统组件,负责建立早期会话环境。它位于注册表HKLM\System\CurrentControlSet\Control\Session Manager下,包含三个重要REG_MULTI_SZ值:BootExecute、SetupExecute与Execute。这三项配置以多字符串(Multi-String)格式存储,解析时需要严格遵循其UTF-16边界规则,否则会导致条目丢失或越界访问。本文介绍如何使用Win32 API安全读取并正确解析这些配置,区分空列表、缺失值和格式异常。

一、理解REG_MULTI_SZ的存储结构
REG_MULTI_SZ是注册表保存多段UTF-16文本的值类型。每段文本以单个NUL(宽字符\0)结束,整个列表以额外一个NUL即双NUL标记终止。例如,一个条目“autocheck autochk *”在缓冲区中的表示为:autocheck autochk *\0\0。两个条目的列表为:first\0second\0\0。合法空列表则是\0\0。RegQueryValueExW返回的长度是字节数,因此需要先将字节数除以sizeof(wchar_t)得到字符数,再按字符索引遍历。

二、定位父键并获取只读句柄
读取前先以KEY_QUERY_VALUE权限打开Session Manager键。代码示例:
  1. HKEY key = nullptr;
  2. if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
  3.     L"System\\CurrentControlSet\\Control\\Session Manager",
  4.     0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) {
  5.     // 读取三个值
  6.     RegCloseKey(key);
  7. }
复制代码
注意:CurrentControlSet是当前启动控制集别名,直接使用它才能反映本次启动的配置。

三、二次查询读取原始字节数据
先以nullptr查询获取所需字节数,再按该大小分配缓冲区读取。若遇到ERROR_MORE_DATA需重试(最多三次)。代码示例:
  1. struct RegistryValueBytes {
  2.     DWORD type = REG_NONE;
  3.     std::vector<BYTE> data;
  4. };
  5. static LSTATUS ReadRegistryValueBytes(HKEY key, const wchar_t* valueName,
  6.                                        RegistryValueBytes& result) {
  7.     for (int attempt = 0; attempt != 3; ++attempt) {
  8.         DWORD type = REG_NONE;
  9.         DWORD requiredBytes = 0;
  10.         LSTATUS status = RegQueryValueExW(key, valueName, nullptr,
  11.                                           &type, nullptr, &requiredBytes);
  12.         if (status != ERROR_SUCCESS) return status;
  13.         std::vector<BYTE> bytes(requiredBytes);
  14.         DWORD actualBytes = requiredBytes;
  15.         status = RegQueryValueExW(key, valueName, nullptr, &type,
  16.                                   bytes.empty() ? nullptr : bytes.data(),
  17.                                   &actualBytes);
  18.         if (status == ERROR_MORE_DATA) continue;
  19.         if (status != ERROR_SUCCESS) return status;
  20.         bytes.resize(actualBytes);
  21.         result.type = type;
  22.         result.data = std::move(bytes);
  23.         return ERROR_SUCCESS;
  24.     }
  25.     return ERROR_MORE_DATA;
  26. }
复制代码
注意:只有字节数能被sizeof(wchar_t)整除时才能按UTF-16解析。

四、按分隔符解析条目列表
将缓冲区指针转为wchar_t*,从起始位置开始查找第一个NUL作为条目结束,然后跳过NUL继续下一个条目,直到遇到双NUL或到达缓冲区末尾。代码示例:
  1. std::vector<std::wstring> entries;
  2. if (status == ERROR_SUCCESS && value.type == REG_MULTI_SZ &&
  3.     value.data.size() % sizeof(wchar_t) == 0) {
  4.     const auto* text = reinterpret_cast<const wchar_t*>(value.data.data());
  5.     const std::size_t count = value.data.size() / sizeof(wchar_t);
  6.     std::size_t begin = 0;
  7.     while (begin < count) {
  8.         std::size_t end = begin;
  9.         while (end < count && text[end] != L'\0') ++end;
  10.         if (end == begin) break; // 双NUL,列表结束
  11.         entries.emplace_back(text + begin, end - begin);
  12.         if (end == count) break; // 缺少最终双NUL,记录异常
  13.         begin = end + 1;
  14.     }
  15. }
复制代码
BootExecute的原始条目如“autocheck autochk *”应整个保存,不要按空格拆分成多个参数。

五、区分空列表、缺失值与格式异常
- 值存在且为空列表:类型为REG_MULTI_SZ,字节数为2(即一个\0\0),entries为空。
- 值不存在:RegQueryValueExW返回ERROR_FILE_NOT_FOUND。
- 类型异常:如果返回类型不是REG_MULTI_SZ,应单独记录。
- 缺少双NUL终止:解析时若while循环因为end == count退出(未遇到第二个NUL),应标注格式异常,但仍可输出已读取条目。

六、实践要点
这三个值分别对应不同启动阶段:BootExecute通常在autochk检查磁盘前执行;SetupExecute用于安装/设置场景;Execute是另一份命令列表。读取程序应分别记录每个值的原始字节长度、条目顺序、是否观察到双NUL及API返回码。不要将空列表、缺失值和异常合并为同一个空字符串输出,否则会丢失诊断信息。

完整可运行的示例项目链接:wangweicm.lanzouu.com/iZ1aq3xtkff…(原文附件)。
回复

使用道具 举报

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

Re: 解析Session Manager的BootExecute/SetupE

感谢楼主的细致分享!一直以为BootExecute只是简单的文本字符串,原来底层是UTF-16的多字符串结构,双NUL终止符和边界整除检查确实很容易踩坑。之前自己写工具解析时遇到过条目丢失的bug,看到您提供的遍历逻辑才意识到是没处理好结尾缺少双NUL的异常情况。另外想请教一下,对于Execute值,如果遇到非REG_MULTI_SZ类型的异常数据(比如REG_SZ),您在实践中有没有推荐的处理策略?是直接忽略还是尝试兼容解析?再次感谢!
回复 支持 反对

使用道具 举报

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

Re: 解析Session Manager的BootExecute/SetupE

感谢分享,讲解得很清晰。不过想请教一下:在区分空列表与值缺失时,代码里通过返回状态来判断缺失(比如值不存在返回ERROR_FILE_NOT_FOUND),但如果是`\0\0`的空列表,`ReadRegistryValueBytes`会成功读取长度为2字节(一个宽字符的NUL),此时`value.data.size()`是2,除以2得1个字符,循环中`begin=0, end=0`,遇到双NUL立即break,得到空容器,这确实区分开了。但假如原值类型是REG_SZ但内容为空,会不会被误认为REG_MULTI_SZ?实际读取时是否应该先校验type是否为REG_MULTI_SZ?另外对SetupExecute和Execute的解析逻辑是否完全一致?期待进一步讨论。
回复 支持 反对

使用道具 举报

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

Re: 解析Session Manager的BootExecute/SetupE

感谢楼主分享这么详细的Session Manager注册表解析方法!特别是对REG_MULTI_SZ结构边界的说明,以及二次查询和双NUL终止符的处理,非常实用。以前用RegQueryValueExW读取BootExecute时偶尔会遇到奇怪的问题,现在看了您的分析,原来是我忽略了UTF-16字符数的对齐检查。另外您提到的“空列表”与“缺失值”的区分也很关键,这一块在MSDN中描述比较模糊,楼主的示例代码直接给出了清晰的处理方式。对习惯使用C++操作注册表的开发者来说,这篇文章太及时了。期待后续还能看到关于Execute和SetupExecute实际应用场景的讨论。
回复 支持 反对

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

GMT+8, 2026-7-22 11:28 , Processed in 0.023822 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部