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

如何使用 C 编译器进行逆向工程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.14/5 (7投票s)

2020年7月27日

CPOL

18分钟阅读

viewsIcon

28773

本文介绍了如何利用真正的C编译器进行逆向工程。

引言

我们可以使用IDA来检查汇编代码,然后手动将反汇编代码翻译成伪C代码。我们甚至可以使用HexRays反编译器。然而,无论伪代码有多好,它仍然是伪代码。它无法被编译和测试。随着伪代码量的增加,出现错误和歧义的可能性也随之增加,最终我们可能会迷失方向。

我考虑了另一种逆向工程方法。我们仍然使用IDA来检查反汇编代码,但我们将基于函数来将反汇编代码翻译成真正的C代码,并强制被检查程序使用我们的代码。我们将把我们的代码累积在一个单独的DLL中,就像IDA对被检查的可执行文件的数据库一样。这样,我们就拥有了坚实的C语言基础(类型系统、清晰的名称、函数原型)。我们可以编写可以实际构建并运行的程序,看看我们是否正确。如果我们出错,程序很可能会崩溃。

随着逆向工程的深入,我们可以翻译更多的函数,当其作用变得清晰时,重命名我们的C代码中的struct字段、函数和变量。我们还可以修改IDA的数据库以保持事物同步。我认为这增加了我们实现逆向工程目标的几率,特别是对于包含数千个函数的大型程序。

让我们看看这个想法

我认为最好像往常一样运行程序,不加任何技巧,比如带有挂起标志的CreateProcess、DLL注入等。这个想法是为我们感兴趣的每个镜像(EXE、DLL)创建一个dlc DLL。这个dlc DLL将与镜像一起(紧随其后)加载,并将用跳转到我们自己函数(在dlc DLL内)的方式替换原始函数。我们如何强制加载dlc DLL?很简单,我们需要修改镜像的导入描述符(对于真实世界的程序,每个镜像总是会导入至少一个模块的函数)。此外,我们需要确保镜像的代码节具有写入权限。

我们来演示一个简单的32位示例。它将是一个空的控制台应用程序(它只是返回一个数字)。

main.c:

int main(int argc, char *argv[])
{
    return 0x10203040;                // to find function in IDA disassembly
}

编译EXE镜像并查看其imports

dumpbin /imports main.exe

0

1

我们可以看到我们的EXE导入了kernel32.dll的函数。所以我们需要构建一个dlc DLL,它将导出所有这些函数并将它们转发到kernel32.dll。创建一个空的DLL项目,并添加包含所有必需函数的模块定义文件。将dumpbin的输出重定向到文件更容易

dumpbin /imports main.exe > main.txt

这样您就可以轻松地将函数复制到模块定义文件中。在我的例子中,def文件如下所示

LIBRARY DLC
EXPORTS
                  GetModuleHandleW                 = kernel32.GetModuleHandleW
                  GetModuleFileNameW               = kernel32.GetModuleFileNameW
                  FreeLibrary                      = kernel32.FreeLibrary
                  VirtualQuery                     = kernel32.VirtualQuery
                  GetProcessHeap                   = kernel32.GetProcessHeap
                  HeapFree                         = kernel32.HeapFree
                  HeapAlloc                        = kernel32.HeapAlloc
                  WideCharToMultiByte              = kernel32.WideCharToMultiByte
                  MultiByteToWideChar              = kernel32.MultiByteToWideChar
                  LoadLibraryExW                   = kernel32.LoadLibraryExW
                  GetProcAddress                   = kernel32.GetProcAddress
                  GetLastError                     = kernel32.GetLastError
                  RaiseException                   = kernel32.RaiseException
                  IsDebuggerPresent                = kernel32.IsDebuggerPresent
                  DecodePointer                    = kernel32.DecodePointer
                  GetSystemTimeAsFileTime          = kernel32.GetSystemTimeAsFileTime
                  GetCurrentThreadId               = kernel32.GetCurrentThreadId
                  GetCurrentProcessId              = kernel32.GetCurrentProcessId
                  QueryPerformanceCounter          = kernel32.QueryPerformanceCounter
                  EncodePointer                    = kernel32.EncodePointer
                  IsProcessorFeaturePresent        = kernel32.IsProcessorFeaturePresent

添加我们自己的main函数

int main(int argc, char *argv[])
{
    printf("You forgot Hello World!\n");
    getchar();
    return 0x10203040;
}

现在用IDA打开main.exe。我们需要获取原始main函数的地址,以便知道应该在哪个地址插入跳转到我们自己的main函数。

2

3

所以我们可以看到原始main函数的地址等于

0x411380

如果函数有另一个名称(没有地址),我们始终可以重命名它(删除其名称),或者打开函数信息窗口查看其地址。请注意,IDA假定默认的镜像基址。对于32位镜像,它等于

0x400000

因此,在初始自动分析期间,IDA会考虑到这个基址来自动生成函数/全局变量的名称。然而,IDA可能会在程序调试开始之前对其进行重定位。在这种情况下,所有自动生成的名称都会被重命名。在这种情况下,您需要将程序重定位回默认基址

Edit -> Segments -> Rebase program...

这是调试会话后函数的示例

4

现在让我们进行重定位到默认基址

5

6

7

所有函数名称都又正常了。

回到正题。要获取运行时函数地址,我们需要将函数的偏移量(从镜像基址开始,幸运的是它不会改变)添加到运行时镜像基址

