Skip to content

Inner Workings

thexappy edited this page Jun 3, 2024 · 1 revision

RemoteNET's rich API is only possible because of neat tricks I learned about .NET and the WinAPI.
This page tries to summarize some of the tricks so they can, hopefully, be studied and improved/copied into other projects.

ClrMD: Searching objects in memory, accessing their fields.

ClrMD (or Microsoft.Diagnostics.Runtime) is library by Microsoft. It's a really powerfull peice of software.
It allows one process to dump another process's memory and then parse low-level .NET structures of the heap and the classes system.
Appearently, while discouraged, a process can also do this kind of inspection to itself. I was made aware of this library and its abilities because of KeeFarce.
They use it to search some critical objects in KeePass and then use them to dump the target's credentials.

AFAIK, ClrMD by itself does not attach itself to the target as a debugger.
Yet the kind of information it can fetch is usually accessed by debuggers. For example WinDBG.

Invoking Intance Methods

While ClrMD is really powerful and showing you which objects exist(ed) in the process you're bound to work with a "snapshot" of the memory.
You can't, by default, interact with the objects ClrMD found in the heap.
To be able do that we need a few more tricks:

  1. Run (managed) code in the target
  2. "Steal" a heap instance into our thread's context
  3. Use reflection to invoke methods/properties' getters & setters.

Running manged code in the target

The problem of injecting code into other processes have been talked about a lot online.
Injecting managed code is a bit less popular.
There are less resources but I found Microsoft's Snoop and Rutherther/NosSmooth.Local to be great references.
Lucky for us, we can (and actually must) inject our code into the application's main AppDomain. So we're able to fetch all meta-classes required for reflection like types, method infos, field infos, etc.

My current solution is injecting an unmanaged dll (written in C++) into the target,
then either find the currently running .NET enviroment and inject an assembly to it (for .NET/.NET Framework targets) or spawn a new one (for native, MSVC compiled, targets) and inject to that instead.

"Stealing" heap instances

Basicly we're trying to solve "ClrMD told us there's an object of type X in address Y. How do we get it as an X reference in our hands?". This is also also done with a trick I found in KeeFarce's repo.
I traced down this trick's origin to a post by Alois Kraus.
Basicly, we're screwing with the CLR. We compile dynamicly a function which doesn't usualy make much sense.
We set the signature to receive 1 long and return 1 object and as the function's body we just load the first argument and return it.
Appearently this is enough for the CLR to do the "casting" of a memory address to an object reference.
I bet there are other (equivilently unsafe) methods to achieve this.

Using relfection to invoke method

So far this one is the most vanilla. Once we have an object in our hand, we're just a couple of GetType() and GetMethod() calls away from invoking virtually any function it has.
Same for reading fields (we were able to do them via ClrMD's snapshot but now we have the "live" object in hand so we can spot changes more quickly).
Properties are basically getters and setters so they're invokable just as any other method.
And we can also subscribe/unsubscribe from events.

Note that invoking static methods, reading static fields and creating instances (with Activator) were already possible with reflection and injected code, we didn't have to capture any objects from the heap for those actions.

Interactivity: HTTP communication.

To allow "users" to use all the cool things our injected code can do, we need to have some API.
For this cause the injected code hosts a HTTP server and the injector can communicate with it back-and-forth to gain info/cause changes to the target as it wishes.

* In some cases, mostly UWP apps which live in a "sandbox" and have much less permissions, the injectee can't start listening on ports.
In that case our injected code can't do that as well.
This case is solved by launching a "proxy" process that both ends (injector + injected code) both connect to. Appearently making outbound TCP connections is less restricted in UWP apps.