首页
社区
课程
招聘
[原创]Session Manager 的 BootExecute、SetupExecute 与 Execute 多字符串解析
发表于: 12小时前 224

[原创]Session Manager 的 BootExecute、SetupExecute 与 Execute 多字符串解析

12小时前
224

Session Manager 的 BootExecute、SetupExecute 与 Execute 多字符串解析

本程序的目标是只读记录 Session Manager(会话管理器)的 BootExecuteSetupExecuteExecute 配置,并准确说明每一项的类型、原始条目和读取状态。会话管理器是 Windows 在用户登录前启动的系统组件,负责建立早期会话环境。三项值位于 HKLM\System\CurrentControlSet\Control\Session Manager,都应按 REG_MULTI_SZ 处理。REG_MULTI_SZ 是注册表保存多段 UTF-16 文本的值类型,每段文本和整个列表都使用 NUL 终止字符标记边界。

要完成这个目标,分为四步:

  1. 定位 Session Manager 键,并确认三项值各自的身份。
  2. 按 API 返回的字节长度读取每项原始数据。
  3. 验证 REG_MULTI_SZ 的 UTF-16 边界,再按原顺序解析条目。
  4. 区分空列表、缺失值、终止异常和读取错误,并保留观察结果。

这四步连接了注册表配置、Windows API(Windows Application Programming Interface,Windows 应用程序编程接口)和最终记录。注册表键提供配置来源,RegOpenKeyExW 提供可查询的键句柄,RegQueryValueExW 返回类型和字节范围,解析器在该范围内恢复条目,最后输出每项值的状态。

Session Manager 键 -> 三项值名 -> 原始类型和字节 -> 多字符串条目 -> 值级状态记录

一、对象关系和完整流程

会话管理器子系统(Session Manager Subsystem)在用户登录前参与系统初始化,通常由 smss.exe 承载。BootExecuteSetupExecuteExecute 是 Session Manager 键下三份独立的 REG_MULTI_SZ 配置。每份配置都应保留自己的条目顺序、类型、终止状态和读取结果。

固件 -> Windows Boot Manager -> 内核与驱动初始化 -> Session Manager 初始化会话与子系统 -> 按启动阶段处理相关配置 -> Winlogon 进入用户登录流程

枚举程序只读取三项配置。它按返回字节长度验证 REG_MULTI_SZ,然后保留原始条目、空列表、缺失值和双 NUL 终止状态。

定位键和值名 -> 读取类型和字节长度 -> 验证 UTF-16 边界并解析条目 -> 记录值级状态

1. Session Manager 参与用户会话出现之前的系统初始化

这三个值位于用户登录前的启动阶段。Windows 从固件、Windows Boot Manager、内核加载到用户登录之间经过多个阶段。内核完成基本初始化后,会启动会话管理器子系统(Session Manager Subsystem,常由 smss.exe 承载)来建立会话、准备子系统环境并处理早期启动相关配置。

HKLM\System\CurrentControlSet\Control\Session Manager 保存的是会话管理器相关配置。CurrentControlSet 是系统当前使用的控制集别名,系统会将它映射到某个实际的 ControlSet00x。读取时使用 CurrentControlSet 能对应本次启动选择的控制集。仅比较另一个 ControlSet00x 的同名值不能代替当前配置的结果。

这类值出现在早期系统阶段,读取时更应保留原始格式。条目文本可以包含由会话管理器或其调用组件解释的参数,不能直接按普通桌面程序命令行的规则拆成“第一个空格前是路径”。

2. 三个值是独立的命令列表,不共享状态

BootExecuteSetupExecuteExecute 要分别记录。BootExecute 常与启动检查阶段相关,常见条目为 autocheck autochk *SetupExecute 面向安装或设置场景。Execute 是同一配置键下的另一份列表。它们共享父键,但系统不一定在相同条件下处理每一项。

每个值至少有四种读取状态:键不存在或无法打开、值不存在、值存在且为空列表、值存在且包含条目。值存在但类型不是 REG_MULTI_SZ 还应作为类型异常单独报告。把所有“没有条目”的情况合并为一个空字符串,会丢失配置和读取失败之间的重要区别。

