Windows软件调试初探-垫片
Windows软件调试初探-垫片
垫片数据库
垫片用于解决软件兼容问题,让两个模块对接一起。用户空间的用来解决第三方代码兼容问题,叫程序兼容引擎ACE,内核空间的用来解决设备驱动兼容问题,叫内核垫片引擎KSE。ACE和KSE都依赖垫片数据库SDB。
在C:\Windows\apppatch下有一堆.sdb文件,即垫片数据库,用sdb2xml可以将其导出为XML。文件名以main结尾都是微软官方维护的,sysmain.sdb解决用户空间应用程序问题,drvmain.sdb用于内核空间,msimain.sdb用于MSI安装包,pcamain.sdb供程序兼容助理使用。
也可以用应用兼容工具包ACT中的兼容管理器浏览SDB数据库内容,用兼容管理员定制新SDB文件。定制的SDB需要安装和注册来生效,注册位置为“HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Widows NT\CurrentVersion\AppCompatFlags\Custom”和“HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Widows NT\CurrentVersion\AppCompatFlags\InstalledSDB”,默认安装位置为“C:\Windows\AppPatch\Custom\SDB”和“C:\Windows\AppPatch\Custom\Custom64”。ACT中还有sdbinst命令行工具用于安装。现在系统已不允许运行兼容管理员程序,但Windows 7上用兼容管理员定制的SDB文件仍可在Windows 10等系统上成功安装。
为有问题的程序定义修补方案时,选择一组垫片叫做修补模式,在文件属性中有“以兼容模式运行这个程序”。
AppHelp
AppHelp.dll帮助程序解决兼容性问题,是垫片机制的核心模块。该模块中有300多个以Sdb开头的函数,他们是操作垫片数据库的数据库引擎,主要函数有:
操作 | 函数 |
---|---|
打开数据库 | SdbOpenDatabase 、SdbOpenDatabaseEx 、SdbOpenDbFromGuid 、SdbOpenLocalDatabase 、SdbOpenApphelpDetailsDatabase |
查询信息 | SdbQueryModule 、SdbQueryName 、SdbQueryApphelpInformation 、SdbQueryContext |
读取标签和数据 | SdbReadBYTETag 、SdbReadStringTag 、SdbReadApphelpData 、SdbRead???Tag |
写标签和数据 | SdbWriteWORDTag 、SdbWrite???Tag |
关闭数据库 | SdbCloseDatabase 、SubCloseLocalDatabase |
AppHelp中包含应用程序垫片引擎ASE。还有个shimeng.dll中所有导出函数入口都为AppHelp中,它只是个导入表而没有具体实现。对外导出函数以“SE_”开头、内部函数以“Sep”开头、公开函数或变量以“Se”开头。全局变量g_Engine
记录初始化后垫片引擎结构,全局变量g_ShimDebugLog
与调试信息输出有关,全局变量g_ShimDebugLogLevel
记录信息输出级别。通过设置环境变量“SHIMENG_DEBUG_LEVEL=9”开启垫片引擎调试信息,可用DebugView或WinDBG接收垫片工作函数打印的调试信息。垫片引擎有:
操作 | 函数 |
---|---|
初始化 | SE_InitializeEngine |
查询信息 | SE_GetShimId 、SE_GetHookAPIs 、SE_GetMaxShimCount 、SE_GetShimCount |
重定向器 | SeFindRedirector 、SeInitializeRedirectors |
挂钩管理器 | SeHookManagerCreate 、SeHookManagerAddHooks 、SeHookManagerResolveHooks |
垫片管理器 | SeShimManagerCreate 、SeShimManagerAddShim 、SeShimManagerGetDhimDllList |
标志管理器 | SeFlagManagerCreate 、SeFlagManagerExecute 、SeFlagManagerAddFlags 、SeFlagManagerDelete |
怪癖管理器 | SeQuirkManagerCreate 、SeQuickManagerExecute 、SeQuirkManagerDumpState |
模块追踪器 | SeModuleTrackerCreate 、SeModuleTrackerLookup 、SeModuleTrackerDelete |
常用操作 | SeplatPatch 修补IAT |
如今Windows安全防范越来越多,有些资源在老版本中可请求到的,新版本可能会被拒绝,收到STATUS_ACCESS_DENIED错误。AppHelp中有个子模块叫AD挂钩,函数以“AdHook_”开头。如apphelp!AdHook_RegOpenKeyA
应对RegOpenKey
函数修补,即当应用调用该API收到AD错误时采取补救措施,并输出调试信息“Access denied detector fires”。全局变量g_AdState
记录AD挂钩状态,全局变量g_AdFireState
记录挂钩被触发情况用于评估挂钩效果。
AppHelp还有穿山甲挂钩技术,用于处理普通挂钩难以应对的情况。全局数组g_ArmadilloHooks
记录该种挂钩。目前要挂接的函数有GetModuelFileName
和GetModuleHandle
。
垫片动态库
AppHelp本身只包含一部分垫片的实现,更多垫片是以DLL垫片动态库形式实现,根据需要动态加载,大多以“Ac”开头。
AcLayers.dll实现了容错堆FTH、虚拟注册表、谎报版本、显示模式相关垫片。
FTH用于修补内存堆有关错误,如程序多次释放内存导致崩溃,启用该垫片后程序用HeapFree
释放内存时会被重定向到垫片的钩子函数NS_FaultTolerantHeap::APIHook_RtlFreeHeap
,该函数检查参数,发现有问题报告“bogus address”,发现内存块已被释放过报告“double free”,这些错误都可被灵活应对如忽略。AcLayers.dll模块中只是FTH客户端NS_FaultTolerantHeap::FthClient
,FTH服务端在系统WDI服务中,其中运行着FthServerMainThreadFunction
和FthServerTrackingThreadFunction
两个工作线程,并用命名管道“\Device\NamedPipe\ProtectedPrefix\LocalService\FTHPIPE”进行通信。
对于虚拟注册表,AcLayers.dll用AcLayers!NS_VirtualRegistry::Build*
模拟旧注册表行为。
对于版本欺骗相关API挂钩有AcLayers!NS_*VersionLie::APIHook_GetVersion(Ex)
等谎报版本号。
还有一些图形模式相关垫片如NS_Force640x480
。
AcGenral.dll里为通用垫片,如针对堆错误的模拟堆AcGenral!NS_EmulateHeap
、字体问题AcGenral!NS_FontMigration
、谎报版本AcGenral!NS_Win2000SP1VersionLie
。
AcSpecfc.dll针对特殊问题,AcXtrnal.dll针对外部问题,AcWinRT.dll针对Windows Runtime API,AcLua.dll针对UAC问题, 每个垫片必须实现GetHookAPIs
和NotifyShims
两个导出函数。
垫片数据是在父进程中准备的。CreateProcessW
用进程创建内部函数CreateProcessInternalW
,后者用BasepGetAppCompatData
获取垫片数据。ApphelpCreateAppcompatData
为垫片引擎的接口函数,用SdbInitDatabaseEx
打开垫片数据库文件。准备好的垫片数据与新进程其他数据提交给内核,内核将该数据映射到新进程用户空间,并将该数组起始地址通过PEB的pShimData字段传递给新进程。
1 | 00 apphelp!SdbInitDatabaseEx |
在新进程在自己用户空间执行初始化工作前,NTDLL.DLL中模块加载器初始化垫片引擎。LdrpInitializeProcess
除了初始化垫片设施,还整理进程参数、初始化异常信息、用RtlpInitializeStackTraceDatabase
初始化记录关键操作过程的栈回溯数据库、用RtlInitializeHeapManager
初始化堆管理器、创建进程默认堆,用LdrDoDebuggerBreak
发起初始端点等。
1 | 00 ntdll!LdrpInitShimEngine |
LdrpInitShimEngine
为发起初始化垫片引擎的重要函数,简称LISE。LISE加载AppHelp.dll并把模块句柄(起始地址)记录到全局变量ntdll!g_pShimEngineModule
。LISE用LdrpGetShimEngineInterface
动态获取垫片引擎接口函数,后者反复用LdrGetProcedureAddressEx
获取垫片引擎模块内部接口函数地址,主要为SE_InstallBeforeInit
、SE_InstallAfterInit
、SE_DllLoaded
、SE_DllUnloaded
、SE_LdrEntryRemoved
、SE_ProcessDying
、SE_LdrResolveDllName
、SE_GetProcAddressForCaller
和ApphelpCheckModule
,并将这些函数地址保存到以“g_pfn”开头的一系列全局变量中。接着LISE用SE_InitializeEngine
初始化引擎,后者用SeProcessAttach
准备全局性层特征垫片,如修改环境变量、模拟注册表等。
LISE在初始化垫片引擎后,用LdrpLoadShimEngine
加载具体垫片模块,这个简称LLSE。
在落实挂钩阶段,拦截API的主要方法是修补模块导入地址表IAT,用ntdll!SE_DllLoaded
实现篡改IAT。每个模块若分别修改IAT可能导致代码重复,所以用SE_InstallBeforeInit
先收集各个垫片模块挂接信息,再统一实施挂接。以下是系统调用每个DLL初始化代码前先通知垫片引擎让其执行前期动作,之后用ntdll!SE_InstallAfterInit
做后期动作。
1 | 00 AcGenral!ShimLib::GetHookAPIs |
以下为加载接口、落实挂钩的过程:
1 | 00 apphelp!SepIatPatch |
例如观察落实挂钩后导入表用dds msvcrt!_imp__HeapAlloc
,此时全局变量ntdll!g_ShimsEnabled
为1(初始值0)。接着LDR调用DLL初始化函数,如以下调用msvcrt.dll初始化函数过程:
1 | 00 msvcrt!__CRTDLL_INIT |
每个DLL初始化过程后LDR用垫片引擎SE_InstallAfterInit
使其做初始化后期工作:
1 | 00 apphelp!SE_InstallAfterInit |
当调用被勾取的API时,用ln poi(<函数指针>)
观察所指向的模块中函数。
内核垫片引擎KSE
安全模式启动时,或启用驱动验证机制后,KSE被禁止。
WinLoad用OslpLoadMiscModules
加载SDB文件到内存,通过LOADER_PARAMETER_BLOCK_EXTENSION的DrvDBImage和DrvDBSize字段传递给内核。KSE还从注册表中获取垫片数据,路径为“\Registry\Machine\System\CurrentControlSet\Control\Compatibility\Device”和“\Registry\Machine\System\CurrentControlSet\Control\Compatibility\Driver”,Device表键针对设备启用垫片,每个子键以设备ID命名描述一个设备垫片信息,Driver表键针对驱动程序启用垫片。
1 | 00 nt!KsepEngineInitialize |
再I/O管理器执行阶段1初始化时初始化KSE,IoInitSystem
为I/O执行体初始化函数,IoInitSystemPreDrivers
为加载驱动前的初始化。KeInitialize
为KSE初始化函数,它还调用KseShimDatabaseBootInitilize
初始化SDB。
微软用KSE_SHIME结构描述KSE垫片,用dq nt!Win7VersionLieShim
等格式查看。
1 | typedef struct _KSE_SHIM { |
一些常见内核垫片:
提供者 | 垫片对象名称 | 描述 |
---|---|---|
内核内建 | nt!KseSkipDriverUnloadShim | 调用驱动城区Unload |
nt!KseDsShim | 用于驱动程序回调函数 | |
nt!Win7VersionLieShim、nt!Win81VersionLieShim | 版本谎报 | |
Storport.sys | StorPort、DeviceIdShim、Srbshim | 磁盘存储有关垫片 |
Usbd.sys | Usbshim | USB设备驱动垫片 |
Ndis.sys | NdisGetVersion640Shim | 网络设备驱动垫片 |
垫片KseDsShim拦截驱动程序回调函数,如各种IRP处理函数。在KseInitialize
中反复用KSE提供的KseRegisterShimEx
注册垫片,全局变量KseEngine
记录已注册的垫片。某些驱动程序也会注册垫片,如磁盘端口驱动storport的StorpRegisterShim
注册垫片。
1 | NTSTATUS KseRegisterShimEx( |
垫片注册:
1 | 3: kd> k |
初始化KSE后,I/O管理器针对每个已加载的启动类型驱动对象用KseDriverLoadImage
部署垫片,该函数收集模块加载事件,简称KDLI。KDLI用KsepGetShimsForDriver
为当前驱动程序寻找匹配垫片,执行:
1 | 3: kd> k |
KsepGetShimsForDriver
内部用nt!KsepResolveApplicableShimsForDriver
匹配具体实用垫片,KDLI再用KsepApplyShimsToDriver
应用垫片,钩子类型垫片会用KsepPatchDriverImportsTable
修补IAT。
1 | 0: kd> k |
对于垫片的执行过程,函数钩子类型垫片与ACE类似,略。对于“跳过Unload垫片”,执行体用KSE驱动程序卸载回调函数KseDriverUnloadImage
,简称KDUI。KDUI用KsepIsModuleShimmed
判断正被卸载的驱动程序是否启用了垫片。该信息也被记录在全局变量KseEngine
中。没启动垫片则KDUI返回0,启用“跳过Unload垫片”则执行垫片,并输出日志“KSE: Shimmed driver unload notification processed.”。
另一类垫片用于杜撰设备数据,略。