Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

blueshogun96

macrumors regular
Original poster
Nov 24, 2012
112
3
Although after looking at a few examples of using mach_inject, it seems rather straight foward. But, unfortunately, I can't figure out why the thread injection causes the process being injected to causes a crash.

What am I doing? I'm writing a program that hooks APIs (in existing processes) that utilize the GPU (OpenGL, Metal, CUDA, OpenCL, etc.) and report to the user on-screen or via Remotery (browser) w/ live updates on things such as GPU usage, FPS, frame time, and so on similar to what Fraps and GPU-z does. On macOS, getting the GPU stats in general is kind of easy if you know what you are searching for but API hooking is kind of a challenge so far.

Now, I already know that you need root access for this to work, which is not really the issue. I am also aware that when using mach_inject, you have to have the right CPU arch matching the target process you are injecting the thread into. Many times have I double checked this. What my program does specifically is basically what Fraps for windows does internally, check every accessible process and see if it uses any API that utilizes the GPU. That being said, I intentionally created two daemon processes to check for both 32-bit (when applicable/supported) and 64-bit processes.

Let me share with you some code as well as some example output:

This is located in a dylib and not the actual app.
Code:
extern "C" __attribute__((visibility("default"))) void mac_hook_thread_entry( ptrdiff_t code_offset, void* param_block, size_t param_size, void* dummy_pthread_data );

void mac_hook_thread_entry( ptrdiff_t code_offset, void* param_block, size_t param_size, void* dummy_pthread_data )
{
    PTHREAD_SET_SELF( dummy_pthread_data );
 
    /* The code would go here, but I didn't execute anything as a test */

    thread_suspend( mach_task_self() );
}

After loading the dylib manually, the function pointer is called like this from the actual cocoa app (and yes, I did double check programatically to ensure the process matches the CPU arch targeted):

Code:
bool perform_hooks( DWORD pid )
{
    if( pid == 0 )
        return false;
   
#ifdef __APPLE__
    mach_error_t err = mach_inject( (mach_inject_entry) mac_hook_thread_entry, NULL, 0, (pid_t) pid, 0 );
    if( err != err_none )
        return false;
#endif
   
    return true;
}

I didn't pass in any parameters because I didn't have anything to add as of yet. Just adding code bit by bit. So, this is the result:

Screen Shot 2020-04-30 at 4.52.44 PM.png


Typically when disassembly starts showing you assembly instructions that keep translating to add al, XXX then you've set the EIP/RIP somewhere into rando land. That's bad but I can't really guarantee that's it because I commented out the other two functions and when the thread entry returns, it returns to the invalid address 0xDEADBEA7DAD (or 0xDEADBEEF for 32-bit) so it does execute for sure, we know that, hence the need to suspend the thread if it reaches the end of the function. Now, I'm sure the problem lies within _pthread_set_self(), but I've never actually seen this function before so I'm not entirely sure what it does minus set the necessary thread data (the open source pages are not easily navigated IMO). Not calling it will cause other functions to crash right away.

I did manage to find a thread on github for osxinj about the same thing, only the OP managed to get it working in and outside of Xcode. Link here: https://github.com/scen/osxinj/issues/3

Actually there's a fundamental issue with the bootstrap function as when it's loaded into the osxinj process the symbol stubs are actually resolved in the osxinj process space NOT in the injected process space. So basically you have the bootstrap function that's calling osxinj's _pthread_set_self which is NOT the same address as the one in the injected process so you get a EXC_BAD_ACCESS. Now I think that Xcode uses a debug friendly version of the libraries and caches them into a big file so the __pthread_set_self address stays the same even in different processes. That's probably why it works in Xcode and not outside of it, basically because of os's ASLR. I made an assembly version of bootstrap where you find two necessary functions' (_pthread_set_self and dlopen) address inside the injected process space and pass them in the registers to the bootstrap function where you initialize the thread using _pthread_set_self and just load the payload using os's dlopen after that you also need to suspend the thread which I did using a syscall.

So I'm not entirely sure what to do here. On that github repo, there's an example on the website and I got it working without any problems. The big difference is that I'm calling mach_inject from a cocoa app, not a terminal program. Later I'll write a test program from scratch and see if that is what makes the big difference.

If you want to see my project's source code (GPL v2), feel free. https://github.com/ShogunateTM/GpuMonEx/

Any ideas? Thanks.

Shogun
 
  • Like
Reactions: casperes1996
Okay, so far I can confirm that it works outside of Xcode when I manually execute the app via terminal w/ the sudo command. If the target process was executed by Xcode w/ the debugger running, I can still successfully inject the thread without any problems. But any crashes within the injected thread won't show up as C/C++ code obviously, which is better than nothing so far.

One thing I did wrong was use mach_task_self() to suspend the thread, that was a typo, the correct code is:
Code:
thread_suspend( mach_thread_self() );
Because letting the thread return will cause a crash at a pre-determined invalid EIP/RIP.

Both 32 and 64 bit seem to work fine when not being debugged through Xcode, but there has to be a way to debug this somehow, right? It could be because I'm using an older version of Xcode and macOS (8.2.1 and 10.11.6, respectively, on a 2006 Mac Pro), and I am in dire need of a new machine.

Shogun.
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.