Articles How to Use C Compiler for Reverse Engineering (pratically a hacker lesson)

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
How to Use C Compiler for Reverse Engineering
Member 13737597 - 27/Jul/2020
[SHOWTOGROUPS=4,20,22]

This article shows how to bring the power of real C compiler for reverse engineering purposes.
Make your reverse engineering effort successful. Switch to real C language, say no to pseudocode.
Introduction
We can use IDA to examine assembly code, and translate disassembly to some pseude C code by hand. We can even use HexRays decompiler. However, no matter how good pseudo code is, it is still pseudocode. It cannot be compiled and tested. The chances of errors and ambiguities increase with amount of pseudocode, and we can finally get lost.
I thought about another method of reverse engineering. We will still use IDA to inspect disassembly, however we will translate disassembly to real C code on function basis, and force inspected program to use our code instead. We will accumulate our code in a separate DLL, just like IDA's database for inspected executable. This way, we will have solid ground of C (type system, clear names, function prototypes). We can write something that actually builds, and we can run the program to see if we got it right. If we made errors, the program will probably crash.
As we advance through reverse engineering process, we can translate more functions, rename struct fields, functions, variables in our C code, when their purpose becomes clear. Also we can make changes to IDA's database to keep things synced. I think it increases our chances to achieve our reverse engineering goals, especially for large programs, that contain thousands of functions.
Let's See the Idea
I think it's preferable to run program as usual, without any tricks, like CreateProcess with suspended flag, DLL injection, etc. The idea is to create dlc DLL for each image (EXE, DLL) we are interested in. This dlc DLL will be loaded together (right after) with image, and will replace original functions with jumps to our own functions (inside dlc DLL). How can we force dlc DLL loading? That's simple, we need to modify image's import descriptor (for real-world programs, each image will always import functions from at least one module). Also, we need to make sure that image's code section has write permission.
Let's demonstrate simple 32-bit example. It will be empty console application (it just returns number).
main.c:
Hide Copy Code
int main(int argc, char *argv[])
{
return 0x10203040; // to find function in IDA disassembly
}
Compile EXE image and see its imports:

dumpbin /imports main.exe

1596129736806.png

1596129747077.png
[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]

We can see that our EXE imports functions from kernel32.dll. So we need to build dlc DLL that will export all these functions and forward them to kernel32.dll. Create an empty DLL project, and add module definition file with all required functions. It's easier to redirect dumpbin output to file:

dumpbin /imports main.exe > main.txt
so you can easily copy functions to module definition file. In my case, def file looks like this:

Hide Copy Code
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
Add our own main function:

int main(int argc, char *argv[])
{
printf("You forgot Hello World!\n");
getchar();
return 0x10203040;
}
Now open main.exe with IDA. We need to get address of original main function, so we know at what address we should insert jump to our own main.

1596129807089.png

1596129838660.png


So we can see that address of original main function is equal to:

0x411380
If function has another name (without address), we can always rename it (delete its name), or open function info window to see its address. Note that IDA assumes default image base. For 32-bit images, it is equal to:

0x400000
So during initial autoanalysis, IDA autogenerates functions/global variables names with this base in mind. However IDA may rebase the program later, right before program debugging starts. In this case, all autogenerated names will be renamed. In this case, you need to rebase program back to default base address:

Edit -> Segments -> Rebase program...
Here is an example of function after debugging session:

1596129886275.png
[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]
Now let's do rebase to default base address:

1596129936127.png 1596129943753.png

1596129952426.png


And all function names are ok again.

Back to the point. To get runtime function address, we need to add offset of the function (from the beginning of image base, luckily it doesn't change) to runtime image base:

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

runtime function address = GetModuleHandle(NULL) + (0x411380 - 0x400000)
In our dlc DLL, we will have:

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
}
We will call DLCInit inside DllMain (on DLL_PROCESS_ATTACH event). What is this DLCReplaceFunction ? Let's see how to make jump from original function to our own function. To achieve this, we will write the following structure at the beginning of the original function:

