首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
编程技术
发新帖
0
0
[原创][原创][自学] RunOnceEx 注册表分组键的安全遍历
发表于: 2026-7-16 22:51
1094
[原创][原创][自学] RunOnceEx 注册表分组键的安全遍历
WangWei_CM.
2026-7-16 22:51
1094
# RunOnceEx 注册表分组键的安全遍历 RunOnceEx 的典型布局包含根键、一级分组子键和分组内的值。读取时要先确定根键与注册表视图,再分别枚举根键直接值和一级分组值。每条结果都要保留所在层级。来源和类型验证完成后,才能解释 `Title`、`Flags` 等数据。 ## 一、对象关系和完整流程 RunOnceEx 的读取流程由注册表层级决定。根键可以直接保存值,也可以包含一级分组键。分组键中又可以保存自己的值。注册表 API 每次只枚举当前键的直接内容,不会自动进入下一层。 ```text 选择 HKLM 或 HKCU -> 选择 32 位或 64 位视图 -> 打开 RunOnceEx 根键 -> 枚举根键直接值 -> 枚举一级分组名 -> 逐个打开分组 -> 枚举分组内的值 -> 按类型解释原始字节 ``` ### 1. 注册表用键和值保存系统与用户设置 读取层级前要先分清键和值。Windows 注册表是由多个配置单元组成的层级数据库。`HKEY_LOCAL_MACHINE`(HKLM)保存计算机范围的设置。`HKEY_CURRENT_USER`(HKCU)保存当前登录用户的设置。每个键可以包含子键,也可以包含多个命名值。每个值由名称、类型和原始字节组成。 路径 `HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnceEx\0001` 中,`RunOnceEx` 与 `0001` 都是键名。`0001` 键里的 `Title`、`Flags` 或自定义名称才是值名。读取程序先打开键,再从该键读取直接值。下一层对象不会自动出现在当前层的枚举结果中。 下表中的数据类型决定了读取程序应如何解释字节。类型信息必须和数据一起保存,脱离类型直接把缓冲区转换为字符串会损坏数值、二进制数据和多字符串数据。 | 类型 | 原始内容 | 合理的读取方式 | | --- | --- | --- | | `REG_SZ` | 一段 UTF-16 文本 | 按返回的字节数构造宽字符串,移除一个真实存在的结尾 NUL。 | | `REG_EXPAND_SZ` | 可能含 `%SystemRoot%` 等环境变量的 UTF-16 文本 | 先保留原文。需要展示展开结果时,再用环境变量展开 API 得到另一份文本。 | | `REG_DWORD` | 恰好 4 字节的无符号整数 | 检查长度为 4 后用 `DWORD` 读取,并按十进制或十六进制展示。 | | `REG_MULTI_SZ` | 多段 UTF-16 文本,以额外 NUL 结束 | 逐段解析,不能当作只有一条命令的普通字符串。 | | `REG_BINARY` | 没有统一语义的字节序列 | 保留长度与十六进制表示,不把它假设为文本。 | ### 2. RunOnceEx 是一类安装阶段使用过的分组配置位置 RunOnceEx 是带一级分组的配置结构。传统 `Run` 和 `RunOnce` 直接在一个键下用值名对应命令。RunOnceEx 还可以建立一级分组键,把标题、标志和多个条目放入同一组。不同 Windows 版本和安装程序可能使用不同的登录初始化方式。某个键存在,只能证明配置存在,不能证明当前系统会处理其中每一项。 RunOnceEx 的根键通常出现在两个范围。HKLM 中的键面向计算机级设置,读取时可能需要更高权限。HKCU 中的键面向当前用户,普通用户通常能读取自己的配置。两个位置要独立枚举。一个位置读取失败时,另一个位置仍要继续处理。 ```text RunOnceEx 根键 根键直接值 ValueName = ValueData 0001 一级分组键 Title = "组件初始化" Flags = DWORD 001 = "命令文本" 0002 ... ``` 这个层级关系决定了记录格式。每条结果至少要保存根配置单元、软件视图、根路径、分组名、值名、类型和原始长度。完整的位置字段可以准确区分 `0001\001` 与 `0002\001`。 ### 3. 32 位和 64 位程序看到的软件视图可能不同 HKLM 软件键还要明确注册表视图。64 位 Windows 为部分 `HKLM\Software` 内容维护 32 位和 64 位视图。32 位程序默认打开 32 位视图,64 位程序默认打开 64 位视图。完整检查需要分别使用 `KEY_WOW64_64KEY` 和 `KEY_WOW64_32KEY`。只读取程序默认视图时,不要加入这两个标志。 `KEY_WOW64_64KEY` 与 `KEY_WOW64_32KEY` 只影响可重定向的注册表区域,不能用于推断系统位数,也不能对 HKCU 的所有路径强行套用。结果中标记“64 位视图”或“32 位视图”能够避免把两个不同对象误合并为同一项。 ### 4. 根键直接值与分组值的读取顺序应独立保存 根层和分组层需要分别记录状态。打开 RunOnceEx 根键后,先枚举它的直接值,再枚举一级子键。每个一级子键都要独立打开,然后枚举其中的值。根键没有分组、某个分组无法打开、分组存在但没有值,都是不同的配置事实。 根键直接值可以与分组内的值同名。例如,根键的 `Install` 和 `0001\Install` 是两个位置不同的注册表值。记录应包含根配置单元、软件视图、根路径、可选分组名、值名、REG 类型、原始数据长度和读取状态。缺少其中任何一个位置字段,都可能让同名项相互覆盖。 ### 5. 名称、数据和类型决定字符串解码是否安全 值解码前要先检查类型和长度。`RegEnumValueW` 返回的值名长度以 UTF-16 宽字符数计算,值数据长度以字节计算。名称缓冲区要额外预留一个 NUL,数据缓冲区则按字节分配。`REG_SZ` 与 `REG_EXPAND_SZ` 的数据长度能被 `sizeof(wchar_t)` 整除时,才能按 UTF-16 解码。结尾 NUL 只在真实存在时移除一个。 `REG_DWORD` 需要恰好四字节才能读取为数值。`REG_MULTI_SZ` 需要按单 NUL 分段,并以双 NUL 结束。`REG_BINARY` 没有通用文本语义。未知值名和未知类型同样属于枚举结果,也要保存类型和原始长度。 ### 6. 并发写入会改变枚举容量和索引含义 注册表内容可能在枚举期间变化。`RegQueryInfoKeyW` 返回的最大名称和数据长度,只能作为当前时刻的初始容量。后续 `RegEnumKeyExW` 或 `RegEnumValueW` 返回 `ERROR_MORE_DATA` 时,要扩大对应缓冲区,再重试同一索引。只有 `ERROR_NO_MORE_ITEMS` 表示本轮枚举结束。访问拒绝和键删除要单独记录,并继续处理同级其它项。 索引只用于当前一轮 API 调用。比较两次读取结果时,要按完整键路径、分组名和值名配对,并保留每次的时间、视图与类型。分组改名、插入或删除后,原来的索引可能代表另一个对象。 ### 7. HKEY、REGSAM 与访问失败的语义也应成为输出的一部分 `HKEY` 是成功打开注册表键后由调用方持有的资源引用,使用结束后要用 `RegCloseKey` 释放。`REGSAM` 是本次请求的访问掩码,决定能否查询值或枚举子键。只读遍历请求 `KEY_QUERY_VALUE`、`KEY_ENUMERATE_SUB_KEYS` 和明确的 WOW64 视图,不请求写入、创建或删除权限。 根键打开失败、分组打开失败、值缺失和缓冲区不足不能合并为一个空结果。每种失败都携带完整路径、视图、请求权限和 `LSTATUS` 返回值,读者才能知道是配置不存在、当前令牌不可读,还是注册表在读取过程中发生变化。 采样时间与调用账户也要记录。它们说明读取结论适用的系统时刻和权限环境。完整保留这些字段后,才能比较同一位置在不同时间的变化。 这一记录模型不依赖固定的分组名和值名,因此也适用于未知配置。后续审阅时,每条结果都能回到原始位置和读取状态。 ## 二、先保留根键和分组键的边界 ### 1. 一条值需要完整位置 层级识别要从 RunOnceEx 根键开始。常见根路径为: ```text HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnceEx HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnceEx ``` 分组值的位置还要包含一级分组名。根键中的 `Install` 与 `RunOnceEx\0001` 中的 `Install` 是不同值。完整位置应包含根键、32 位或 64 位视图、根路径、分组名和值名。 数字分组名不能直接解释为处理顺序。`0001`、`0002` 只是子键名称,注册表 API 返回的索引也只属于本次读取。没有对应规范时,不能靠字典序推导系统处理次序。 ## 三、先打开根键,再读取直接值和一级分组 ### 2. 打开键时只申请查询和子键枚举权限 打开键的宽字符 API 会返回一个新的 `HKEY`。返回 `ERROR_SUCCESS` 表示成功,`KEY_QUERY_VALUE` 允许读取直接值,`KEY_ENUMERATE_SUB_KEYS` 允许读取一级分组名。 ```cpp // 意义:打开 hKey 下的一个现有键并返回新 HKEY。 // 返回:ERROR_SUCCESS 表示成功。其它 LSTATUS 表示键缺失、权限不足等错误。 LSTATUS RegOpenKeyExW( HKEY hKey, LPCWSTR lpSubKey, DWORD ulOptions, REGSAM samDesired, // 输入:KEY_QUERY_VALUE、KEY_ENUMERATE_SUB_KEYS 和 KEY_WOW64_*。 PHKEY phkResult // 输出:成功时得到的键句柄,使用后关闭。 ); // 意义:关闭成功打开的 HKEY。 // 返回:ERROR_SUCCESS 表示成功。一个打开句柄只能关闭一次。 LSTATUS RegCloseKey(HKEY hKey); ``` 正确做法:显式指定 HKLM 软件视图。根键读取成功后,先处理直接值,再进入一级分组。某个分组失败时,仍要继续处理其它分组。 ```cpp HKEY rootKey = nullptr; const LSTATUS status = RegOpenKeyExW( HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\RunOnceEx", 0, KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS | KEY_WOW64_64KEY, &rootKey); if (status == ERROR_SUCCESS) { // 先枚举 rootKey 的直接值,再枚举 rootKey 的一级子键。 RegCloseKey(rootKey); } ``` 错误示例:只枚举根键值。根键的值枚举操作不会递归进入子键。下面的循环没有进入一级分组,因此无法读取 `RunOnceEx\0001` 内部的值。 ```cpp // 错误:只处理 rootKey 的直接值,缺少“枚举一级分组并打开分组”的步骤。 for (DWORD index = 0; index < directValueCount; ++index) { // 这里只会得到根键层的值。 } ``` ### 3. 查询容量时分开处理子键名和值数据 查询键信息的宽字符 API 返回子键和值的最大长度。最长子键名与最长值名以宽字符个数计算。最长值数据以字节计算。两个名称长度都不包含结尾 NUL。 ```cpp // 意义:读取 hKey 的直接子键和值的统计信息与容量上限。 // 返回:ERROR_SUCCESS 表示成功。其它 LSTATUS 表示查询失败。 LSTATUS RegQueryInfoKeyW( HKEY hKey, LPWSTR lpClass, LPDWORD lpcchClass, LPDWORD lpReserved, LPDWORD lpcSubKeys, // 输出:一级子键数量。 LPDWORD lpcMaxSubKeyLen, // 输出:最长一级子键名字符数,不含 NUL。 LPDWORD lpcMaxClassLen, LPDWORD lpcValues, LPDWORD lpcMaxValueNameLen, // 输出:最长值名字符数,不含 NUL。 LPDWORD lpcMaxValueLen, // 输出:最长值数据字节数。 LPDWORD lpcbSecurityDescriptor, PFILETIME lpftLastWriteTime ); ``` 正确做法:给名称多预留一个宽字符,数据则按字节分配。根键和每个分组键都需要单独查询上限。根键的最大值长度不能代替分组内的最大值长度。 ```cpp DWORD maxGroupNameChars = 0; const LSTATUS status = RegQueryInfoKeyW( rootKey, nullptr, nullptr, nullptr, nullptr, &maxGroupNameChars, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); if (status != ERROR_SUCCESS) { return; } std::vector<wchar_t> groupName(maxGroupNameChars + 1, L'\0'); ``` 错误示例:遗漏名称终止符。最大长度分组名恰好填满 `maxGroupNameChars` 个字符时,下面的缓冲区没有位置保存 NUL。 ```cpp // 错误:最长名称长度不包含 NUL。 std::vector<wchar_t> groupName(maxGroupNameChars); ``` ## 四、枚举一级子键,再枚举每个子键中的值 ### 4. 子键枚举 API 只返回一级名称 按索引读取子键名的宽字符 API 返回当前键的直接子键。返回 `ERROR_NO_MORE_ITEMS` 表示枚举结束,`ERROR_MORE_DATA` 表示名称缓冲区不足。 ```cpp // 意义:读取 hKey 下索引为 dwIndex 的一个直接子键名。 // 返回:ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示正常结束。 // ERROR_MORE_DATA 表示容量不足。 LSTATUS RegEnumKeyExW( HKEY hKey, DWORD dwIndex, LPWSTR lpName, LPDWORD lpcchName, // 输入/输出:名称字符容量与实际长度,不含 NUL。 LPDWORD lpReserved, LPWSTR lpClass, LPDWORD lpcchClass, PFILETIME lpftLastWriteTime ); ``` 正确做法:用返回的分组名打开子键,再单独枚举其中的值。子键名使用显式长度构造 `std::wstring`,然后通过 `c_str()` 提供 NUL 终止路径。 ```cpp DWORD groupChars = static_cast<DWORD>(groupName.size()); const LSTATUS enumStatus = RegEnumKeyExW( rootKey, groupIndex, groupName.data(), &groupChars, nullptr, nullptr, nullptr, nullptr); if (enumStatus == ERROR_SUCCESS) { const std::wstring groupPath(groupName.data(), groupChars); HKEY groupKey = nullptr; if (RegOpenKeyExW(rootKey, groupPath.c_str(), 0, KEY_QUERY_VALUE, &groupKey) == ERROR_SUCCESS) { // 查询 groupKey 自己的值长度上限,再枚举 groupKey 内部的直接值。 RegCloseKey(groupKey); } } ``` 错误示例:把分组名拼进固定数组。异常长分组名或截断路径会让后续打开的键偏离原始对象。固定的 `wchar_t path[260]` 和 `wcscat` 不适合处理未经长度验证的 API 返回数据。 ## 五、按名称和类型识别分组元数据 ### 5. Title 与 Flags 不能当成同一种文本 元数据要同时检查值名和类型。`Title` 常作为分组文本说明,`Flags` 常作为 `REG_DWORD` 数值。未知值也要保留原始名称、类型和字节,不能因为名称不在已知列表中就丢弃。 `REG_DWORD` 必须恰好为四字节。`REG_BINARY` 可能包含可显示片段,`REG_MULTI_SZ` 也由宽字符组成。这两种类型都不能按单个命令文本解释。 只有 `REG_SZ` 与 `REG_EXPAND_SZ` 可以按实际字节长度构造 UTF-16 文本。奇数字节长度无法形成完整宽字符。末尾 NUL 只在真实存在时移除一个。不要使用 `wcslen` 搜索缓冲区外的终止符。 遇到 `ERROR_MORE_DATA` 时,要重新查询根键或分组键的当前上限。重新分配缓冲区后,有限次数重试同一索引。连续失败时记录位置和返回码,再继续处理同级其它对象。 ## 六、保持结论的时间边界 ### 6. 枚举索引不等于系统处理顺序 索引只能作为本次读取的定位信息。子键和值可能在读取期间增加、删除或改名。索引不能跨两次读取作为稳定身份,也不能代表 RunOnceEx 的实际处理顺序。 注册表状态和实际活动需要分别判断。某个分组或值在本次读取中存在,只能证明它位于对应根键、视图和路径。它是否被系统处理,目标是否完成,需要独立的进程或事件证据。 完整可运行程序在附件。 
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
上传的附件:
003-RunOnceEx注册表分组键的安全遍历.zip
(168.25kb,4次下载)
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
WangWei_CM.
30
发帖
1
回帖
40
RANK
关注
私信
他的文章
[原创] 进程模块枚举、映像身份与线程启动地址关联
215
[原创]Windows 进程 CPU、内存、I/O 与调度指标采样
981
[原创]跨位数进程的映像、命令行与环境块读取
535
[原创]进程实例身份、父子关系与会话归属
293
[原创]Windows 进程枚举NtQuerySystemInformation(SystemProcessInformation
635
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
huangyalei
Fzzz
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部