首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
编程技术
发新帖
1
0
[原创]KnownDlls 与 KnownDLLs32:系统 DLL 映射表的双架构审计
发表于: 2026-7-24 10:47
274
[原创]KnownDlls 与 KnownDLLs32:系统 DLL 映射表的双架构审计
WangWei_CM.
2026-7-24 10:47
274
# KnownDlls 与 KnownDLLs32:系统 DLL 映射表的双架构审计 本篇要完成的工作是:只读列出 `KnownDlls` 和 `KnownDlls32` 在 64 位、32 位注册表视图中的全部映射值,并为每项保留键名、视图、值名、值类型和原始数据。结果只说明当前注册表中的映射配置,不推断某个进程是否已经加载对应模块。 Known DLL 是 Windows 加载器为常见系统模块维护的名称映射。Windows 加载器负责把可执行文件需要的 DLL 映射到进程地址空间。进程请求 DLL 时,加载器会先处理已加载模块、API Set 重定向和 Known DLL 等特殊规则,再按适用规则处理普通 DLL 搜索。`KnownDlls` 与 `KnownDlls32` 保存不同架构环境可用的映射记录。 要完成这项读取任务,分为四步: 1. 确定两个映射表与两种注册表视图 2. 只读打开每个来源并取得枚举边界 3. 按索引读取全部值并处理配置变化 4. 按类型安全解释数据并输出带来源的记录 完整流程如下: ```text 确定 KnownDlls 与 KnownDlls32 的 32/64 位来源 -> 打开键并查询值数与最大长度 -> 逐项读取值名、类型和原始字节 -> 仅解码满足条件的文本或数值并保留来源 ``` ## 一、第一步:确定映射表、映射字段与四个读取来源 第一步要明确每条记录从哪里来,以及每个字段表示什么。注册表映射、磁盘文件、内核节对象和进程模块属于不同观察对象,本篇读取的是第一类对象。 ### DLL 加载需要先解析模块名称,再确定映像来源 Known DLL 需要放回 Windows DLL 加载过程理解。应用导入 DLL 时,Windows 加载器会根据模块名称、已加载模块、系统目录、应用所在目录和安全加载规则等信息寻找合适映像。DLL 名称并不天然等于某个唯一磁盘路径,同名文件也可能在不同目录出现。 Known DLL 是系统维护的一组特殊模块映射。对处于该映射表中的常见系统 DLL,加载器可使用系统预先准备的映像节对象,以减少重复加载并维持系统组件的一致性。注册表项提供的是“映射别名与 DLL 文件名”的配置线索,具体加载器行为仍由系统版本和内核对象状态决定。 这个机制解释了为什么不能把 KnownDlls 当作启动项。它不定义登录时要启动哪个进程,也不保存带参数的命令行。每一项是一个模块名称映射记录。 ### 值名和 DLL 名称分别表达逻辑别名与文件名 一条映射记录由两个字段组成。`KnownDlls` 键下的值名常是模块别名,例如 `kernel32`。值数据常是文件名,例如 `kernel32.dll`。两者配合形成映射关系,不能只保存其中一侧,也不应把它们拼成当前工作目录下的相对路径。 文件名没有目录时,合理的下一步是以文档化的系统目录规则形成“候选文件位置”,并把候选来源写明。候选路径、文件存在性、PE 架构和签名都是后续验证结果,不能覆盖原始别名或注册表值数据。 ### KnownDlls 与 KnownDLLs32 处理不同架构环境 32 位与 64 位进程不能共享同一 DLL 映像。64 位进程需要 64 位模块,32 位进程需要 32 位模块。`KnownDlls` 与 `KnownDlls32` 是用于区分相应架构映射的键名。不同系统版本上键是否存在、内容是否相同都应通过实际读取得出。 注册表视图又增加了一层身份。读取时应对每个键名分别使用 64 位和 32 位访问标志,结果至少包含键名、视图、别名和值数据。即使值文本相同,也不能在输出时合并,因为它们来自不同的配置位置。 ### 注册表配置与当前加载模块需要独立观察 读取结果需要限制结论范围。读取到 `KnownDlls` 值说明当前配置中存在该映射条目。它不能单独证明特定进程已经加载相应 DLL,也不能说明内存中的模块文件来自哪个映像节对象。运行时模块列表、系统句柄或进程调试信息属于另一类观察来源。 安全的读取记录可包含读取时间、完整注册表来源、原始类型、原始字节数和可安全解码的文本。这样能够保留配置事实,也能说明结果的边界。 ### 映像节对象与磁盘文件不是同一个观察对象 磁盘路径与内存映射需要分别理解。磁盘上的 DLL 文件是一个文件系统对象。加载器使用的映像节对象是内核管理的内存映射对象。进程模块列表中的记录又是特定进程地址空间中的观察结果。Known DLL 机制与映像节对象有关,因此“系统目录中找到同名文件”无法直接说明某个进程如何得到它。 只读取注册表时,结果应表述为映射配置。文件信息需要文件系统和签名查询,运行时加载信息需要进程模块查询。三类信息来自不同 API 或系统对象,不能省略来源标签后合成一句结论。 ### Known DLL 处理发生在普通路径搜索之前 Known DLL 映射位于加载器的特殊规则阶段。进程请求 `kernel32.dll` 一类模块时,加载器会先处理已加载模块、API Set 重定向、Known DLL 映射等特殊规则,再在适用情形下进入普通 DLL 搜索路径。普通搜索路径涉及应用所在目录、系统目录和安全加载设置。Known DLL 命中则使用系统维护的映像对象,不以应用当前目录为候选来源。 因此,检查某目录里是否存在同名 DLL 不能改变 Known DLL 映射的含义。应用所在目录中有 `kernel32.dll`,并不等于加载器会对标准系统模块采用该文件。同样,KnownDlls 键中有映射文本,也不能替代对文件系统、签名策略或运行时模块的独立观察。 ### 映射项、对象管理器目录和节对象属于不同层次 注册表、对象管理器和进程地址空间属于不同层次。注册表 `KnownDlls` 键保存可读配置文本。对象管理器是内核管理命名对象与引用计数的组件,Known DLL 相关的节对象在其命名空间中有自己的对象身份。节对象描述一段可映射到进程地址空间的字节范围。进程加载模块时,内存管理器和加载器根据节对象建立映像视图。 这三个层次可以这样表示: ```text KnownDlls 注册表值 -> 别名与 DLL 文件名 -> 对象管理器中的系统映像节对象 -> 某个进程地址空间中的模块视图 ``` 每层都需要不同的查询方法和权限。注册表值读取失败不代表节对象不存在。进程模块枚举失败不代表注册表映射无效。审计报告必须写出数据来源,防止读者把不同层次的空结果误当成相互矛盾。 ### WOW64 让同名模块的架构判定更重要 WOW64 的职责决定 32 位映射的边界。WOW64 是 Windows 在 64 位系统上运行 32 位用户态应用的兼容层。32 位进程拥有自己的用户态地址空间和模块架构,不能把 64 位 DLL 直接映射进去。KnownDlls32 与 32 位注册表视图正是为了让这类进程拥有适用的系统模块映射。 读取结果应把“键名”“注册表视图”“值名”和“值数据”记录为四个字段。例如,`KnownDlls32` 的一个 `REG_SZ` 值说明 32 位映射配置。PE `Machine` 值需要文件检查才能说明文件架构,进程模块列表需要进程检查才能说明某次采样的实际映射。各类结果都应依据实际读取,而不是从文件名猜测。 ### 映射的安全意义来自受控加载路径,检查仍需分层 系统机制与应用自身的判断需要分开。Known DLL 映射为常见系统模块提供受控的加载路径,减少普通目录搜索对这些模块的影响。它解决的是加载器如何选择系统映像这一类问题,不能自动验证应用加载的所有第三方 DLL,也不能替代系统的代码完整性、签名、应用控制或进程保护机制。 读取映射表时,不应从映射存在推出“系统绝对安全”或“某个进程必然正常”。需要进行扩展检查时,可并列记录映射配置、文件版本和签名、进程模块地址范围、加载时间、相关系统策略以及采样失败状态。每一条记录只证明自己来源提供的事实,下一层证据用于缩小解释范围。 完成第一步后,读取方已经知道每一条记录的键名和视图身份,也知道值名与值数据不能互相替代。接下来需要用只读权限打开每个来源,并在枚举前取得长度边界。 ## 二、第二步:只读打开每个来源并取得枚举边界 `KnownDlls` 与 `KnownDlls32` 是两个不同的键名。每个键还可能在 64 位或 32 位注册表视图中出现,审计记录至少需要保存键名、视图、值名、值类型和值数据。 ### HKEY、DWORD、UTF-16 与 NUL 是读取时的基础词汇 打开和枚举 API 的单位与所有权需要先明确。`HKEY` 是当前进程打开注册表键后得到的系统资源引用,成功打开后由调用方 `RegCloseKey`。`DWORD` 是 32 位无符号整数。在值枚举中,一个 `DWORD` 可能保存 UTF-16 字符数量,也可能保存原始数据的字节数量,变量名应明确区分。 UTF-16 是 Windows 宽字符 API 常用的文本编码,`wchar_t` 在 Windows 上保存一个 UTF-16 代码单元。NUL 是数值为零的终止字符。注册表值数据不保证总有结尾 NUL,因此文本构造只能使用 API 返回的明确长度。`REG_SZ` 与 `REG_EXPAND_SZ` 才适合尝试按 UTF-16 解码,`REG_BINARY`、`REG_DWORD` 和未知类型应保留原始表示。 ```cpp // 意义:打开一个注册表子键。 // 返回:ERROR_SUCCESS 表示成功。其它返回值是 Win32 错误码。 // 成功时 phkResult 接收 HKEY,调用方必须关闭。 LSTATUS RegOpenKeyExW( HKEY hKey, LPCWSTR lpSubKey, DWORD ulOptions, REGSAM samDesired, PHKEY phkResult ); // 意义:关闭由注册表打开函数返回的键句柄。 // 返回:ERROR_SUCCESS 表示成功。 LSTATUS RegCloseKey(HKEY hKey); ``` ```cpp const struct { const wchar_t* path; REGSAM view; } sources[] = { { L"System\\CurrentControlSet\\Control\\Session Manager\\KnownDlls", KEY_WOW64_64KEY }, { L"System\\CurrentControlSet\\Control\\Session Manager\\KnownDlls", KEY_WOW64_32KEY }, { L"System\\CurrentControlSet\\Control\\Session Manager\\KnownDlls32", KEY_WOW64_64KEY }, { L"System\\CurrentControlSet\\Control\\Session Manager\\KnownDlls32", KEY_WOW64_32KEY }, }; ``` 键不存在是可见状态。部分系统没有 `KnownDlls32`,这个状态不能与 `KnownDlls` 的空值表或读取失败合并。 打开成功后还需要取得本次枚举的容量参考。`RegQueryInfoKeyW` 返回当前值数、最大值名长度和最大值数据长度。值名长度的单位是 UTF-16 字符,值数据长度的单位是字节,两者必须保存到不同变量中。 ```cpp // 意义:取得键在本次查询时的值数和缓冲区长度参考。 // 返回:ERROR_SUCCESS 表示成功。lpcValues、lpcchMaxValueNameLen // 和 lpcbMaxValueLen 都是输出参数,可为 nullptr 时表示调用方不需要该字段。 LSTATUS RegQueryInfoKeyW( HKEY hKey, LPWSTR lpClass, LPDWORD lpcchClass, LPDWORD lpReserved, LPDWORD lpcSubKeys, LPDWORD lpcbMaxSubKeyLen, LPDWORD lpcbMaxClassLen, LPDWORD lpcValues, LPDWORD lpcbMaxValueNameLen, LPDWORD lpcbMaxValueLen, LPDWORD lpcbSecurityDescriptor, PFILETIME lpftLastWriteTime ); ``` ```cpp DWORD valueCount = 0; DWORD maxNameChars = 0; DWORD maxDataBytes = 0; const LSTATUS infoStatus = RegQueryInfoKeyW( knownDlls, // 输入:已打开的只读 HKEY,调用方仍拥有关闭责任。 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, &valueCount, // 输出:当前值数。 &maxNameChars, // 输出:最大值名长度,单位为 UTF-16 字符,不含结尾 NUL。 &maxDataBytes, // 输出:最大值数据长度,单位为字节。 nullptr, nullptr); if (infoStatus != ERROR_SUCCESS) { // 记录错误并停止读取当前键。不能使用未初始化的长度继续分配缓冲区。 } ``` 完成第二步后,读取方已经拥有明确的键句柄、来源标签和本次查询的长度参考。注册表内容可能在下一次调用前变化,第三步仍要按 API 返回的实际长度调整缓冲区。 ## 三、第三步:按索引读取全部值并处理配置变化 映射表使用值枚举结构。值名通常是模块别名,值数据通常是 DLL 文件名。两者具有不同角色,任何一个都不等于完整文件路径。 ### 映射表枚举需要处理配置变化和异常类型 映射表可能在读取期间变化。`RegQueryInfoKeyW` 给出的最大值名和数据长度只适用于本次查询时刻。下一次 `RegEnumValueW` 可能返回更长的名称或数据,`ERROR_MORE_DATA` 表示应扩大相应缓冲区并重试同一索引。 值数据通常是字符串,仍需要检查类型与 UTF-16 字节对齐。遇到数值、二进制、未知类型或无结尾 NUL 时,记录类型、长度和安全的原始表示即可。读取目标是完整反映注册表内容,不把每个值强制转换成可用 DLL 名称。 ### 标准枚举流程先保证资源,再解释文本 资源管理要先于文本解释。`RegOpenKeyExW` 成功后交出 `HKEY`,调用方必须 `RegCloseKey`。`RegQueryInfoKeyW` 返回的最大长度分别以字符和字节计。`RegEnumValueW` 返回 `ERROR_NO_MORE_ITEMS` 时才可结束。只有在类型为 `REG_SZ` 或 `REG_EXPAND_SZ` 且数据长度是 `sizeof(wchar_t)` 的整数倍时,值数据才可按 UTF-16 文本解码。 正确代码先保存原始值名、类型和字节数,再生成可选文本字段。出现 `ERROR_MORE_DATA` 时重新分配并重试当前索引,出现 `ERROR_ACCESS_DENIED` 时保留来源和错误,出现未知类型时输出安全的长度和十六进制表示。这样做能够保证配置中任何异常项都不会越界访问,也不会被悄悄丢弃。 ```cpp // 意义:按索引读取一个映射值。 // 返回: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 ); ``` ### 完整正确示范要同时处理两个长度单位和关闭责任 下面的流程只展示一个值的安全读取要点。先以 `KEY_QUERY_VALUE` 打开键,成功后读取一个索引。`nameCapacityChars` 与 `dataCapacityBytes` 不复用。`ERROR_NO_MORE_ITEMS` 是正常结束,`ERROR_MORE_DATA` 是容量不足。任一成功打开的键都会在离开作用域前关闭。 ```cpp HKEY knownDlls = nullptr; const LSTATUS openStatus = RegOpenKeyExW( HKEY_LOCAL_MACHINE, // 输入:机器范围根键。系统所有,调用方不关闭。 knownDllsPath, // 输入:NUL 结尾 UTF-16 相对路径。 0, // 输入:保留,必须为 0。 KEY_QUERY_VALUE, // 输入:只枚举值所需最低权限。 &knownDlls); // 输出:成功时得到 HKEY,本段负责关闭。 if (openStatus == ERROR_SUCCESS) { std::vector<wchar_t> name(256, L'\0'); std::vector<BYTE> data(1024); DWORD nameCapacityChars = static_cast<DWORD>(name.size()); DWORD dataCapacityBytes = static_cast<DWORD>(data.size()); DWORD type = REG_NONE; const LSTATUS enumStatus = RegEnumValueW( knownDlls, 0, name.data(), &nameCapacityChars, nullptr, &type, data.data(), &dataCapacityBytes); if (enumStatus == ERROR_SUCCESS) { // 记录 nameCapacityChars 个名称字符、type 和 dataCapacityBytes 个原始数据字节。 } else if (enumStatus == ERROR_MORE_DATA) { // 正确:分别扩大两个缓冲区,重试索引 0。不能当作没有值。 } RegCloseKey(knownDlls); // 释放调用方拥有的 HKEY。此后句柄失效。 } ``` 正确枚举时为名称和数据使用独立缓冲区。读取期间映射表变化时分别扩大容量,不能让数据长度覆盖值名长度变量。 ```cpp std::vector<wchar_t> name(maxNameChars + 1, L'\0'); std::vector<BYTE> data(maxDataBytes); DWORD nameChars = static_cast<DWORD>(name.size()); DWORD dataBytes = static_cast<DWORD>(data.size()); DWORD type = REG_NONE; LSTATUS status = RegEnumValueW( key, index, name.data(), &nameChars, nullptr, &type, data.empty() ? nullptr : data.data(), &dataBytes); if (status == ERROR_SUCCESS) { std::wstring alias(name.data(), nameChars); data.resize(dataBytes); } ``` 下面的错误复用一个长度变量,第一次 API 可能把它改为数据字节数,第二次又被当作名称字符数使用。单位错位会造成名称截断或缓冲区管理错误。 ```cpp DWORD length = 260; RegEnumValueW(knownDlls, index, name, &length, nullptr, &type, data, &length); // 错误:同一个 length 同时承担字符数与字节数。 ``` 字符串值仅在 `REG_SZ` 或 `REG_EXPAND_SZ` 且字节数符合 UTF-16 对齐时解码。其它类型保留类型和原始字节数,避免把任意二进制值解释成 DLL 名称。 ```cpp // 错误示例:把值数据直接拼接到当前目录。 std::wstring path = L".\\" + decodedValue; // Known DLL 值数据可能只是 kernel32.dll 这类映射名称。 ``` 完成第三步后,读取方已经取得每个值的名称、类型和精确字节数,也已把键不存在、权限错误、容量不足和枚举结束区分开。最后一步只对类型和长度允许的数据进行解释,异常类型继续保留原始信息。 ## 四、第四步:按类型安全解释数据并输出带来源的记录 第四步要控制文本解释的边界。值名如 `kernel32` 与值数据如 `kernel32.dll` 共同构成映射记录。`REG_SZ` 和 `REG_EXPAND_SZ` 只有在字节数能够被 `sizeof(wchar_t)` 整除时,才可以按 UTF-16 文本读取。末尾出现一个 NUL 时可以去掉该终止符,内部 NUL、回车和换行仍要在输出前转义,避免内容改变控制台记录的结构。 `REG_DWORD` 只有在数据长度恰好为 4 字节时,才可以复制到 `DWORD` 后输出十进制数值。`REG_BINARY`、`REG_MULTI_SZ`、长度异常的字符串和未知类型都要保留类型名与原始字节数,不能强行当作 DLL 文件名。这样能让错误配置和未来的未知数据类型继续可见。 每条输出都应带上完整注册表路径与 32 位或 64 位视图标签。相同文本来自不同键或不同视图时,仍是不同来源的记录。值数据没有目录时,也不能按命令行、服务路径或 Run 键规则切分。文件系统路径、PE 架构、签名和进程加载状态需要额外查询,不能从映射值自动得出。 完整可运行程序在附件  <mark class="encrypted">6c5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9f1q4i4x3K6M7K6P5h3y4D9P5i4N6Z5</mark>
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
上传的附件:
016-KnownDLLs与KnownDLLs32双架构映射表审计.zip
(159.58kb,4次下载)
收藏
・
1
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
WangWei_CM.
30
发帖
1
回帖
40
RANK
关注
私信
他的文章
[原创] 进程模块枚举、映像身份与线程启动地址关联
216
[原创]Windows 进程 CPU、内存、I/O 与调度指标采样
981
[原创]跨位数进程的映像、命令行与环境块读取
535
[原创]进程实例身份、父子关系与会话归属
295
[原创]Windows 进程枚举NtQuerySystemInformation(SystemProcessInformation
637
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
Fzzz
jyotidwi
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部