3. REG_MULTI_SZ 是有顺序的 UTF-16 字符串数组

多字符串不是普通字符串。REG_MULTI_SZ 的数据由多段 UTF-16 文本串接而成。每段以单个 NUL 结束,列表以额外 NUL 结束。条目顺序就是 API 返回缓冲区中的顺序,应按原顺序输出。

一个条目的列表:
autocheck autochk * \0 \0

两个条目的列表:
first \0 second \0 \0

合法空列表:
\0 \0

每个 \0 代表一个 UTF-16 NUL 宽字符而非字面反斜杠和零。RegQueryValueExW 的长度参数仍以字节为单位,因此程序先用字节数分配缓冲区,再通过 字节数 / sizeof(wchar_t) 得到可检查的字符数量。读取长度为奇数、缺少双 NUL 或在第二次查询中扩容的情况都应保留为格式或读取状态。

4. 配置值与实际执行是两类观察结果

配置值与实际执行需要分开。注册表中出现 BootExecute 条目,只证明读取时配置包含该文本。条目是否在某次启动中被处理、具体工具是否运行成功、是否因为磁盘状态或安装阶段被跳过,需要启动日志、事件记录或独立运行时证据。

读取程序可以安全地报告:完整键路径、值名、类型、条目顺序、原始长度、是否观察到双 NUL 和 API 返回码。它不应仅凭某个词出现在条目内就断言系统已经执行过相应程序。

5. 读取注册表前需要认识句柄、权限和长度单位

下方 API 使用几个基础对象。HKEY 是已打开注册表键的句柄,不是路径字符串,也不是键内容。RegOpenKeyExW 成功后产生它,调用方负责用 RegCloseKey 归还系统资源。KEY_QUERY_VALUE 是只读查询值的访问权限,请求它不允许修改配置。

DWORD 是 32 位无符号整数类型。注册表 API 中保存长度的 DWORD 可能表示两种单位:名称长度通常是 UTF-16 宽字符个数,原始值数据长度通常是字节数。BYTE 是一个字节。wchar_t 在 Windows 上通常承载一个 UTF-16 代码单元。将字节数直接当作字符数会造成缓冲区过大或过小。

NUL 是数值为零的终止字符。C 和 Win32 的许多宽字符串 API 依靠 NUL 找到文本结尾。REG_MULTI_SZ 则把 NUL 同时用作条目分隔符和列表终止符。因此解析该类型时必须以 API 返回长度为边界,不能只调用寻找第一个 NUL 的字符串函数。

二、第一步:定位父键并区分三项配置

三个值共享同一个父键。BootExecute 用于启动检查阶段,SetupExecute 用于安装与设置阶段,Execute 是另一份会话管理器命令列表。同一父键不代表三项配置具有相同状态。

打开键时只申请查询权限。成功返回的 HKEY 是系统资源,读取结束后必须关闭。

// 意义:在已有根键或父键下打开一个注册表子键。
// 返回:ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示键不存在。
//        ERROR_ACCESS_DENIED 表示当前令牌没有 samDesired 所请求的权限。
//        成功时 *phkResult 是新 HKEY,调用方拥有它,必须恰好调用一次 RegCloseKey。
LSTATUS RegOpenKeyExW(
    HKEY hKey,          // 输入:HKEY_LOCAL_MACHINE 等预定义根键,或已打开的父键。预定义根键不关闭。
    LPCWSTR lpSubKey,   // 输入:相对 hKey 的 NUL 结尾 UTF-16 路径。可为 nullptr,表示 hKey 本身。
    DWORD ulOptions,    // 输入:保留参数,必须为 0。
    REGSAM samDesired,  // 输入:访问掩码。只读本节值时使用 KEY_QUERY_VALUE,不请求写权限。
    PHKEY phkResult     // 输出:接收新句柄的非空指针。失败时不得使用其中的值。
);

