Injecting a .NET Native AOT assembly to call internal functions
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
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.
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.
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)
.
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}");
}
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.
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)
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'.
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.