Windows Shim Engine,即Windows 兼容性模式实现引擎,在exe文件的属性对话框中有一个兼容性选项卡,用户可设置此exe程序完美工做的系统版本,Windows会尝试模拟老的系统环境运行此程序。
那Windows是如何模拟的呢?Windows认为老程序出问题的缘由在于它们调用的API上,因新版本的Windows更新API,或者加入新的flag,或者取消老的API功能等等因素,若是老的程序在新版本的Win上不正确的使用了老API(如ChangeDisplayConfig等),则会出现错误或没法达到预期的效果,致使后续的一连串错误发生。因此兼容性模式引擎的核心原理就十分简单了——修复那些有问题的API调用。
如何修复?不外乎Hook。
如何Hook?不少应用程序可不会有HotPatch这种预留的东西,因此兼容性模式引擎使用的是比较安全通用的IAT Hook。
而出问题的API多数在用户模式,因此兼容性引擎核心也运行在R3层。
本文的研究基本上彻底在Win8进行,对Win7有必定相通性,不过据我所知,Shim Engine从XP到Win8一直在变更,因此这篇文章仅仅只能是参考而已。
ReactOS有XP的Shim Engine实现源代码,Google搜索LdrpLoadShimEngine便可看到。
1)兼容性模式引擎Dll的载入
兼容性模式引擎的核心Dll有2个,分别为ntdll.dll和apphelp.dll,其中ntdll扮演着统辖全局的工做,apphelp则负责Sdb解包及逻辑判断和为功能实现核心作跳板,其余的诸如AcLayers.dll则为功能实现引擎。
随便让一个程序开启兼容性,OD载入,让其停在LdrInitializeThunk,我把启动过程Dump出来,就像下面:缓存
1安全
2app
3函数
4测试
5ui
6编码
7.net
83d
9指针
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
LdrInitializeThunk
LdrpInitialize
_LdrpInitialize
_LdrpInitializeProcess
_LdrpInitializeNlsInfo(RtlInitNlsTables\RtlResetRtlTranslations)
_LdrpInitializeExecutionOptions
_RtlpInitDeferredCriticalSection
RtlInitializeBitMap(Fls)
RtlInitializeBitMap(Tls)
RtlInitializeBitMap(TlsExpansion)
RtlInitializeCriticalSectionEx(
for
RtlAcquirePebLock)
_RtlInitializeHeapManager(use NtGlobalFlags)
RtlCreateHeap
RtlAllocateActivationContextStack
RtlInitializeSListHead(
for
Etw)
_TpInitializePackage
RtlReleaseMemoryStream
RtlpInitEnvironmentBlock
RtlpInitParameterBlock
ZwOpenDirectoryObject(use _LdrpKnownDllDirectoryHandle)
ZwOpenSymbolicLinkObject
ZwQuerySymbolicLinkObject(use _LdrpKnownDllPath)
ZwClose
_LdrpInitializeDllPath
_LdrpInitializeLoadContext
LdrpAllocateDataTableEntry
LdrpProcessMappedModule
RtlpInitCurrentDir
LdrpAllocateTls
LdrLoadDll(_LdrpKernel32DllName)
LdrGetProcedureAddress(_Kernel32ThreadInitThunkFunction)
LdrGetProcedureAddress(TermsrvGetWindowsDirectoryW)
LdrGetProcedureAddress(BaseQueryModuleData)
LdrpCodeAuthzInitialize
ZwQueryInformationProcess(ProcessExecuteFlags)
[B]SbObtainTraceHandle(Query pShimData)->LdrpInitShimEngine[
/B
]
LdrpAcquireLoaderLock(_LdrpModuleEnumLock\_LdrpLoaderLock)
LdrpPrepareModuleForExecution(->Load IAT Modules)
LdrpReleaseLoaderLock
kernel32!_IsSystemLUID
kernel32!_IsTSAppCompatEnabled
LdrpInitializePerUserWindowsDirectory(->TermsrvGetWindowsDirectoryW)
LdrpAcquireLoaderLock
LdrpReleaseLoaderLock
LdrpReleaseDllPath
ZwTestAlert
咱们要关心的是SbObtainTraceHandle,这个东西从PEB的pShimData(peb+0x1E8)拿回数据,而且ntdll下面就有一个判断,判断pShimData的数据是否是一个UNICODE字符串的指针,当你开启兼容性模式后,这个字符串是C:\Windows\system32\apphelp.dll ,这个路径是在内核建立进程就写进去的,而后LdrpInitShimEngine获得执行。
LdrpInitShimEngine中有一个无符号名称的CALL,此CALL执行载入pShimData中指向的文件路径,即apphelp:
这个call仅仅是Map apphelp和它依赖的模块到内存而已,并不执行apphelp的DllMain,call返回后,下面接着就执行一个GetInterface函数,此函数从apphelp中取得指定的导出函数,用于接收来自ntdll的通知:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
SE_InitializeEngine -> SE核心初始化,这个函数最早被ntdll执行
NTSTATUS WINAPI SE_InitializeEngine(PUNICODE_STRING pusCoreDllFile,PUNICODE_STRING pusExecuteFileName,PVOID pShimData)
这个必须返回STATUS_SUCCESS
SE_InstallBeforeInit
BOOL WINAPI SE_InstallBeforeInit(PUNICODE_STRING pusExecuteFileName,PVOID pShimData) 这个必须返回TRUE
VOID WINAPI SE_InstallBeforeInit()
//WIN8
SE_InstallAfterInit -> 引擎初始化完成后这个函数会被调用
BOOL WINAPI SE_InstallAfterInit(PUNICODE_STRING pusExecuteFileName,PVOID pShimData)
这个必须返回TRUE
SE_ShimDllLoaded -> hModule == AcLayers
VOID WINAPI SE_ShimDllLoaded(HMODULE hModule)
SE_DllLoaded -> Dll载入通知,同LdrRegisterDllNotification
VOID WINAPI SE_DllLoaded(PLDR_DATA_TABLE_ENTRY pLdrModuleLoaded)
SE_DllUnloaded -> Dll卸载通知
VOID WINAPI SE_DllUnloaded(PLDR_DATA_TABLE_ENTRY pLdrModuleUnload)
SE_LdrEntryRemoved
VOID WINAPI SE_LdrEntryRemoved(PLDR_DATA_TABLE_ENTRY pLdrEntryRemoved);
SE_ProcessDying
VOID WINAPI SE_ProcessDying();
SE_LdrResolveDllName -> 常常被调用
VOID WINAPI SE_LdrResolveDllName(PUNICODE_STRING pusUnknown,PVOID pvModuleDataUnk,PUNICODE_STRING pusModuleFileName)
SE_GetProcAddressLoad -> WIN7才有
VOID WINAPI SE_GetProcAddressLoad(PLDR_DATA_TABLE_ENTRY pLdrEntry)
SE_GetProcAddressForCaller -> 常常被调用
VOID WINAPI SE_GetProcAddressForCaller(PVOID pvUnknown0,PVOID pvUnknown1,PVOID pfnCallProcAddr,ULONG_PTR ulZero,PVOID pfnReturnToAddr)
ApphelpCheckModule
BOOL WINAPI ApphelpCheckModule(PUNICODE_STRING pusModuleName,PVOID pvUnknown1,PVOID pvUnknown2,PVOID pvUnknown3,PVOID pvUnknown4,PVOID pvUnknown5,PVOID pvUnknown6)
这些函数是Win8的ntdll中dump出来的,跟Win7有一点出入,这个函数表变更很大,好比Win8.1可能就有变化了。
在Win7上,若是任何一个导出函数地址取得失败,ntdll就会卸载SE核心Dll,失败返回。Win8却是不会,不过若是拿不到完整的导出函数,则你的SE核心的功能就残废了。
当GetInterface成功返回后,ntdll才真正去执行apphelp和它的小伙伴们的DllMain。以上函数的地址已经被保存在ntdll的全局变量中,当有事件发生的时候,ntdll会调用这些函数。
DllMain执行后,ntdll接着就执行apphelp的SE_InitializeEngine导出函数,apphelp会在SE_InitializeEngine中判断当前exe是不是开启了兼容性模式,而且查询功能实现的Dll文件名:
1
2
3
4
5
SE_InitializeEngine
HANDLE WINAPI SdbInitDatabaseEx(DWORD dwZero0,DWORD dwZero1,DWORD dwFlags); dwFlags == 0x14C
BOOL WINAPI SdbUnpackAppCompatData(HANDLE hInitData,LPWSTR lpszExeFile,PVOID pShimData,PVOID pvUnpackData);
VOID WINAPI SdbReleaseDatabase(HANDLE hInitData);
apphelp._SepSdbProcessShim@28->SdbGetDllPath
这里涉及解包sysmain.sdb文件,获取Tag数据,进行逻辑比对等等,我没仔细看下去。想了解sdb文件可看这篇文章(中文):http://blog.csdn.net/celestialwy/article/details/707148
系统兼容性模式是AcLayers.dll文件,SdbGetDllPath返回文件名,这个文件名是硬编码在sysmain.sdb中的,只是设置一个Layers逻辑的名称而已,其实设置一个应用开启兼容性模式仅仅须要下面一行代码就行:
1
2
Private Declare Function SdbSetPermLayerKeys& Lib
"apphelp"
(ByVal lpszExeFile&, ByVal lpszSystemMode&, ByVal flags&)
SdbSetPermLayerKeys StrPtr(
"C:\1.exe"
), StrPtr(
"~ WINXPSP3"
), 0
这个函数只是简单的NtSetValueKey,在下面这个注册表添加一个字符串值:HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers
(其实添加字符串值完成后,应该还要执行NtApphelpCacheControl刷新一下注册表缓存)
跑题了,继续。apphelp要求功能实现Dll文件必须在AppPatch下,后面会进行字符串连接,因此仅须要文件名就好了。
AcLayers须要导出2个函数,一个让apphelp拿到要Hook的API列表,一个接收来自ntdll->apphelp->AcLayers的通知。
apphelp会把AcLayers.dll写入到SE_InitializeEngine的第一个UNICODE_STRING参数中,并返回,ntdll会接着从里面拿到AcLayers.dll的完整路径,而后载入AcLayers.dll并执行DllMain,而后通知apphelp的SE_ShimDllLoaded函数。
接着ntdll执行apphelp的SE_InstallBeforeInit函数,意为“安装前通知”,而后LdrpInitShimEngine返回。
接下来ntdll开始载入exe程序依赖的IAT表模块,而且会一个个通知apphelp的SE_DllLoaded:
apphelp会根据从AcLayers拿到的Hook API列表和总数进行IAT Hook:
1
2
3
4
5
6
7
8
9
10
11
al.GetHookAPIs
PVOID WINAPI GetHookAPIs(LPCSTR lpszVerb,LPCWSTR lpszTagId,PVOID lpHookAPIs)
lpHookAPIs -> put Count;
return
struct SE_HOOKAPI{
LPCSTR lpszDllName;
LPCSTR lpszFuncName;
PVOID pfnHookToProc;
DWORD dwZero1;
DWORD dwZero2;
DWORD dwZero3;
}
GetHookAPIs根据前2个字符串参数来查表决定须要返回哪些Hook的API列表。
而后apphelp还会通知AcLayers:
1
2
3
4
5
al.NotifyShims
PVOID WINAPI NotifyShims(DWORD dwNotifyType,PVOID pvParameter)
pvParameter = PLDR_MODULE
dwNotifyType = 0x3:ModuleLoad
dwNotifyType = 0x69:ModuleUnload
全部IAT模块递归载入和通知完成后,会执行“安装后通知”SE_InstallAfterInit,返回值是一个BOOLEAN,这个若是执行失败,Ldr就会卸载SE的Dll。
至此,_LdrpInitializeProcess已经接近返回,Shim Engine的Ldr初始化也结束了,剩下就是进程运行过程当中的Dll Load and Unload,apphelp和AcLayers会实时接到通知,进行Hook处理。
附带一下,LdrInitializeThunk返回后:
ZwContinue->RtlUserThreadStart->RtlInitializeExceptionChain(TOP SEH)->ntdll_offset_000368A3(var_Kernel32ThreadInitThunkFunction)->kernel32!BaseThreadInitThunk->exe!ModuleEntryPoint->RtlExitUserThread
2)LdrInitShimEngineDynamic
你能够在ntdll尚未初始化SE的Dll的时候(好比启动时ShellCode改Eip注入Dll),执行LdrInitShimEngineDynamic函数,可让你的Dll接收到Shim Engine的通知,前提是你的Dll导出上面列表中的函数,而且LdrInitShimEngineDynamic这个ntdll导出函数在Win7和Win8下居然参数不一样:
1
2
NTSTATUS NTAPI LdrInitShimEngineDynamic(HMODULE hModule,PLDR_DATA_TABLE_ENTRY pLdrEntry)
//win8
NTSTATUS NTAPI LdrInitShimEngineDynamic(HMODULE hModule)
//win7
其的实现也很简单,仅仅是判断有没有SE的Dll已经载入,没有就执行一下上面说到的GetInterface函数:
3)利用Shim Engine来Dll注入
这个我相信有很多人已经在用了,其实就是在pShimData中写入咱们的Dll文件,而且模拟成一个SE的Dll,ntdll会跟普通载入dll那样载入。(Win8下你不导出函数也行,DllMain同样获得执行)
Dll源代码在下面,把Win32Protect6.dll放到C盘下,执行exe便可。(仅在win8系统有效,我如今没win7了)
exe源代码(工程删了):
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
26
27
28
typedef struct _PROCESS_BASIC_INFORMATION{
NTSTATUS ExitStatus;
PVOID PebBaseAddress;
ULONG_PTR AffinityMask;
LONG BasePriority;
HANDLE UniqueProcessId;
HANDLE InheritedFromUniqueProcessId;
}PROCESS_BASIC_INFORMATION,*PPROCESS_BASIC_INFORMATION;
void main()
{
STARTUPINFO si = {};
si.cb = sizeof(STARTUPINFO);
PROCESS_INFORMATION pi;
CreateProcessW(L
"C:\\Windows\\notepad.exe"
,NULL,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,NULL,&si,&pi);
typedef NTSTATUS (NTAPI* fnNtQueryInformationProcess)(HANDLE,ULONG,PVOID,ULONG,PULONG);
PROCESS_BASIC_INFORMATION pbi;
((fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleA(
"ntdll.dll"
),
"NtQueryInformationProcess"
))(pi.hProcess,0,&pbi,sizeof(PROCESS_BASIC_INFORMATION),NULL);
LPVOID lpShimData = (LPVOID)((ULONG_PTR)pbi.PebBaseAddress + 0x1E8);
PVOID pShimData = NULL;
ReadProcessMemory(pi.hProcess,lpShimData,&pShimData,sizeof(PVOID),NULL);
LPWSTR lpszDllFile = L
"C:\\Win32Project6.dll"
;
if
(pShimData) WriteProcessMemory(pi.hProcess,pShimData,lpszDllFile,lstrlenW(lpszDllFile) * sizeof(wchar_t),NULL);
ResumeThread(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
ExitProcess(0);
}
4)利用Shim Engine来Dll劫持(win8测试经过)
打开某个exe的文件属性,选择兼容性选项卡,勾选使用兼容性模式运行,下拉框随便选一个系统,肯定保存。
进入C:\Windows\AppPatch下面找到AcLayers.dll,更名,好比改为AcLayers.dl_。(须要管理员取得全部权)
本身写一个Dll,名称就是AcLayers.dll,放到AppPatch下面,Dll须要导出下面这2个名称的函数:
1
2
PVOID WINAPI GetHookAPIs(LPCSTR lpszVerb,LPCWSTR lpszTagId,PVOID lpHookAPIs)
PVOID WINAPI NotifyShims(DWORD dwNotifyType,PVOID pvParameter)
DllMain里面判断载入咱们的Dll的进程是***.exe,就进行LoadLibrary核心Dll,balalalala...,而后把GetHookAPIs和NotifyShims这2个导出函数直接return 0。(不使用兼容性Hook,不过NotifyShims仍是能够接到Dll和Load和Unload事件,附加福利啊haha~)
若是不是,LoadLibrary("AcLayers.dl_"),把GetHookAPIs和NotifyShims这2个导出函数JMP到AcLayers.dl_的上面。(实现其余程序的兼容性模式设置)
顺便附带上32和64的win8 apphelp.dll的导出函数lib文件。
小弟最近失眠得厉害,也就暑假回家了才有时间整理这篇文章出来,若是哪里写得很差,但愿你们海涵~ 上传的附件: