-
-
[原创][自学][Windows编程]Windows 兼容与特殊登录启动键的枚举
-
发表于: 5天前 842
-
Windows 兼容与特殊登录启动键的枚举
兼容启动键、策略型启动键和命令解释器配置键分散在多个注册表位置。枚举时要先确定来源,再读取原始数据,最后按各自的用途解释。这个顺序可以避免漏项,也能防止把配置记录误当成程序已经运行的证据。
一、对象关系和完整流程
1. 先分清系统使用流程和枚举流程
系统使用流程从一个触发动作开始。触发动作可能是用户登录、启动 cmd.exe,也可能是进入终端服务的安装模式。负责该动作的 Windows 组件会寻找对应的注册表位置。它读取值类型和数据后,才决定是否处理其中的命令。
登录 Shell 是用户登录后提供桌面和开始菜单的程序,Windows 默认使用 explorer.exe。命令解释器是读取并执行命令文本的程序,这里指 cmd.exe。终端服务安装模式用于处理多用户环境中的应用安装兼容问题。三个场景由不同组件处理,因此不能把相关注册表值都叫作“开机自启”。
安装程序、管理员或策略写入配置 -> 形成“根键 + 注册表视图 + 子键路径 + 值名”记录 -> 登录、cmd.exe 启动或终端服务安装模式触发 -> 对应的 Windows 组件读取配置 -> 解释值类型和文本 -> 决定是否执行
枚举流程只读取配置。它不会触发这些命令,也不能单独证明某个命令曾经执行。
列出所有来源 -> 确定根键和注册表视图 -> 用只读权限打开键 -> 按 API 返回的长度读取原始字节 -> 记录值名、类型、长度和读取状态 -> 对已验证的文本生成解释结果
2. 一条记录由四部分共同定位
注册表是 Windows 管理的分层配置数据库。子键类似目录,值类似目录中的具名配置项。根键 HKEY_LOCAL_MACHINE(HKLM)保存本机范围配置,通常影响多个用户。根键 HKEY_CURRENT_USER(HKCU)保存当前用户配置,通常只影响当前登录身份。
一条记录的位置由根键、注册表视图、子键路径和值名共同组成。例如:
根键:HKCU
子键:Software\Microsoft\Command Processor
值名:Autorun
这四部分中的任一项不同,都应视为另一条记录。子键不存在、值不存在、值为空字符串和读取失败也是四种不同状态。枚举结果要分别显示这些状态,避免把权限不足写成“没有配置”。
HKLM
Software\Microsoft\Windows\CurrentVersion\Run
SecurityHealth = %windir%\system32\SecurityHealthSystray.exe
3. 注册表视图决定读取哪一份数据
64 位 Windows 会为部分软件配置维护 64 位和 32 位注册表视图。视图可以理解为同一路径下的两套独立数据。64 位程序默认看到 64 位视图,32 位程序默认看到 32 位视图。
KEY_WOW64_64KEY 明确选择 64 位视图。KEY_WOW64_32KEY 明确选择 32 位视图。枚举程序应主动选择视图,使结果不受程序自身位数影响。
HKLM\Software
64 位视图:供 64 位组件使用
32 位视图:供 WOW64 下的 32 位组件使用
4. 值类型决定原始字节的解释方法
注册表 API 返回值类型和一段原始字节。REG_SZ 保存普通 UTF-16 文本。UTF-16 是 Windows 宽字符 API 常用的文本编码,每个编码单元占两个字节。REG_EXPAND_SZ 也保存 UTF-16 文本,其中可以包含 %SystemRoot% 这样的环境变量标记。
REG_DWORD 保存四字节无符号整数。REG_MULTI_SZ 保存多段文本。每段文本以一个 NUL 结束,整个列表以额外的 NUL 结束。NUL 是数值为零的终止字符。
文本转换前必须先检查值类型和返回的字节数。类型不匹配、字节数为奇数或文本没有终止符时,都要按异常数据处理。
REG_SZ: explorer.exe
REG_EXPAND_SZ: %SystemRoot%\System32\example.exe
REG_DWORD: 0x00000001
REG_MULTI_SZ: first\0second\0\0
5. 读取时先保留来源
打开键之前,先记录 HKLM 或 HKCU、完整子键路径和 32 位或 64 位视图。枚举值时,再记录值名、REG 类型、原始字节长度和 API 返回码。来源信息与原始数据保存完成后,才能解释 REG_SZ 或 REG_EXPAND_SZ 文本。
环境变量展开、路径检查、签名验证和命令行拆分都属于后续分析。它们不能覆盖原始文本,也不能代替来源信息。
读取状态也要保留。ERROR_FILE_NOT_FOUND 表示对象不存在。ERROR_ACCESS_DENIED 表示当前访问权限不足。ERROR_MORE_DATA 表示缓冲区太小,需要扩容后重试。ERROR_NO_MORE_ITEMS 表示枚举正常结束。每个来源都要独立读取,一个来源失败后仍应继续处理其它来源。
6. 配置存在不等于程序已经运行
配置层只回答键和值是否存在,以及读取到了什么原始数据。处理层回答当前 Windows 版本和相关组件是否会读取该位置。运行层回答命令是否执行、目标进程是否创建,以及程序是否正常完成。
枚举程序只能直接观察配置层。它应记录完整路径、值名、REG 类型、原始字节长度、注册表视图、调用账户和读取时间。实际执行情况需要结合进程、事件或日志证据判断。
7. API 类型说明资源和长度单位
HKEY 是打开注册表键后得到的引用。调用方完成读取后,要用 RegCloseKey 释放它。REGSAM 是访问权限的位集合。KEY_QUERY_VALUE 允许读取值,KEY_WOW64_32KEY 和 KEY_WOW64_64KEY 用于选择注册表视图。DWORD 是 32 位无符号整数,可以保存访问位、值类型或长度。
名称长度通常以 UTF-16 宽字符个数计算。值数据长度通常以字节计算。注册表返回的文本不一定带有结尾 NUL,因此只能按 API 返回的精确长度构造字符串。使用 wcslen 扫描未确认终止的缓冲区可能越过有效数据边界。
二、先把不同位置分开记录
1. 每个键的作用范围不同
枚举应先列出所有独立来源。常见的兼容或特殊登录位置包括:
HKCU\Software\Microsoft\Windows\CurrentVersion\RunServices
HKLM\Software\Microsoft\Windows\CurrentVersion\RunServices
HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run
HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run
HKCU\Software\Microsoft\Windows NT\CurrentVersion\Windows (Run、Load)
HKCU\Software\Microsoft\Command Processor (Autorun)
HKLM\Software\Microsoft\Command Processor (Autorun)
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Terminal Server\Install\...
根键必须作为来源的一部分保留。HKCU 保存当前用户配置,HKLM 保存本机配置。同名值出现在两处时,代表两个独立设置。
不同键也有不同用途。RunServices 属于兼容位置。策略型 Explorer\Run 位于策略路径。Windows 键中的 Run 和 Load 是固定值名。Command Processor 的 Autorun 是命令解释器初始化设置。
HKLM 的 32 位和 64 位软件视图需要分别读取。KEY_WOW64_64KEY 和 KEY_WOW64_32KEY 可以固定读取范围。结果中必须写明视图。路径文本相同的值也不能合并。
2. 打开键时只请求读取权限
打开现有键的宽字符 API 返回一个新的 HKEY。返回 ERROR_SUCCESS 表示成功,其它 LSTATUS 返回值表示键不存在、权限不足等 Win32 错误。
// 意义:在 hKey 下打开 lpSubKey,并请求 samDesired 指定的权限和注册表视图。
// 返回:ERROR_SUCCESS 表示成功。其它 LSTATUS 是可记录的 Win32 错误码。
LSTATUS RegOpenKeyExW(
HKEY hKey, // 输入:预定义根键或已经打开的父键。
LPCWSTR lpSubKey, // 输入:相对 hKey 的、以 NUL 结束的键路径。
DWORD ulOptions, // 输入:保留,必须为 0。
REGSAM samDesired, // 输入:KEY_QUERY_VALUE 与可选 KEY_WOW64_* 标志。
PHKEY phkResult // 输出:成功时得到的新 HKEY,调用方负责关闭。
);
// 意义:释放 RegOpenKeyExW 成功得到的键句柄。
// 返回:ERROR_SUCCESS 表示成功。同一个打开句柄只能关闭一次。
LSTATUS RegCloseKey(
HKEY hKey // 输入:已打开且尚未关闭的子键句柄。
);
正确做法:先记录键路径和视图,再用最小权限打开键。一个键不能读取时,记录该来源的失败状态,然后继续处理其它来源。
HKEY key = nullptr;
const LSTATUS status = RegOpenKeyExW(
HKEY_LOCAL_MACHINE,
L"Software\\Microsoft\\Command Processor",
0,
KEY_QUERY_VALUE | KEY_WOW64_64KEY,
&key);
if (status == ERROR_SUCCESS) {
// 读取 Autorun 等直接值。
RegCloseKey(key);
}
错误示例:把默认视图当成完整范围。下面的调用在 32 位程序中默认读取 32 位视图,无法固定得到 64 位 Command Processor 设置。
// 错误:缺少 KEY_WOW64_64KEY,结果依赖调用方位数。
RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Command Processor",
0, KEY_QUERY_VALUE, &key);
三、用长度上限枚举每个键的直接值
3. 查询容量时区分名称字符数与数据字节数
查询键信息的宽字符 API 返回直接值数量、最长值名和最长值数据长度。最长值名以宽字符个数计算。最长数据以字节计算。最大值名长度不包含结尾 NUL。
// 意义:读取已打开键的值数量、最长值名和最长值数据长度。
// 返回:ERROR_SUCCESS 表示成功。其它 LSTATUS 表示键句柄或访问状态异常。
LSTATUS RegQueryInfoKeyW(
HKEY hKey, LPDWORD lpClass, LPDWORD lpcchClass, LPDWORD lpReserved,
LPDWORD lpcSubKeys, LPDWORD lpcMaxSubKeyLen, LPDWORD lpcMaxClassLen,
LPDWORD lpcValues, // 输出:当前直接值数量。
LPDWORD lpcMaxValueNameLen, // 输出:最长值名字符数,不含 NUL。
LPDWORD lpcMaxValueLen, // 输出:最长值数据字节数。
LPDWORD lpcbSecurityDescriptor, PFILETIME lpftLastWriteTime
);
正确做法:为值名额外预留一个 wchar_t,值数据则按字节分配。Run、Load 和 Autorun 都可能是空文本、未终止文本或环境变量文本。数据长度只能使用 API 返回值。
DWORD valueCount = 0, maxNameChars = 0, maxDataBytes = 0;
const LSTATUS status = RegQueryInfoKeyW(
key, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
&valueCount, &maxNameChars, &maxDataBytes, nullptr, nullptr);
if (status != ERROR_SUCCESS) {
return;
}
std::vector<wchar_t> valueName(maxNameChars + 1, L'\0');
std::vector<BYTE> valueData(maxDataBytes == 0 ? 1 : maxDataBytes);
错误示例:把数据字节数当成宽字符数。这个写法会掩盖单位错误,也没有为最大长度值名预留 NUL 位置。
// 错误:maxDataBytes 的单位是字节,不能用来构造 wchar_t 缓冲区。
std::vector<wchar_t> valueData(maxDataBytes);
4. 枚举值时重新填入两个容量参数
按索引读取值的宽字符 API 只读取当前键的直接值。返回成功后,名称长度变为实际字符数,数据长度变为实际字节数。下一次调用前,需要重新填入缓冲区容量。
// 意义:读取 hKey 中 dwIndex 指定的一个直接值。
// 返回:ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示正常结束。
// ERROR_MORE_DATA 表示容量不足。
LSTATUS RegEnumValueW(
HKEY hKey, DWORD dwIndex, LPWSTR lpValueName,
LPDWORD lpcchValueName, LPDWORD lpReserved, LPDWORD lpType,
LPBYTE lpData, LPDWORD lpcbData
);
正确做法:每次调用前恢复容量。遇到 ERROR_MORE_DATA 时,重新查询长度上限,再有限重试同一索引。只有 ERROR_NO_MORE_ITEMS 表示当前键枚举正常结束。值数量只是查询时刻的统计结果。
DWORD nameChars = static_cast<DWORD>(valueName.size());
DWORD dataBytes = static_cast<DWORD>(valueData.size());
DWORD type = REG_NONE;
const LSTATUS status = RegEnumValueW(
key, index, valueName.data(), &nameChars, nullptr,
&type, valueData.data(), &dataBytes);
if (status == ERROR_SUCCESS) {
valueName.resize(nameChars); // 实际字符数,不包含 NUL。
valueData.resize(dataBytes); // 实际字节数。
}
错误示例:把所有值都解释为启动命令。Windows 键中的 Run 和 Load 需要按固定值名识别。Command Processor 的 Autorun 也需要单独识别。策略键和 RunServices 中的普通值仍要保留完整位置与类型。未知值不能只靠名称猜测用途。
四、保留文本、环境变量和命令解释边界
5. 原始 REG_EXPAND_SZ 文本与展开文本不同
原始字节必须先保留。一个 REG_EXPAND_SZ 值可以是 %SystemRoot%\System32\cmd.exe /d /c task.cmd。其中,%SystemRoot% 来自当前进程的环境变量。不同用户和服务账户可能得到不同的 %APPDATA% 与 %USERPROFILE% 展开结果。
命令文本必须完整保留。"C:\\Program Files\\Tool\\tool.exe" /background 的第一个空格位于引号内。cmd.exe /c 后面的内容也要按命令解释器规则处理。按第一个空格截断会得到错误的路径。
只有 REG_SZ 和 REG_EXPAND_SZ 可以按单个 UTF-16 文本处理。REG_BINARY、REG_DWORD 和 REG_MULTI_SZ 需要保留原始类型与字节长度。数据看起来可以显示,也不能直接按命令文本解释。
五、让结论保持在只读范围内
6. 同名值不应跨位置去重
条目位置必须包含根键、注册表视图、键路径和值名。策略型 Run 与普通 Run 属于不同位置。HKCU 与 HKLM 也属于不同位置。32 位与 64 位视图即使出现相同命令文本,也不能合并为同一条设置。
注册表状态和实际活动需要分别判断。读取到兼容键或 Autorun 值,只能说明该值在本次查询时存在。它是否被具体系统组件读取,是否创建目标进程,目标是否完成,都需要独立的进程或事件证据。
每个成功打开的 HKEY 都要关闭。程序只请求 KEY_QUERY_VALUE,不会创建、修改或删除键值。这样的读取不会改变观察对象。
完整可运行程序在附件。

[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。