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开头的函数,他们是操作垫片数据库的数据库引擎,主要函数有:

操作 函数
打开数据库 SdbOpenDatabaseSdbOpenDatabaseExSdbOpenDbFromGuidSdbOpenLocalDatabaseSdbOpenApphelpDetailsDatabase
查询信息 SdbQueryModuleSdbQueryNameSdbQueryApphelpInformationSdbQueryContext
读取标签和数据 SdbReadBYTETagSdbReadStringTagSdbReadApphelpDataSdbRead???Tag
写标签和数据 SdbWriteWORDTagSdbWrite???Tag
关闭数据库 SdbCloseDatabaseSubCloseLocalDatabase

AppHelp中包含应用程序垫片引擎ASE。还有个shimeng.dll中所有导出函数入口都为AppHelp中,它只是个导入表而没有具体实现。对外导出函数以“SE_”开头、内部函数以“Sep”开头、公开函数或变量以“Se”开头。全局变量g_Engine记录初始化后垫片引擎结构,全局变量g_ShimDebugLog与调试信息输出有关,全局变量g_ShimDebugLogLevel记录信息输出级别。通过设置环境变量“SHIMENG_DEBUG_LEVEL=9”开启垫片引擎调试信息,可用DebugView或WinDBG接收垫片工作函数打印的调试信息。垫片引擎有:

操作 函数
初始化 SE_InitializeEngine
查询信息 SE_GetShimIdSE_GetHookAPIsSE_GetMaxShimCountSE_GetShimCount
重定向器 SeFindRedirectorSeInitializeRedirectors
挂钩管理器 SeHookManagerCreateSeHookManagerAddHooksSeHookManagerResolveHooks
垫片管理器 SeShimManagerCreateSeShimManagerAddShimSeShimManagerGetDhimDllList
标志管理器 SeFlagManagerCreateSeFlagManagerExecuteSeFlagManagerAddFlagsSeFlagManagerDelete
怪癖管理器 SeQuirkManagerCreateSeQuickManagerExecuteSeQuirkManagerDumpState
模块追踪器 SeModuleTrackerCreateSeModuleTrackerLookupSeModuleTrackerDelete
常用操作 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记录该种挂钩。目前要挂接的函数有GetModuelFileNameGetModuleHandle

垫片动态库

AppHelp本身只包含一部分垫片的实现,更多垫片是以DLL垫片动态库形式实现,根据需要动态加载,大多以“Ac”开头。

AcLayers.dll实现了容错堆FTH、虚拟注册表、谎报版本、显示模式相关垫片。

FTH用于修补内存堆有关错误,如程序多次释放内存导致崩溃,启用该垫片后程序用HeapFree释放内存时会被重定向到垫片的钩子函数NS_FaultTolerantHeap::APIHook_RtlFreeHeap,该函数检查参数,发现有问题报告“bogus address”,发现内存块已被释放过报告“double free”,这些错误都可被灵活应对如忽略。AcLayers.dll模块中只是FTH客户端NS_FaultTolerantHeap::FthClient,FTH服务端在系统WDI服务中,其中运行着FthServerMainThreadFunctionFthServerTrackingThreadFunction两个工作线程,并用命名管道“\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问题, 每个垫片必须实现GetHookAPIsNotifyShims两个导出函数。

垫片数据是在父进程中准备的。CreateProcessW用进程创建内部函数CreateProcessInternalW,后者用BasepGetAppCompatData获取垫片数据。ApphelpCreateAppcompatData为垫片引擎的接口函数,用SdbInitDatabaseEx打开垫片数据库文件。准备好的垫片数据与新进程其他数据提交给内核,内核将该数据映射到新进程用户空间,并将该数组起始地址通过PEB的pShimData字段传递给新进程。

1
2
3
4
5
6
00 apphelp!SdbInitDatabaseEx
01 apphelp!ApphelpCreateAppcompatData
02 KERNEL32!BaseGenerateAppcompatData
03 KERNEL32!BasepGetAppcompatData
04 KERNELBASE!CreateProcessInternalW
05 KERNELBASE!CreateProcessW

在新进程在自己用户空间执行初始化工作前,NTDLL.DLL中模块加载器初始化垫片引擎。LdrpInitializeProcess除了初始化垫片设施,还整理进程参数、初始化异常信息、用RtlpInitializeStackTraceDatabase初始化记录关键操作过程的栈回溯数据库、用RtlInitializeHeapManager初始化堆管理器、创建进程默认堆,用LdrDoDebuggerBreak发起初始端点等。

1
2
3
4
00 ntdll!LdrpInitShimEngine
01 ntdll!LdrpInitializeProcess
02 ntdll!_LdrpInitialize
03 ntdll!LdrInitializeThunk