runtime function address = runtime image base + (IDA's function address - default image base)

runtime function address = GetModuleHandle(NULL) + (0x411380 - 0x400000)

在我们的dlc DLL中,我们将有

const unsigned int DefExeBase = 0x400000;
BYTE *g_ExeBase;

void DLCInit()
{
    g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
    DLCReplaceFunction(g_ExeBase + 
    (0x411380 - DefExeBase), (BYTE*)main);   // 411380 is address of function in IDA
}

我们将在DllMain(在DLL_PROCESS_ATTACH事件上)中调用DLCInit。这个DLCReplaceFunction是什么?让我们看看如何从原始函数跳转到我们自己的函数。为了实现这一点,我们将在原始函数的开头写入以下结构

#pragma pack(push, 1)
typedef struct _Sorry       // 6 bytes of space needed
{
    struct
    {
        BYTE Opcode;
        INT Value;
    } Push;                 // push func
    struct
    {
        BYTE Opcode;
    } Ret;                  // retn
} Sorry;
#pragma pack(pop)

我们将我们自己函数的地址推送到堆栈上,然后用retn弹出它。这被称为“向后函数调用”。为了编码它,我们只需要6个字节,所有寄存器都得以保留,并且我们使用直接地址。那为什么不呢?现在是DLCReplaceFunction

void DLCReplaceFunction(BYTE *OldFunc, BYTE *NewFunc)
{
    Sorry *s = (Sorry*)OldFunc;
    s->Push.Opcode = 0x68;
    s->Push.Value = (INT)NewFunc;
    s->Ret.Opcode = 0xC3;
}

我们用这个结构覆盖了原始函数的开头。您可以看到这里有一些x86操作码值。我们怎么知道所有这些的呢?查找所需操作码字节的最佳方法是使用Nasm。写下您感兴趣的指令

op.asm:

[BITS 32]

push 0x10203040     ; to clearly see the value in assembled code
retn

编译原始二进制文件

nasm -f bin op.asm

8

并在十六进制编辑器中检查它

9

使用Nasm更方便,无需参考操作码表。此外,出错的可能性也较小。

让我们看看dlc DLL的完整代码

#include <Windows.h>
#include <stdio.h>

#pragma pack(push, 1)
typedef struct _Sorry       // 6 bytes of space needed
{
    struct
    {
        BYTE Opcode;
        INT Value;
    } Push;                 // push func
    struct
    {
        BYTE Opcode;
    } Ret;                  // retn
} Sorry;
#pragma pack(pop)

const unsigned int DefExeBase = 0x400000;
BYTE *g_ExeBase;

void DLCReplaceFunction(BYTE *OldFunc, BYTE *NewFunc)
{
    Sorry *s = (Sorry*)OldFunc;
    s->Push.Opcode = 0x68;
    s->Push.Value = (INT)NewFunc;
    s->Ret.Opcode = 0xC3;
}

int main(int argc, char *argv[])
{
    printf("You forgot Hello World!\n");
    getchar();
    return 0x10203040;
}

void DLCInit()
{
    g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
    DLCReplaceFunction(g_ExeBase + 
    (0x411380 - DefExeBase), (BYTE*)main);   // 411380 is address of function in IDA
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DLCInit();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

现在构建dlc DLL,并将其复制到main.exe的文件夹。下一步是修改main.exe的导入描述符(我们决定滥用kernel32.dll的导入描述符)和代码节权限(默认情况下,代码节只有execute/read权限)。我们将使用这个Stack Overflow问题中的一个小型实用程序

我们将对其进行少量修改以获取所需信息

#include <Windows.h>

DWORD Rva2Offset(DWORD rva, PIMAGE_SECTION_HEADER psh, PIMAGE_NT_HEADERS pnt);
int _tmain(int argc, _TCHAR* argv[])
{
//>>>>> Change file name
    LPCWSTR fNmae = L"E:\\Reverse\\HULK2\\Main\\Debug\\Main.exe";
//<<<<<
    HANDLE handle = CreateFile(fNmae/*"messagebox.exe"*/, 
                    GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    DWORD byteread, size = GetFileSize(handle, NULL);
    PVOID virtualpointer = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE);
    ReadFile(handle, virtualpointer, size, &byteread, NULL);
    CloseHandle(handle);
    // Get pointer to NT header
    PIMAGE_NT_HEADERS           ntheaders = (PIMAGE_NT_HEADERS)(PCHAR(virtualpointer) + 
                                             PIMAGE_DOS_HEADER(virtualpointer)->e_lfanew);
    PIMAGE_SECTION_HEADER       pSech = IMAGE_FIRST_SECTION(ntheaders);//Pointer to 
                                                                // first section header
    PIMAGE_IMPORT_DESCRIPTOR    pImportDescriptor; //Pointer to import descriptor 

//>>>>> Add this block (note: executable may have multiple code sections, 
//we don't consider this case here)
    for (int i = 0; i < ntheaders->FileHeader.NumberOfSections; ++i)
    {
        printf("Section: %s\n", pSech->Name);
        if (!_stricmp((char*)pSech->Name, ".text"))  // case insensitive compare
        {
            UINT d = (PCHAR)(&pSech->Characteristics) - (PCHAR)virtualpointer;
            printf("Code Section Permissions FileOffset: %X\n", d);
            getchar();
        }
        ++pSech;
    }
    pSech = IMAGE_FIRST_SECTION(ntheaders);
//<<<<<
    __try
    {
        if (ntheaders->OptionalHeader.DataDirectory
            [IMAGE_DIRECTORY_ENTRY_IMPORT].Size != 0)/*if size of the table 
                                                       is 0 - Import Table does not exist */
        {
            pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD_PTR)virtualpointer + \
                Rva2Offset(ntheaders->OptionalHeader.DataDirectory
                [IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress, pSech, ntheaders));
            LPSTR libname[256];
            size_t i = 0;
            // Walk until you reached an empty IMAGE_IMPORT_DESCRIPTOR
            while (pImportDescriptor->Name != NULL)
            {
                printf("Library Name   :");
                //Get the name of each DLL
                libname[i] = (PCHAR)((DWORD_PTR)virtualpointer + 
                              Rva2Offset(pImportDescriptor->Name, pSech, ntheaders));
                printf("%s\n", libname[i]);

//>>>>>> Add this block
                if (!_stricmp(libname[i], "kernel32.dll"))    // case insensitive compare
                {
                    UINT d = libname[i] - (PCHAR)virtualpointer;
                    printf("DLL Name FileOffset: %X\n", d);
                    getchar();
                }

                PIMAGE_THUNK_DATA ThunkData = (PIMAGE_THUNK_DATA)((DWORD_PTR)virtualpointer + 
                    Rva2Offset(pImportDescriptor->OriginalFirstThunk, pSech, ntheaders));

                while (ThunkData->u1.AddressOfData)
                {
                    PIMAGE_IMPORT_BY_NAME ImportByName = (PIMAGE_IMPORT_BY_NAME)
                      ((DWORD_PTR)virtualpointer + 
                      Rva2Offset(ThunkData->u1.AddressOfData, pSech, ntheaders));
                    printf("\t%s\n", ImportByName->Name);
                    ++ThunkData;
                }
//<<<<<<
                pImportDescriptor++; //advance to next IMAGE_IMPORT_DESCRIPTOR
                i++;

            }

        }
        else
        {
            printf("No Import Table!\n");
            return 1;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        if (EXCEPTION_ACCESS_VIOLATION == GetExceptionCode())
        {
            printf("Exception: EXCEPTION_ACCESS_VIOLATION\n");
            return 1;
        }

    }
    if (virtualpointer)
        VirtualFree(virtualpointer, size, MEM_DECOMMIT);

//>>>>>> Add getchar
    getchar();
//<<<<<<
    return 0;
}
/*Convert Virtual Address to File Offset */
DWORD Rva2Offset(DWORD rva, PIMAGE_SECTION_HEADER psh, PIMAGE_NT_HEADERS pnt)
{
    size_t i = 0;
    PIMAGE_SECTION_HEADER pSeh;
    if (rva == 0)
    {
        return (rva);
    }
    pSeh = psh;
    for (i = 0; i < pnt->FileHeader.NumberOfSections; i++)
    {
        if (rva >= pSeh->VirtualAddress && rva < pSeh->VirtualAddress +
            pSeh->Misc.VirtualSize)
        {
            break;
        }
        pSeh++;
    }
    return (rva - pSeh->VirtualAddress + pSeh->PointerToRawData);
}

由于我们正在研究32位可执行文件,所以我们也应该将此工具构建为32位。否则,将使用64位PE结构定义,并且工具将无法正常工作。让我们运行它并查看我们需要修补的文件偏移量

10

11

所以代码节权限的文件偏移量等于

0x22C

我们将要导入的kernel32 DLL名称的文件偏移量等于

0x68A4

创建main.exe的副本,并在十六进制编辑器中打开它(我使用的是Hex Editor Neo)

12

13

您可以按Ctrl + g,选择绝对偏移量,输入所需的偏移量值,然后您将处于正确的时间、正确的位置。我们需要在代码节特性中设置写入标志。在此处查看IMAGE_SECTION_HEADER结构和Characteristics标志

Characteristics是4字节DWORD字段。我们看到它包含以下字节

20 00 00 60

我们是小端字节序,所以值为

0x60000020

以人类可读的形式

0x40000000 | 0x20000000 | 0x00000020

IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_CNT_CODE

我们将添加IMAGE_SCN_MEM_WRITE

0x80000000 | 0x40000000 | 0x20000000 | 0x00000020

IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_CNT_CODE

所以我们需要这个值

0xE0000020

以及这些字节

20 00 00 E0

如下所示

14

现在让我们将kernel32.dll更改为dlc.dll

15

最后,让我们运行我们修补后的main.exe

16

我们可以看到我们的main函数被调用了,而不是旧的函数(它被我们的跳转覆盖了)。

现在让我们重复相同的示例,只是针对64位。将main编译为64位,并转储导入。对我来说,导入函数的列表没有改变(嗯,它应该不会改变),所以我们的dlc DLL的模块定义文件保持不变。

现在回到IDA。我想提一下,较新版本的IDA是64位的,所以我们可以本地调试64位应用程序。我100%确定IDA 6.7仍然是32位的,而IDA 7.2已经是64位的了。我不知道IDA发生这个重大变化的确切版本。

用IDA打开main.exe。我们需要获取原始main函数的地址,以便知道应该在哪个地址插入跳转到我们自己的main函数。

17

18

所以我们可以看到原始main函数的地址等于

0x140001010

如果函数有另一个名称(没有地址),我们始终可以重命名它(删除其名称),或者打开function info窗口查看其地址。请注意,IDA假定默认的镜像基址。对于64位镜像,它等于

0x140000000

在我们的dlc DLL中,我们将有

const unsigned long long DefExeBase = 0x140000000;
BYTE *g_ExeBase;

void DLCInit()
{
    g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
    DLCReplaceFunction(g_ExeBase + 
    (0x140001010 - DefExeBase), (BYTE*)main);  // 140001010 is address of function in IDA
}

我们将在DllMain(在DLL_PROCESS_ATTACH事件上)中调用DLCInit。这个DLCReplaceFunction是什么?让我们看看如何从原始函数跳转到我们自己的函数。为了实现这一点,我们将在原始函数的开头写入以下结构

#pragma pack(push, 1)
typedef struct _Sorry            // 12 bytes of space needed, rax value is lost
{
    struct
    {
        BYTE Force64bit;
        BYTE Opcode;
        INT64 Value;
    } MovToRax;                  // mov rax, func
    struct
    {
        BYTE Opcode;
        BYTE Reg;
    } JmpRax;                    // jmp rax
} Sorry;
#pragma pack(pop)

我们将我们自己函数的地址移动到rax寄存器中,并通过它跳转。为了编码它,我们需要12个字节,除了rax之外,所有寄存器都得以保留,并且我们使用直接地址。那为什么不呢?现在是DLCReplaceFunction

void DLCReplaceFunction(BYTE *OldFunc, BYTE *NewFunc)
{
    Sorry *s = (Sorry*)OldFunc;
    s->MovToRax.Force64bit = 0x48;
    s->MovToRax.Opcode = 0xb8;
    s->MovToRax.Value = (INT64)NewFunc;
    s->JmpRax.Opcode = 0xff;
    s->JmpRax.Reg = 0xe0;
}

我们用这个结构覆盖了原始函数的开头。您可以看到这里有一些x64操作码值。写下您感兴趣的指令

op.asm:

[BITS 64]

mov rax, 0x1020304050607080             ; to clearly see the value in assembled code
jmp rax

编译原始二进制文件

nasm -f bin op.asm

19

并在十六进制编辑器中检查它

20

让我们看看dlc DLL的完整代码

#include <Windows.h>
#include <stdio.h>

const unsigned long long DefExeBase = 0x140000000;
BYTE *g_ExeBase;

#pragma pack(push, 1)
typedef struct _Sorry            // 12 bytes of space needed, rax value is lost
{
    struct
    {
        BYTE Force64bit;
        BYTE Opcode;
        INT64 Value;
    } MovToRax;                  // mov rax, func
    struct
    {
        BYTE Opcode;
        BYTE Reg;
    } JmpRax;                    // jmp rax
} Sorry;
#pragma pack(pop)

void DLCReplaceFunction(BYTE *OldFunc, BYTE *NewFunc)
{
    Sorry *s = (Sorry*)OldFunc;
    s->MovToRax.Force64bit = 0x48;
    s->MovToRax.Opcode = 0xb8;
    s->MovToRax.Value = (INT64)NewFunc;
    s->JmpRax.Opcode = 0xff;
    s->JmpRax.Reg = 0xe0;
}

int main(int argc, char *argv[])
{
    printf("You forgot Hello World!\n");
    getchar();
    return 0x10203040;
}

void DLCInit()
{
    g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
    DLCReplaceFunction(g_ExeBase + (0x140001010 - DefExeBase), 
       (BYTE*)main);          // 140001010 is address of function in IDA
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DLCInit();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

现在将dlc DLL构建为64位,并将其复制到main.exe的文件夹。下一步是修改main.exe的导入描述符和代码节权限。在我们的工具中,如果需要,请替换文件名。由于我们现在正在研究64位可执行文件,所以我们也需要将其构建为64位,否则它将无法正常工作。让我们看看文件偏移量

21

22

创建main.exe的副本,并在十六进制编辑器中打开它

23

24

以及修补后

25

26

运行我们修补后的main.exe

27

我们可以看到我们的main函数被调用了,而不是旧的函数(它被我们的跳转覆盖了)。

更现实的例子

让我们看一个更复杂的例子。这是来自《绿巨人浩克》 - 2003年游戏(32位)的hulk.exe。转储导入

dumpbin /imports hulk.exe

28

29

我们可以看到我们的EXE导入了binkw32.dll的函数。这比标准的kernel32.dll更有趣,所以我们将继续使用它。所以我们需要构建一个dlc DLL,它将导出所有这些函数并将它们转发到binkw32.dll。创建一个空的DLL项目,并添加以下模块定义文件

LIBRARY DLC

EXPORTS

                   _BinkSetPan@12                 = binkw32._BinkSetPan@12
                   _BinkSetVolume@12              = binkw32._BinkSetVolume@12
                   _BinkGetError@0                = binkw32._BinkGetError@0
                   _BinkPause@8                   = binkw32._BinkPause@8
                   _BinkOpen@8                    = binkw32._BinkOpen@8
                   _BinkSetIO@4                   = binkw32._BinkSetIO@4
                   _BinkSetSoundTrack@8           = binkw32._BinkSetSoundTrack@8
                   _BinkSetSoundSystem@8          = binkw32._BinkSetSoundSystem@8
                   _BinkOpenDirectSound@4         = binkw32._BinkOpenDirectSound@4
                   _RADSetMemory@8                = binkw32._RADSetMemory@8
                   _BinkClose@4                   = binkw32._BinkClose@4
                   _BinkNextFrame@4               = binkw32._BinkNextFrame@4
                   _BinkCopyToBufferRect@44       = binkw32._BinkCopyToBufferRect@44
                   _BinkDoFrame@4                 = binkw32._BinkDoFrame@4
                   _BinkWait@4                    = binkw32._BinkWait@4
                   _RADTimerRead@0                = binkw32._RADTimerRead@0

请注意,尽管binkw32.dll不是“标准”DLL(Windows提供的,带有导入库文件以链接),但我们不需要创建lib文件就可以使其工作。这可能是因为导出的函数名称被装饰了(__declspec(dllexport) __stdcall,只能在32位镜像中找到)。如果我们有binkw32.dll,其导出的函数名称没有装饰,就像这样

BinkSetPan

我们就必须为它创建一个lib文件,否则dlc DLL的构建将会失败。如果您遇到这种情况,请等到64位示例(紧随32位示例之后)。在那里,您将看到如何为非“标准”DLL创建导入lib文件(对于32位和64位,过程是相同的)。

回到正题。用IDA打开hulk.exe。为了我们的目的,我们将选择一个直接创建游戏主窗口的函数。

30

31

我们需要收集此函数使用的所有全局变量/函数的列表。我们以与函数替换相同的方式计算运行时地址。之后,我们使用指针bind到地址。让我们bind到所有必需的项目

DWORD *dword_69E600;
HINSTANCE *hInstance;
char *IconName;
char *lpClassName;
WNDPROC sub_46DF00;
char *WindowName;
HWND *hWnd;

void DLCBindGlobals()
{
    dword_69E600 = (DWORD*)(g_ExeBase + (0x69E600 - DefExeBase));
    hInstance = (HINSTANCE*)(g_ExeBase + (0x6B2910 - DefExeBase));
    IconName = (char*)(g_ExeBase + (0x61D744 - DefExeBase));
    lpClassName = (char*)(g_ExeBase + (0x69E608 - DefExeBase));
    sub_46DF00 = (WNDPROC)(g_ExeBase + (0x46DF00 - DefExeBase));
    WindowName = (char*)(g_ExeBase + (0x61D6C4 - DefExeBase));
    hWnd = (HWND*)(g_ExeBase + (0x6B2908 - DefExeBase));
}

然后替换函数本身

void DLCBindFunctions()
{
    DLCReplaceFunction(g_ExeBase + (0x46E200 - DefExeBase), (BYTE*)sub_46E200);
}

初始化

void DLCInit()
{
    g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
    DLCBindGlobals();
    DLCBindFunctions();
}

现在让我们编写我们自己的函数版本。为了看到效果,我们将进行一个小修改

void sub_46E200()
{
    DWORD Style;

    if (*dword_69E600 == 1) Style = 0xCF0000;
    else Style = 0xC80000;

    WNDCLASSA WndClass;
    RECT Rect;

    WndClass.cbClsExtra = 4;
    WndClass.cbWndExtra = 4;
    WndClass.style = 0x203;
    WndClass.lpfnWndProc = sub_46DF00;
    WndClass.hInstance = *hInstance;
    WndClass.hIcon = LoadIconA(*hInstance, IconName);
    WndClass.hCursor = NULL;
    WndClass.hbrBackground = NULL;
    WndClass.lpszMenuName = NULL;
    WndClass.lpszClassName = lpClassName;

    RegisterClassA(&WndClass);

    Rect.left = 0;
    Rect.top = 0;
    Rect.right = 0x280;
    Rect.bottom = 0x1E0;

    if (*dword_69E600 == 1) ExitProcess(0);

    // DLC extra
    //AdjustWindowRect(&Rect, Style, FALSE);

    // DLC extra
    std::string WindowsNameDLC = std::string(WindowName) + std::string(" --> with DLC!");

    *hWnd = CreateWindowExA(0, lpClassName, WindowsNameDLC.c_str(), 
            Style, Rect.left, Rect.top, Rect.right - Rect.left, Rect.bottom - Rect.top,
        NULL, NULL, *hInstance, NULL);

    if (!*hWnd) ExitProcess(0);

    int CmdShow;

    if (*dword_69E600 == 1) CmdShow = 5;
    else CmdShow = 3;

    ShowWindow(*hWnd, CmdShow);
    ShowCursor(FALSE);

    // DLC extra
    Sleep(5000);
}

我们更改了窗口文本,添加了sleep以便实际看到它,并删除了讨厌的AdjustWindowRect调用。AdjustWindowRect弄乱了一些东西,导致窗口没有完全全屏。现在一切都如应有的那样。

正如您所见,我们无法直接访问全局变量,只能通过指针访问。另请注意,如果我们懒得翻译代码分支,而我们知道它们不会被调用(可以在IDA调试时检查),我们可以用ExitProcess调用来解决。这样,如果未实现的 कोड分支被调用,进程将终止。

完整的dlc DLL代码

#include <Windows.h>
#include <string>

#pragma pack(push, 1)
typedef struct _Sorry
{
    struct
    {
        BYTE Opcode;
        INT Value;
    } Push;
    struct
    {
        BYTE Opcode;
    } Ret;
} Sorry;
#pragma pack(pop)

BYTE *g_ExeBase;
const unsigned int DefExeBase = 0x400000;

DWORD *dword_69E600;
HINSTANCE *hInstance;
char *IconName;
char *lpClassName;
WNDPROC sub_46DF00;
char *WindowName;
HWND *hWnd;

void sub_46E200();

void DLCBindGlobals()
{
    dword_69E600 = (DWORD*)(g_ExeBase + (0x69E600 - DefExeBase));
    hInstance = (HINSTANCE*)(g_ExeBase + (0x6B2910 - DefExeBase));
    IconName = (char*)(g_ExeBase + (0x61D744 - DefExeBase));
    lpClassName = (char*)(g_ExeBase + (0x69E608 - DefExeBase));
    sub_46DF00 = (WNDPROC)(g_ExeBase + (0x46DF00 - DefExeBase));
    WindowName = (char*)(g_ExeBase + (0x61D6C4 - DefExeBase));
    hWnd = (HWND*)(g_ExeBase + (0x6B2908 - DefExeBase));
}

void DLCReplaceFunction(BYTE *OldFunc, BYTE *NewFunc)
{
    Sorry *s = (Sorry*)OldFunc;
    s->Push.Opcode = 0x68;
    s->Push.Value = (INT)NewFunc;
    s->Ret.Opcode = 0xC3;
}

void DLCBindFunctions()
{
    DLCReplaceFunction(g_ExeBase + (0x46E200 - DefExeBase), (BYTE*)sub_46E200);
}

void DLCInit()
{
    g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
    DLCBindGlobals();
    DLCBindFunctions();
}

void sub_46E200()
{
    DWORD Style;

    if (*dword_69E600 == 1) Style = 0xCF0000;
    else Style = 0xC80000;

    WNDCLASSA WndClass;
    RECT Rect;

    WndClass.cbClsExtra = 4;
    WndClass.cbWndExtra = 4;
    WndClass.style = 0x203;
    WndClass.lpfnWndProc = sub_46DF00;
    WndClass.hInstance = *hInstance;
    WndClass.hIcon = LoadIconA(*hInstance, IconName);
    WndClass.hCursor = NULL;
    WndClass.hbrBackground = NULL;
    WndClass.lpszMenuName = NULL;
    WndClass.lpszClassName = lpClassName;

    RegisterClassA(&WndClass);

    Rect.left = 0;
    Rect.top = 0;
    Rect.right = 0x280;
    Rect.bottom = 0x1E0;

    if (*dword_69E600 == 1) ExitProcess(0);

    // DLC extra
    //AdjustWindowRect(&Rect, Style, FALSE);

    // DLC extra
    std::string WindowsNameDLC = std::string(WindowName) + std::string(" --> with DLC!");

    *hWnd = CreateWindowExA(0, lpClassName, WindowsNameDLC.c_str(), 
            Style, Rect.left, Rect.top, Rect.right - Rect.left, Rect.bottom - Rect.top,
        NULL, NULL, *hInstance, NULL);

    if (!*hWnd) ExitProcess(0);

    int CmdShow;

    if (*dword_69E600 == 1) CmdShow = 5;
    else CmdShow = 3;

    ShowWindow(*hWnd, CmdShow);
    ShowCursor(FALSE);

    // DLC extra
    Sleep(5000);
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DLCInit();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

现在构建dlc DLL,并将其复制到游戏的文件夹。下一步是修改hulk.exe的导入描述符和代码节权限。让我们看看文件偏移量

32

33

创建hulk.exe的副本,并在十六进制编辑器中打开它

34

35

36

37

现在运行修补后的程序。我们可以看到我们的更改(抱歉,我无法捕捉屏幕,即使使用Fraps)。很酷,我们可以不仅进行逆向工程,还可以修改游戏的行为,而无需触碰原始EXE(除了导入描述符和代码节权限)。它非常方便(moddy)。

还有一个问题。游戏以全屏模式运行,为了调试,我们至少需要看到调试器。我们想用IDA调试,并想在Visual Studio中调试我们自己的dlc代码。并非所有游戏都提供窗口模式。那么我们该怎么办?

要使游戏窗口化 - 使用DxWnd。我们可以告诉DxWnd向程序传递额外的命令行参数,这样在dlc DLL中,我们将知道是否应该等待调试器附加,或者像往常一样继续。

DxWnd主窗口。

38

我们将添加到profiles。第一个是normal,第二个是debug。这是normal

39

40

对于debug配置文件,我们传递一个额外的命令行参数:-debug-wait

41

42

如果您在没有保存的情况下退出DxWnd,系统会询问您是否要保存

43

现在让我们看看-debug-wait参数是如何实现的。首先,我们检查是否以-debug-wait参数启动。如果我们有它,我们调用DLCWaitForDebugger

void DLCInit()
{
    char *_cmd = GetCommandLineA();
    std::string cmd(_cmd);
    std::string suffix("-debug-wait");
    std::size_t found = cmd.find(suffix);
    bool debug_wait = false;

    while (found != std::string::npos)
    {
        if (cmd.length() == (found + suffix.length()))
        {
            _cmd[found] = 0;  // probably we should hide this parameter from program
            debug_wait = true;
            break;
        }
        found = cmd.find(suffix, found + 1);
    }

    g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
    DLCBindGlobals();
    DLCBindFunctions();
    if (debug_wait) DLCWaitForDebugger();
}

在这里,我们检查命令行字符串是否以-debug-wait结尾。我们还通过在缓冲区中插入null字符来将其截断,以防被检查程序对未知参数有所评论。

现在,我们对start函数(程序的入口点)感兴趣

44

45

我们将保存start函数开头的字节,并用跳转到我们自定义函数的指令覆盖它们。自定义函数将恢复原始字节,并在循环中等待调试器。调试器附加后,我们将发出debug break,然后自己调用start。让我们看看代码

typedef int(__stdcall *Start)();

int __stdcall sub_5D095E();

bool debug_wait;
Start start_ptr;
Sorry start_sorry;

void DLCWaitForDebugger()
{
    start_ptr = (Start)(g_ExeBase + (0x5D095E - DefExeBase));
    start_sorry = *((Sorry*)start_ptr);
    DLCReplaceFunction((BYTE*)start_ptr, (BYTE*)sub_5D095E);
}

int __stdcall sub_5D095E()
{
    *((Sorry*)start_ptr) = start_sorry;
    while (!IsDebuggerPresent()) Sleep(500);
    DebugBreak();
    return start_ptr();
}

我们有一些“一次性”蹦床。现在我们把所有东西放在一起看

#include <Windows.h>
#include <string>

#pragma pack(push, 1)
typedef struct _Sorry
{
    struct
    {
        BYTE Opcode;
        INT Value;
    } Push;
    struct
    {
        BYTE Opcode;
    } Ret;
} Sorry;
#pragma pack(pop)

BYTE *g_ExeBase;
const unsigned int DefExeBase = 0x400000;

typedef int(__stdcall *Start)();

int __stdcall sub_5D095E();

Start start_ptr;
Sorry start_sorry;

DWORD *dword_69E600;
HINSTANCE *hInstance;
char *IconName;
char *lpClassName;
WNDPROC sub_46DF00;
char *WindowName;
HWND *hWnd;

void sub_46E200();

void DLCBindGlobals()
{
    dword_69E600 = (DWORD*)(g_ExeBase + (0x69E600 - DefExeBase));
    hInstance = (HINSTANCE*)(g_ExeBase + (0x6B2910 - DefExeBase));
    IconName = (char*)(g_ExeBase + (0x61D744 - DefExeBase));
    lpClassName = (char*)(g_ExeBase + (0x69E608 - DefExeBase));
    sub_46DF00 = (WNDPROC)(g_ExeBase + (0x46DF00 - DefExeBase));
    WindowName = (char*)(g_ExeBase + (0x61D6C4 - DefExeBase));
    hWnd = (HWND*)(g_ExeBase + (0x6B2908 - DefExeBase));
}

void DLCReplaceFunction(BYTE *OldFunc, BYTE *NewFunc)
{
    Sorry *s = (Sorry*)OldFunc;
    s->Push.Opcode = 0x68;
    s->Push.Value = (INT)NewFunc;
    s->Ret.Opcode = 0xC3;
}

void DLCBindFunctions()
{
    DLCReplaceFunction(g_ExeBase + (0x46E200 - DefExeBase), (BYTE*)sub_46E200);
}

int __stdcall sub_5D095E()
{
    *((Sorry*)start_ptr) = start_sorry;
    while (!IsDebuggerPresent()) Sleep(500);
    DebugBreak();
    return start_ptr();
}

void DLCWaitForDebugger()
{
    start_ptr = (Start)(g_ExeBase + (0x5D095E - DefExeBase));
    start_sorry = *((Sorry*)start_ptr);
    DLCReplaceFunction((BYTE*)start_ptr, (BYTE*)sub_5D095E);
}

void DLCInit()
{
    char *_cmd = GetCommandLineA();
    std::string cmd(_cmd);
    std::string suffix("-debug-wait");
    std::size_t found = cmd.find(suffix);
    bool debug_wait = false;

    while (found != std::string::npos)
    {
        if (cmd.length() == (found + suffix.length()))
        {
            _cmd[found] = 0;  // probably we should hide this parameter from program
            debug_wait = true;
            break;
        }
        found = cmd.find(suffix, found + 1);
    }

    g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
    DLCBindGlobals();
    DLCBindFunctions();
    if (debug_wait) DLCWaitForDebugger();
}

void sub_46E200()
{
    DWORD Style;

    if (*dword_69E600 == 1) Style = 0xCF0000;
    else Style = 0xC80000;

    WNDCLASSA WndClass;
    RECT Rect;

    WndClass.cbClsExtra = 4;
    WndClass.cbWndExtra = 4;
    WndClass.style = 0x203;
    WndClass.lpfnWndProc = sub_46DF00;
    WndClass.hInstance = *hInstance;
    WndClass.hIcon = LoadIconA(*hInstance, IconName);
    WndClass.hCursor = NULL;
    WndClass.hbrBackground = NULL;
    WndClass.lpszMenuName = NULL;
    WndClass.lpszClassName = lpClassName;

    RegisterClassA(&WndClass);

    Rect.left = 0;
    Rect.top = 0;
    Rect.right = 0x280;
    Rect.bottom = 0x1E0;

    if (*dword_69E600 == 1) ExitProcess(0);

    // DLC extra
    //AdjustWindowRect(&Rect, Style, FALSE);

    // DLC extra
    std::string WindowsNameDLC = std::string(WindowName) + std::string(" --> with DLC!");

    *hWnd = CreateWindowExA(0, lpClassName, WindowsNameDLC.c_str(), 
            Style, Rect.left, Rect.top, Rect.right - Rect.left, Rect.bottom - Rect.top,
        NULL, NULL, *hInstance, NULL);

    if (!*hWnd) ExitProcess(0);

    int CmdShow;

    if (*dword_69E600 == 1) CmdShow = 5;
    else CmdShow = 3;

    ShowWindow(*hWnd, CmdShow);
    ShowCursor(FALSE);

    // DLC extra
    Sleep(5000);
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DLCInit();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

让我们尝试在DxWnd中使用-debug-wait启动游戏,并用IDA附加(当我使用IDA 6.7时它能工作,当我尝试IDA 7.2时,由于某个内部错误它崩溃了)

46

加载各种DLL的符号可能需要一段时间。程序将命中debug break并挂起。在已替换函数的开头设置断点,然后继续执行。这是

47

48

现在我们在我们的dlc

49

50

有时,即使使用IDA 6.7,我也会遇到奇怪的错误行为。当我用DxWnd和-debug-wait启动游戏时,游戏在CreateWindowExA调用时退出了,没有明显的原因。此外,如果我在原始start函数的开头设置断点,IDA会忽略它。在DxWnd中,我删除了debug配置文件,然后重新创建了它。事情又开始奏效了。所以您应该在这里多做一些实验,或者甚至改善我们的调试器附加体验。

现在让我们尝试Visual Studio调试。我们不应该在构建后更新dlc DLL的源代码,换句话说,源代码和二进制文件应该匹配,否则我们就无法调试。设置断点

51

附加到进程

52

我们将命中debug break,继续进程。断点将被命中

53

现在,假设我们对hulk.exebinkw32.dll之间的接口感兴趣。让我们以_RADSetMemory@8 API为例。当我们在其原型中弄清楚时,我们就不能再转发这个函数,而是提供我们自己的函数,该函数会调用原始函数。从IDA的反汇编中,我们可以看出这个函数接受两个函数指针,并且其返回值被忽略。

54

让我们注释掉模块定义文件中的这个函数

LIBRARY DLC

EXPORTS

                   _BinkSetPan@12                 = binkw32._BinkSetPan@12
                   _BinkSetVolume@12              = binkw32._BinkSetVolume@12
                   _BinkGetError@0                = binkw32._BinkGetError@0
                   _BinkPause@8                   = binkw32._BinkPause@8
                   _BinkOpen@8                    = binkw32._BinkOpen@8
                   _BinkSetIO@4                   = binkw32._BinkSetIO@4
                   _BinkSetSoundTrack@8           = binkw32._BinkSetSoundTrack@8
                   _BinkSetSoundSystem@8          = binkw32._BinkSetSoundSystem@8
                   _BinkOpenDirectSound@4         = binkw32._BinkOpenDirectSound@4
                   
;                   _RADSetMemory@8                = binkw32._RADSetMemory@8

                   _BinkClose@4                   = binkw32._BinkClose@4
                   _BinkNextFrame@4               = binkw32._BinkNextFrame@4
                   _BinkCopyToBufferRect@44       = binkw32._BinkCopyToBufferRect@44
                   _BinkDoFrame@4                 = binkw32._BinkDoFrame@4
                   _BinkWait@4                    = binkw32._BinkWait@4
                   _RADTimerRead@0                = binkw32._RADTimerRead@0

并提供我们自己的实现

typedef void *type_SomeFunctionPointer;
typedef void(__stdcall *type_binkw32_RADSetMemory)
            (type_SomeFunctionPointer, type_SomeFunctionPointer);
type_binkw32_RADSetMemory binkw32_RADSetMemory;

void DLCInit()
{
    ...
    
    HMODULE binkw32 = LoadLibraryA("binkw32.dll");
    binkw32_RADSetMemory = 
            (type_binkw32_RADSetMemory)GetProcAddress(binkw32, "_RADSetMemory@8");
}

extern "C"
{
    __declspec(dllexport) void __stdcall RADSetMemory
              (type_SomeFunctionPointer f1, type_SomeFunctionPointer f2)
    {
        binkw32_RADSetMemory(f1, f2);
    }
}

让我们用这个小改动来构建我们的dlc DLL,并验证它的导出

dumpbin /exports dlc.dll

55

我们可以像调试任何其他代码一样调试它

56

为什么要费心提供这样的存根?这样,我们可以将反汇编的API接口表示为C代码,并且如果我们的API接口不正确(程序可能会崩溃),我们会立即知道,因为这个函数实际上会被调用。我们可以修补多个导入描述符,使用我们的dlc DLL名称,并根据我们的反汇编进度,要么将API转发到相应的DLL,要么提供我们自己的存根。

现在我们来展示64位示例。它将是doom3源代码的一个端口,称为dhewm3

我们可以将其构建为64位,这是官方doom3源代码无法做到的

Windows没有预先构建的64位版本。所以我们必须自己构建。虽然推荐使用MinGW进行64位构建,但我设法用Visual Studio(通过一些项目属性调整)构建了一个64位版本。我成功地进入了游戏的主菜单,游戏在关卡加载过程中仍然崩溃(可能我应该使用MinGW)。不过,对于我们的目的来说,它已经足够了。

让我们看看doom 3的导入

dumpbin /imports dhewm3.exe

57

58

让我们坚持使用sdl2.dll。将导入重定向到文件,这样我们就可以轻松地将它们复制到模块定义文件中。这是dumpbin输出的SDL2部分

    SDL2.dll
             1439BC3B0 Import Address Table
             1439BD818 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                         14C SDL_Quit
                          F1 SDL_HasAltiVec
                          F6 SDL_HasMMX
                          EF SDL_Has3DNow
                          F8 SDL_HasSSE
                          F9 SDL_HasSSE2
                          23 SDL_CreateMutex
                         124 SDL_LockMutex
                         1B9 SDL_UnlockMutex
                          37 SDL_DestroyMutex
                          21 SDL_CreateCond
                          36 SDL_DestroyCond
                          19 SDL_CondSignal
                          1A SDL_CondWait
                          2D SDL_CreateThread
                          FF SDL_Init
                          BC SDL_GetThreadID
                         1C7 SDL_WaitThread
                          35 SDL_Delay
                          93 SDL_GetModState
                         18A SDL_SetModState
                         149 SDL_PumpEvents
                         148 SDL_PollEvent
                         14A SDL_PushEvent
                         1EB SDL_malloc
                         1E0 SDL_getenv
                         200 SDL_strlen
                         1FF SDL_strlcpy
                         1FE SDL_strlcat
                         204 SDL_strrchr
                         1D7 SDL_atoi
                          25 SDL_CreateRGBSurfaceFrom
                          4C SDL_FreeSurface
                          2E SDL_CreateWindow
                          C7 SDL_GetWindowFlags
                         1A2 SDL_SetWindowIcon
                          D0 SDL_GetWindowSize
                         1A1 SDL_SetWindowGrab
                         1A0 SDL_SetWindowGammaRamp
                          3B SDL_DestroyWindow
                          56 SDL_GL_GetProcAddress
                          5B SDL_GL_SetAttribute
                          4F SDL_GL_CreateContext
                          5C SDL_GL_SetSwapInterval
                          5D SDL_GL_SwapWindow
                          50 SDL_GL_DeleteContext
                         18D SDL_SetRelativeMouseMode
                         1A9 SDL_ShowCursor
                         1C9 SDL_WasInit
                          8B SDL_GetError
                          BE SDL_GetTicks
                           2 SDL_AddTimer
                          C1 SDL_GetVersion
                         15D SDL_RemoveTimer
                         1B3 SDL_ThreadID

如果函数太多,我们可以自动化def文件创建过程。例如,我们可以删除标题并用此文本替换(正则表达式)

.*SDL_                            # any characters before "SDL_" and "SDL_" itself

用这个:

SDL_

接下来,我们为sdl2.dll创建模块定义文件(这次我们将需要它)。并将所有函数放入其中

sdl2.def:

LIBRARY SDL2

EXPORTS

SDL_Quit
SDL_HasAltiVec
SDL_HasMMX
SDL_Has3DNow
SDL_HasSSE
SDL_HasSSE2
SDL_CreateMutex
SDL_LockMutex
SDL_UnlockMutex
SDL_DestroyMutex
SDL_CreateCond
SDL_DestroyCond
SDL_CondSignal
SDL_CondWait
SDL_CreateThread
SDL_Init
SDL_GetThreadID
SDL_WaitThread
SDL_Delay
SDL_GetModState
SDL_SetModState
SDL_PumpEvents
SDL_PollEvent
SDL_PushEvent
SDL_malloc
SDL_getenv
SDL_strlen
SDL_strlcpy
SDL_strlcat
SDL_strrchr
SDL_atoi
SDL_CreateRGBSurfaceFrom
SDL_FreeSurface
SDL_CreateWindow
SDL_GetWindowFlags
SDL_SetWindowIcon
SDL_GetWindowSize
SDL_SetWindowGrab
SDL_SetWindowGammaRamp
SDL_DestroyWindow
SDL_GL_GetProcAddress
SDL_GL_SetAttribute
SDL_GL_CreateContext
SDL_GL_SetSwapInterval
SDL_GL_SwapWindow
SDL_GL_DeleteContext
SDL_SetRelativeMouseMode
SDL_ShowCursor
SDL_WasInit
SDL_GetError
SDL_GetTicks
SDL_AddTimer
SDL_GetVersion
SDL_RemoveTimer
SDL_ThreadID

调用以下命令生成导入lib文件

lib /def:sdl2.def /out:sdl2.lib /machine:x64

您将获得sdl2.lib文件,我们将需要链接到我们的dlc DLL中。现在让我们为dlc DLL创建模块定义文件。复制sdl2.def文件,并删除“LIBRARY SDL2”和“EXPORTS”行,我们将其命名为sdl2.txt。我们可以使用以下python脚本

gen.py:

file = open("dlc.def", "w")
file.write("LIBRARY DLC\n")
file.write("EXPORTS\n")
for i, line in enumerate(open("sdl2.txt", "r")):
    line = line.strip("\n")
    format = "%s = sdl2.%s\n"
    file.write(format % (line, line))
file.close()

运行脚本

python gen.py

我们将得到以下dlc def文件

LIBRARY DLC
EXPORTS
SDL_Quit = sdl2.SDL_Quit
SDL_HasAltiVec = sdl2.SDL_HasAltiVec
SDL_HasMMX = sdl2.SDL_HasMMX
SDL_Has3DNow = sdl2.SDL_Has3DNow
SDL_HasSSE = sdl2.SDL_HasSSE
SDL_HasSSE2 = sdl2.SDL_HasSSE2
SDL_CreateMutex = sdl2.SDL_CreateMutex
SDL_LockMutex = sdl2.SDL_LockMutex
SDL_UnlockMutex = sdl2.SDL_UnlockMutex
SDL_DestroyMutex = sdl2.SDL_DestroyMutex
SDL_CreateCond = sdl2.SDL_CreateCond
SDL_DestroyCond = sdl2.SDL_DestroyCond
SDL_CondSignal = sdl2.SDL_CondSignal
SDL_CondWait = sdl2.SDL_CondWait
SDL_CreateThread = sdl2.SDL_CreateThread
SDL_Init = sdl2.SDL_Init
SDL_GetThreadID = sdl2.SDL_GetThreadID
SDL_WaitThread = sdl2.SDL_WaitThread
SDL_Delay = sdl2.SDL_Delay
SDL_GetModState = sdl2.SDL_GetModState
SDL_SetModState = sdl2.SDL_SetModState
SDL_PumpEvents = sdl2.SDL_PumpEvents
SDL_PollEvent = sdl2.SDL_PollEvent
SDL_PushEvent = sdl2.SDL_PushEvent
SDL_malloc = sdl2.SDL_malloc
SDL_getenv = sdl2.SDL_getenv
SDL_strlen = sdl2.SDL_strlen
SDL_strlcpy = sdl2.SDL_strlcpy
SDL_strlcat = sdl2.SDL_strlcat
SDL_strrchr = sdl2.SDL_strrchr
SDL_atoi = sdl2.SDL_atoi
SDL_CreateRGBSurfaceFrom = sdl2.SDL_CreateRGBSurfaceFrom
SDL_FreeSurface = sdl2.SDL_FreeSurface
SDL_CreateWindow = sdl2.SDL_CreateWindow
SDL_GetWindowFlags = sdl2.SDL_GetWindowFlags
SDL_SetWindowIcon = sdl2.SDL_SetWindowIcon
SDL_GetWindowSize = sdl2.SDL_GetWindowSize
SDL_SetWindowGrab = sdl2.SDL_SetWindowGrab
SDL_SetWindowGammaRamp = sdl2.SDL_SetWindowGammaRamp
SDL_DestroyWindow = sdl2.SDL_DestroyWindow
SDL_GL_GetProcAddress = sdl2.SDL_GL_GetProcAddress
SDL_GL_SetAttribute = sdl2.SDL_GL_SetAttribute
SDL_GL_CreateContext = sdl2.SDL_GL_CreateContext
SDL_GL_SetSwapInterval = sdl2.SDL_GL_SetSwapInterval
SDL_GL_SwapWindow = sdl2.SDL_GL_SwapWindow
SDL_GL_DeleteContext = sdl2.SDL_GL_DeleteContext
SDL_SetRelativeMouseMode = sdl2.SDL_SetRelativeMouseMode
SDL_ShowCursor = sdl2.SDL_ShowCursor
SDL_WasInit = sdl2.SDL_WasInit
SDL_GetError = sdl2.SDL_GetError
SDL_GetTicks = sdl2.SDL_GetTicks
SDL_AddTimer = sdl2.SDL_AddTimer
SDL_GetVersion = sdl2.SDL_GetVersion
SDL_RemoveTimer = sdl2.SDL_RemoveTimer
SDL_ThreadID = sdl2.SDL_ThreadID

注释掉以下行

SDL_HasMMX = sdl2.SDL_HasMMX
SDL_Has3DNow = sdl2.SDL_Has3DNow
SDL_HasSSE = sdl2.SDL_HasSSE
SDL_HasSSE2 = sdl2.SDL_HasSSE2
SDL_HasAltiVec = sdl2.SDL_HasAltiVec

就像这样

; SDL_HasMMX = sdl2.SDL_HasMMX
; SDL_Has3DNow = sdl2.SDL_Has3DNow
; SDL_HasSSE = sdl2.SDL_HasSSE
; SDL_HasSSE2 = sdl2.SDL_HasSSE2
; SDL_HasAltiVec = sdl2.SDL_HasAltiVec

我们将提供我们自己的存根,因为我们必须自己调用这个API。现在让我们将注意力转移到dlc DLL代码上。我们将选择以下函数

59

让我们在IDA中打开EXE并找到这个函数

60

61

62

63

64

65

让我们看看代码

#include <Windows.h>

#pragma pack(push, 1)
typedef struct _Sorry            // 12 bytes of space needed
{
    struct
    {
        BYTE Force64bit;
        BYTE Opcode;
        INT64 Value;
    } MovToRax;                  // mov rax, func
    struct
    {
        BYTE Opcode;
        BYTE Reg;
    } JmpRax;                    // jmp rax
} Sorry;
#pragma pack(pop)

const unsigned long long DefExeBase = 0x140000000;
BYTE *g_ExeBase;

typedef int(*type_sdl2_api)();
type_sdl2_api sdl2_HasMMX;
type_sdl2_api sdl2_Has3DNow;
type_sdl2_api sdl2_HasSSE;
type_sdl2_api sdl2_HasSSE2;
type_sdl2_api sdl2_HasAltiVec;

void DLCInit()
{
    g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
    DLCBindGlobals();
    DLCBindFunctions();

    HMODULE sdl2 = LoadLibraryA("SDL2.dll");
    sdl2_HasMMX = (type_sdl2_api)GetProcAddress(sdl2, "SDL_HasMMX");
    sdl2_Has3DNow = (type_sdl2_api)GetProcAddress(sdl2, "SDL_Has3DNow");
    sdl2_HasSSE = (type_sdl2_api)GetProcAddress(sdl2, "SDL_HasSSE");
    sdl2_HasSSE2 = (type_sdl2_api)GetProcAddress(sdl2, "SDL_HasSSE2");
    sdl2_HasAltiVec = (type_sdl2_api)GetProcAddress(sdl2, "SDL_HasAltiVec");
}

void DLCBindGlobals()
{
    // we don't use globals in our function, so nothing here
}

void DLCReplaceFunction(BYTE *OldFunc, BYTE *NewFunc)
{
    Sorry *s = (Sorry*)OldFunc;
    s->MovToRax.Force64bit = 0x48;
    s->MovToRax.Opcode = 0xb8;
    s->MovToRax.Value = (INT64)NewFunc;
    s->JmpRax.Opcode = 0xff;
    s->JmpRax.Reg = 0xe0;
}

void DLCBindFunctions()
{
    DLCReplaceFunction(g_ExeBase + (0x1402E3F50 - DefExeBase), (BYTE*)sub_1402E3F50);
}

typedef enum
{
    CPUID_DEFAULT = 0x00002,
    CPUID_MMX = 0x00010,
    CPUID_3DNOW = 0x00020,
    CPUID_SSE = 0x00040,
    CPUID_SSE2 = 0x00080,
    CPUID_ALTIVEC = 0x00200,
};

extern "C"
{
    __declspec(dllexport) int SDL_HasMMX()
    {
        return sdl2_HasMMX();
    }

    __declspec(dllexport) int SDL_Has3DNow()
    {
        return sdl2_Has3DNow();
    }

    __declspec(dllexport) int SDL_HasSSE()
    {
        return sdl2_HasSSE();
    }

    __declspec(dllexport) int SDL_HasSSE2()
    {
        return sdl2_HasSSE2();
    }

    __declspec(dllexport) int SDL_HasAltiVec()
    {
        return sdl2_HasAltiVec();
    }
}

int sub_1402E3F50()
{
    int flags = CPUID_DEFAULT;

    if (SDL_HasMMX())
        flags |= CPUID_MMX;

    // DLC extra
    //if (SDL_Has3DNow())
        flags |= CPUID_3DNOW;

    if (SDL_HasSSE())
        flags |= CPUID_SSE;

    if (SDL_HasSSE2())
        flags |= CPUID_SSE2;

    // DLC extra
    //if (SDL_HasAltiVec())
        flags |= CPUID_ALTIVEC;

    return flags;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DLCInit();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

我的CPU不支持3DNowAltiVec,所以我稍微修改了函数。这个修改没有产生视觉效果(遗憾的是,游戏甚至没有崩溃),但对于我们的例子来说,足够了。此外,我们必须调用一些sdl2 API,这就是为什么我们立即遵循我们的想法,将模块到模块的接口表示为C代码。

这里有一个注意事项:如果您有一个32位程序并遵循相同的方案

def文件中注释函数

; Api = some_dll.Api

并将其放入代码中

extern "C"
{
    __declspec(dllexport) int Api(int param) { ... }
}

您将有一个具有__cdecl调用约定的Api函数。然而,有可能从32位DLL导出__stdcall函数而不进行任何名称修饰(使用def文件,而不是使用__declspec(dllexport))。您应该检查程序的反汇编,以查看它是__cdecl还是__stdcall。如果它是__stdcall,而您的函数是__cdecl,则可能会崩溃,因为您的函数没有从其参数中清理堆栈,而被检查的程序期望它会这样做。要解决这个问题,您需要这样做

def文件

Api = _Api@4

代码如下

extern "C"
{
        int __stdcall Api(int param) { ... }
}

在这种情况下,导出的名称没有修饰,并且函数是__stdcall,符合预期。

现在回到64位示例。让我们添加-debug-wait。这是start_0函数(start只是一个跳转到start_0,所以我们没有足够的空间来编写Sorry结构)

66

让我们看看它的地址

67

-debug-wait的实现与32位没有区别

typedef int(*Start)();    // __stdcall no longer needed

Start start_ptr;
Sorry start_sorry;

void DLCInit()
{
    char *_cmd = GetCommandLineA();
    std::string cmd(_cmd);
    std::string suffix("-debug-wait");
    std::size_t found = cmd.find(suffix);
    bool debug_wait = false;

    while (found != std::string::npos)
    {
        if (cmd.length() == (found + suffix.length()))
        {
            _cmd[found] = 0;  // probably we should hide this parameter from program
            debug_wait = true;
            break;
        }
        found = cmd.find(suffix, found + 1);
    }

    g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
    DLCBindGlobals();
    DLCBindFunctions();
    if (debug_wait) DLCWaitForDebugger();

    ...
}

void DLCWaitForDebugger()
{
        start_ptr = (Start)(g_ExeBase + (0x1403F59A0 - DefExeBase));
        start_sorry = *((Sorry*)start_ptr);
        DLCReplaceFunction((BYTE*)start_ptr, (BYTE*)sub_1403F59A0);
}

int sub_1403F59A0()
{
    *((Sorry*)start_ptr) = start_sorry;
    while (!IsDebuggerPresent()) Sleep(500);
    DebugBreak();
    return start_ptr();
}

现在让我们看看完整的代码

#include <Windows.h>
#include <string>

#pragma pack(push, 1)
typedef struct _Sorry            // 12 bytes of space needed
{
    struct
    {
        BYTE Force64bit;
        BYTE Opcode;
        INT64 Value;
    } MovToRax;                  // mov rax, func
    struct
    {
        BYTE Opcode;
        BYTE Reg;
    } JmpRax;                    // jmp rax
} Sorry;
#pragma pack(pop)

const unsigned long long DefExeBase = 0x140000000;
BYTE *g_ExeBase;

typedef int(*Start)(); // __stdcall no longer needed

Start start_ptr;
Sorry start_sorry;

typedef int(*type_sdl2_api)();
type_sdl2_api sdl2_HasMMX;
type_sdl2_api sdl2_Has3DNow;
type_sdl2_api sdl2_HasSSE;
type_sdl2_api sdl2_HasSSE2;
type_sdl2_api sdl2_HasAltiVec;

void DLCInit()
{
    char *_cmd = GetCommandLineA();
    std::string cmd(_cmd);
    std::string suffix("-debug-wait");
    std::size_t found = cmd.find(suffix);
    bool debug_wait = false;

    while (found != std::string::npos)
    {
        if (cmd.length() == (found + suffix.length()))
        {
            _cmd[found] = 0;  // probably we should hide this parameter from program
            debug_wait = true;
            break;
        }
        found = cmd.find(suffix, found + 1);
    }

    g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
    DLCBindGlobals();
    DLCBindFunctions();
    if (debug_wait) DLCWaitForDebugger();

    HMODULE sdl2 = LoadLibraryA("SDL2.dll");
    sdl2_HasMMX = (type_sdl2_api)GetProcAddress(sdl2, "SDL_HasMMX");
    sdl2_Has3DNow = (type_sdl2_api)GetProcAddress(sdl2, "SDL_Has3DNow");
    sdl2_HasSSE = (type_sdl2_api)GetProcAddress(sdl2, "SDL_HasSSE");
    sdl2_HasSSE2 = (type_sdl2_api)GetProcAddress(sdl2, "SDL_HasSSE2");
    sdl2_HasAltiVec = (type_sdl2_api)GetProcAddress(sdl2, "SDL_HasAltiVec");
}

void DLCBindGlobals()
{
    // we don't use globals in our function, so nothing here
}

void DLCReplaceFunction(BYTE *OldFunc, BYTE *NewFunc)
{
    Sorry *s = (Sorry*)OldFunc;
    s->MovToRax.Force64bit = 0x48;
    s->MovToRax.Opcode = 0xb8;
    s->MovToRax.Value = (INT64)NewFunc;
    s->JmpRax.Opcode = 0xff;
    s->JmpRax.Reg = 0xe0;
}

void DLCBindFunctions()
{
    DLCReplaceFunction(g_ExeBase + (0x1402E3F50 - DefExeBase), (BYTE*)sub_1402E3F50);
}

void DLCWaitForDebugger()
{
    start_ptr = (Start)(g_ExeBase + (0x1403F59A0 - DefExeBase));
    start_sorry = *((Sorry*)start_ptr);
    DLCReplaceFunction((BYTE*)start_ptr, (BYTE*)sub_1403F59A0);
}

int sub_1403F59A0()
{
    *((Sorry*)start_ptr) = start_sorry;
    while (!IsDebuggerPresent()) Sleep(500);
    DebugBreak();
    return start_ptr();
}

typedef enum
{
    CPUID_DEFAULT = 0x00002,
    CPUID_MMX = 0x00010,
    CPUID_3DNOW = 0x00020,
    CPUID_SSE = 0x00040,
    CPUID_SSE2 = 0x00080,
    CPUID_ALTIVEC = 0x00200,
};

extern "C"
{
    __declspec(dllexport) int SDL_HasMMX()
    {
        return sdl2_HasMMX();
    }

    __declspec(dllexport) int SDL_Has3DNow()
    {
        return sdl2_Has3DNow();
    }

    __declspec(dllexport) int SDL_HasSSE()
    {
        return sdl2_HasSSE();
    }

    __declspec(dllexport) int SDL_HasSSE2()
    {
        return sdl2_HasSSE2();
    }

    __declspec(dllexport) int SDL_HasAltiVec()
    {
        return sdl2_HasAltiVec();
    }
}

int sub_1402E3F50()
{
    int flags = CPUID_DEFAULT;

    if (SDL_HasMMX())
        flags |= CPUID_MMX;

    // DLC extra
    //if (SDL_Has3DNow())
        flags |= CPUID_3DNOW;

    if (SDL_HasSSE())
        flags |= CPUID_SSE;

    if (SDL_HasSSE2())
        flags |= CPUID_SSE2;

    // DLC extra
    //if (SDL_HasAltiVec())
        flags |= CPUID_ALTIVEC;

    return flags;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DLCInit();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

dlc DLL构建为64位,并将其复制到游戏的文件夹。现在,是时候看看文件偏移量了

68

69

创建dhewm3.exe的副本,并在十六进制编辑器中打开它

70

71

然后进行修补

72

73

现在我们可以运行我们修补后的doom 3并享受游戏。

结论

我们演示了在逆向工程过程中使用C编译器的概念。它很方便,因为我们可以编写真正的代码并进行测试,我们拥有真正编程语言的所有优势。我们可以为我们正在反汇编的程序的每个镜像都有一个dlc DLL(就像IDA数据库一样)。

我们应该提及如何处理小型函数。它们太小了,以至于我们无法在函数开头编写Sorry结构而不覆盖另一个函数或数据。嗯,如果函数很小,就很容易理解。我们仍然可以编写自己的版本,只是它不会被调用。

当然,如果镜像包含自修改代码,或者检查是否已被修补,或者其他反逆向技巧,事情很可能会失败。然而,我们在这里讨论的是逆向工程方法,而不是解决反调试技巧。

有些事情没有被涵盖。例如,数据导入的转发,以及C++之类的东西。我们无法涵盖所有内容,而且C++会使事情变得更加复杂。我倾向于使用C,并通过良好的程序设计(例如,将“”分开到单独的.c文件中)和良好的注释来弥补C++表达能力的不足。稍后,当我们有足够的信息,并且我们清楚地认为我们可以使用C++时,我们将利用C++的功能。

另一个重要的事情是:这种方法需要编写代码,并帮助您感觉像一个真正的程序员,而不仅仅是一个入侵者。我们可以一个接一个地将函数翻译成C,并逐渐改变“入侵者,在另一个家伙编写的恶意可执行文件中”的角色,变成“创造者,代码的作者”。比如:我当然知道它是如何工作的,我自己写的!

我们需要存储我们正在反汇编的程序的特定版本,以及我们的dlc DLL。这是因为我们的dlc DLL是为特定镜像而设计的。如果发生更新并且可执行文件发生更改(想想所有那些硬编码的函数偏移量),我们的dlc很可能会停止工作。所以我们需要有原始版本的程序,当发生更新时,我们也可以更新我们的dlc DLL以使其与新可执行文件一起工作。这样,我们可以慢慢地切入程序并保持更新。

我打算自己尝试这种方法。我想为这个旧的Hulk游戏做一些模组(例如,添加新的浩克攻击)。我小时候真的很喜欢它,能够粉碎一切是很酷的。

祝您在所有逆向工程的努力中一切顺利。愿C编译器的力量与您同在。感谢您的阅读。

历史

  • 2020年7月27日:初始版本
© . All rights reserved.