// 意义:释放 RegOpenKeyExW 成功返回的注册表键句柄。
// 返回:ERROR_SUCCESS 表示关闭成功。预定义根键无需也不应传入。
LSTATUS RegCloseKey(
    HKEY hKey           // 输入:调用方拥有的已打开 HKEY。关闭后该值不能重复查询或关闭。
);

正确读取时为每个值保留独立结果。父键存在而 Execute 未设置时,BootExecuteSetupExecute 仍应照常输出。

constexpr wchar_t kKeyPath[] =
    L"System\\CurrentControlSet\\Control\\Session Manager";
HKEY key = nullptr;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, kKeyPath, 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) {
    // 分别读取 BootExecute、SetupExecute、Execute。
    RegCloseKey(key);
}
// 错误示例:读取到一个值后直接结束。
// 后两个值的缺失、空列表或实际条目都不会被观察到。
RegQueryValueExW(key, L"BootExecute", nullptr, nullptr, data, &size);
return;

完成第一步(定位父键并区分三项配置)后,已经取得只读查询所需的 HKEY,也明确了每次查询对应的值名。键句柄只能说明查询入口有效,尚未提供值的内容。下一步需要让注册表 API 返回每项数据的类型和准确字节范围。

三、第二步:按返回字节数读取原始数据

注册表查询函数的长度参数单位是字节。先传入空数据指针取得长度,再按该长度读取原始数据。第二次读取遇到 ERROR_MORE_DATA 时需要扩大缓冲区并有限重试。

// 意义:读取一个注册表值的类型和原始字节。
// 返回:ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示值名不存在。
//        ERROR_MORE_DATA 表示 *lpcbData 指定的字节容量不足。此函数不把数据转换为字符串。
LSTATUS RegQueryValueExW(
    HKEY hKey,              // 输入:已打开且至少有 KEY_QUERY_VALUE 权限的键句柄。
    LPCWSTR lpValueName,    // 输入:NUL 结尾 UTF-16 值名。nullptr 或 L"" 表示默认值。
    LPDWORD lpReserved,     // 输入:保留参数,必须为 nullptr。
    LPDWORD lpType,         // 输出:接收 REG_MULTI_SZ 等类型。可为 nullptr,本文必须提供以验证类型。
    LPBYTE lpData,          // 输出:接收原始字节。只查询所需长度时传 nullptr。
    LPDWORD lpcbData        // 输入/输出:传入 lpData 容量(字节),返回实际或所需字节数。不可为 nullptr。
);

正确的两次读取保留 API 返回的类型和实际字节数。REG_MULTI_SZ 只有在字节数能被 sizeof(wchar_t) 整除时才按 UTF-16 解析。

#include <Windows.h>
#include <utility>
#include <vector>

struct RegistryValueBytes {
    DWORD type = REG_NONE;          // API 返回的注册表类型。
    std::vector<BYTE> data;         // API 原样返回的数据。长度单位始终是字节。
};

// 正确示范:读取一个值并处理两次查询之间长度变化造成的 ERROR_MORE_DATA。
static LSTATUS ReadRegistryValueBytes(
    HKEY key,                       // 输入:已成功打开、具有 KEY_QUERY_VALUE 的键。
    const wchar_t* valueName,       // 输入:NUL 结尾的值名,本例为 BootExecute 等固定名称。
    RegistryValueBytes& result)     // 输出:仅在 ERROR_SUCCESS 时保存类型和完整字节。
{
    for (int attempt = 0; attempt != 3; ++attempt) { // 有界重试,避免持续修改的键导致无限循环。
        DWORD type = REG_NONE;
        DWORD requiredBytes = 0;
        LSTATUS status = RegQueryValueExW(
            key, valueName, nullptr, &type, nullptr, &requiredBytes);
        if (status != ERROR_SUCCESS) {
            return status;          // ERROR_FILE_NOT_FOUND 与访问错误由调用者按值身份记录。
        }

        std::vector<BYTE> bytes(requiredBytes); // requiredBytes 是字节,不乘 sizeof(wchar_t)。
        DWORD actualBytes = requiredBytes;
        status = RegQueryValueExW(
            key, valueName, nullptr, &type,
            bytes.empty() ? nullptr : bytes.data(), &actualBytes);
        if (status == ERROR_MORE_DATA) {
            continue;               // 值在两次调用之间变长。重新取得新的长度。
        }
        if (status != ERROR_SUCCESS) {
            return status;
        }

        bytes.resize(actualBytes);  // 只保留 API 实际写入的字节。
        result.type = type;
        result.data = std::move(bytes);
        return ERROR_SUCCESS;
    }
    return ERROR_MORE_DATA;         // 连续变化三次,保留此状态而不猜测内容。
}

