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






4.14/5 (7投票s)
本文介绍了如何利用真正的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
我们可以看到我们的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函数。
所以我们可以看到原始main函数的地址等于
0x411380
如果函数有另一个名称(没有地址),我们始终可以重命名它(删除其名称),或者打开函数信息窗口查看其地址。请注意,IDA假定默认的镜像基址。对于32位镜像,它等于
0x400000
因此,在初始自动分析期间,IDA会考虑到这个基址来自动生成函数/全局变量的名称。然而,IDA可能会在程序调试开始之前对其进行重定位。在这种情况下,所有自动生成的名称都会被重命名。在这种情况下,您需要将程序重定位回默认基址
Edit -> Segments -> Rebase program...
这是调试会话后函数的示例
现在让我们进行重定位到默认基址
所有函数名称都又正常了。
回到正题。要获取运行时函数地址,我们需要将函数的偏移量(从镜像基址开始,幸运的是它不会改变)添加到运行时镜像基址
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
并在十六进制编辑器中检查它
使用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结构定义,并且工具将无法正常工作。让我们运行它并查看我们需要修补的文件偏移量
所以代码节权限的文件偏移量等于
0x22C
我们将要导入的kernel32
DLL名称的文件偏移量等于
0x68A4
创建main.exe的副本,并在十六进制编辑器中打开它(我使用的是Hex Editor Neo)
您可以按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
如下所示
现在让我们将kernel32.dll更改为dlc.dll
最后,让我们运行我们修补后的main.exe
我们可以看到我们的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函数。
所以我们可以看到原始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
并在十六进制编辑器中检查它
让我们看看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位,否则它将无法正常工作。让我们看看文件偏移量
创建main.exe的副本,并在十六进制编辑器中打开它
以及修补后
运行我们修补后的main.exe。
我们可以看到我们的main函数被调用了,而不是旧的函数(它被我们的跳转覆盖了)。
更现实的例子
让我们看一个更复杂的例子。这是来自《绿巨人浩克》 - 2003年游戏(32位)的hulk.exe。转储导入
dumpbin /imports hulk.exe
我们可以看到我们的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。为了我们的目的,我们将选择一个直接创建游戏主窗口的函数。
我们需要收集此函数使用的所有全局变量/函数的列表。我们以与函数替换相同的方式计算运行时地址。之后,我们使用指针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的导入描述符和代码节权限。让我们看看文件偏移量
创建hulk.exe的副本,并在十六进制编辑器中打开它
现在运行修补后的程序。我们可以看到我们的更改(抱歉,我无法捕捉屏幕,即使使用Fraps)。很酷,我们可以不仅进行逆向工程,还可以修改游戏的行为,而无需触碰原始EXE(除了导入描述符和代码节权限)。它非常方便(moddy)。
还有一个问题。游戏以全屏模式运行,为了调试,我们至少需要看到调试器。我们想用IDA调试,并想在Visual Studio中调试我们自己的dlc
代码。并非所有游戏都提供窗口模式。那么我们该怎么办?
要使游戏窗口化 - 使用DxWnd
。我们可以告诉DxWnd
向程序传递额外的命令行参数,这样在dlc
DLL中,我们将知道是否应该等待调试器附加,或者像往常一样继续。
DxWnd
主窗口。
我们将添加到profiles
。第一个是normal
,第二个是debug
。这是normal
对于debug
配置文件,我们传递一个额外的命令行参数:-debug-wait
如果您在没有保存的情况下退出DxWnd
,系统会询问您是否要保存
现在让我们看看-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
函数(程序的入口点)感兴趣
我们将保存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时,由于某个内部错误它崩溃了)
加载各种DLL的符号可能需要一段时间。程序将命中debug break并挂起。在已替换函数的开头设置断点,然后继续执行。这是
现在我们在我们的dlc
中
有时,即使使用IDA 6.7,我也会遇到奇怪的错误行为。当我用DxWnd和-debug-wait
启动游戏时,游戏在CreateWindowExA
调用时退出了,没有明显的原因。此外,如果我在原始start
函数的开头设置断点,IDA会忽略它。在DxWnd
中,我删除了debug
配置文件,然后重新创建了它。事情又开始奏效了。所以您应该在这里多做一些实验,或者甚至改善我们的调试器附加体验。
现在让我们尝试Visual Studio调试。我们不应该在构建后更新dlc
DLL的源代码,换句话说,源代码和二进制文件应该匹配,否则我们就无法调试。设置断点
附加到进程
我们将命中debug break,继续进程。断点将被命中
现在,假设我们对hulk.exe和binkw32.dll之间的接口感兴趣。让我们以_RADSetMemory@8
API为例。当我们在其原型中弄清楚时,我们就不能再转发这个函数,而是提供我们自己的函数,该函数会调用原始函数。从IDA的反汇编中,我们可以看出这个函数接受两个函数指针,并且其返回值被忽略。
让我们注释掉模块定义文件中的这个函数
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
我们可以像调试任何其他代码一样调试它
为什么要费心提供这样的存根?这样,我们可以将反汇编的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
让我们坚持使用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代码上。我们将选择以下函数
让我们在IDA中打开EXE并找到这个函数
让我们看看代码
#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不支持3DNow
和AltiVec
,所以我稍微修改了函数。这个修改没有产生视觉效果(遗憾的是,游戏甚至没有崩溃),但对于我们的例子来说,足够了。此外,我们必须调用一些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
结构)
让我们看看它的地址
-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位,并将其复制到游戏的文件夹。现在,是时候看看文件偏移量了
创建dhewm3.exe的副本,并在十六进制编辑器中打开它
然后进行修补
现在我们可以运行我们修补后的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日:初始版本