使用 Pragmas 创建代理 DLL
一篇关于如何使用 MSVC pragmas 创建函数转发 DLL 的文章。
引言
本文介绍了一种利用 MSVC 编译器 `#pragma comment` 功能创建代理 DLL 的方法。其灵感来源于 Ivo Ivanov 在 CodeProject 上发表的一篇精彩文章 [参考 1]。
背景
早在 96 或 97 年,我在一次去西雅图的商务旅行中购买了一套游戏,包括《机甲战士 2》(MW2) 及其扩展包《幽灵熊的遗产》,以及《机甲战士 2:雇佣兵》。不幸的是,我一直没有时间完成《雇佣兵》。前段时间,我决定再试一次,结果发现这款游戏在 Windows XP 上无法运行。由于我喜欢弄清楚事物的工作原理,我用 OllyDbg [参考 2] 调试了游戏二进制文件,发现 `BitBlt()` 函数现在只返回一个布尔值来表示成功/失败,导致游戏无法启动。
问题:有人能确认一下吗?`BitBlt()` 的返回值是否从返回操作中位图块传送的行数,变成了现在这样返回 `true`/`false`?我尝试查找 API 规范,但我找到的所有信息都描述的是当前的状态。
在阅读了与这一系列游戏相关的网络论坛后,我发现有很多人和我一样试图在 XP 上玩这款游戏。我曾想过发布我的游戏版本 (v1.05) 如何打补丁才能兼容 XP 的信息。然后,我想到一个更复杂的办法,可以让 MW2 系列的全部三款游戏都能在 XP 上运行。因此,我需要一种方法来让 `BitBlt()` 返回位图块传送的行数。
经过一番网络搜索,我找到了 Ivo 的页面,特别是关于代理 DLL 的想法。请参考 Ivo 的页面了解解释。我在此不再赘述。
使用代码
提供的示例代码使用 Visual C++ 2005 Express Edition 的编译器在命令行上构建。我使用 Bakefile [参考 3] 来创建我使用的 makefiles。Bakefile 和此发行版中的一个工具都要求系统中安装 Python。**注意**:在我开发此代码时,我使用的是 Python v2.4,之后我已将其升级到 v2.5。旧版本也可以工作。
新建的代理 DLL 也可以在 IDE 中编译。您只需创建一个 DLL 项目,并将生成/创建的文件包含进去。我个人更喜欢使用命令行来处理大多数任务,因为我觉得这样我能更好地控制。
以下是创建代理 DLL 的步骤。我将以 GDI32 为例,因为它是我那个《机甲战士》项目所需的。
- 运行源代码包中提供的 `make_pragmas-tool`。
- 为我们自己的函数实现创建一个 CPP 文件。
- 编译代码。
- 修改第 1 步生成的头文件。
- 重新编译并验证。
- 修改要欺骗的可执行文件。
这个脚本运行 `DUMPBIN` 工具并解析其输出以生成一个头文件。以下是它在不带任何参数运行时显示的用法信息:
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.
对于这个例子,命令如下:
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:>
查看生成的头文件,我们会看到这个:
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")
正如您所见,所有导出都是指向真实 DLL 函数的函数转发器。接下来是创建一个包含 `DLLMain()` 的代码文件,以及需要被调用的、而不是真实 DLL 函数的例程。在本例中,它的名称是 `GDI42.CPP`。
创建 CPP 文件时,请注意以下几点。首先,我们需要定义一个函数指针,它将保存真实的 GDI32 的 `BitBlt` 例程。
//! The real BitBlt. static BOOL (WINAPI *GDI32_BitBlt)( HDC hdcDest, // handle to destination DC int nXDest, // x-coord of destination upper-left corner int nYDest, // y-coord of destination upper-left corner int nWidth, // width of destination rectangle int nHeight, // height of destination rectangle HDC hdcSrc, // handle to source DC int nXSrc, // x-coordinate of source upper-left corner int nYSrc, // y-coordinate of source upper-left corner DWORD dwRop // raster operation code );
接下来,我们需要实现我们自己的位图块传送例程:
//! Hooked BitBlt(). extern "C" int WINAPI HookedBitBlt( HDC hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, HDC hdcSrc, int nXSrc, int nYSrc, DWORD dwRop ) { // Call the real blitter, and store its return value. BOOL retVal = GDI32_BitBlt( hdcDest, nXDest, nYDest, nWidth, nHeight, hdcSrc, nXSrc, nYSrc, dwRop ); if( retVal ) { // Success, return the input number of lines to blit. return nHeight; } else { // Failed, return 0 lines. return 0; } }
当然,还有代理的附加/分离代码,在 `DLLMain` 中实现:
//! Attach or detach this proxy. BOOL WINAPI DllMain( HINSTANCE hInst,DWORD reason,LPVOID ) { if( reason == DLL_PROCESS_ATTACH ) { OutputDebugString( "GDI32 Proxy DLL starting.\n" ); hLThis = hInst; // Load the real library, in this case GDI32. hGDI32 = LoadLibrary( "gdi32" ); if( !hGDI32 ) { return FALSE; } // Store the real BitBlt's address for hooked function to call. *(void **)&GDI32_BitBlt = (void *)GetProcAddress( hGDI32, "BitBlt" ); } else if( reason == DLL_PROCESS_DETACH ) { // Unload GDI32. FreeLibrary( hGDI32 ); OutputDebugString( "GDI32 Proxy DLL signing off.\n" ); } return TRUE; }
编译完成后,使用 `DUMPBIN` 查找代理函数的名称(经过修饰或装饰后的名称)。声明要导出的函数时需要用到它。
C:>dumpbin -symbols bin\gdi42_gdi42.obj
<lots of output, and within that output:>
00C 00000000 SECT4 notype () External | _HookedBitBlt@36
打开生成的头文件 `gdi32_fwd.h`,并将以下行更改为:
#pragma comment(linker, "/export:BitBlt=gdi32.BitBlt")
改为使用修饰后的名称作为 `BitBlt` 导出的源:
#pragma comment(linker, "/export:BitBlt=_HookedBitBlt@36")
重新编译后,使用 `DUMPBIN` 检查一切是否正常,即,伪造的 GDI32,在此例中是 GDI42,从 DLL 中导出了 `BitBlt`。
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)
接下来,启动您选择的十六进制编辑器,打开要欺骗的可执行文件。在文件靠前的位置,会有一个字符串 `GDI32`。将其改为 `GDI42` 后,应用程序在调用 `BitBlt` 例程时将使用新的代理 DLL,而不是 system32 的 GDI32。
缺点
如果被欺骗的应用程序的导入表被混淆了,这种方法将不起作用。但对于来自上一个千年的游戏,它是有效的。
参考文献
历史
- 2007 年 3 月 20 日:更新了 Python 工具,使其 Python v2.3 不会因为 `basicConfig()` 带有参数而出现问题。在“使用代码”部分的第一个段落中添加了一些关于 Python 版本的说明。
- 2007 年 3 月 6 日:更新了源代码包中的 Python 工具。旧版本无法很好地处理包含函数转发器的 DLL。Kernel32 就是这样一个 DLL。更新后的脚本现在应该可以正确处理它们了。
- 2007 年 3 月 5 日:文档创建。