#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)
We push the address of our own function on the stack and pop it with retn. It is so called "backward function call". To encode it, we need only 6 bytes, all registers are preserved, and we use direct address. So why not? Now the DLCReplaceFunction:

void DLCReplaceFunction(BYTE *OldFunc, BYTE *NewFunc)
{
Sorry *s = (Sorry*)OldFunc;
s->Push.Opcode = 0x68;
s->Push.Value = (INT)NewFunc;
s->Ret.Opcode = 0xC3;
}
We overwrite the original function start with this structure. You can see that we have some x86 opcode values here. How do we know all these in the first place? The best way to find out opcode bytes needed is to use Nasm. Write down instructions you are interested in:

op.asm:

[BITS 32]

push 0x10203040 ; to clearly see the value in assembled code
retn
Compile raw binary:

nasm -f bin op.asm

1596130005945.png

And examine it in hex editor:

1596130019165.png
[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]

It's more conveninent to use Nasm, no need to refer to opcode tables. Also, error is less likely.

Let's see dlc DLL full code:

#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;
}
Now build dlc DLL, and copy it to main.exe's folder. Next step is to modify main.exe's import descriptor (we decided to abuse import descriptor for kernel32.dll) and code section permissions (by default, code section has only execute/read permissions). We will use a small utility from this stackoverflow question:
We will make small modifications to get information we need:

#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 = (PCHAR)((DWORD_PTR)virtualpointer +
Rva2Offset(pImportDescriptor->Name, pSech, ntheaders));
printf("%s\n", libname);

//>>>>>> Add this block
if (!_stricmp(libname, "kernel32.dll")) // case insensitive compare
{
UINT d = libname - (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);
}

[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]
Since we are digging into 32-bit executable, we should build this tool as 32-bit also. Otherwise, 64-bit PE structure definitions will be used, and tool won't work correctly. Let's run it and see file offsets we will need to patch:

1596130122079.png

1596130128814.png


So the file offset of code section permissions is equal to:

0x22C
and file offset of kernel32 DLL name we are going to import from is equal to:

0x68A4
Create a copy of main.exe and open it in hex editor (I use hex editor Neo):

1596130150607.png
[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]
1596130234864.png


You can press Ctrl + g, choose absolute offset, enter required offset value, and you are at the right time, at the right place. We need to set write flag in code section characteristics. See IMAGE_SECTION_HEADER struct and Characteristics flags here:
Characteristics is 4-byte DWORD field. We see that it contains the following bytes:

20 00 00 60
We have little endian byte order, so the value is:

0x60000020
In human readable form:

0x40000000 | 0x20000000 | 0x00000020

IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_CNT_CODE
We will add IMAGE_SCN_MEM_WRITE:

0x80000000 | 0x40000000 | 0x20000000 | 0x00000020

IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_CNT_CODE
So we need this value:

0xE0000020
And these bytes:

20 00 00 E0
Here it is:

1596130287378.png

Now let's change kernel32.dll to dlc.dll:

1596130324741.png

And finally, let's run our patched main.exe:

1596130351602.png
[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]

We can see that our main function gets called, not the old one (it is overwritten with our jump).

Now let's repeat the same example, only for 64-bit. Compile main as 64-bit, and dump imports. For me, the list of imported functions didn't change (well, it shouldn't), so module definition file of our dlc DLL remains the same.

Now back to IDA. I want to mention that later versions of IDA are 64-bit, so we can debug 64-bit applications natively. I know 100% that IDA 6.7 is still 32-bit, and IDA 7.2 is already 64-bit. Don't know the exact version of IDA when this significant change occured.

Open main.exe with IDA. We need to get the address of original main function, so we know at what address we should insert jump to our own main.

b0.jpg


b1.jpg



So we can see that address of original main function is equal to:

0x140001010
If function has another name (without address), we can always rename it (delete its name), or open function info window to see its address. Note that IDA assumes default image base. For 64-bit images, it is equal to:

0x140000000
In our dlc DLL, we will have:

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
}
We will call DLCInit inside DllMain (on DLL_PROCESS_ATTACH event). What is this DLCReplaceFunction ? Let's see how to make jump from original function to our own function. To achieve this, we will write the following structure at the beginning of the original function:

