Introduction
This article explains a way to create a Proxy DLL employing the MSVC compiler '#pragma comment
' feature. The source of inspiration for this was Ivo Ivanov's excellent article here in CodeProject [ref 1].
Background
Back in '96 or '97, I bought a set of games, MechWarrior 2 (MW2) plus Ghost Bear Legacy, and MW2:Mercenaries, on a business trip to Seattle. Unfortunately, I never had the time to finish Mercenaries. Some time ago, I decided to give it a try, only to find out that the game didn't work on Windows XP. As I like to see why/how things work, I fiddled around the game binaries with OllyDbg [ref 2], and found out that because BitBlt()
nowadays only returns a Boolean flag for success/failure, the game didn't start.
Question: can someone confirm this: has the return value for BitBlt()
changed from returning the number of lines blitted during the operation into true
/false
as it is now. I've tried to look for API specs on this, but everything I've found describes the current state.
After reading through web forums related to this series of games, I found out that I am not alone in trying to play it on XP. I thought about releasing the info of how my version (v1.05) of the game could be patched to be XP compatible. Then, I thought that a more elaborate method could make all three games of the MW2 series to run on XP. Therefore, I needed some way to make BitBlt()
to return the number of lines blitted.
After some web browsing, I ended up at Ivo's page, and more specifically, the idea of proxy DLL. See Ivo's page on the explanation. I won't repeat it here.
Using the code
The provided sample code is built on the command line using Visual C++ 2005 Express Edition's compiler. I use Bakefile [ref 3] to create the makefiles I use. Bakefile and one tool in this distribution dictate the need for Python in the system. Note: When I was developing this, I was using Python v2.4, and since then I've upgraded Python into v2.5. Older versions can work, too.
Compiling the newly created proxy DLL can be done in the IDE, too. You just need to create a DLL project and include the generated/created files into it. I personally prefer the command-line for most jobs, because I feel that I'm somewhat in control then.
Here are the steps for creating a proxy DLL. I'll use GDI32 as an example as that was needed in my MechWarrior foolery project.
- Run the make_pragmas-tool provided in the source package.
This script runs the tool DUMPBIN and parses its output to generate a header. Here's its usage print displayed when it is being run without any arguments:
Usage:
make_pragmas.py -o [output dir] [source DLL name]
Where:
source DLL name is the name of the DLL
where the exports are extracted from
Example:
C:\>python make_pragmas.py gdi32
Generates a 'gdi32_fwd.h' in the current directory.
Options:
--version show program's version number and exit
-h, --help show this help message and exit
-o DIR, --output-dir=DIR
Specify output directory.
For this example, the command is like follows:
C:>python make_pragmas.py -o inc \winnt\system32\gdi32.dll
05-Mar-07 12:13:26 INFO Processing '\winnt\system32\gdi32.dll'.
05-Mar-07 12:13:26 INFO Generating 'inc/gdi32_fwd.h'.
05-Mar-07 12:13:27 INFO All done, TTFN.
C:>
Looking at the generated header file, we see this:
C:>more inc\gdi32_fwd.h
#pragma comment(linker, "/export:AbortDoc=gdi32.AbortDoc")
#pragma comment(linker, "/export:AbortPath=gdi32.AbortPath")
#pragma comment(linker, "/export:AddFontMemResourceEx=gdi32.AddFontMemResourceEx")
#pragma comment(linker, "/export:AddFontResourceA=gdi32.AddFontResourceA")
#pragma comment(linker, "/export:AddFontResourceExA=gdi32.AddFontResourceExA")
#pragma comment(linker, "/export:AddFontResourceExW=gdi32.AddFontResourceExW")
#pragma comment(linker, "/export:AddFontResourceTracking=
gdi32.AddFontResourceTracking")
#pragma comment(linker, "/export:AddFontResourceW=gdi32.AddFontResourceW")
#pragma comment(linker, "/export:AngleArc=gdi32.AngleArc")
#pragma comment(linker, "/export:AnimatePalette=gdi32.AnimatePalette")
#pragma comment(linker, "/export:AnyLinkedFonts=gdi32.AnyLinkedFonts")
#pragma comment(linker, "/export:Arc=gdi32.Arc")
#pragma comment(linker, "/export:ArcTo=gdi32.ArcTo")
#pragma comment(linker, "/export:BRUSHOBJ_hGetColorTransform=
gdi32.BRUSHOBJ_hGetColorTransform")
#pragma comment(linker, "/export:BRUSHOBJ_pvAllocRbrush=
gdi32.BRUSHOBJ_pvAllocRbrush")
#pragma comment(linker, "/export:BRUSHOBJ_pvGetRbrush=gdi32.BRUSHOBJ_pvGetRbrush")
#pragma comment(linker, "/export:BRUSHOBJ_ulGetBrushColor=
gdi32.BRUSHOBJ_ulGetBrushColor")
#pragma comment(linker, "/export:BeginPath=gdi32.BeginPath")
// Below is the original BitBlt, pointing to GDI32.
#pragma comment(linker, "/export:BitBlt=gdi32.BitBlt")
#pragma comment(linker, "/export:CLIPOBJ_bEnum=gdi32.CLIPOBJ_bEnum")
#pragma comment(linker, "/export:CLIPOBJ_cEnumStart=gdi32.CLIPOBJ_cEnumStart")
- Create a CPP-file for our own function implementation.
As one can see, all exports are function forwarders into the real DLL's functions. The next part is to create a code file containing the DLLMain()
, and routines that are to needed to be called instead of the real DLL's functions. In this case, its name is GDI42.CPP.
When creating the CPP-file, please note the following points. First off, we need to define a function pointer that will hold the real GDI32's BitBlt
routine.
static BOOL (WINAPI *GDI32_BitBlt)(
HDC hdcDest,
int nXDest,
int nYDest,
int nWidth,
int nHeight,
HDC hdcSrc,
int nXSrc,
int nYSrc,
DWORD dwRop
);
Next, we need to implement our own routine for blitting:
extern "C" int WINAPI HookedBitBlt(
HDC hdcDest, int nXDest, int nYDest, int nWidth, int nHeight,
HDC hdcSrc, int nXSrc, int nYSrc, DWORD dwRop
)
{
BOOL retVal = GDI32_BitBlt( hdcDest,
nXDest,
nYDest,
nWidth,
nHeight,
hdcSrc,
nXSrc,
nYSrc,
dwRop );
if( retVal )
{
return nHeight;
}
else
{
return 0;
}
}
And, of course, the proxy attach/detach code, implemented in DLLMain
:
BOOL WINAPI DllMain( HINSTANCE hInst,DWORD reason,LPVOID )
{
if( reason == DLL_PROCESS_ATTACH )
{
OutputDebugString( "GDI32 Proxy DLL starting.\n" );
hLThis = hInst;
hGDI32 = LoadLibrary( "gdi32" );
if( !hGDI32 )
{
return FALSE;
}
*(void **)&GDI32_BitBlt = (void *)GetProcAddress( hGDI32, "BitBlt" );
}
else if( reason == DLL_PROCESS_DETACH )
{
FreeLibrary( hGDI32 );
OutputDebugString( "GDI32 Proxy DLL signing off.\n" );
}
return TRUE;
}
- Compile the code.
After compilation is done, use DUMPBIN to find out the mangled, or decorated, name given to the proxy function. It will be needed when declaring the function to export:
C:>dumpbin -symbols bin\gdi42_gdi42.obj
<lots of output, and within that output:>
00C 00000000 SECT4 notype () External | _HookedBitBlt@36
- Modify the header generated in step 1.
Open the generated header, gdi32_fwd.h, and change the line that reads:
#pragma comment(linker, "/export:BitBlt=gdi32.BitBlt")
into using the mangled name as the source for the BitBlt
export:
#pragma comment(linker, "/export:BitBlt=_HookedBitBlt@36")
- Recompile and verify.
After recompiling, check with DUMPBIN that everything is as it should be, i.e., the fake GDI32, in this case, GDI42, exports BitBlt
from within the DLL.
C:>dumpbin -exports bin\gdi42.dll | grep -C 1 BitBlt
18 11 BeginPath (forwarded to gdi32.BeginPath)
19 12 00001000 BitBlt
20 13 CLIPOBJ_bEnum (forwarded to gdi32.CLIPOBJ_bEnum)
- Modify the executable to be fooled.
Next, launch the hex editor of your choice, and open the executable file to be fooled into it. Early in the file, there will be a string, GDI32
. After that is changed into GDI42
, the application will use the fresh proxy DLL, instead of the system32's GDI32, when calling the BitBlt
routine.
Shortcomings
This approach won't work if the application to fool is protected in such a way that its import table is obfuscated. But for a game from the previous millennium, it works.
References
- Ivo Ivanov's 'API hooking revealed'
- OllyDbg by Oleh Yuschuk
- Bakefile by Vaclav Slavik
History
- 20-Mar-07: Updated the Python-tool so that Python v2.3 won't choke on
basicConfig()
having arguments. Added some mention of Python versions into the 'Using the Code' section's first paragraph.
- 06-Mar-07: Updated the Python tool within the source package. The old version didn't cope well with DLLs that contained function forwarders. Kernel32 is such a DLL. The updated script should now handle them properly.
- 05-Mar-07: Doc created.