For any developers wondering how I did this, here's the code I wrote to hook into natives. Well actually the following code is an enhanced version, cause the .dll I released doesn't call back to the original native (but always returns something != 0). However that doesn't change anything in terms of functionality, so I won't release a new version of the compiled dll.
Code:
#define MAX_HOOKS 1000
typedef struct _HOOK_INFO
{
ULONG_PTR Function;
ULONG_PTR Hook;
ULONG_PTR OrigBytes;
} HOOK_INFO, *PHOOK_INFO;
HOOK_INFO HookInfo[MAX_HOOKS];
UINT NumberOfHooks = 0;
BYTE *pOrigBytesBuffer = NULL;
HOOK_INFO *GetHookInfoFromFunction(ULONG_PTR OriginalFunction)
{
if (NumberOfHooks == 0)
return NULL;
for (UINT x = 0; x < NumberOfHooks; x++)
{
if (HookInfo[x].Function == OriginalFunction)
return &HookInfo[x];
}
return NULL;
}
void WriteJump(void *pAddress, ULONG_PTR JumpTo)
{
DWORD dwOldProtect = 0;
VirtualProtect(pAddress, 14, PAGE_EXECUTE_READWRITE, &dwOldProtect);
BYTE *pCur = (BYTE *)pAddress;
*pCur = 0xff; // jmp [rip+addr]
*(++pCur) = 0x25;
*((DWORD *) ++pCur) = 0; // addr = 0
pCur += sizeof(DWORD);
*((ULONG_PTR *)pCur) = JumpTo;
DWORD dwBuf = 0;
VirtualProtect(pAddress, 14, dwOldProtect, &dwBuf);
}
void *BackupOrigBytes(ULONG_PTR originalFunction)
{
if (pOrigBytesBuffer == NULL) {
pOrigBytesBuffer = (BYTE *)VirtualAlloc(NULL, MAX_HOOKS * 14, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
}
VOID *pOrigBytes = (VOID *)&pOrigBytesBuffer[NumberOfHooks * 14];
memcpy(&pOrigBytesBuffer[NumberOfHooks * 14], (void *)originalFunction, 14);
return pOrigBytes;
}
void hookNative(UINT64 hash, ULONG_PTR hookFunction = NULL)
{
auto originalFunction = GetNativeHandler(hash);
HOOK_INFO *hinfo = GetHookInfoFromFunction((ULONG_PTR) originalFunction);
if (hinfo) {
WriteJump(originalFunction, hinfo->Hook);
}
else
{
if (NumberOfHooks == (MAX_HOOKS - 1))
return;
VOID *pOrigBytes = BackupOrigBytes((ULONG_PTR)originalFunction);
HookInfo[NumberOfHooks].Function = (ULONG_PTR)originalFunction;
HookInfo[NumberOfHooks].OrigBytes = (ULONG_PTR)pOrigBytes;
HookInfo[NumberOfHooks].Hook = hookFunction;
NumberOfHooks++;
WriteJump(originalFunction, hookFunction);
}
}
void unhookNative(UINT64 hash) {
auto originalFunction = GetNativeHandler(hash);
HOOK_INFO *hinfo = GetHookInfoFromFunction((ULONG_PTR)originalFunction);
DWORD dwOldProtect = 0;
VirtualProtect(originalFunction, 14, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy((void *)hinfo->Function, (void *)hinfo->OrigBytes, 14);
DWORD dwBuf = 0;
VirtualProtect(originalFunction, 14, dwOldProtect, &dwBuf);
}
void* __cdecl MY_IS_DLC_PRESENT(NativeContext *cxt) {
Hash DlcHash = cxt->GetArgument<Hash>(0);
if (DlcHash == 2532323046) { // DEV
// game game requested dev dlc -> return true;
cxt->SetResult(0, true);
return cxt;
}
// restore original function
unhookNative(0x2D6859674806FDCE);
// get result of original function
BOOL result = DLC2::IS_DLC_PRESENT(cxt->GetArgument<Hash>(0));
cxt->SetResult(0, result);
// hook us up again
hookNative(0x2D6859674806FDCE);
return cxt;
}
void HookNatives() {
hookNative(0x2D6859674806FDCE, (ULONG_PTR) &MY_IS_DLC_PRESENT);
}
Adding this code in a new cpp to m0d-s0beit-v Redux and calling HookNatives() in dllmain actually makes the client think you are a developer (Dev T-Shirts, SCTV and something in the Creator). The game just checks for a specific DLC to set the dev flag.
I got many ideas from Daniel Pistelli's NtHookEngine, but decided to leave out the disassambly and bridge/trampoline part and instead just create a simple unhook/hook. So my code isn't thread-safe and theoretically another thread could call the native when it's unhooked/rehooked to call the original function, resulting in an unhooked call. However, my tests showed that only one thread is calling the natives anyway.
Should work for any native this way! Arguments may need to be fetched in reversed order for functions with multiple parameters. Didn't try yet though.
Obviously there aren't many error checks and you probably want to create a new thread that's waiting for the natives to be ready and checks the result of GetNativeHandler before blindly writing into GTA's memory to avoid game crashes when injecting prior to the game menu. But I just wrote that code quick&dirty when I had a bit of spare time at work yesterday + today. So feel free to make this bulletproof, use for your menu bases and build new interesting features using hooked natives!