#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)


We move the address of our own function in rax register and jump through it. To encode it, we need 12 bytes, all registers are preserved save for rax, and we use direct address. So why not? Now the DLCReplaceFunction:

Hide Copy Code
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;
}
We overwrite the original function start with this structure. You can see that we have some x64 opcode values here. Write down instructions you are interested in:

op.asm:

[BITS 64]

mov rax, 0x1020304050607080 ; to clearly see the value in assembled code
jmp rax
Compile raw binary:

nasm -f bin op.asm

1596130553613.png

And examine it in hex editor:

b10.jpg

[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]

Let's see dlc DLL full code:

Hide Shrink
arrow-up-16.png
Copy Code
#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;
}
Now build dlc DLL as 64-bit, and copy it to main.exe's folder. The next step is to modify main.exe's import descriptor and code section permissions. In our tool, replace fine name if needed. Since we are digging into 64-bit executable now, we need to build it as 64-bit also, otherwise, it will not work correctly. Let's see file offsets:

b2.jpg


b3.jpg


Make a copy of main.exe and open it in hex editor:

b4.jpg


b5.jpg

[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]
And after patching:

b6.jpg


b7.jpg


Run our patched main.exe.

b8.jpg



We can see that our main function gets called, not the old one (it is overwritten with our jump).

More Realistic Examples
Let's see a more complex example. It will be hulk.exe from The Hulk - 2003 game (32-bit). Dump imports:

dumpbin /imports hulk.exe

c0.jpg


c1.jpg



We can see that our EXE imports functions from binkw32.dll. It's more interesting than standard kernel32.dll, so we will stick to it. So we need to build dlc DLL that will export all these functions and forward them to binkw32.dll. Create empty DLL project, and add the following module definition file:

Hide Copy Code
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
Note that despite the fact that binkw32.dll is not "standard" DLL (Windows provided, with import lib file to link agains), we don't need to create lib file for it to make it work. It is probably due to decorated exported function names (__declspec(dllexport) __stdcall, can be found only in 32-bit images). If we had binkw32.dll with exported function names without decoration, just like this:

BinkSetPan

[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]

we would have to create lib file for it, otherwise dlc DLL build would have failed. If you have this case, wait untill 64-bit example (immediately follows 32-bit). There, you will see how to create import lib file nof not "standard" DLL (the process is the same for 32-bit and 64-bit).

So back to the point. Open hulk.exe in IDA. For our purposes, we will pick a straighforward function that creates game's main window.

c2.jpg


c3.jpg



We need to collect list of all global variables/functions used in this function. We compute runtime address in the same way as we did for function replacement. After this, we bind to address with pointer. Let's bind to all required items:

Hide Copy Code
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));
}
And replace function itself:

Hide Copy Code
void DLCBindFunctions()
{
DLCReplaceFunction(g_ExeBase + (0x46E200 - DefExeBase), (BYTE*)sub_46E200);
}
Initialization:

Hide Copy Code
void DLCInit()
{
g_ExeBase = (BYTE*)GetModuleHandleA(NULL);
DLCBindGlobals();
DLCBindFunctions();
}
Now let's write our own version of function. To see the effect, we will make a small modification:

Hide Shrink
arrow-up-16.png
Copy Code
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);
}
We changed window's text, added sleep to actually see it, and removed nasty AdjustWindowRect call. AdjustWindowRect screwed something and window was not fully full screen. Now everything is as it should be.


[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]

