Go back

Injecting a .NET Native AOT assembly to call internal functions

#.NET #Csharp #Reversing #Ahead-of-time

When people think about injecting a library inside a native process, most people would directly associate this with C++ and the DllMain entry point. While this is valid, have you ever considered doing this in .NET(C#)?

Let me take you on an interesting adventure with Native AOT where we'll sneak our .NET DLL into Notepad.exe and have some fun calling its internal functions.

The complete project can be found in the public repository.

Configuring the entry point

When creating a dynamic-link library (DLL) in C++, you can use the DllMain entry point. This function is called by the system when the DLL is loaded, unloaded or e.g. LoadLibrary is called. However, in .NET, unlike in C++, you do not directly expose a DllMain. Instead, you can use the UnmanagedCallersOnly attribute to specify an entry point for unmanaged code.

To start off, we will need to expose our very own representation of DllMain. To do this, create a new Class Library project and add the following:

private const uint DllProcessDetach = 0,
    DllProcessAttach = 1,
    DllThreadAttach = 2,
    DllThreadDetach = 3;

[UnmanagedCallersOnly(EntryPoint = "DllMain")]
public static bool DllMain(nint module, uint reason, nint reserved)
{
    switch (reason)
    {
        case DllProcessDetach: break;
        case DllProcessAttach: break;
        case DllThreadAttach: break;
        case DllThreadDetach: break;
    }
    return true;
}

In essence this compiles down to a similar approach of that in C++ if you were to compare both assemblies, decompiled.

When we publish the code above with dotnet publish -r win-x64 -c Release and you inject the library, for example with LoadLibrary, it will call the entry point. Then use DllProcessAttach to execute code upon the DLL's attachment to the process.

Function Pointers (Delegates)

The release of C# 9 provided us with something called function pointers. This concept may be familiar to C/C++ developers, but to give a brief explanation, this feature allows us to interact with unmanaged code. We do this by assigning the address of the function to a delegate* and importantly, specify whether it's managed or unmanaged. In our case we want to work with unmanaged code(code that is not managed by the .NET runtime). Here is an example:

var method = (delegate* unmanaged<void>)methodAddress;

Note: no calling convention is provided here and thus the runtime platform default is selected. If you want, you can specify a convention e.g. unmanaged[Cdecl].

In this snippet, methodAddress would be the memory address of the function you want to call. The unmanaged part specifies that the function at this pointer does not expect to interact with managed objects directly and returns nothing (void).

Typically, in .NET, we work with managed code, also known as 'safe' code. This is code that doesn't directly access memory with pointers or allocate raw memory; instead, it creates managed objects. However, the example above is considered 'unsafe' code, which can't be verified by the .NET runtime's safety checks. When writing unsafe code, we must explicitly indicate this by, for example, marking a class as unsafe, configuring the csproj file, or wrapping the method in unsafe blocks.

rust-csharp-meme

Now, let's start by retrieving some actual functions!

Finding functions in Notepad

As our library will target Notepad we will look around in Notepad to find a couple of functions to call.

Function #1: Go To Line

The first one I am interested in is the Go To Line window, which can be opened using Ctrl + G.

gotoline This function essentially positions our cursor at the specified line number.

We need to find the address of this function, I won't go into a lot of detail here, but basically you can use a combination of dynamic / static analysis on the notepad binary to find the location of these. Right now I am just using IDA, Cheat Engine and or x64dbg. You could also query the Microsoft Symbol server and grab the pdb file.

The decompiled code shows the return type, calling convention, the address and any arguments passed in: LRESULT __fastcall sub_140007E98(int a1). ida-gotoline

With this information we can create our function pointer. We need to pass in an integer, that being the lineNumber to go to and return a void as we aren't interested in the return.

public void JumpToLine(int lineNumber)
{
    var jumpToLineAddress = baseAddress + 0x7E98;
    var jumpToLine = (delegate* unmanaged<int, void>)jumpToLineAddress;

    jumpToLine(lineNumber);
}

If we modify our entry point to invoke this method, publish our native AOT assembly, and use LoadLibrary or a ready-to-use injector, it should execute the function. To confirm it worked we can look at the application UI, or... read the values of the fields the method adjusted. Let's do that:

[StructLayout(LayoutKind.Explicit)]
internal struct CursorPosition
{
    [FieldOffset(0x320E0)]
    public int ColumnNumber;

    [FieldOffset(0x320E4)]
    public int LineNumber;
}

public void PrintCursorPosition()
{
    var cursorPosition = (CursorPosition*)(baseAddress);
    Console.WriteLine($"LineNumber: {cursorPosition->LineNumber} - ColumnNumber: {cursorPosition->ColumnNumber}");
}
reading-cursor

Function #2: Search in Notepad

Finding the search function in Notepad was interesting to say the least. It works a little different than I expect a search function to work. In this version of Notepad, much of the relevant data, including the search string, is stored in the Windows Registry. Since we are in .NET we could actually set the key using Registry.SetValue and then call the function that sets all global variables, but where's the fun in that..? Instead lets directly write to the global search string. ida-globalvars

Let’s create a method to modify the search string. The decompiled code shows wchar_t StringValue[128], so we'll take this limitation into account.

private void ModifySearchTerm(string query)
{
    var bufferSize = 128 * 2; // 128 characters, 256 bytes for 16-bit.
    var querySize = Encoding.Unicode.GetByteCount(query);

    if (querySize > bufferSize)
    {
        return;
    }

    var queryBuffer = stackalloc char[bufferSize];

    _ = Encoding.Unicode.GetBytes(query, new Span<byte>(queryBuffer, bufferSize));

    var searchQueryAddress = (char*)(baseAddress + 0x32470);
    Unsafe.CopyBlock(searchQueryAddress, queryBuffer, (uint)bufferSize);
}

After going through the x-refs of StringValue there are several methods it's used in, e.g. initializing globals, saving registry keys, but most importantly the search function!

char __fastcall sub_14001C834(__int64 a1, char a2)

ida-search This is the most complete search function, which highlights, searches the text field and indicates success or not. Only thing you may need to consider is the initial cursor position when you want to start searching, luckily, we have already developed a method to manage that!
public void Search(string query, SearchDirection searchDirection)
{
    ModifySearchTerm(query);

    var searchAddress = baseAddress + 0x1C834;
    var search = (delegate* unmanaged<nint, SearchDirection, char>)searchAddress;

    var result = search(nint.Zero, searchDirection) == 1;

    Console.WriteLine($"hasResult: {result}");
}

internal enum SearchDirection
{
    Forward,
    Reverse
}

Result

And as a result, we inject the DLL it calls JumpToLine(3) and then starts searching for 'Some text'. gotoandsearch

Hope you found this interesting, lets hope Microsoft continues to enhance the overall usability of native aot.

Resources

Remarks

This is done on notepad.exe(10.0.19041.3996) on Windows 10 22h2. Any other version is unlikely to work without changing offsets.