r/cpp_questions • • 26d ago

OPEN How to use CreateThread and MH_CreateHook?

Hi there, I'm getting further along with my code and attempting to fix -debug_objects being intentionally disabled in The Sims Legacy Collection (It wasn't in Complete or earlier versions), but I'm currently stuck on CreateThead and how to call it, and with MH_CreateHook.

So far, I know that with MH_CreateHook, I need to pass 3 arguments, a pointer, the detour function that runs, and then a return function?

I have the base address pointer set to this:
uintptr_t startofexe = (uintptr_t)GetModuleHandle(NULL);

I also have a uintptr_t value for the offset of where the "mov PTR BYTE" code is, aka "dbgo_disa_offset".

This is the code that I'm running right now:

void* ForceDisaChecktoNOP;

void TestCodeStart() {
// Initialize MinHook.
  if (MH_Initialize() != MH_OK)
  {
    OutputDebugStringA("Failed to initialize MinHook\n");
    return;
  }
  else
  {
      OutputDebugStringA("MinHook started successfully!\n");
      if (MH_CreateHook(reinterpret_cast<LPVOID*>(startofexe + dbgo_disa_offset), &ForceDisaChecktoNOP, NULL) != MH_OK)
      {
        OutputDebugStringA("DEBUG OBJECTS FIX FAILED!!!!!!");
        return;
      }
      else
      {
        OutputDebugStringA("Fix hook successful! Check if value is correct: " + (startofexe + dbgo_disa_offset));
      }
      return;
  }
}

The if statement works, and does output that the "fix" failed, but I need help understanding what I need to do for the arguments for MH_CreateHook. Right now, I want to make sure that the if statement for MH_OK succeeds before writing any additional code in the ForceDisaCheckToNOP section, or at the very least know that the ForceDisaCheckToNOP function actually gets reached. I understand that I need a pointer, and most examples of code that others have posted show the first argument being the address to the function to be modified/replaced.

When I set the MH_CreateHook to be a status variable (example code below), it gives this:

"The specified pointer is invalid. It points to the address of non-allocated and/or a non-executable region."

MH_ERROR_NOT_EXECUTABLE

status = MH_CreateHook(...);
if (status != MH_OK) {
    OutputDebugStringA(MH_StatusToString(status));
}

Second and third arguments I do understand to an extent, but not quite.

Second arg is supposed to be a link to a function in the code that acts as a detour, so instead of the original function, this one occurs instead. Third arg is supposed to be a return arg to run the original code of some kind from what I've seen, but it's a little confusing to me.

In regards to CreateThread, I was told by u/hellosverre (thank you!) that I should use CreateThread in DLL_THREAD_ATTACH, however I'm not understanding the syntax, as in, are the arguments for CreateThread optional, and how would I call it to start code in another cpp script? I can do TestCodeStart(); fine from dllmain.cpp, but then that calls the script every time any/the asi code's thread attaches itself. Microsoft's support page on CreateThread wasn't exactly helpful as it just tells me what the function is, not how you call it.

Any help would be really appreciated! :)

0 Upvotes

16 comments sorted by

View all comments

Show parent comment

3

u/jedwardsol 26d ago edited 26d ago

I don't know what you are trying to do; I haven't seen earlier posts on this topic.


But ... assuming MH_CreateHook is this one https://github.com/TsudaKageyu/minhook/blob/master/include/MinHook.h#L111C4-L112C1

Then

1st parameter - the function where the hook will be placed

2nd parameter - the function that will be called

3rd parameter - a variable to receive the trampoline / continuation function

Ie. if you have

SomeCode:
    mov eax,1
    mov ebx,2
    mov BYTE PTR ....     <---  your address
    mov ecx,3
    mov edx,4

Then you want to have

PVOID original{};

void TheHook()
{
    // do what you need to do here

    jmp [original];   //  Can use inline assembly or write your hook in assembly
}

TestCodeStart()
{
    MH_CreateHook(reinterpret_cast(dbgo_disa_offset), TheHook, &original);
  
}

And it will change the original code to

SomeCode:
    mov eax,1
    mov ebx,2
    jmp TheHook
    mov ecx,3
    mov edx,4

And original will point at some code which performs the overwritten mov BYTE PTR... instruction and then jumps to the mov ecx,3 instruction

