必须显式启用WTD_REVOKE_CHECK_END_CERT和WTD_TIMESTAMP_VERIFY标志验证Authenticode签名,并严格规范化ZIP路径防止遍历;否则签名过期或恶意路径将导致提权或更新失效。

校验 Windows 可执行文件的 Authenticode 签名是否有效
Windows 下不能只靠 WinVerifyTrust 或简单检查证书链——它默认不验证时间戳,签名过期或未带时间戳的证书会导致后续静默失败。必须显式启用 WTD_REVOKE_CHECK_END_CERT 和 WTD_TIMESTAMP_VERIFY 标志,并传入完整文件路径(不能是内存映射或临时流)。
常见错误是直接对下载后的 .exe 文件调用 WinVerifyTrust 却忽略 CRYPT_PROVIDER_DATA 中的 fCommercial 字段:若为 FALSE,说明签名未通过商业信任根(如使用自签名或测试证书),需提前允许白名单或改用 CertGetCertificateChain + 自定义策略验证。
- 务必用
WINTRUST_DATA.dwUIChoice = WTD_UI_NONE,避免弹窗阻塞自动更新流程 - 签名验证前先用
GetFileAttributes确认文件非只读、非正在运行(否则解压时可能被系统锁定) - 若程序自身是 32 位,但下载的是 64 位主程序,
WinVerifyTrust在 Wow64 下仍可验证,但后续替换必须匹配架构
安全解压 ZIP 到临时目录并防止路径遍历
别用 minizip 或 libzip 的默认 unzOpen + unzLocateFile 流程——它们不校验 .. 路径,攻击者可构造恶意 ZIP 把文件写到任意位置(如覆盖 C:\Windows\System32\ntdll.dll)。必须在解压前对每个 unz_file_info 的 szFileName 做严格规范化检查。
正确做法:用 PathCchCanonicalizeEx(Win10+)或手动拆分路径组件,确保所有路径都以目标临时目录为根且不含 ..、.、空组件或驱动器字母。例如目标目录是 C:\Users\Alice\AppData\Local\MyApp\update_temp,则 ../evil.dll 和 \Windows\system32\kernel32.dll 都应拒绝。
立即学习“C++免费学习笔记(深入)”;
- 解压前创建带随机后缀的临时目录(如
update_temp_8a3f),避免竞态条件导致旧临时目录被复用 - ZIP 内文件权限应忽略(Windows 不支持 ZIP 权限位),但需确保解压后主程序有
GENERIC_EXECUTE权限 - 若 ZIP 含多个文件,只提取明确清单中的文件(如
MyApp.exe、core.dll),其余一律跳过
原子化替换正在运行的主程序
Windows 不允许直接覆盖正在加载的 EXE 文件,但可用 MoveFileEx 的 MOVEFILE_DELAY_UNTIL_REBOOT 或 MOVEFILE_REPLACE_EXISTING 组合实现“软原子”替换:先将新程序移到目标位置同名路径,再触发重启或延迟生效。
更稳妥的做法是:用 MoveFileEx 将旧程序重命名为 MyApp.exe.old,再把新程序 MoveFileEx 到原路径。这要求主程序启动时检测并清理残留的 .old 文件(否则磁盘空间泄漏)。注意:若进程被调试器附加,MoveFileEx 可能失败,需捕获 ERROR_ACCESS_DENIED 并提示用户关闭调试器。
- 替换前调用
FlushFileBuffers确保解压文件已落盘,避免缓存导致校验通过但实际写入不全 - 不要用
CopyFile替代MoveFileEx:复制会触发杀毒软件二次扫描,且无法保证原子性 - 若主程序是服务,需先
ControlService发送SERVICE_CONTROL_STOP,等状态变为SERVICE_STOPPED后再替换
验证替换后的新程序能否正常加载
替换完成不等于安全——新 EXE 可能因依赖缺失、签名损坏或 PE 结构异常而无法启动。不能只检查 GetLastError,得用 CreateProcess 启动新程序并立即挂起(CREATE_SUSPENDED),再用 GetModuleHandleEx 检查其主模块是否成功加载。
关键点:启动参数必须包含 /verify_only 这类内部开关(需主程序自己支持),或让新程序在初始化阶段快速返回错误码(如加载失败时输出 ERR_DLL_NOT_FOUND 到 stderr)。若启动超时(建议 5 秒),强制终止并回滚到旧版本。
- 验证过程必须在独立进程(而非当前更新进程)中进行,避免 DLL 冲突或资源占用干扰判断
- 若验证失败,用之前保存的
.old文件恢复,并删除新文件,同时记录GetLastError()和GetFileVersionInfo获取的版本号用于排查 - 不要依赖
IsDebuggerPresent或CheckRemoteDebuggerPresent判断是否被调试——某些安全软件会设断点,误判导致更新中断
整个流程里最容易被忽略的是时间戳验证和路径规范化——签名没时间戳,更新包在证书过期后就失效;ZIP 解压不检查路径,一次恶意更新就能提权。这两处没做对,其他步骤再严谨也没意义。


