RegistryValueBytes value;
const LSTATUS status = ReadRegistryValueBytes(key, L"BootExecute", value);
if (status == ERROR_SUCCESS && value.type == REG_MULTI_SZ) {
    // 下面只在确认类型正确后,才将 value.data 按 UTF-16 多字符串解释。
}

完成第二步(按返回字节数读取原始数据)后,已经获得类型与限定长度的字节缓冲区。此时缓冲区仍只是原始数据,直接当作普通字符串会丢失后续条目,或在终止字符缺失时越出返回范围。下一步需要确认它确实符合 UTF-16 多字符串的边界规则,再恢复原有顺序。

四、第三步:验证格式并按顺序解析条目

REG_MULTI_SZ 使用单个 NUL 分隔条目,使用双 NUL 终止整个列表。autocheck autochk *\0\0 表示一个完整条目。\0\0 表示合法空列表。值不存在是第三种状态。

// 正确示范:先验证类型和字节对齐,再以 API 返回范围为唯一边界解析。
std::vector<std::wstring> entries;
if (status == ERROR_SUCCESS && value.type == REG_MULTI_SZ &&
    value.data.size() % sizeof(wchar_t) == 0) {
    const auto* text = reinterpret_cast<const wchar_t*>(value.data.data());
    const std::size_t count = value.data.size() / sizeof(wchar_t);

    std::size_t begin = 0;
    while (begin < count) {
        std::size_t end = begin;
        while (end < count && text[end] != L'\0') ++end;
        if (end == begin) break;          // 已到达双 NUL 中的第一个字符。
        entries.emplace_back(text + begin, end - begin);
        if (end == count) break;          // 缺少 NUL 时严格停在 API 返回范围内。
        begin = end + 1;
    }
}

BootExecute 条目需要保留为完整原始文本。autocheckautochk* 的组合具有该值自身的语义,按普通 Run 键的“首个空格前是程序路径”规则拆分会改变原始记录。

// 错误示例:把多字符串缓冲区直接当作一个普通 C 字符串。
const wchar_t* text = reinterpret_cast<const wchar_t*>(value.data.data());
std::wcout << text; // 只得到第一段,且缺少 NUL 时可能越界读取。

完成第三步(验证格式并按顺序解析条目)后,已经可以在 API 返回范围内得到可显示的原始条目。条目为空或解析结束并不代表所有配置情况相同。下一步需要把空列表、未设置的值、格式异常和 API 错误分别记录。

五、第四步:记录空列表、缺失值与终止异常

空列表和缺失值需要分开显示。空 SetupExecute 通常表现为 REG_MULTI_SZ 与零条目,Execute 未设置时则由 RegQueryValueExW 返回 ERROR_FILE_NOT_FOUND

双 NUL 缺失需要作为格式状态保存。解析器仍可在 API 返回的最后字符处停止并输出已读取条目,不能继续扫描注册表缓冲区之外的内存。

if (status == ERROR_SUCCESS && entries.empty() && value.type == REG_MULTI_SZ) {
    // 合法空列表:类型存在,条目数为 0。
}
if (status == ERROR_FILE_NOT_FOUND) {
    // 值未设置:与空列表不同。
}

完整可运行程序在附件
图片描述
d1fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9g2Z5I4j5i4p5K6P5s2c8C8k6X3k6Y4


[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!

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