65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Pragmas 创建代理 DLL

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.28/5 (12投票s)

2007年3月5日

CPOL

4分钟阅读

viewsIcon

83986

downloadIcon

1375

一篇关于如何使用 MSVC pragmas 创建函数转发 DLL 的文章。

Screenshot - MechProxy.png

引言

本文介绍了一种利用 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 为例,因为它是我那个《机甲战士》项目所需的。

  1. 运行源代码包中提供的 `make_pragmas-tool`。
  2. 这个脚本运行 `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")
  3. 为我们自己的函数实现创建一个 CPP 文件。
  4. 正如您所见,所有导出都是指向真实 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;
    }
  5. 编译代码。
  6. 编译完成后,使用 `DUMPBIN` 查找代理函数的名称(经过修饰或装饰后的名称)。声明要导出的函数时需要用到它。

    C:>dumpbin -symbols bin\gdi42_gdi42.obj
    <lots of output, and within that output:>
    00C 00000000 SECT4  notype ()    External     | _HookedBitBlt@36
  7. 修改第 1 步生成的头文件。
  8. 打开生成的头文件 `gdi32_fwd.h`,并将以下行更改为:

    #pragma comment(linker, "/export:BitBlt=gdi32.BitBlt")

    改为使用修饰后的名称作为 `BitBlt` 导出的源:

    #pragma comment(linker, "/export:BitBlt=_HookedBitBlt@36")
  9. 重新编译并验证。
  10. 重新编译后,使用 `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)
  11. 修改要欺骗的可执行文件。
  12. 接下来,启动您选择的十六进制编辑器,打开要欺骗的可执行文件。在文件靠前的位置,会有一个字符串 `GDI32`。将其改为 `GDI42` 后,应用程序在调用 `BitBlt` 例程时将使用新的代理 DLL,而不是 system32 的 GDI32。

缺点

如果被欺骗的应用程序的导入表被混淆了,这种方法将不起作用。但对于来自上一个千年的游戏,它是有效的。

参考文献

  1. Ivo Ivanov 的《API 挂钩揭秘
  2. Oleh Yuschuk 的OllyDbg
  3. Vaclav Slavik 的Bakefile

历史

  • 2007 年 3 月 20 日:更新了 Python 工具,使其 Python v2.3 不会因为 `basicConfig()` 带有参数而出现问题。在“使用代码”部分的第一个段落中添加了一些关于 Python 版本的说明。
  • 2007 年 3 月 6 日:更新了源代码包中的 Python 工具。旧版本无法很好地处理包含函数转发器的 DLL。Kernel32 就是这样一个 DLL。更新后的脚本现在应该可以正确处理它们了。
  • 2007 年 3 月 5 日:文档创建。
© . All rights reserved.