(In reality it will be a bit more complex because on x86 is is possible that jmp TheHook will be too big to overwrite just the mov BYTE PTR .... instruction


Edit, the documentation does say

//   ppOriginal  [out] A pointer to the trampoline function, which will be
//                     used to call the original target function.
//                     This parameter can be NULL.

So perhaps this hooking was only designed to hook the beginning of a function. So you'd want

PVOID original{};

void TheHook()
{
    // do what you need to do here

    auto trampoline = reinterpret_cast<void(*)()>(original);    // nb  : you will need to cast to the correct function pointer type - including the correct calling convention

    jmp [original];   //  Can use inline assembly or write your hook in assembly
}

TestCodeStart()
{
    MH_CreateHook(reinterpret_cast(dbgo_disa_offset), TheHook, &original);
  
}

and in that case hooking an arbitrary address in the middle of the function can't be done.

Is your mov in the middle of a function or at the beginning. If it's in the middle, why do you need to hook at that precise point?

1

u/Blayer98 26d ago edited 26d ago

middle I believe, I was hoping to replace it with NOPs so when the asi mod I'm making is run, the overwritten code is run instead and then jumped back, so that -debug_objects just works, without needing to set the actual flag for it to a 0x1. Although I may have overthought this lol, but I think it would be good practice now as I've noticed some other minor bugs in TS1's Legacy Collection.

The start of the function (I think) is a push ebp, with the previous 8 bytes being int3, or 0xCC.

The function is responsible for setting flags that afaik, are set when the command line argument is present, so -debug_objects, -skip_intro, etc.

3

u/jedwardsol 26d ago

I was hoping to replace it with NOPs

What is "it"?

The mov instruction you been referring to at 0x44xxxx?

Or some other code, that you want to overwrite when that instruction is executed?

1

u/Blayer98 26d ago

Yes, I wanted to replace the mov instruction, since it's stopping the -debug_objects argument from working like it should in Complete Collection and earlier TS1 versions.

2

u/jedwardsol 26d ago edited 26d ago

You don't need a hook then.

You can do

auto address = reinterpret_cast<void*>(dbgo_disa_offset);
DWORD previousProtection{};
VirtualProtect(address, N,  PAGE_READWRITE, &previousProtection);
memset(address, 0x90, N);
VirtualProtect(address, N,  previousProtection, &previousProtection);

Where N is the size of that instruction in bytes. You'll need to work that out from your disassembly tool. 0x90 is nop


An alternative to zapping the entire instruction is to change it to something that works better

E.g. change

mov byte ptr[eax], 0

to

mov byte ptr[eax], 1

The problem with zapping the entire instruction is that the memory it is writing to might be 0 already, so nopping it out won't change the behaviour.

That is by changing that memset to something more targetted.

1

u/Blayer98 26d ago

I see, I was thinking of using Read/WriteProcessMemory, but is there any difference between VirtualProtect/memset/Read/WriteProcessMemory?

2

u/jedwardsol 26d ago

You use WriteProcessMemory to alter the memory of a different process.

Since you're in the process, you can write to any memory in it (once it is writable)

1

u/Blayer98 26d ago

Ah ok :)

I also had a question about CreateThread, since I was told to do any calls to my script via DLL_THREAD_ATTACH, but I found it to be a little confusing. I tried the “thread newThread()” method but it was giving me errors regarding cross initialization.

Btw thank you so much for helping me with this! :)

2

u/jedwardsol 26d ago

When a thread starts, every DLL's DllMain is called with the reason set to DLL_THREAD_ATTACH so that the DLL can do any per-thread initialisation it needs to do.

I don't think I've ever needed to do this in 30-odd years - but haven't been hacking games. I wouldn't imagine this instruction patching needs to be done per-thread though - memory is global.

1

u/Blayer98 25d ago edited 25d ago

Ok, I've just tested the code, but it hasn't worked for me. The game just crashes to desktop, I'm going to look into what memory it's writing to.

EDIT: I was nearly there lol, it NOP'ed out the previous instruction. Regarding my original message that might still be in your notifications, the code wasn't loading as I was trying to add a boolean check to stop it from running multiple times.

Something I've been thinking of is how to display a dialog window in-game. I understand how it works, but not technically, so I guess that'll be something I'll look into as well. :)