LdrpInitShimEngine为发起初始化垫片引擎的重要函数,简称LISE。LISE加载AppHelp.dll并把模块句柄(起始地址)记录到全局变量ntdll!g_pShimEngineModule。LISE用LdrpGetShimEngineInterface动态获取垫片引擎接口函数,后者反复用LdrGetProcedureAddressEx获取垫片引擎模块内部接口函数地址,主要为SE_InstallBeforeInitSE_InstallAfterInitSE_DllLoadedSE_DllUnloadedSE_LdrEntryRemovedSE_ProcessDyingSE_LdrResolveDllNameSE_GetProcAddressForCallerApphelpCheckModule,并将这些函数地址保存到以“g_pfn”开头的一系列全局变量中。接着LISE用SE_InitializeEngine初始化引擎,后者用SeProcessAttach准备全局性层特征垫片,如修改环境变量、模拟注册表等。

LISE在初始化垫片引擎后,用LdrpLoadShimEngine加载具体垫片模块,这个简称LLSE。

在落实挂钩阶段,拦截API的主要方法是修补模块导入地址表IAT,用ntdll!SE_DllLoaded实现篡改IAT。每个模块若分别修改IAT可能导致代码重复,所以用SE_InstallBeforeInit先收集各个垫片模块挂接信息,再统一实施挂接。以下是系统调用每个DLL初始化代码前先通知垫片引擎让其执行前期动作,之后用ntdll!SE_InstallAfterInit做后期动作。

1
2
3
4
5
6
00 AcGenral!ShimLib::GetHookAPIs
01 apphelp!SeEngineInstallHooks
02 apphelp!SE_InstallBeforeInit
03 ntdll!LdrpLoadShimEngine
04 ntdll!LdrpInitShimEngine
...

以下为加载接口、落实挂钩的过程:

1
2
3
4
5
6
7
8
00 apphelp!SepIatPatch
01 apphelp!SepRouterHookImportedApi
02 apphelp!SepRouterHookIAT
03 apphelp!SE_DllLoaded
04 ntdll!LdrpSendShimEngineInitialNotifications
05 ntdll!LdrpSendShimEngineInitialNotifications
06 ntdll!LdrLoadShimEngine
...

例如观察落实挂钩后导入表用dds msvcrt!_imp__HeapAlloc,此时全局变量ntdll!g_ShimsEnabled为1(初始值0)。接着LDR调用DLL初始化函数,如以下调用msvcrt.dll初始化函数过程:

1
2
3
4
5
6
7
8
00 msvcrt!__CRTDLL_INIT
01 ntdll!LdrxCallInitRoutine
02 ntdll!LdrpCallInitRoutine
03 ntdll!LdrpInitializeNode
04 ntdll!LdrpInitializeGraphRecurse
05 ntdll!LdrLoadShimEngine
06 ntdll!LdrpLoadShimEngine
...

每个DLL初始化过程后LDR用垫片引擎SE_InstallAfterInit使其做初始化后期工作:

1
2
3
00 apphelp!SE_InstallAfterInit
01 ntdll!LdrpInitializeProcess
...

当调用被勾取的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
2
3
4
5
6
00 nt!KsepEngineInitialize
01 nt!KseInitialize
02 nt!IoInitSystemPreDrivers
03 nt!IoInitSystem
04 nt!Phase1Initialization
...

再I/O管理器执行阶段1初始化时初始化KSE,IoInitSystem为I/O执行体初始化函数,IoInitSystemPreDrivers为加载驱动前的初始化。KeInitialize为KSE初始化函数,它还调用KseShimDatabaseBootInitilize初始化SDB。

微软用KSE_SHIME结构描述KSE垫片,用dq nt!Win7VersionLieShim等格式查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typedef struct _KSE_SHIM {
_In_ SIZE_T Size;
_In_ PGUID ShimGuid;
_In_ PWCHAR ShimName;
_Out_ PVOID KseCallbackRoutine;
_In_ PVOID ShimmedDriverTargetedNotification;
_In_ PVOID ShimmedDriverUntargetedNotification;
_In_ PVOID HookCollectionsArray; //指向_KSE_HOOK_COLLECTION数组
} KSE_SHIM, * PKSE_SHIM;

typedef struct _KSE_HOOK_COLLECTION {
ULONG Type; //0为NT Export 1为HAL Export 2为Driver Export 3为Callback
PWCHAR ExportDriverName; //Type为2时
PVOID HookArray; //指向_KSE_HOOK数组
}KSE_HOOK_COLLECTION, * PKSE_HOOK_COLLECTION;

typedef struct _KSE_HOOK {
_In_ ULONG Type; //1为Function 2为IRP Callback
union {
_In_ PCHAR FunctionName; //Type为1时
_In_ ULONG CallbackId; //Type为2时
};
_In_ PVOID HookFunction;
_Out_opt_ PVOID OrginalFunction; //Type为1时
}KSE_HOOK, * PKSE_HOOK;

一些常见内核垫片:

提供者 垫片对象名称 描述
内核内建 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
2
3
4
5
6
NTSTATUS KseRegisterShimEx(
PKSE_SHIM pShim,
PVOID ignored,
ULONG flags,
PDRIVER_OBJECT pDrv_Obj
);

