Friday, February 13, 2009

Hooking Direct3D9 with C#.

So I wanted to hook Direct3D9 and override some of the calls with my own so I could either track the call itself or even change the incoming parameters or the returned results.  There are a number of uses for this type of thing ranging from profiling, cheating in 3D games to full screen monitor spanning.
This sort of thing has been done before in C++ many times and there are a number of sources of information and samples on how to do it such as over at GameDev.net.  However, I wanted to be able to write my code in C# both because I felt it was time to learn the language a little bit and I have worked with C++ enough to know that I don't really want to deal with all the headaches associated with such a low level language.
First off, I needed to be able to hook a Direct3D9 application with C#.  Luckily, there is a free (and extremely well documented) library available that does just that called EasyHook.  Getting it up and running was quite easy since it came with a sample application that did almost exactly what I wanted, it just hooked CreateFile in kernel32.dll instead of Direct3DCreate9 in d3d9.dll which was a simple change.
So now anytime Direct3DCreate9 was called by the hooked application instead of calling the real Direct3DCreate9 it would instead call my C# function, MyDirect3DCreate9.  Initially, MyDirect3DCreate9 would call the real Direct3DCreate9 (in d3d9.dll) and return whatever result the real one returned (as an IntPtr).
Of course , Direct3DCreate9 returns an IDirect3D9 interface which then does all the work.  At this point things get tricky because I wanted to be able to override the member funtions of IDirect3D9, not just the exported functions in d3d9.dll (of which there are only a few).  My initial thought was to use the COM interop services provided in the .NET framework and implement IDirect3D9 on my own.  My IDirect3D9 object could then instantiate a real IDirect3D9 object (from d3d9.dll) and call those functions as necessary.
Unfortunately, after much trial and error I was unable to get such a system working.  I think the main problem stems from the fact that Direct3DCreate9 needed to return an IDirect3D9 object to the calling native code (the hooked game) and it wasn't too happy getting a managed IDirect3D9 implementation instead.
Then I came across this site which, although a total hack, allowed me to do exactly what I wanted.  Basically, we know what the IDirect3D9 virtual function table has in it because we have the C++ header file d3d9.h, which defines exactly what the functions look like.  In order to fool the calling application into thinking it has a real IDirect3D9 object all we have to do is provide it with an object containing a virtual function table containing 17 members (as seen in d3d9.h) all of which have signitures that it is familiar with.
First things first we create a real IDirect3D9 object and take a look at it's virtual function table.  We can't modify the original table because it's protected by the OS but we can point the IDirect3D9 at a different virtual function table.  So what we do is create our own virtual function table in unprotected memory and copy all the function pointers from the original into it.  Then we tell IDirect3D9 to point at our custom table instead of the real one.
Now that we have a writable virtual function table we can then change some of those functions to point at our own C# functions instead.  Those C# functions can then call into the original function table when we want to call the real IDirect3D9 functions!
Here are the DllImport statements for calling into kernel32 (for memory management) and d3d9.  You will notice that I have also created an IDirect3D9 structure in C# that has a signature that matches the native IDirect3D9 signature (which only has an array of function pointers).  This will allow me to receive an IDirect3D9 object from native code and get to the virtual functions from managed code.
Here is my custom IDirect3D9 class.  It's not really an IDirect3D9 implementation but instead it's a container where I keep all my IDirect3D function overrides, store the native IDirect3D9 object and manage the virtual function table.
And here is my injection code, (some of the remoting stuff removed for brevity).

