Windows Vista Shim Internals Basics: How Shims Work to Address Compatibility Issues (and What are the Ramifications?)
I want to talk a little bit today about shims, specifically addressing how they work to address compatibility issues, and what the security ramifications are when you use a shim to address a compatibility issue.
Merriam Webster defines a shim as:
"a thin often tapered piece of material (as wood, metal, or stone) used to fill in space between things (as for support, leveling, or adjustment of fit)"
(Wow, I feel like an 8th grader beginning a term paper, using a definition like that!)
But the name shim comes exactly from this definition - we jam a thin piece of code between things - specifically, between the application's code and Windows code.
This works specifically because we implement an Import Address Table (IAT) to link to DLLs - specifically, to Windows DLLs.
Rather than talk about this, I figured I would just show this to you in a debugger. I began with the following source code:
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
MessageBox(NULL, TEXT("Hello world."), TEXT("Hello"), MB_OK);
return 0;
}
Nothing too fancy. In fact, it's pretty much identical to what you'll find at the beginning of Charles Petzold's Programming Windows book. Let's see how this looks in assembler:
0:000> uf ShimsAndIat!WinMain
ShimsAndIat!WinMain [c:\...\shimsandiat\shim.c @ 5]:
5 00401000 6a00 push 0
6 00401002 68f4204000 push offset ShimsAndIat!`string' (004020f4)
6 00401007 68fc204000 push offset ShimsAndIat!`string' (004020fc)
6 0040100c 6a00 push 0
6 0040100e ff15ac204000 call dword ptr [ShimsAndIat!_imp__MessageBoxA (004020ac)]
7 00401014 33c0 xor eax,eax
8 00401016 c21000 ret 10h
When we call into Windows to invoke the MessageBoxA function, we don't jump there directly. We jump to a local symbol: _imp__MessageBoxA. If we then take a look at what we find at this address, we'll find our pointer to the function of interest:
0:000> dds 4020ac
004020ac 76ff56df USER32!MessageBoxA
Because we are jumping to the location pointed to by this pointer, we can see how easy it is to inject some new code: we simply need to replace the contents of the pointer (at 0x004020ac) with a new value - one that points to our shim code! The application we are trying to fix thinks it is calling in to Windows, but the shim code runs instead, and this shim code may call into Windows.
Let's take a look at a shimmed up application.
In this application, we are making a call to GetVersionEx. The application has a bug - it is checking for equality of the version of Windows to 5.1 (Windows XP). Here is the portion of the unassembled function doing this comparison:
0:000> uf DWM_Compositing_Rendering_Demo!WinMain
DWM_Compositing_Rendering_Demo!WinMain:
... (disassembly elided for clarity)
10 0040103d ff1528204000 call dword ptr [DWM_Compositing_Rendering_Demo!_imp__GetVersionExW (00402028)]
11 00401043 837c240805 cmp dword ptr [esp+8],5
11 00401048 753e jne DWM_Compositing_Rendering_Demo!WinMain+0x88 (00401088)
11 0040104a 837c240c01 cmp dword ptr [esp+0Ch],1
11 0040104f 7537 jne DWM_Compositing_Rendering_Demo!WinMain+0x88 (00401088)
... (disassembly elided for clarity)
But this application is shimmed. If we look at what _imp_GetVersionExW is pointing to, it's not GetVersionEx. It's:
0:000> dds 00402028
00402028 6cd54c66 AcLayers!NS_WinXPSP2VersionLie::APIHook_GetVersionExW
Yep - it's been redirected to a version lie, which causes this check to succeed. (If only every developer's keyboard included a > symbol...)
The shim itself doesn't need to call into Windows, but most do. They have to in many cases to do anything interesting. Because, as you can see, from the perspective of Windows, shim code is no different than application code. It's just arbitrary code that happens to be calling in to Windows to do some work for it. It can't circumvent any protections, it can't do anything less securely - shim code can only do what your application's code could do. No more, no less.
That's why there are no shims for "don't give me a UAC prompt for this application" - because your application can't do it either.
It also means that anything we can fix using a shim you could fix using code. After all, we did it with some code sitting between your application and Windows, you could just write similar code directly in your application and not have to use a shim at all.