垫片注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3: kd> k
# Child-SP RetAddr Call Site
00 ffffc68b`ab2069c8 fffff804`6819ec5c nt!KseRegisterShimEx
01 ffffc68b`ab2069d0 fffff804`68463b43 nt!KseRegisterShim+0xc
02 ffffc68b`ab206a00 fffff804`68438fa7 nt!KseDriverScopeInitialize+0x17
03 ffffc68b`ab206a30 fffff804`6843b8f5 nt!KseInitialize+0xcb
04 ffffc68b`ab206a70 fffff804`68460abd nt!IoInitSystemPreDrivers+0x7f9
05 ffffc68b`ab206bb0 fffff804`681946db nt!IoInitSystem+0x15
06 ffffc68b`ab206be0 fffff804`67ca29a5 nt!Phase1Initialization+0x3b
07 ffffc68b`ab206c10 fffff804`67dfc868 nt!PspSystemThreadStartup+0x55
08 ffffc68b`ab206c60 00000000`00000000 nt!KiStartSystemThread+0x28
3: kd> dq @rcx
fffff804`68603140 00000000`00000038 fffff804`68612008
fffff804`68603150 fffff804`67a21280 00000000`00000000
fffff804`68603160 fffff804`67f21180 fffff804`67f21160
fffff804`68603170 fffff804`68604f30 00000000`00000038
fffff804`68603180 fffff804`68612048 fffff804`67a21340
fffff804`68603190 00000000`00000000 fffff804`67f223f0
fffff804`686031a0 fffff804`67f223d0 fffff804`68605008
fffff804`686031b0 00000000`00000000 00000000`00000000
3: kd> dU fffff804`67a21280
fffff804`67a21280 "driverscope"

初始化KSE后,I/O管理器针对每个已加载的启动类型驱动对象用KseDriverLoadImage部署垫片,该函数收集模块加载事件,简称KDLI。KDLI用KsepGetShimsForDriver为当前驱动程序寻找匹配垫片,执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
3: kd> k
# Child-SP RetAddr Call Site
00 ffffc68b`ab206608 fffff804`68130abc nt!KsepGetShimsForDriver
01 ffffc68b`ab206610 fffff804`68454fd0 nt!KseDriverLoadImage+0xb0
02 ffffc68b`ab206680 fffff804`68454bd1 nt!IopInitializeBuiltinDriver+0x3b4
03 ffffc68b`ab206760 fffff804`68453b1c nt!PnpInitializeBootStartDriver+0x119
04 ffffc68b`ab206820 fffff804`684542d2 nt!PipInitializeCoreDriversByGroup+0xec
05 ffffc68b`ab2068c0 fffff804`6843bc20 nt!IopInitializeBootDrivers+0x146
06 ffffc68b`ab206a70 fffff804`68460abd nt!IoInitSystemPreDrivers+0xb24
07 ffffc68b`ab206bb0 fffff804`681946db nt!IoInitSystem+0x15
08 ffffc68b`ab206be0 fffff804`67ca29a5 nt!Phase1Initialization+0x3b
09 ffffc68b`ab206c10 fffff804`67dfc868 nt!PspSystemThreadStartup+0x55
0a ffffc68b`ab206c60 00000000`00000000 nt!KiStartSystemThread+0x28

KsepGetShimsForDriver内部用nt!KsepResolveApplicableShimsForDriver匹配具体实用垫片,KDLI再用KsepApplyShimsToDriver应用垫片,钩子类型垫片会用KsepPatchDriverImportsTable修补IAT。

1
2
3
4
5
6
7
8
9
10
11
12
13
0: kd> k
# Child-SP RetAddr Call Site
00 ffffc68b`ab206648 fffff804`682bc161 nt!KsepPatchDriverImportsTable
01 ffffc68b`ab206650 fffff804`68130ad6 nt!KsepApplyShimsToDriver+0xb1
02 ffffc68b`ab2066b0 fffff804`68454fd0 nt!KseDriverLoadImage+0xca
03 ffffc68b`ab206720 fffff804`68454bd1 nt!IopInitializeBuiltinDriver+0x3b4
04 ffffc68b`ab206800 fffff804`684548e5 nt!PnpInitializeBootStartDriver+0x119
05 ffffc68b`ab2068c0 fffff804`6843bc20 nt!IopInitializeBootDrivers+0x759
06 ffffc68b`ab206a70 fffff804`68460abd nt!IoInitSystemPreDrivers+0xb24
07 ffffc68b`ab206bb0 fffff804`681946db nt!IoInitSystem+0x15
08 ffffc68b`ab206be0 fffff804`67ca29a5 nt!Phase1Initialization+0x3b
09 ffffc68b`ab206c10 fffff804`67dfc868 nt!PspSystemThreadStartup+0x55
0a ffffc68b`ab206c60 00000000`00000000 nt!KiStartSystemThread+0x28

对于垫片的执行过程,函数钩子类型垫片与ACE类似,略。对于“跳过Unload垫片”,执行体用KSE驱动程序卸载回调函数KseDriverUnloadImage,简称KDUI。KDUI用KsepIsModuleShimmed判断正被卸载的驱动程序是否启用了垫片。该信息也被记录在全局变量KseEngine中。没启动垫片则KDUI返回0,启用“跳过Unload垫片”则执行垫片,并输出日志“KSE: Shimmed driver unload notification processed.”。

另一类垫片用于杜撰设备数据,略。