当前位置: 首页 > news >正文

AppCertDlls:进程创建路径上的 DLL 入口

AppCertDlls:进程创建路径上的 DLL 入口

AppCertDlls位于HKLM\System\CurrentControlSet\Control\Session Manager\AppCertDlls。本文的程序功能是只读列出这个键在 64 位和 32 位注册表视图中的全部值,并显示每条值的来源、名称、类型和可安全显示的数据。

温馨提示:大多数系统中,这个玩意儿是空的

一、目标、对象关系与四步流程

要完成 AppCertDlls 配置的只读枚举,分为四步:

  1. 选择 64 位和 32 位注册表视图,并打开 AppCertDlls 键。
  2. 查询键中的值数量和最大长度,为值名和数据准备独立缓冲区。
  3. 按索引读取每个值,处理缓冲区不足和枚举期间的变更。
  4. 按注册表类型保留原始记录,并显示经过边界检查的文本或数值。

注册表是 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_SZREG_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[]=L"System\\CurrentControlSet\\Control\\Session Manager\\AppCertDlls";HKEY key64=nullptr;constLSTATUS status64=RegOpenKeyExW(HKEY_LOCAL_MACHINE,kPath,0,KEY_QUERY_VALUE|KEY_WOW64_64KEY,&key64);if(status64==ERROR_SUCCESS){// 此处只借用 key64 查询值。查询结束后必须释放这一次打开得到的 HKEY。RegCloseKey(key64);}elseif(status64==ERROR_FILE_NOT_FOUND){// 记录 64 位视图中的键不存在,不能借用 32 位视图的结果替代它。}else{// 记录 status64。ERROR_ACCESS_DENIED 表示读取权限不足。}HKEY key32=nullptr;constLSTATUS status32=RegOpenKeyExW(HKEY_LOCAL_MACHINE,kPath,0,KEY_QUERY_VALUE|KEY_WOW64_32KEY,&key32);if(status32==ERROR_SUCCESS){// 与 64 位视图分别处理。此处完成查询后同样关闭 key32。RegCloseKey(key32);}

下面的写法没有检查返回值,就把失败时未初始化的输出当作有效句柄关闭。此时RegCloseKey的参数不代表一次成功打开的键,错误状态也会丢失。

HKEY key=nullptr;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::vector<wchar_t>分配空间时还要额外加一个字符存放结尾零。

DWORD valueCount=0;DWORD maxNameChars=0;DWORD maxDataBytes=0;constLSTATUS queryStatus=RegQueryInfoKeyW(key,// 输入:第一步成功打开的 HKEY,调用方仍拥有关闭责任。nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,&valueCount,// 输出:当前直接值数量,单位为个数。&maxNameChars,// 输出:最长值名长度,单位为 UTF-16 字符数,不含 NUL。&maxDataBytes,// 输出:最长值数据长度,单位为字节。nullptr,nullptr);if(queryStatus==ERROR_SUCCESS){std::vector<wchar_t>name(maxNameChars+1,L'\0');// 为值名额外保留结尾 NUL。std::vector<BYTE>data(maxDataBytes);// 数据缓冲区的容量直接按字节计算。// name 和 data 将在第三步传给值枚举 API。}else{// 记录 queryStatus 并停止使用该键进行枚举,随后由所属代码关闭 key。}

空键也会查询成功,此时valueCount为零,maxNameCharsmaxDataBytes可以为零。空数据缓冲区传给后续 API 时要配合nullptr,并保留长度变量。这样既能表示“当前没有字节空间”,也能接收 API 返回的实际所需长度。

下面的错误示例把值名上限直接当作字节数分配数据。值名长度很短而值数据很长时,首次读取会反复失败,错误日志也无法区分是哪种缓冲区不足。

std::vector<wchar_t>name(maxNameChars+1,L'\0');std::vector<BYTE>data(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 是当前从零开始的值索引。constexprunsignedlonglongkMaximumNameCapacity=32768;// 单位为 wchar_t,含结尾 NUL。constexprunsignedlonglongkMaximumDataBytes=16ULL*1024*1024;// 单位为字节,限制单条记录分配量。if(static_cast<unsignedlonglong>(maxNameChars)+1>kMaximumNameCapacity||maxDataBytes>kMaximumDataBytes){// 记录 ERROR_BUFFER_OVERFLOW,并跳过当前键。不能按异常统计值直接分配内存。}else{DWORD nameCapacity=maxNameChars+1;// 名称容量单位为 wchar_t,额外保留 NUL。DWORD dataCapacity=maxDataBytes;// 数据容量单位为字节。for(intattempt=0;attempt<3;++attempt){std::vector<wchar_t>name(nameCapacity,L'\0');std::vector<BYTE>data(dataCapacity);DWORD nameChars=nameCapacity;// 输入:本次名称缓冲区容量,单位为字符。DWORD dataBytes=dataCapacity;// 输入:本次数据缓冲区容量,单位为字节。DWORD type=REG_NONE;constLSTATUS status=RegEnumValueW(key,index,name.data(),&nameChars,nullptr,&type,data.empty()?nullptr:data.data(),&dataBytes);if(status==ERROR_SUCCESS){std::wstringvalueName(name.data(),nameChars);// 只取 API 回写的实际字符数。data.resize(dataBytes);// 只保留 API 回写的实际字节数。// 保存 valueName、type、data 和本次视图,再读取下一个索引。break;}if(status==ERROR_MORE_DATA){constunsignedlonglongnextName=std::max<unsignedlonglong>(static_cast<unsignedlonglong>(nameChars)+1,static_cast<unsignedlonglong>(nameCapacity)*2);constunsignedlonglongnextData=std::max<unsignedlonglong>(dataBytes,dataCapacity==0?1ULL:static_cast<unsignedlonglong>(dataCapacity)*2);if(nextName>kMaximumNameCapacity||nextData>kMaximumDataBytes){// 记录 ERROR_BUFFER_OVERFLOW,并停止重试这个索引。break;}nameCapacity=static_cast<DWORD>(nextName);dataCapacity=static_cast<DWORD>(nextData);continue;// 使用更大的两类容量,重新读取同一个 index。}// ERROR_NO_MORE_ITEMS 结束外层枚举。其它错误记录错误码后处理下一个索引。break;}}

读取期间注册表可能变化。ERROR_MORE_DATA时扩大相应缓冲区并有限重试,ERROR_NO_MORE_ITEMS时结束枚举。一个索引读取失败不应阻断其它值。实际实现还需要设置名称与数据容量上限,例如名称不超过注册表允许的最大值名长度,数据不超过应用愿意分配的最大字节数。异常长度应记录为读取失败,不能根据返回长度无条件申请大量内存。

// 错误示例:固定数组同时限制值名和数据,并把两个长度单位混在一起。wchar_tbuffer[260];DWORD length=sizeof(buffer);RegEnumValueW(key,index,buffer,&length,nullptr,&type,reinterpret_cast<BYTE*>(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_BINARYREG_MULTI_SZ、长度异常的字符串和未知类型不按路径或单个字符串解释。它们仍应显示注册表类型和原始字节数,以便保留已经读取到的配置事实。

if((type==REG_SZ||type==REG_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(type==REG_DWORD&&data.size()==sizeof(DWORD)){DWORD number=0;std::memcpy(&number,data.data(),sizeof(number));// 长度已验证,复制四个原始字节为无符号数。// 输出本次视图、valueName、REG_DWORD 和 number。}else{// 输出本次视图、valueName、type 和原始字节数,不将未知数据改写成文本。}

下面的错误示例忽略字符串类型与数据长度,直接把任意字节转换为宽字符文本。REG_BINARY数据或奇数字节长度会得到无意义内容,也可能在实现中产生越界读取。

std::wstringtext(reinterpret_cast<constwchar_t*>(data.data()));// 错误:data 未保证是 NUL 结尾 UTF-16 字符串,也未验证类型和字节长度。

第四步完成后,输出中的每条记录都能回到明确的注册表来源。值名和字符串数据属于配置证据。文件是否存在、映像架构、签名状态和某个进程是否已加载模块,需要由单独的路径检查、文件检查或运行时观察得出。单条 AppCertDlls 值不能直接推出这些结果。

完整可运行程序在附件

https://wangweicm.lanzouu.com/i9Xp13y7s2pa

http://www.jsqmd.com/news/1250945/

相关文章:

  • 超声波高效切水口设备直销厂家哪家强?注塑厂选型指南 - 热点品牌推荐
  • 隐藏了IP却躲不开“数字指纹”:微软遥测技术是如何协助FBI抓获黑客的?
  • FreeRTOS应用(参数,优先级,延时,钩子,任务,删除函数)
  • 影刀RPA 自动化舆情监控:关键词预警与报告生成
  • 手机拍2寸证件照全攻略:尺寸参数、拍摄技巧和免费生成工具 - 软件小管家
  • iptables 规则写完不生效,3 个常见排查点
  • 学术AI搜索工具深度测评(2024真实数据对比):Scite、Elicit、Consensus、Perplexity、Semantic Scholar谁才是论文加速器?
  • 亨得利服务项目及价格查询|服务电话及全部维修地址权威信息通告(2026年7月更新) - 亨得利官方博客
  • 权威发布:百达翡丽深圳2026年7月最新客户服务网点地址及售后电话! - 百达翡丽官方售后中心
  • 巴中CMA甲醛检测公司怎么选:只测不除的专业实验室——国康CMA检测及公共卫生检测 - 信誉隆金银铂奢回收
  • 流程引擎BPM设计之:流程二开的三种模式
  • 电瓶车托运找什么物流?慧寄侠整车直发,260元起无忧寄车 - 快递物流资讯
  • 隐私小号中间号技术架构实战:隐私通话、号码保护、AXB中继模式与企业落地避坑
  • 2026年数据分析平台选型参考 - 科技焦点
  • 2026年想采购干辣椒自动去蒂剪把机加工厂怎么联系 - 热点品牌推荐
  • 交通运输数据安全新规落地,灾备成硬性合规要求
  • 基于深度强化学习的电池储能系统优化控制实践
  • 手机证件照自己拍全教程:拍摄技巧、换底色与免费工具盘点 - AI测评专家
  • 2026年7月最新|浪琴香港售后服务中心地址汇总+客户服务电话全解析 - 浪琴官方售后服务中心
  • 2026年热门的世界魔鬼城旅游公司出行选择全攻略 - 热点品牌推荐
  • 东莞卫浴螺丝工厂选购指南 卫浴行业紧固件采购实用参考 - 热点品牌推荐
  • AI病害识别怎么分级?千寻驰观技术解读
  • 从交易到社群:车主服务生态的构建与用户价值延伸
  • 2026年贵阳工伤维权选对=省心 王兴波律师护航赔偿 - 本地品牌推荐
  • 福州CMA甲醛检测公司怎么选:只测不除的专业实验室——安鑫CMA甲醛检测及公共卫生检测 - 信誉隆金银铂奢回收
  • 2026年BI大数据分析平台横向测评 - 科技焦点
  • 《你别回头找我》为何能成为可传播的试听入口
  • 企业短信平台技术架构与合规落地实战:验证码、通知、营销短信底层原理与避坑方案
  • 北京四合院的味道,怎么“搬”进墓园?
  • 低价订阅plus