KnownDlls 与 KnownDLLs32:系统 DLL 映射表的双架构审计

KnownDlls 与 KnownDLLs32:系统 DLL 映射表的双架构审计 KnownDlls 与 KnownDLLs32系统 DLL 映射表的双架构审计本篇要完成的工作是只读列出KnownDlls和KnownDlls32在 64 位、32 位注册表视图中的全部映射值并为每项保留键名、视图、值名、值类型和原始数据。结果只说明当前注册表中的映射配置不推断某个进程是否已经加载对应模块。Known DLL 是 Windows 加载器为常见系统模块维护的名称映射。Windows 加载器负责把可执行文件需要的 DLL 映射到进程地址空间。进程请求 DLL 时加载器会先处理已加载模块、API Set 重定向和 Known DLL 等特殊规则再按适用规则处理普通 DLL 搜索。KnownDlls与KnownDlls32保存不同架构环境可用的映射记录。要完成这项读取任务分为四步确定两个映射表与两种注册表视图只读打开每个来源并取得枚举边界按索引读取全部值并处理配置变化按类型安全解释数据并输出带来源的记录完整流程如下确定 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 相关的节对象在其命名空间中有自己的对象身份。节对象描述一段可映射到进程地址空间的字节范围。进程加载模块时内存管理器和加载器根据节对象建立映像视图。这三个层次可以这样表示KnownDlls 注册表值 - 别名与 DLL 文件名 - 对象管理器中的系统映像节对象 - 某个进程地址空间中的模块视图每层都需要不同的查询方法和权限。注册表值读取失败不代表节对象不存在。进程模块枚举失败不代表注册表映射无效。审计报告必须写出数据来源防止读者把不同层次的空结果误当成相互矛盾。WOW64 让同名模块的架构判定更重要WOW64 的职责决定 32 位映射的边界。WOW64 是 Windows 在 64 位系统上运行 32 位用户态应用的兼容层。32 位进程拥有自己的用户态地址空间和模块架构不能把 64 位 DLL 直接映射进去。KnownDlls32 与 32 位注册表视图正是为了让这类进程拥有适用的系统模块映射。读取结果应把“键名”“注册表视图”“值名”和“值数据”记录为四个字段。例如KnownDlls32的一个REG_SZ值说明 32 位映射配置。PEMachine值需要文件检查才能说明文件架构进程模块列表需要进程检查才能说明某次采样的实际映射。各类结果都应依据实际读取而不是从文件名猜测。映射的安全意义来自受控加载路径检查仍需分层系统机制与应用自身的判断需要分开。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和未知类型应保留原始表示。// 意义打开一个注册表子键。// 返回ERROR_SUCCESS 表示成功。其它返回值是 Win32 错误码。// 成功时 phkResult 接收 HKEY调用方必须关闭。LSTATUSRegOpenKeyExW(HKEY hKey,LPCWSTR lpSubKey,DWORD ulOptions,REGSAM samDesired,PHKEY phkResult);// 意义关闭由注册表打开函数返回的键句柄。// 返回ERROR_SUCCESS 表示成功。LSTATUSRegCloseKey(HKEY hKey);conststruct{constwchar_t*path;REGSAM view;}sources[]{{LSystem\\CurrentControlSet\\Control\\Session Manager\\KnownDlls,KEY_WOW64_64KEY},{LSystem\\CurrentControlSet\\Control\\Session Manager\\KnownDlls,KEY_WOW64_32KEY},{LSystem\\CurrentControlSet\\Control\\Session Manager\\KnownDlls32,KEY_WOW64_64KEY},{LSystem\\CurrentControlSet\\Control\\Session Manager\\KnownDlls32,KEY_WOW64_32KEY},};键不存在是可见状态。部分系统没有KnownDlls32这个状态不能与KnownDlls的空值表或读取失败合并。打开成功后还需要取得本次枚举的容量参考。RegQueryInfoKeyW返回当前值数、最大值名长度和最大值数据长度。值名长度的单位是 UTF-16 字符值数据长度的单位是字节两者必须保存到不同变量中。// 意义取得键在本次查询时的值数和缓冲区长度参考。// 返回ERROR_SUCCESS 表示成功。lpcValues、lpcchMaxValueNameLen// 和 lpcbMaxValueLen 都是输出参数可为 nullptr 时表示调用方不需要该字段。LSTATUSRegQueryInfoKeyW(HKEY hKey,LPWSTR lpClass,LPDWORD lpcchClass,LPDWORD lpReserved,LPDWORD lpcSubKeys,LPDWORD lpcbMaxSubKeyLen,LPDWORD lpcbMaxClassLen,LPDWORD lpcValues,LPDWORD lpcbMaxValueNameLen,LPDWORD lpcbMaxValueLen,LPDWORD lpcbSecurityDescriptor,PFILETIME lpftLastWriteTime);DWORD valueCount0;DWORD maxNameChars0;DWORD maxDataBytes0;constLSTATUS infoStatusRegQueryInfoKeyW(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时保留来源和错误出现未知类型时输出安全的长度和十六进制表示。这样做能够保证配置中任何异常项都不会越界访问也不会被悄悄丢弃。// 意义按索引读取一个映射值。// 返回ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示结束。// ERROR_MORE_DATA 表示名称或数据缓冲区不足。LSTATUSRegEnumValueW(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是容量不足。任一成功打开的键都会在离开作用域前关闭。HKEY knownDllsnullptr;constLSTATUS openStatusRegOpenKeyExW(HKEY_LOCAL_MACHINE,// 输入机器范围根键。系统所有调用方不关闭。knownDllsPath,// 输入NUL 结尾 UTF-16 相对路径。0,// 输入保留必须为 0。KEY_QUERY_VALUE,// 输入只枚举值所需最低权限。knownDlls);// 输出成功时得到 HKEY本段负责关闭。if(openStatusERROR_SUCCESS){std::vectorwchar_tname(256,L\0);std::vectorBYTEdata(1024);DWORD nameCapacityCharsstatic_castDWORD(name.size());DWORD dataCapacityBytesstatic_castDWORD(data.size());DWORD typeREG_NONE;constLSTATUS enumStatusRegEnumValueW(knownDlls,0,name.data(),nameCapacityChars,nullptr,type,data.data(),dataCapacityBytes);if(enumStatusERROR_SUCCESS){// 记录 nameCapacityChars 个名称字符、type 和 dataCapacityBytes 个原始数据字节。}elseif(enumStatusERROR_MORE_DATA){// 正确分别扩大两个缓冲区重试索引 0。不能当作没有值。}RegCloseKey(knownDlls);// 释放调用方拥有的 HKEY。此后句柄失效。}正确枚举时为名称和数据使用独立缓冲区。读取期间映射表变化时分别扩大容量不能让数据长度覆盖值名长度变量。std::vectorwchar_tname(maxNameChars1,L\0);std::vectorBYTEdata(maxDataBytes);DWORD nameCharsstatic_castDWORD(name.size());DWORD dataBytesstatic_castDWORD(data.size());DWORD typeREG_NONE;LSTATUS statusRegEnumValueW(key,index,name.data(),nameChars,nullptr,type,data.empty()?nullptr:data.data(),dataBytes);if(statusERROR_SUCCESS){std::wstringalias(name.data(),nameChars);data.resize(dataBytes);}下面的错误复用一个长度变量第一次 API 可能把它改为数据字节数第二次又被当作名称字符数使用。单位错位会造成名称截断或缓冲区管理错误。DWORD length260;RegEnumValueW(knownDlls,index,name,length,nullptr,type,data,length);// 错误同一个 length 同时承担字符数与字节数。字符串值仅在REG_SZ或REG_EXPAND_SZ且字节数符合 UTF-16 对齐时解码。其它类型保留类型和原始字节数避免把任意二进制值解释成 DLL 名称。// 错误示例把值数据直接拼接到当前目录。std::wstring pathL.\\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 架构、签名和进程加载状态需要额外查询不能从映射值自动得出。完整可运行程序在附件https://wangweicm.lanzouu.com/iAW373yclywh