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.”。
另一类垫片用于杜撰设备数据,略。