Windows PPL服务无法运行:System32下fcon.dll代码完整性错误
问题描述
我使用具备Early Launch Antimalware (ELAM)能力的证书签名驱动和用户态程序,此前运行正常。但添加启动Protected Process Light (PPL)子进程的代码后,事件查看器出现以下错误:
Code Integrity is unable to verify the image integrity of the file \Device\HarddiskVolume3\Windows\System32\fcon.dll because the set of per-page image hashes could not be found on the system.
测试环境为Windows 11驱动开发虚拟机,已将vcruntime140.dll静态链接到服务中解决了此前的错误。
疑问:
fcon.dll是C:\Windows\System32下的合法Windows DLL,为何无法在PPL服务中使用?Windows已正常加载ntdll.dll、kernel32.dll等核心文件。- 如何解决该问题?由于无法将
fcon.dll静态链接到二进制文件中,这是否是Windows自身的问题?目前能想到的唯一方法是复制该DLL并用PPL证书签名后加载,但这无法适配不同Windows版本的DLL差异,相关资料极少,特寻求解决方案。
附服务的Rust代码(即使子进程路径无效仍会报错):
/// The service entrypoint for the binary #[unsafe(no_mangle)] pub unsafe extern "system" fn ServiceMain(_: u32, _: *mut PWSTR) { // register the service with SCM (service control manager) let h_status = match unsafe {RegisterServiceCtrlHandlerW( PCWSTR(svc_name().as_ptr()), Some(service_handler) )} { Ok(h) => h, Err(e) => panic!("[!] Could not register service. {e}"), }; // notify SCM that service is starting unsafe { update_service_status(h_status, SERVICE_START_PENDING.0) }; // start the service main loop run_service(h_status); } /// Main service execution loop fn run_service(h_status: SERVICE_STATUS_HANDLE) { unsafe { update_service_status(h_status, SERVICE_RUNNING.0); // // spawn child PPL // // todo restart VM and try this, and so on until you get the error let mut startup_info = STARTUPINFOEXW::default(); let mut attribute_size_list: usize = 0; if let Err(e) = InitializeProcThreadAttributeList( None, 1, // The count of attributes to be added to the list. None, // This parameter is reserved and must be zero. &mut attribute_size_list) { panic!("Error calling InitializeProcThreadAttributeList. {e}"); } if attribute_size_list == 0 { panic!("Attribute size list should not be 0. Win32 error code: {}", GetLastError().0); } let mut attribute_list_mem = vec![0u8; attribute_size_list]; startup_info.lpAttributeList = LPPROC_THREAD_ATTRIBUTE_LIST(attribute_list_mem.as_mut_ptr() as *mut _); if let Err(e) = InitializeProcThreadAttributeList( Some(startup_info.lpAttributeList), 1, // The count of attributes to be added to the list. None, // This parameter is reserved and must be zero. &mut attribute_size_list) { panic!("Error calling InitializeProcThreadAttributeList after attribute list initialised. {e}"); } // update protection level to be the same as the PPL service let mut protection_level = PROTECTION_LEVEL_SAME; if let Err(e) = UpdateProcThreadAttribute( startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL as _, Some(&mut protection_level as *mut _ as *mut _), size_of_val(&protection_level), None, None, ) { panic!("[!] Could not update protection level for child process. {e}"); } // start the process let mut process_info = PROCESS_INFORMATION::default(); // todo update this let path: Vec<u16> = r"C:\Users\flux\AppData\Roaming\Svc\etw_consumer.exe" .encode_utf16() .chain(std::iter::once(0)) .collect(); if let Err(e) = CreateProcessW( PCWSTR(path.as_ptr()), None, None, None, false, EXTENDED_STARTUPINFO_PRESENT | CREATE_PROTECTED_PROCESS, None, PCWSTR::null(), &mut startup_info.StartupInfo as *mut _ as *const _, &mut process_info ) { panic!("[!] Could not create child process. {e}"); } // Main loop while !SERVICE_STOP.load(Ordering::SeqCst) { sleep(Duration::from_secs(1)); } update_service_status(h_status, SERVICE_STOPPED.0); } } fn svc_name() -> Vec<u16> { let mut svc_name: Vec<u16> = vec![]; "ppl_runner".encode_utf16().for_each(|c| svc_name.push(c)); svc_name.push(0); svc_name } /// Handles service control events (e.g., stop) unsafe extern "system" fn service_handler(control: u32) { match control { SERVICE_CONTROL_STOP => { SERVICE_STOP.store(true, Ordering::SeqCst); } _ => {} } } /// Update the service status in the SCM unsafe fn update_service_status(h_status: SERVICE_STATUS_HANDLE, state: u32) { let mut service_status = SERVICE_STATUS { dwServiceType: SERVICE_WIN32_OWN_PROCESS, dwCurrentState: SERVICE_STATUS_CURRENT_STATE(state), dwControlsAccepted: if state == SERVICE_RUNNING.0 { 1 } else { 0 }, dwWin32ExitCode: ERROR_SUCCESS.0, dwServiceSpecificExitCode: 0, dwCheckPoint: 0, dwWaitHint: 0, }; unsafe {let _ = SetServiceStatus(h_status, &mut service_status); } } fn main() { let mut service_name: Vec<u16> = "PPLRunner\0".encode_utf16().collect(); let service_table = [ SERVICE_TABLE_ENTRYW { lpServiceName: PWSTR(service_name.as_mut_ptr()), lpServiceProc: Some(ServiceMain), }, SERVICE_TABLE_ENTRYW::default(), ]; unsafe { StartServiceCtrlDispatcherW(service_table.as_ptr()).unwrap(); } }
解决方案
原因分析
fcon.dll属于Windows的可选系统组件,并非所有Windows版本或配置下都预装了它的分页哈希(per-page hashes)。PPL进程的代码完整性验证要求所有加载的模块必须有对应的分页哈希(或被信任证书签名),而ntdll.dll、kernel32.dll这类核心系统DLL的分页哈希默认存在于系统代码完整性数据库中,因此能正常加载。
解决方法
- 排查加载来源:确认PPL服务/子进程是直接还是间接加载了
fcon.dll。如果是第三方依赖导致的,尝试替换或移除该依赖,改用不依赖fcon.dll的实现。 - 启用对应Windows功能:
fcon.dll通常和特定Windows可选功能绑定(如语言包、管理工具组件),可通过「启用或关闭Windows功能」界面安装对应组件,系统会自动生成该DLL的分页哈希并加入代码完整性数据库。 - 调整PPL保护级别:若不需要子进程继承当前服务的最高保护级别,可将
PROTECTION_LEVEL_SAME改为PROTECTION_LEVEL_WINTCB_LIGHT或更低级别,部分低级别PPL对代码完整性的验证要求更宽松,但会降低进程保护强度。 - 测试环境临时修复:在测试虚拟机中,使用系统自带签名工具重新签名
fcon.dll,再更新代码完整性缓存:
此方法仅适用于测试环境,生产环境修改系统DLL签名会触发安全机制。signtool sign /f <系统证书路径> /t http://timestamp.digicert.com C:\Windows\System32\fcon.dll certutil -setreg chain\ChainCacheResyncFiletime @now - 避免动态加载:检查代码中是否有显式调用
LoadLibrary加载fcon.dll的逻辑,改为延迟加载或条件加载,仅在非PPL环境下加载该DLL。
内容的提问来源于stack exchange,提问作者letters_and_numbers
相关产品推荐
相关产品推荐

