首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
编程技术
发新帖
1
0
[原创][Windows C ]# AppInit_DLLs 的 32 与 64 位解析器
发表于: 2026-7-20 21:06
901
[原创][Windows C ]# AppInit_DLLs 的 32 与 64 位解析器
WangWei_CM.
2026-7-20 21:06
901
# AppInit_DLLs 的 32 与 64 位解析器 目标是完整读取 32 位和 64 位 `AppInit_DLLs` 配置,保留每个候选 DLL 的来源,并给出路径、PE 类型和本机签名检查结果。`AppInit_DLLs` 位于 `HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows`,同一台 64 位 Windows 计算机可以为两个架构保存不同的值。 要完成这项读取工作,分为四步: 1. 打开 64 位和 32 位注册表视图。 2. 按原始字节读取 DLL 列表和两个关联开关。 3. 保留原文,解析条目并展开环境变量。 4. 定位每个候选文件,检查文件属性、PE 类型和签名。 ## 一、先建立配置、进程架构和检查结果之间的关系 AppInit 配置由 DLL 列表、加载开关与签名要求共同组成。符合条件的进程加载 `User32.dll` 时,Windows 才可能依据这些配置处理候选 DLL。32 位与 64 位配置位于不同注册表视图,并分别对应同位数的 User32 进程和 DLL。 ```text 读取 64 位或 32 位 AppInit 配置 -> 检查 LoadAppInit_DLLs 与 RequireSignedAppInit_DLLs -> 进程加载对应位数的 User32.dll -> 系统按安全与兼容条件处理 DLL 列表 -> 运行时结果由独立证据确认 ``` 这四步从配置来源开始,逐步形成可以复查的文件检查记录。整个读取过程只查询注册表和文件系统,不加载 DLL。 ```text 打开指定注册表视图 -> 读取 DLL 列表与两个开关 -> 保留原始文本 -> 展开环境变量 -> 切分候选条目 -> 检查文件、PE 类型和签名 -> 记录每项状态 ``` ### AppInit DLL 是依赖 User32 加载过程的历史机制 AppInit 影响的进程范围需要先明确。AppInit 是 Windows 提供过的一种 DLL 加载配置机制。符合条件的进程加载 `User32.dll` 时,系统可以根据 AppInit 配置处理 DLL 列表。进程是否加载 User32、系统版本、策略和安全设置都会影响实际结果。 `AppInit_DLLs` 保存候选 DLL 的文本列表,`LoadAppInit_DLLs` 是与之配套的加载开关,`RequireSignedAppInit_DLLs` 用于表达签名要求。三者应作为一组读取,因为只有 DLL 列表无法说明列表是否被启用,也无法说明签名限制是否参与判断。 配置读取只能证明注册表中存在这些值。实际加载还取决于进程架构、DLL 是否能解析、依赖库是否可用、代码完整性策略、系统安全功能以及进程是否进入相关加载路径。输出中应把“配置文本”“派生文件检查”“运行时观察”分别标记。 ### DLL 与加载它的进程必须处于匹配的架构环境 32 位与 64 位的边界需要分别处理。64 位进程只能加载 64 位 DLL,32 位进程只能加载 32 位 DLL。Windows 对部分软件注册表路径维护不同视图,使 32 位应用和 64 位应用可以保存各自适用的 AppInit 设置。 从 64 位进程查询 HKLM 软件键时,默认一般落在 64 位视图。从 32 位进程查询时,默认一般落在 32 位视图。读取过程若只依赖默认值,结果会随调用方架构改变。显式使用 `KEY_WOW64_64KEY` 与 `KEY_WOW64_32KEY` 并在结果中记录来源视图,才能完整回答两个架构各自保存了什么。 ```text 64 位 Windows 上的关系 64 位 AppInit 配置 -> 64 位 User32 进程 -> 64 位 DLL 32 位 AppInit 配置 -> 32 位 User32 进程 -> 32 位 DLL ``` 配置文本相同也不能消除架构差异。一个路径可能在两个视图中都出现,而目标文件只提供一种 PE 架构。另一个路径可能通过文件系统重定向解析到不同对象。输出应保留注册表视图、原始条目、展开后文本和 `GetBinaryTypeW` 返回的实际二进制类型。 ### 原始条目与展开后路径代表两个不同事实 注册表保存的原文需要完整保留。`REG_EXPAND_SZ` 可能包含 `%SystemRoot%`、`%ProgramFiles%` 等环境变量标记,例如 `%SystemRoot%\System32\Example.dll`。原文可说明配置使用了哪个变量。调用 `ExpandEnvironmentStringsW` 得到的文本则反映当前进程环境下的解析结果。 带空格的路径通常需要用双引号包围,例如 `"C:\Program Files\Vendor\Module.dll"`。分隔列表时需要识别引号范围,不能简单调用按空格切分的函数。未结束引号、空条目和引号后的额外字符都应产生明确的解析状态,防止把相邻条目合并成错误路径。 ### 文件存在、PE 类型和签名验证解决不同问题 候选文件要按字段分别检查。`GetFileAttributesW` 只回答路径是否能访问以及它是不是目录。`GetBinaryTypeW` 用于识别可执行映像的架构类别。`WinVerifyTrust` 依据本机信任状态验证签名。三个结果不能互相替代。 例如,路径存在而签名验证失败时,文件仍是存在的,应显示路径、二进制类型和失败状态。路径不存在时,签名检查没有对象可验证,应记录文件查找失败。签名成功也不能改变该条目来自哪个注册表视图。分字段保存可以让每一个结论保持可复查性。 ## 二、第一步:同时读取两个注册表视图 64 位系统上的 `HKLM\Software` 同时提供 64 位和 32 位视图。`AppInit_DLLs`、`LoadAppInit_DLLs`、`RequireSignedAppInit_DLLs` 必须从同一个明确视图读取,默认视图会随调用方位数变化。 打开键的函数通过 `samDesired` 指定查询权限和 WOW64 视图。返回值是 Win32 错误码,成功后的 `HKEY` 由调用方负责关闭。 ```cpp // 意义:打开注册表子键并指定访问权限。 // 返回:ERROR_SUCCESS 表示成功。其它返回值是 Win32 错误码。 // 成功时 phkResult 接收 HKEY,调用方必须调用 RegCloseKey。 LSTATUS RegOpenKeyExW( HKEY hKey, // 预定义根键或已打开的父键。 LPCWSTR lpSubKey, // 相对于 hKey 的键路径。 DWORD ulOptions, // 保留,必须为 0。 REGSAM samDesired, // KEY_QUERY_VALUE 与 KEY_WOW64_32KEY/KEY_WOW64_64KEY 的组合。 PHKEY phkResult // 接收已打开键句柄。 ); // 意义:关闭由注册表打开函数返回的键句柄。 // 返回:ERROR_SUCCESS 表示成功。预定义根键不需要关闭。 LSTATUS RegCloseKey(HKEY hKey); ``` 64 位视图与 32 位视图使用两个独立调用。每次调用都只申请 `KEY_QUERY_VALUE`,读取完成后立即关闭键句柄。 ```cpp constexpr wchar_t kWindowsKey[] = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows"; HKEY key64 = nullptr; LSTATUS status64 = RegOpenKeyExW( HKEY_LOCAL_MACHINE, kWindowsKey, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &key64); HKEY key32 = nullptr; LSTATUS status32 = RegOpenKeyExW( HKEY_LOCAL_MACHINE, kWindowsKey, 0, KEY_QUERY_VALUE | KEY_WOW64_32KEY, &key32); if (status64 == ERROR_SUCCESS) RegCloseKey(key64); if (status32 == ERROR_SUCCESS) RegCloseKey(key32); ``` 把两个读取结果按字符串内容合并会丢失视图身份。即使两边文本相同,也应保留“64 位视图”和“32 位视图”标签,后续文件检查才能对应到正确的配置来源。 ```cpp // 错误示例:x64 程序默认读取 64 位视图,结果没有任何 32 位视图信息。 RegOpenKeyExW(HKEY_LOCAL_MACHINE, kWindowsKey, 0, KEY_QUERY_VALUE, &key); ``` 第一步完成后,64 位视图和 32 位视图已经有了各自独立的键句柄与来源标签。接下来需要从每个键读取同一组关联值,才能判断 DLL 列表和加载条件。 ## 三、第二步:按字节长度读取三个关联值 读取注册表值的函数用字节数描述数据缓冲区。`AppInit_DLLs` 常为 `REG_SZ` 或 `REG_EXPAND_SZ`,两个开关通常为 `REG_DWORD`。类型和长度都要在解释数据前检查。 ```cpp // 意义:读取一个注册表值的类型和原始数据。 // 返回:ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示该值未设置。 // ERROR_MORE_DATA 表示 lpData 指向的字节容量不足。 LSTATUS RegQueryValueExW( HKEY hKey, // 已打开且具有 KEY_QUERY_VALUE 权限的键。 LPCWSTR lpValueName, // 值名。nullptr 或空字符串表示默认值。 LPDWORD lpReserved, // 保留,必须为 nullptr。 LPDWORD lpType, // 接收 REG_SZ、REG_EXPAND_SZ、REG_DWORD 等类型。 LPBYTE lpData, // 接收原始字节。只查询长度时传 nullptr。 LPDWORD lpcbData // 输入为容量、输出为实际长度,单位始终是字节。 ); ``` 正确读取先查询长度,再按返回字节数分配缓冲区。读取期间若配置被修改,第二次调用可能返回 `ERROR_MORE_DATA`,此时扩大缓冲区并有限重试。 ```cpp DWORD type = REG_NONE; DWORD bytes = 0; LSTATUS status = RegQueryValueExW(key, L"AppInit_DLLs", nullptr, &type, nullptr, &bytes); if (status == ERROR_SUCCESS) { std::vector<BYTE> raw(bytes); DWORD copiedBytes = bytes; status = RegQueryValueExW( key, L"AppInit_DLLs", nullptr, &type, raw.empty() ? nullptr : raw.data(), &copiedBytes); if (status == ERROR_SUCCESS) { raw.resize(copiedBytes); } } ``` `AppInit_DLLs` 的字符串只能在类型为 `REG_SZ` 或 `REG_EXPAND_SZ` 且字节数能被 `sizeof(wchar_t)` 整除时按 UTF-16 解码。返回数据可能没有结尾 NUL,字符串长度必须来自 API 返回的字节数。 ```cpp std::wstring text; if ((type == REG_SZ || type == REG_EXPAND_SZ) && raw.size() % sizeof(wchar_t) == 0) { text.resize(raw.size() / sizeof(wchar_t)); std::memcpy(text.data(), raw.data(), raw.size()); if (!text.empty() && text.back() == L'\0') { text.pop_back(); } } ``` 两个开关要单独保留。`LoadAppInit_DLLs` 控制该列表是否参与加载,`RequireSignedAppInit_DLLs` 记录签名要求。值缺失、值为 0、值为 1、类型错误都属于不同的读取结果。 ```cpp DWORD enabled = 0; if (type == REG_DWORD && raw.size() == sizeof(enabled)) { std::memcpy(&enabled, raw.data(), sizeof(enabled)); // enabled 是实际 REG_DWORD 数值。原始字节和所属视图仍应一同保存。 } ``` 固定长度缓冲区会截断较长列表。忽略 `ERROR_MORE_DATA` 后继续输出还会把未初始化字节当成文本。 ```cpp // 错误示例:260 个 wchar_t 不是 AppInit_DLLs 的长度上限。 wchar_t text[260]; DWORD bytes = sizeof(text); RegQueryValueExW(key, L"AppInit_DLLs", nullptr, nullptr, reinterpret_cast<BYTE*>(text), &bytes); std::wcout << text; ``` 第二步完成后,DLL 列表的原始字节、注册表类型、实际长度和两个开关的值都已保留下来。接下来需要把符合字符串条件的原文拆成单个条目,才能安全地定位每个候选文件。 ## 四、第三步:保留原始条目后再展开环境变量 DLL 列表中的条目通常由空白分隔。带空格的路径需要用双引号包围,解析时保留未展开条目,才能区分注册表原文与当前进程环境下的派生路径。 ```cpp // 输入:L"%SystemRoot%\\System32\\a.dll \"C:\\Program Files\\Vendor\\b.dll\"" // 输出:两个 string_view,分别指向未展开的 a.dll 与 b.dll 条目。 std::vector<std::wstring_view> items = ParseDllList(originalText); ``` 列表解析应保留未结束引号、结束引号后紧跟文本等格式状态。遇到这类文本时仍可显示已识别片段,不能把错误片段静默丢弃。 环境变量展开函数返回所需字符数,返回长度包含结尾 NUL。传给它的输入必须有结尾 NUL,因此长度受限视图需要先复制到独立的 `std::wstring`。 ```cpp // 意义:展开文本中的 %变量名% 标记。 // 返回:成功时返回写入字符数,包含结尾 NUL。0 表示失败。 // nSize 与返回值的单位都是 wchar_t 个数。 DWORD ExpandEnvironmentStringsW( LPCWSTR lpSrc, // NUL 结尾的输入文本。 LPWSTR lpDst, // 输出缓冲区。只查询长度时传 nullptr。 DWORD nSize // 输出缓冲区容量,单位为 wchar_t 个数。 ); ``` 正确展开先查询所需字符数,再创建刚好容纳结尾 NUL 的缓冲区。输出时移除结尾 NUL,原始条目和展开结果分别保存。 ```cpp const std::wstring source(rawItem); const DWORD required = ExpandEnvironmentStringsW(source.c_str(), nullptr, 0); if (required != 0) { std::wstring expanded(required, L'\0'); const DWORD copied = ExpandEnvironmentStringsW( source.c_str(), expanded.data(), required); if (copied != 0 && copied <= required) { expanded.resize(copied - 1); } } ``` 直接把 `REG_EXPAND_SZ` 当作已经展开的路径会得到 `%SystemRoot%\System32\x.dll` 这类原始文本。原文适合记录配置,展开结果适合后续文件查询,两个字段都需要保留。 第三步完成后,每个候选 DLL 都保留了来自哪个视图的原始条目,以及当前环境下的展开文本。接下来需要按条目查询文件,避免一个无效路径掩盖其它条目的状态。 ## 五、第四步:对每个候选文件分别检查 文件检查从候选路径解析开始。`SearchPathW` 只查询系统搜索规则,不加载文件。成功时返回实际写入的字符数,缓冲区不足时返回所需容量。 ```cpp // 意义:按指定或系统默认路径规则查找文件。 // 返回:成功时为输出路径字符数,不包含结尾 NUL。 // 缓冲区不足时为所需容量,包含结尾 NUL。0 表示失败。 DWORD SearchPathW( LPCWSTR lpPath, // 搜索路径。nullptr 使用系统默认搜索规则。 LPCWSTR lpFileName, // 要查找的文件名或路径。 LPCWSTR lpExtension, // 可选扩展名。DLL 条目已有扩展名时传 nullptr。 DWORD nBufferLength, // lpBuffer 的容量,单位为 wchar_t 个数。 LPWSTR lpBuffer, // 接收解析后的完整路径。 LPWSTR* lpFilePart // 可选,接收输出路径中文件名部分的地址。 ); ``` 每个条目使用独立的路径缓冲区。文件名找不到时记录该条目的错误并继续检查列表中的下一项,不能因为第一项失败而跳过其它 DLL。 文件属性函数先区分不存在、访问失败和目录。目录没有可供 PE 或签名检查的文件内容,应在这一层结束该条目。 ```cpp // 意义:读取文件或目录的属性位。 // 返回:成功时为 FILE_ATTRIBUTE_* 位组合。INVALID_FILE_ATTRIBUTES 表示失败, // 失败原因由 GetLastError 返回。 DWORD GetFileAttributesW(LPCWSTR lpFileName); ``` PE 类型函数用于读取候选文件的二进制类别。`SCS_32BIT_BINARY`、`SCS_64BIT_BINARY` 等返回值要和原始条目、解析路径一起输出,避免把路径文本推断成文件架构。 ```cpp // 意义:读取可执行映像或 DLL 的二进制类型。 // 返回:非零表示成功,lpBinaryType 接收 SCS_32BIT_BINARY、SCS_64BIT_BINARY 等常量。 // 0 表示失败,错误原因由 GetLastError 返回。 BOOL GetBinaryTypeW( LPCWSTR lpApplicationName, // 要检查的已解析文件路径。 LPDWORD lpBinaryType // 接收二进制类型。 ); ``` 嵌入签名检查使用 `WinVerifyTrust`。调用时关闭用户界面、关闭联机吊销检查并限制为本地缓存,检查结果会受到本机证书和缓存状态影响,失败状态码需要原样保留。 ```cpp // 意义:按指定信任操作验证文件签名。 // 返回:ERROR_SUCCESS 表示信任检查成功。其它 LONG 值是信任提供程序状态码。 LONG WinVerifyTrust( HWND hwnd, // UI 宿主。无 UI 检查时传 nullptr。 GUID* pgActionID, // 指向 WINTRUST_ACTION_GENERIC_VERIFY_V2。 LPVOID pWVTData // 指向已初始化的 WINTRUST_DATA。 ); WINTRUST_FILE_INFO fileInfo{}; fileInfo.cbStruct = sizeof(fileInfo); fileInfo.pcwszFilePath = resolvedPath.c_str(); WINTRUST_DATA trustData{}; trustData.cbStruct = sizeof(trustData); trustData.dwUIChoice = WTD_UI_NONE; trustData.fdwRevocationChecks = WTD_REVOKE_NONE; trustData.dwUnionChoice = WTD_CHOICE_FILE; trustData.pFile = &fileInfo; trustData.dwStateAction = WTD_STATEACTION_VERIFY; trustData.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL; GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2; LONG status = WinVerifyTrust(nullptr, &action, &trustData); trustData.dwStateAction = WTD_STATEACTION_CLOSE; WinVerifyTrust(nullptr, &action, &trustData); ``` 把签名失败直接当作文件不存在会混淆两类问题。文件存在且签名不受当前信任链认可时,路径、PE 类型和信任状态都应保留。无法找到文件时,不应继续对不存在的路径请求签名检查。 第四步完成后,每个候选条目都有来源视图、原始文本、展开结果和文件检查状态。接下来应把这些配置与独立的运行时观察并列,避免仅凭注册表文本推断 DLL 已被加载。 完整可运行程序在附件  <mark class="encrypted">083K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9h3R3@1x3s2f1K6P5r3&6X3x3U0q4A6</mark>
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
1
点赞
・
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、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部