Print Monitor Driver:打印子系统中的持久化 DLL

Print Monitor Driver:打印子系统中的持久化 DLL
Print Monitor Driver打印子系统中的持久化 DLL目标与四步流程本篇要完成的目标是只读列出当前 Windows 打印监视器的名称、Driver配置文本、值类型和读取状态。打印作业由应用创建Print Spooler打印后台处理服务保存和调度打印监视器把作业关联到端口通信。DLLDynamic-Link Library动态链接库是可由进程按需装入的代码文件Print Spooler 在配置与端口需要时装入相应监视器 DLL。读取结果描述注册表中的配置事实不能单独证明服务已经加载 DLL。要得到这组配置证据需要按以下四步完成确认打印组件的职责并定位当前生效的Monitors配置目录。打开根键计算子键名称容量逐个取得监视器名称。打开每个监视器子键按字节长度读取并验证Driver值。区分配置与运行状态记录读取结果并评估关联文件和变更的影响。完整关系如下应用创建打印作业 - Print Spooler 调度队列 - 打印监视器处理端口路径。读取路径为Monitors根键 - 监视器子键 -Driver原始值 - 类型与长度校验 - 配置证据。Windows 打印由应用、打印后台处理服务、打印队列、打印处理器、端口监视器和打印监视器共同完成。应用把文档转换成打印作业并提交到队列。服务名为Spooler的 Print Spooler 服务保存和调度作业。后续组件把作业转换并发送到本地端口、网络端口或设备厂商提供的通道。每个组件只负责其中一段工作排查配置时先确认组件身份才不会把普通驱动、端口和监视器混成一个对象。打印监视器Print Monitor是由后台处理服务加载的 DLL。它负责端口通信、作业状态和厂商扩展的打印路径常见的系统组件包括本地端口、USB、TCP/IP 端口相关监视器。它随打印服务的启动、配置重新加载或端口使用进入服务进程运行身份和普通交互程序不同。注册项存在、文件存在、服务已经加载、某个队列正在使用分别描述四个层次的事实。1. 确认组件职责并定位 Monitors 配置目录打印监视器的注册目录是HKLM\SYSTEM\CurrentControlSet\Control\Print\Monitors。该目录中的直接子键代表监视器名称子键中的Driver值提供 DLL 名称或路径。Driver值只是一项属性。“监视器”这个配置对象由子键名称、值类型、Driver值和可能存在的厂商值共同组成。CurrentControlSet是当前实际生效控制集的别名。系统保留ControlSet001、ControlSet002等控制集启动过程选择一个控制集后将其映射为CurrentControlSet。读取运行中的打印配置应使用该别名。枚举全部ControlSet00x会把备用、历史配置也加入结果。关系可以按下面的顺序理解Monitors根键 - 监视器子键 -Driver值与厂商附加配置 - Print Spooler 服务按配置加载监视器 DLL - 监视器与打印端口、队列发生关联。Driver可能只是localspl.dll这样的文件名。文件名、绝对路径和包含%SystemRoot%的环境变量文本需要分别记录。服务侧 DLL 搜索规则、服务进程环境和厂商安装程序都会影响最终加载位置调查进程不能把相对名称直接拼成固定的当前目录路径。完成第一步后已经知道应读取当前控制集中的哪个根键也知道子键名称与Driver值分别代表什么。接下来需要在不猜测名称长度的前提下打开根键并取得每个监视器名称。2. 打开根键并枚举监视器名称此步骤从打开Monitors根键开始。HKEY注册表键句柄是进程打开键后持有的访问引用Windows 注册表管理器在打开成功时创建该引用调用方在完成读取后关闭它。RegOpenKeyExW成功时返回ERROR_SUCCESS并在最后一个参数交出 HKEY。这个句柄的所有权属于调用方必须以RegCloseKey关闭。失败时函数直接返回 Win32 错误码不能使用无关的GetLastError替代返回值。// 作用打开一个注册表子键。// 返回ERROR_SUCCESS 表示成功。其它 LSTATUS 值就是错误码。LSTATUSRegOpenKeyExW(HKEY hKey,// 根键或已经打开的键不转移所有权LPCWSTR lpSubKey,// 相对路径。nullptr 表示当前键DWORD ulOptions,// 必须为 0REGSAM samDesired,// 请求权限例如 KEY_READPHKEY phkResult// 输出新 HKEY。成功后调用方必须 RegCloseKey);HKEY monitorsnullptr;LSTATUS statusRegOpenKeyExW(HKEY_LOCAL_MACHINE,LSYSTEM\\CurrentControlSet\\Control\\Print\\Monitors,0,KEY_READ,monitors);if(status!ERROR_SUCCESS){// 记录 status。这里没有可用的 monitors 句柄。}打开成功后RegQueryInfoKeyW得到直接子键数量及最长名称长度。该函数返回LSTATUSlpcMaxSubKeyLen的长度单位是宽字符且不包含结尾 NUL。枚举缓冲区因此要额外预留一个wchar_t。枚举只针对直接子键避免把厂商自己的嵌套配置误当成另一个监视器。// 作用取得键的元信息例如直接子键数和最长子键名。// 返回ERROR_SUCCESS 表示输出参数有效。其它值表示查询失败。LSTATUSRegQueryInfoKeyW(HKEY hKey,// 输入已经打开的键。调用方仍拥有 HKEYLPWSTR lpClass,// 输出可选 UTF-16 键类名缓冲区。无需求传 nullptrLPDWORD lpcchClass,// 输入/输出lpClass 容量和实际字符数单位 wchar_t。lpClass 为 nullptr 时传 nullptrLPDWORD lpReserved,// 必须为 nullptrLPDWORD lpcSubKeys,// 可选输出直接子键数量LPDWORD lpcMaxSubKeyLen,// 可选输出最长子键名字符数不含结尾 NULLPDWORD lpcMaxClassLen,// 可选输出最长子键类名字符数不含结尾 NULLPDWORD lpcValues,// 可选输出直接值数量LPDWORD lpcMaxValueNameLen,// 可选输出最长值名字符数不含结尾 NULLPDWORD lpcMaxValueLen,// 可选输出最大值数据长度单位字节LPDWORD lpcbSecurityDescriptor,// 可选输出安全描述符长度单位字节PFILETIME lpftLastWriteTime// 可选输出最后写入时间。nullptr 表示忽略);// 作用按零基索引读取一个直接子键名。// 返回ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示索引超过最后。LSTATUSRegEnumKeyExW(HKEY hKey,// 输入已打开键。不转移所有权DWORD dwIndex,// 输入零基直接子键索引LPWSTR lpName,// 输出UTF-16 子键名缓冲区。不能为 nullptrLPDWORD lpcchName,// 输入容量、输出实际字符数。单位 wchar_t不含结尾 NULLPDWORD lpReserved,// 必须为 nullptrLPWSTR lpClass,// 输出可选类名缓冲区。通常传 nullptrLPDWORD lpcchClass,// lpClass 非空时输入/输出字符数。否则传 nullptrPFILETIME lpftLastWriteTime// 可选输出子键最后写入时间。传 nullptr 表示忽略);完成第二步后已经获得了每个直接子键的名称并且每个名称都来自注册表实际返回的字符长度。接下来要打开这些子键按照注册表值报告的字节长度读取Driver才能避免把路径文本截断或误作其他类型的数据。3. 读取 Driver 值并验证类型、长度和边界打开每个监视器子键后再读取其Driver值。RegQueryValueExW的lpcbData始终以字节为单位第一次传入lpDatanullptr获取所需字节数第二次分配相同字节数后读取。两次调用间值可能被其他进程改写第二次出现ERROR_MORE_DATA时应重新测量并限制重试次数。// 作用查询一个值的类型和原始数据。// 返回ERROR_SUCCESS 表示成功。ERROR_MORE_DATA 表示容量不足。其它值是错误码。LSTATUSRegQueryValueExW(HKEY hKey,// 已打开的键不转移所有权LPCWSTR lpValueName,// 值名。nullptr 查询默认值LPDWORD lpReserved,// 必须为 nullptrLPDWORD lpType,// 输出REG_SZ、REG_EXPAND_SZ 等类型LPBYTE lpData,// 输出原始字节。测量阶段可为 nullptrLPDWORD lpcbData// 输入容量、输出实际长度单位为字节);DWORD typeREG_NONE;DWORD bytes0;LSTATUS statusRegQueryValueExW(monitor,LDriver,nullptr,type,nullptr,bytes);if(statusERROR_SUCCESS(typeREG_SZ||typeREG_EXPAND_SZ)bytes64*1024bytes%sizeof(wchar_t)0){std::vectorwchar_ttext(bytes/sizeof(wchar_t)1,L\0);DWORD capacitybytes;statusRegQueryValueExW(monitor,LDriver,nullptr,type,reinterpret_castBYTE*(text.data()),capacity);}REG_SZ是普通 UTF-16 文本。REG_EXPAND_SZ允许%SystemRoot%、%ProgramFiles%一类环境变量。例如REG_EXPAND_SZ可保存%SystemRoot%\\System32\\tcpmon.dll原始文本描述配置当前服务环境中的展开结果才描述候选路径。只有REG_EXPAND_SZ适合进行环境变量展开。capacity为字节数bytes / sizeof(wchar_t)才是 UTF-16 代码单元数量。注册表 API 返回的字节序列不保证一定带结尾 NUL。为安全显示可以在自己的缓冲区最后额外留一个 NUL构造std::wstring时仍然只使用 API 返回的capacity范围。长度为奇数时不能构成完整 UTF-16 代码单元。类型为REG_BINARY时也不能按字符串解释。这些状态应与缺少Driver值、访问拒绝和子键已删除分别输出。错误写法常用固定数组接收任意长度的数据wchar_tdriver[260]{};RegQueryValueExW(monitor,LDriver,nullptr,nullptr,reinterpret_castBYTE*(driver),nullptr);// 错误没有输入容量未检查返回值、类型、实际字节数和结尾 NUL。固定数组会遗漏长路径nullptr长度参数也使 API 无法知道缓冲区容量。正确读取先测量长度再限制允许的最大值最后使用显式返回长度构造文本。监视器子键可以在枚举与打开之间被删除循环遇到单项失败后应记录并继续处理其他子键。完成第三步后已经把可读的Driver值与类型异常、长度异常、缺失值和访问失败区分开了。接下来需要限定这些数据能够说明的范围并把单条配置记录组织成可核对的证据。4. 记录配置证据并限定关联文件与变更的结论将Driver文本用于验证前需要明确它属于哪一种输入。绝对路径可以先检查路径语法、文件属性、版本资源和签名状态。带环境变量的值应同时保留原文和展开文本。仅含文件名的值只能标记为“需要按服务加载环境定位”。文件存在也只说明路径可见不能证明 Print Spooler 已加载它更不能证明它当前服务某个端口。记录一条监视器信息时至少保存监视器子键名、根键路径、注册表视图、Driver的原始类型、原始字节长度、原始文本、可选展开结果和每个失败码。这样可区分“根键不可读”“条目缺少Driver”“值类型异常”“路径尚未定位”和“候选文件不存在”。配置变更影响的是完整配置对象移除监视器时目标是Monitors\\监视器名整个配置子树。只删除Driver值会留下同名但不完整的监视器配置。删除错误子键会影响端口、打印队列、厂商状态页或已经安装的设备。开始前需要确认监视器是否为系统组件、哪些端口引用它、是否有业务作业正在处理以及备份能否恢复完整的键和值类型。已经加载的 DLL 不会因为注册表值被删除而立刻从服务进程卸载。重启 Print Spooler 会影响队列和进行中的作业。重启系统影响范围更大。变更应在维护窗口完成并在变更后重新读取注册表、服务状态和相关队列确认配置与运行状态一致。从输入到证据的完整读取过程这一流程的输入是管理员或调查程序指定的Monitors注册表目录输出是一组“监视器子键 Driver 原始数据 读取状态”的记录。注册表是 Windows 由 Configuration Manager 管理的分层配置数据库。注册表键是其中的目录对象键值是键持有的类型化数据对象。HKEY是进程持有的已打开键引用RegOpenKeyExW创建它RegCloseKey结束它的使用期。KEY_READ是访问检查使用的权限掩码权限不足时读取动作停止在对应键不应扩大权限或改变配置。打开 Monitors 键获得 HKEY - 读取子键数量和最长名称 - 逐个枚举监视器名称 - 打开单个监视器 - 两次查询 Driver 的类型和字节数 - 只把 REG_SZ / REG_EXPAND_SZ 解码为 UTF-16 文本 - 关闭单个键和根键 - 输出原始配置证据下面的代码从打开到关闭都写在同一段中。它不依赖自定义帮助函数std::vector的生命周期覆盖每次 API 使用所取得的HKEY在每条分支结束前释放。#includewindows.h#includestring#includevectorintReadFirstPrintMonitorDriver(){HKEY monitorsnullptr;LSTATUS statusRegOpenKeyExW(HKEY_LOCAL_MACHINE,// 输入机器范围根键。预定义根键无需关闭LSYSTEM\\CurrentControlSet\\Control\\Print\\Monitors,// 输入UTF-16 相对路径0,// 输入必须为 0KEY_READ,// 输入只读权限。ACL 拒绝时返回 ERROR_ACCESS_DENIEDmonitors);// 输出成功时获得 HKEY调用方随后 RegCloseKeyif(status!ERROR_SUCCESS)returnstatic_castint(status);DWORD subKeyCount0;DWORD longestNameChars0;statusRegQueryInfoKeyW(monitors,nullptr,nullptr,nullptr,// 不读取键类名。所有可选指针为 nullptrsubKeyCount,longestNameChars,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr);if(status!ERROR_SUCCESS||subKeyCount0){RegCloseKey(monitors);// 输入当前调用方拥有的 HKEY。之后不能再使用returnstatusERROR_SUCCESS?ERROR_FILE_NOT_FOUND:static_castint(status);}std::vectorwchar_tname(static_castsize_t(longestNameChars)1,L\0);DWORD nameCapacityCharsstatic_castDWORD(name.size());// 输入容量以 wchar_t 字符计量。字节数不适用statusRegEnumKeyExW(monitors,0,name.data(),nameCapacityChars,nullptr,nullptr,nullptr,nullptr);if(statusERROR_SUCCESS){HKEY monitornullptr;statusRegOpenKeyExW(monitors,name.data(),0,KEY_READ,monitor);if(statusERROR_SUCCESS){DWORD typeREG_NONE;DWORD bytes0;statusRegQueryValueExW(monitor,LDriver,nullptr,type,nullptr,bytes);if(statusERROR_SUCCESS(typeREG_SZ||typeREG_EXPAND_SZ)bytes%sizeof(wchar_t)0bytes64*1024){std::vectorwchar_tvalue(bytes/sizeof(wchar_t)1,L\0);DWORD capacityBytesbytes;// 输入/输出均为字节第二次返回实际长度statusRegQueryValueExW(monitor,LDriver,nullptr,type,reinterpret_castBYTE*(value.data()),capacityBytes);}RegCloseKey(monitor);// 关闭单个子键不影响 monitors 根键}}RegCloseKey(monitors);returnstatic_castint(status);}RegCloseKey的完整声明如下。预定义根键例如HKEY_LOCAL_MACHINE由系统提供不传给它。RegOpenKeyExW返回的键才是本段代码负责关闭的资源。// 作用释放调用方已经打开的注册表键引用。// 返回ERROR_SUCCESS 表示关闭请求完成。失败时返回 LSTATUS。hKey 不能为 nullptr。LSTATUSRegCloseKey(HKEY hKey// 输入调用方拥有的已打开 HKEY。调用后该值失效);ERROR_FILE_NOT_FOUND表示目标键或值在当前读取时不存在ERROR_ACCESS_DENIED表示当前令牌缺少 ACL 允许的读取权限ERROR_MORE_DATA表示第二次查询容量不足ERROR_NO_MORE_ITEMS表示枚举索引到最后。根键与子键都可能在两次调用之间变化。每个返回值都应单独成为记录的一部分。完成第四步后读取结果同时保留了配置文本、类型、长度和失败状态也明确了它与 DLL 实际加载、端口引用和服务运行状态之间的界限。这样得到的记录可以用于后续核对而不会把注册表配置直接当成运行时结论。完整可运行程序在附件https://wangweicm.lanzouu.com/iUg4Q3yufyqd