As you can see, we can't access global variables directly, only through pointer. Also note, that if we are too lazy to translate code branches, that we know don't get called (we can check it while debugging in IDA), we can get away with ExitProcess call. This way, the process will terminate if unimplemented code branch will somehow get called.

Full dlc DLL code:

Hide Shrink
arrow-up-16.png
Copy Code
#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;
}
Now build dlc DLL, and copy it to game's folder. Next step is to modify hulk.exe's import descriptor and code section permissions. Let's see file offsets:

c4.jpg


c5.jpg


Make a copy of hulk.exe and open it with hex editor:

c6.jpg


c7.jpg


c8.jpg


c9.jpg



Now run the patched program. We can see our changes (sorry, I couldn't capture the screen as it is, even with Fraps). Cool, we can not only reverse, but also modify game's behavior, without touching the original EXE (save for import descriptor and code section permissions). It's pretty handy (moddy).

There is one more question left. Games run full screen, and to debug, we need to see debugger at least. We want to debug with IDA, and to debug our own dlc code in Visual Studio. Not all games provide windowed mode. So what can we do about it?

To make game windowed - use DxWnd. We can tell DxWnd to pass additional command line argument to the program, this way in dlc DLL, we will know whether we should wait for debugger attach or proceed as usual.

DxWnd main window.

c10.jpg

[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]
We will add to profiles. First one will be normal, and second one will be debug. Here is normal:

c11.jpg
c12.jpg


For debug profile, we pass additional command line parameter: -debug-wait

c13.jpg
c14.jpg


If you exit DxWnd without save, you will be asked whether you want to save:

c15.jpg



Now let's see how -debug-wait parameter is actually implemented. First, we check whether we were started with -debug-wait paramter. If we have it, we call DLCWaitForDebugger.

Hide Copy Code
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();
}
Here, we check whether command line string ends with -debug-wait. Also we cut it off by inserting null character in buffer, in case inspected program has something to say about unknown parameters.

Now, we are interested in start function (program's entry point):

f0.jpg


f1.jpg

[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]

We will save bytes at the beginning of start function, and overwrite it with jump to our custom function. Custom function will restore original bytes, and will wait for debugger in loop. After debugger attach, we will issue debug break, and will call start ourselves. Let's see the code:

Hide Copy Code
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();
}
We have some kind of "one-shot" trampoline. Now let's see all together:

Hide Shrink
arrow-up-16.png
Copy Code
#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;
}
Let's try to start game in DxWnd with -debug-wait, and attach with IDA (when I used IDA 6.7 it worked, when I tried IDA 7.2, it crashed due to some internal error):

c17.jpg

[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]
It can take a while to load symbols for various DLLs. The program will hit debug break and suspend. Put breakpoint at the beginning of function we have replaced, and continue execution. Here it is:

f2.jpg


f3.jpg


Now we are inside our dlc:

f4.jpg


c22.jpg



Sometimes, I got strange buggy behavior, even with IDA 6.7. When I started the game with DxWnd with -debug-wait, the game exited on CreateWindowExA call, for no obvious reason. Also, if I set breakpoint at the beginning of original start function, it was ignored by IDA. In DxWnd, I deleted debug profile, and created it again. Things started to work again. So you should experiment a little here, or maybe even improve our debugger attachment experience.

Now let's try Visual Studio debugging. We should not update dlc DLL source after its build, in other words source and binary shall match, otherwise, we won't be able to debug. Set breakpoint:

c23.jpg

[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20,22]
Attach to process:

c24.jpg


We will hit debug break, continue process. Breakpoint will be hit:

c25.jpg


Now let's say we are interested in interface between hulk.exe and binkw32.dll. Let's take _RADSetMemory@8 API. When we made out its prototype, we can no longer forward this function, but provide our own, that calls original one. From IDA's disassembly, we can make out that this function takes two function pointers and its return value is ignored.

f5.jpg

[/SHOWTOGROUPS]