11 comments:

  1. Thanks for sharing this, Micah.

    I've got what you posted working with EasyHook. I'm using the CreateAndInject approach, and everything works up until the point where EasyHook.RemoteHooking.WakeUpProcess() is called within my injection dll. When WakeUpProcess() gets called, the host DirectX application crashes. I traced this to EasyHook32.dll, where it calls RtlInstallService() in DllImport.cs.

    To make sure the EasyHook stuff is working, I went ahead and implemented a proper IDirect3D9 interface in C# by porting d3d9.h. This approach works when I return my full IDirect3D9 interface from Direct3DCreate9(), but of course the DirectX calls are not hooked.

    Couple questions:

    1. Did you experience this (crash) issue?

    2. Did you have to implement the entire IDirect3D9 interface, or was your "IntPtr* VFTable" approach sufficient?

    Regards,
    Alex

    ReplyDelete
  2. I didn't experience the crash you are referring to.

    If you hook Direct3DCreate9 and have your hooked function just call the real one and return the IntPtr the real one returns, do you still experience the crash?

    I also initially ported IDirect3D9 over to C# at first but as you said, you can't return your custom IDirect3D9 object because it's managed and the hooked application expects an unmanaged IDirect3D9 object.

    By typecasting the IntPtr that Direct3DCreate9 returns to a C# structure with an IntPtr* was the only way I could figure out to successfully gain access to the native IDirect3D9 structure through managed code.

    To note, I have since dropped this method of doing things because the native > managed > native transition was too expensive for use in a real-time gaming application (frame rate dropped from ~60fps to ~40fps just calling one of my functions every frame. Depending on your needs, this may not be an issue but for my needs it caused problems so I'm trying a managed C++ approach now. To be fair, the C# code wasn't the problem specifically, it was transitioning between managed and unmanaged code very often that appeared to cause the slowdown. Supposedly the transition between managed C++ and unmanaged C++ is faster (though still has an overhead).

    ReplyDelete
  3. Took your advice, returned the IntPtr the real one returns, and it worked - no crashing. The issue was the DirectX application I'm hooking calls Direct3DCreate9() three times. I was able to sort out which one to hook and got everything going.

    Yeah, I was worried about the native/managed marshalling frame rate hit. Sounds like we're attempting the same thing: grab the frame buffer during game play and encode/save/do whatever to it. I've already written an unmanaged C++ implementation doing the same thing, so I may take your advice and use managed C++ instead of C#.

    Thanks again.

    Regards,
    Alex

    ReplyDelete
  4. Hi Micah, did you have any experience with hook EndScene in IDirect3DDevice9? do you think it is possible to start from your work? I

    ReplyDelete
  5. I did end up hooking EndScene at one point in order to test all my work. When the user called EndScene I would draw a box in the scene before calling the real EndScene.

    While this all works, the overhead of transitioning between managed and native code is quite high making it unrealistic for most applications. :/

    ReplyDelete
  6. Oh, I understand. However do you have any examples? I am stuck with CreateDevice, i don't know exactly how to procede.

    Regards,
    Luca

    ReplyDelete
  7. You should hook CreateDevice the same way that I have hooked GetAdapterCount in the example. Once you have done that, you want to call the original CreateDevice to get a pointer to an IDirect3DDevice9 object. Then you want to build a new class in C# that is similar to the IDirect3D9 class that I have in the example, except yours will be IDirect3DDevice9. In your IDirect3D9::CreateDevice function you will want to instantiate your IDirect3DDevice9 and pass in to the constructor the native IDirect3DDevice9 that you got by calling the native CreateDevice.

    Basically you want to do all the same things that I have done with IDirect3D9, except you will do it with IDirect3DDevice9.

    ReplyDelete
  8. Thank you very much Micah, I will try to follow up your instructions.

    Have a nice day,
    Luca

    ReplyDelete
  9. Hi Micah,

    I am having trouble overriding the functions. I am able too inject my DLL and I am using your code to override GetAdapterCount() and CreateDevice() but they are never called after that and I have no idea why. I am logging all the functions and my last log is from the OverrideFunctions() method. There is no log from GetAdapterCount() or CreateDevice(). Did you experience such behavior? Can you please assist me on that?

    Please take a look at the code from my DLL.
    http://pastebin.com/m142a0101
    http://pastebin.com/da70d7d2
    http://pastebin.com/d79babbdb

    Do you think I am doing s.th. wrong?

    Steven

    ReplyDelete
  10. Hello!

    Are there any way to hook up to already running application, when you cannot use Direct3DCreate9 method? I know, that FRAPS and some other programs do just that.

    ReplyDelete
  11. IntPtr Direct3DCreate9_Hooked(uint SDKVersion){
    IntPtr id3dPtr = Direct3DCreate9(SDKVersion);

    if (id3dPtr != API.InvalidHandle)
    {
    Direct3D.IDirect3D9 id3d = (Direct3D.IDirect3D9)Marshal.GetObjectForIUnknown(id3dPtr);
    this.hookedD3D9 = new HookedIDirect3D9(this, id3d);

    return Marshal.GetComInterfaceForObject( hookedD3D9, typeof(Direct3D.IDirect3D9));
    }
    else
    Log("Failed to create Direct3D9 interface.");
    return id3dPtr;
    }

    After much trial and error that is how I was able to pass the managed object back to the native code without it knowing. If you run into any errors it is most likely that your implementation of the interface is not correct (parameters are wrong types or missing some MarshalAs's)

    ReplyDelete