AppCertDlls进程创建路径上的 DLL 入口AppCertDlls位于HKLM\System\CurrentControlSet\Control\Session Manager\AppCertDlls。本文的程序功能是只读列出这个键在 64 位和 32 位注册表视图中的全部值并显示每条值的来源、名称、类型和可安全显示的数据。温馨提示大多数系统中这个玩意儿是空的一、目标、对象关系与四步流程要完成 AppCertDlls 配置的只读枚举分为四步选择 64 位和 32 位注册表视图并打开 AppCertDlls 键。查询键中的值数量和最大长度为值名和数据准备独立缓冲区。按索引读取每个值处理缓冲区不足和枚举期间的变更。按注册表类型保留原始记录并显示经过边界检查的文本或数值。注册表是 Windows 用于保存系统与软件配置的数据库。HKEY_LOCAL_MACHINE是其中的机器范围根键由系统维护。AppCertDlls是该根键下的一个配置键。键中的每个“值”由值名、类型和原始数据组成。值名用于标识条目类型决定数据的解释方法原始数据保存实际字节。程序从这个配置键取得输入Windows 注册表管理器负责按选定视图返回数据最后由程序输出带视图标签的记录。AppCertDlls 注册表值 - 选定 64 位或 32 位视图 - 打开键并查询容量 - 枚举值名、类型和原始数据 - 按类型安全显示 - 输出配置记录AppCertDlls 与进程创建路径有关。Windows 在创建进程时会处理可执行映像、进程地址空间和初始线程。系统版本、进程架构、策略和代码完整性条件会影响相关机制是否参与。注册表中的一条记录描述配置状态运行时是否载入某个 DLL 需要从目标进程模块、事件记录或其它运行时证据获得。本文的程序只读取配置层面的事实。它不创建进程不加载 DLL不展开环境变量也不检查文件是否存在、PE 架构或签名。字符串中的路径文本仅作为注册表数据展示。把配置记录、文件状态和运行时模块状态分别保存才能知道每项结论对应的证据来源。64 位 Windows 会为一部分注册表路径提供 64 位和 32 位视图。视图可以理解为同一逻辑路径的两个独立配置入口。64 位进程通常从 64 位视图读取32 位进程通常从 32 位视图读取。显式指定视图后枚举结果不依赖检查程序本身的位数。同名值出现在两个视图时仍是两条来源不同的配置记录。注册表值的长度单位需要在开始前分清。值名使用 UTF-16 宽字符文本容量以字符数表示。值数据保存原始字节容量以字节数表示。UTF-16 是 Windows 宽字符 API 使用的 Unicode 编码每个wchar_t占两个字节。名称容量和数据容量混用时短值可能看似正常长值会造成截断、重试失效或错误的数据解释。每条输出记录至少应保留完整键路径、视图、值名、注册表类型、原始数据长度和读取状态。REG_SZ与REG_EXPAND_SZ在字节数满足 UTF-16 对齐时可显示为文本。REG_DWORD在原始数据恰好为四字节时可显示为无符号整数。REG_BINARY、长度异常的字符串和未知类型仍保留类型与字节数避免把任意字节误写成 DLL 路径。二、第一步分别读取两个注册表视图这一步要把两个注册表视图分别作为来源读取。KEY_WOW64_64KEY选择 64 位视图KEY_WOW64_32KEY选择 32 位视图。程序对两个视图各打开一次同一条逻辑路径因此输出中的“64 位视图”和“32 位视图”表示数据来源不表示正在运行的程序本身是 64 位或 32 位。这一步还要理解HKEY的所有权。HKEY是成功打开注册表键后得到的系统资源引用。根键HKEY_LOCAL_MACHINE由系统提供调用方借用它不关闭它。RegOpenKeyExW成功返回的新HKEY由调用方拥有完成查询后必须调用一次RegCloseKey。访问掩码使用KEY_QUERY_VALUE它只允许查询键的值不允许写入、删除或创建配置。键不存在、键存在但为空、键存在且有多个值、打开时被拒绝访问是四种不同状态。ERROR_FILE_NOT_FOUND说明当前所选视图中没有这个键。ERROR_ACCESS_DENIED说明当前访问令牌没有所请求的读取权限。成功打开后还要继续查询值数量不能把“打开成功”理解为“键中已有值”。// 意义从 hKey 指定的根键或父键打开一个现有子键。// 返回ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示键不存在。// ERROR_ACCESS_DENIED 表示当前令牌缺少 samDesired 请求的权限。// 成功时 *phkResult 是调用方拥有的新 HKEY必须恰好调用一次 RegCloseKey。LSTATUSRegOpenKeyExW(HKEY hKey,// 输入HKEY_LOCAL_MACHINE 等预定义根键或已打开父键。预定义根键不关闭。LPCWSTR lpSubKey,// 输入相对路径NUL 结尾 UTF-16。可为 nullptr 表示 hKey 本身。DWORD ulOptions,// 输入保留参数必须为 0。REGSAM samDesired,// 输入KEY_QUERY_VALUE 与 KEY_WOW64_64KEY/KEY_WOW64_32KEY 的组合。PHKEY phkResult// 输出接收新 HKEY 的非空指针。失败时不得使用输出值。);// 意义释放 RegOpenKeyExW 成功返回的键句柄。// 返回ERROR_SUCCESS 表示成功。关闭后 hKey 不可再使用。LSTATUSRegCloseKey(HKEY hKey// 输入调用方拥有的已打开 HKEY。不能传入预定义根键。);constexprwchar_tkPath[]LSystem\\CurrentControlSet\\Control\\Session Manager\\AppCertDlls;HKEY key64nullptr;constLSTATUS status64RegOpenKeyExW(HKEY_LOCAL_MACHINE,kPath,0,KEY_QUERY_VALUE|KEY_WOW64_64KEY,key64);if(status64ERROR_SUCCESS){// 此处只借用 key64 查询值。查询结束后必须释放这一次打开得到的 HKEY。RegCloseKey(key64);}elseif(status64ERROR_FILE_NOT_FOUND){// 记录 64 位视图中的键不存在不能借用 32 位视图的结果替代它。}else{// 记录 status64。ERROR_ACCESS_DENIED 表示读取权限不足。}HKEY key32nullptr;constLSTATUS status32RegOpenKeyExW(HKEY_LOCAL_MACHINE,kPath,0,KEY_QUERY_VALUE|KEY_WOW64_32KEY,key32);if(status32ERROR_SUCCESS){// 与 64 位视图分别处理。此处完成查询后同样关闭 key32。RegCloseKey(key32);}下面的写法没有检查返回值就把失败时未初始化的输出当作有效句柄关闭。此时RegCloseKey的参数不代表一次成功打开的键错误状态也会丢失。HKEY keynullptr;RegOpenKeyExW(HKEY_LOCAL_MACHINE,kPath,0,KEY_QUERY_VALUE,key);RegCloseKey(key);// 错误没有确认 RegOpenKeyExW 是否成功。第一步完成后程序已经分别获得两个视图的打开结果。成功打开的键还没有可直接使用的数组大小下一步要查询值数量和最大长度为名称与数据分配独立的初始容量。三、第二步查询值数量和初始容量这一步要读取键当前的统计信息。AppCertDlls 的值名没有固定名称任何一个值都可能构成配置记录。RegQueryInfoKeyW返回的值数量用于显示当前观察结果最长值名和最大值数据长度用于准备首次调用RegEnumValueW的缓冲区。最大长度只是这次查询时的上限。注册表可在读取期间被其他进程修改随后遇到更长的数据时仍可能收到ERROR_MORE_DATA。因此查询结果适合作为初始容量不能把它当作永远有效的内存边界。// 意义查询键的值数量与枚举时可使用的初始容量。// 返回ERROR_SUCCESS 表示成功。最大值名长度单位是 UTF-16 字符数不含 NUL// 最大值数据长度单位是字节。这些容量只是瞬时上限仍须处理后续 ERROR_MORE_DATA。LSTATUSRegQueryInfoKeyW(HKEY hKey,// 输入已打开且具有查询权限的键。LPWSTR lpClass,// 输出可选的类名缓冲区。本节不读取传 nullptr。LPDWORD lpcchClass,// 输入/输出类名容量/长度单位为 wchar_t。lpClass 为 nullptr 时传 nullptr。LPDWORD lpReserved,// 保留参数必须为 nullptr。LPDWORD lpcSubKeys,// 输出直接子键数。本节不需要传 nullptr。LPDWORD lpcbMaxSubKeyLen,// 输出最长子键名字符数。本节不需要传 nullptr。LPDWORD lpcbMaxClassLen,// 输出最长子键类名字符数。本节不需要传 nullptr。LPDWORD lpcValues,// 输出直接值数量。LPDWORD lpcbMaxValueNameLen,// 输出最长值名字符数不含结尾 NUL。LPDWORD lpcbMaxValueLen,// 输出最大原始值数据长度单位为字节。LPDWORD lpcbSecurityDescriptor,// 输出安全描述符长度。本节不需要传 nullptr。PFILETIME lpftLastWriteTime// 输出最后写入时间。本节不需要传 nullptr。);正确调用时三个输出长度必须使用三个不同的变量。valueCount是值的数量maxNameChars是最长值名的字符数maxDataBytes是最长值数据的字节数。最长名称不含结尾 NUL因此为std::vectorwchar_t分配空间时还要额外加一个字符存放结尾零。DWORD valueCount0;DWORD maxNameChars0;DWORD maxDataBytes0;constLSTATUS queryStatusRegQueryInfoKeyW(key,// 输入第一步成功打开的 HKEY调用方仍拥有关闭责任。nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,valueCount,// 输出当前直接值数量单位为个数。maxNameChars,// 输出最长值名长度单位为 UTF-16 字符数不含 NUL。maxDataBytes,// 输出最长值数据长度单位为字节。nullptr,nullptr);if(queryStatusERROR_SUCCESS){std::vectorwchar_tname(maxNameChars1,L\0);// 为值名额外保留结尾 NUL。std::vectorBYTEdata(maxDataBytes);// 数据缓冲区的容量直接按字节计算。// name 和 data 将在第三步传给值枚举 API。}else{// 记录 queryStatus 并停止使用该键进行枚举随后由所属代码关闭 key。}空键也会查询成功此时valueCount为零maxNameChars与maxDataBytes可以为零。空数据缓冲区传给后续 API 时要配合nullptr并保留长度变量。这样既能表示“当前没有字节空间”也能接收 API 返回的实际所需长度。下面的错误示例把值名上限直接当作字节数分配数据。值名长度很短而值数据很长时首次读取会反复失败错误日志也无法区分是哪种缓冲区不足。std::vectorwchar_tname(maxNameChars1,L\0);std::vectorBYTEdata(maxNameChars);// 错误此处需要 maxDataBytes单位为字节。第二步完成后程序已经有了本次枚举的初始容量。键仍可能被其他进程改写第三步必须按索引逐项读取并在容量不足时重新为同一个索引准备空间。四、第三步按索引枚举全部值并处理变化这一步要从索引零开始读取全部值。RegEnumValueW每次返回一个值名、注册表类型和原始字节。dwIndex只表示当前键中的位置不能充当可长期保存的对象身份。读取过程中条目可能被创建、删除或移动程序因此使用ERROR_NO_MORE_ITEMS作为当前枚举结束条件而不把先前得到的valueCount作为唯一循环终点。// 意义按从零开始的索引读取一个值名、类型和原始字节。// 返回ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示正常结束。// ERROR_MORE_DATA 表示值名或数据缓冲区不足两个容量都可能需要扩大。LSTATUSRegEnumValueW(HKEY hKey,// 输入已打开且具有 KEY_QUERY_VALUE 的键。DWORD dwIndex,// 输入从零开始的本次枚举索引。它表示当前位置不能作为稳定对象身份。LPWSTR lpValueName,// 输出接收 NUL 结尾值名的 UTF-16 缓冲区。可与 lpcchValueName 一同传 nullptr 跳过名称。LPDWORD lpcchValueName,// 输入/输出值名容量/实际长度单位为 wchar_t实际长度不含 NUL。LPDWORD lpReserved,// 保留参数必须为 nullptr。LPDWORD lpType,// 输出接收 REG_SZ、REG_BINARY 等类型。可为 nullptr本节必须读取。LPBYTE lpData,// 输出接收原始字节的缓冲区。只读名称时可为 nullptr。LPDWORD lpcbData// 输入/输出lpData 容量/实际长度单位为字节。lpData 非空时不可为 nullptr。);正确枚举时让值名和数据使用独立缓冲区。lpcchValueName的输入和输出单位是 UTF-16 字符数lpcbData的输入和输出单位是字节。每次重试同一个索引前都要重新把缓冲区容量写入这两个变量。上一次调用返回的实际长度不能直接作为下一次的完整容量。// key 是第二步成功查询过的 HKEY。index 是当前从零开始的值索引。constexprunsignedlonglongkMaximumNameCapacity32768;// 单位为 wchar_t含结尾 NUL。constexprunsignedlonglongkMaximumDataBytes16ULL*1024*1024;// 单位为字节限制单条记录分配量。if(static_castunsignedlonglong(maxNameChars)1kMaximumNameCapacity||maxDataByteskMaximumDataBytes){// 记录 ERROR_BUFFER_OVERFLOW并跳过当前键。不能按异常统计值直接分配内存。}else{DWORD nameCapacitymaxNameChars1;// 名称容量单位为 wchar_t额外保留 NUL。DWORD dataCapacitymaxDataBytes;// 数据容量单位为字节。for(intattempt0;attempt3;attempt){std::vectorwchar_tname(nameCapacity,L\0);std::vectorBYTEdata(dataCapacity);DWORD nameCharsnameCapacity;// 输入本次名称缓冲区容量单位为字符。DWORD dataBytesdataCapacity;// 输入本次数据缓冲区容量单位为字节。DWORD typeREG_NONE;constLSTATUS statusRegEnumValueW(key,index,name.data(),nameChars,nullptr,type,data.empty()?nullptr:data.data(),dataBytes);if(statusERROR_SUCCESS){std::wstringvalueName(name.data(),nameChars);// 只取 API 回写的实际字符数。data.resize(dataBytes);// 只保留 API 回写的实际字节数。// 保存 valueName、type、data 和本次视图再读取下一个索引。break;}if(statusERROR_MORE_DATA){constunsignedlonglongnextNamestd::maxunsignedlonglong(static_castunsignedlonglong(nameChars)1,static_castunsignedlonglong(nameCapacity)*2);constunsignedlonglongnextDatastd::maxunsignedlonglong(dataBytes,dataCapacity0?1ULL:static_castunsignedlonglong(dataCapacity)*2);if(nextNamekMaximumNameCapacity||nextDatakMaximumDataBytes){// 记录 ERROR_BUFFER_OVERFLOW并停止重试这个索引。break;}nameCapacitystatic_castDWORD(nextName);dataCapacitystatic_castDWORD(nextData);continue;// 使用更大的两类容量重新读取同一个 index。}// ERROR_NO_MORE_ITEMS 结束外层枚举。其它错误记录错误码后处理下一个索引。break;}}读取期间注册表可能变化。ERROR_MORE_DATA时扩大相应缓冲区并有限重试ERROR_NO_MORE_ITEMS时结束枚举。一个索引读取失败不应阻断其它值。实际实现还需要设置名称与数据容量上限例如名称不超过注册表允许的最大值名长度数据不超过应用愿意分配的最大字节数。异常长度应记录为读取失败不能根据返回长度无条件申请大量内存。// 错误示例固定数组同时限制值名和数据并把两个长度单位混在一起。wchar_tbuffer[260];DWORD lengthsizeof(buffer);RegEnumValueW(key,index,buffer,length,nullptr,type,reinterpret_castBYTE*(buffer),length);第三步完成后每个成功读取的条目都有来源、名称、类型和原始字节。接下来只对长度与类型都符合条件的数据做显示转换其余数据仍按原始记录输出避免显示逻辑改变配置事实。五、第四步按类型保留原始记录并安全显示这一步要先确认类型与长度再决定显示方式。REG_SZ表示单个 UTF-16 字符串。REG_EXPAND_SZ也保存 UTF-16 文本其中可能包含%SystemRoot%这类环境变量标记。程序展示它们的原始文本不在当前机器环境中展开。展开后的路径会受运行账户和环境变量影响已经属于另一项派生结果。文本类型还要满足字节数能被sizeof(wchar_t)整除。Windows 的wchar_t为两个字节长度不对齐时无法按 UTF-16 元素安全读取。注册表字符串常以结尾 NUL 结束NUL 是数值零的宽字符用于表示字符串结尾。读取到原始字节后可移除最后一个结尾 NUL同时要把文本中的其它 NUL、换行和回车转成可见转义符避免控制字符截断控制台输出。REG_DWORD表示四字节无符号数只有原始数据长度恰好等于sizeof(DWORD)才能复制到DWORD变量。REG_BINARY、REG_MULTI_SZ、长度异常的字符串和未知类型不按路径或单个字符串解释。它们仍应显示注册表类型和原始字节数以便保留已经读取到的配置事实。if((typeREG_SZ||typeREG_EXPAND_SZ)data.size()%sizeof(wchar_t)0){// data 的所有权属于当前记录。text 获得独立副本单位从字节换为 wchar_t 个数。std::wstringtext(data.size()/sizeof(wchar_t),L\0);if(!data.empty()){std::memcpy(text.data(),data.data(),data.size());// 仅在字节数已验证对齐后复制。}if(!text.empty()text.back()L\0){text.pop_back();// 移除一个注册表字符串常见的结尾 NUL不猜测其它数据含义。}// 输出本次视图、valueName、type、text 和 data.size()保留完整来源。}elseif(typeREG_DWORDdata.size()sizeof(DWORD)){DWORD number0;std::memcpy(number,data.data(),sizeof(number));// 长度已验证复制四个原始字节为无符号数。// 输出本次视图、valueName、REG_DWORD 和 number。}else{// 输出本次视图、valueName、type 和原始字节数不将未知数据改写成文本。}下面的错误示例忽略字符串类型与数据长度直接把任意字节转换为宽字符文本。REG_BINARY数据或奇数字节长度会得到无意义内容也可能在实现中产生越界读取。std::wstringtext(reinterpret_castconstwchar_t*(data.data()));// 错误data 未保证是 NUL 结尾 UTF-16 字符串也未验证类型和字节长度。第四步完成后输出中的每条记录都能回到明确的注册表来源。值名和字符串数据属于配置证据。文件是否存在、映像架构、签名状态和某个进程是否已加载模块需要由单独的路径检查、文件检查或运行时观察得出。单条 AppCertDlls 值不能直接推出这些结果。完整可运行程序在附件https://wangweicm.lanzouu.com/i9Xp13y7s2pa