If you need help installing the correct tools for this task, refer to Part 1

References#

First, we need to understand what’s mono and how it works.

Mono embedding docs

For a complete Mono embedding C code, see teste.c

Optional If you really want to understand how mono works, see here: www.mono-project.com/docs/advanced/runtime/docs/.

What’s mono#

Mono, the open source development platform based on the .NET Framework, allows developers to build cross-platform applications with improved developer productivity. Mono’s .NET implementation is based on the ECMA standards for C# and the Common Language Infrastructure. From mono-project.com

What we are really interested in here is the Mono Runtime.

Mono Runtime - The runtime implements the ECMA Common Language Infrastructure (CLI). The runtime provides a Just-in-Time (JIT) compiler, an Ahead-of-Time compiler (AOT), a library loader, the garbage collector, a threading system and interoperability functionality. From mono-project.com

In short: The Mono Runtime is what allows us to execute C# code

C & C##

From the docs

The Mono runtime provides two mechanisms to expose C code to the CIL universe: internal calls and native C code. Internal calls are tightly integrated with the runtime, and provide no support for marshalling between runtime types and C types.

P/Invoke#

Basically, a P/Invoke looks like this.

using System.Runtime.InteropServices;

[DllImport ("__Internal", EntryPoint="DoSomething")]
static extern void DoSomething ();

Now, we know how to identify P/Invoke in a C# function. Let’s try it.

Back to Unity, let’s open up a bunch of .dll to check for Object. In Unity, Object is the base of all interesting objects.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
using UnityEngine.Bindings;
using UnityEngine.Internal;
using UnityEngine.Scripting;
using UnityEngineInternal;

#nullable disable
namespace UnityEngine
{
  [NativeHeader("Runtime/SceneManager/SceneManager.h")]
  [NativeHeader("Runtime/GameCode/CloneObject.h")]
  [RequiredByNativeCode(GenerateProxy = true)]
  [NativeHeader("Runtime/Export/Scripting/UnityEngineObject.bindings.h")]
  [StructLayout(LayoutKind.Sequential)]
  public class Object
    // Snip

    [FreeFunction("UnityEngineObjectBindings::GetName")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern string GetName(Object obj);
    // Snip
}

Not quite the same.

Internal call#

If we scroll down a little further in Mono doc, we get to:

mono_add_internal_call ("Hello::Sample", sample);
using System;
using System.Runtime.CompilerServices;

class Hello {
  [MethodImplAttribute(MethodImplOptions.InternalCall)]
  extern static string Sample ();
}

(Note, MethodImpl is MethodImplAttribute. DotPeek, as all decompilers/disassemblers/whatever.exe can’t generate 100% accurate code.)

So now, we know that Unity uses internal calls ! But can we find it’s definition ?

Unity GetName impl

Not a 100% match, but given the proximity with other functions and name proximity, that looks like it.

When we search for reference in Ghidra, we find:

scripting_add_internal_call

🎉 A small victory.

So now, we have a way to find the definition of functions we encounter in the Unity assembly.


Thought process: Find class in C# > Find it’s internal name from it’s attributes > Find where it’s passed on to Mono for registering > We now have the function pointer

Now, we know that anything that’s an unity Object, is a C# object. Thus, it is managed by Mono.

We have to understand Objects.

How objects are born.#

First, we need to understand how mono creates those objects, and what they look like in memory.

My approach is to see how they are created when in the C universe.

From the docs

/* we usually get the class we need during initialization */
MonoImage *image = mono_assembly_get_image (assembly); // Load the Image from the assembly
MonoClass *my_class = mono_class_from_name (image, "MyNamespace", "MyClass"); // Get the MonoClass from the assembly, located by it's namespace and class name
...
/* allocate memory for the object */
MonoObject *my_class_instance = mono_object_new (domain, my_class); // Instanciate the class
/* execute the default argument-less constructor */
mono_runtime_object_init (my_class_instance); // Call the ctor.

Now we know 3 structs:

  • MonoImage: The Assembly, with a memory representation
  • MonoClass: The class definition. Contains the infos
  • MonoObject: The class instance

So, I can assume that MonoObject* holds the instance data.

Let’s have a look at how mono_object_new is implemented: mono/mono/metadata/object.c It’s a wrapper around mono_object_new_checked. Memory safety is for the weak. True programmers SEGFaults like real man /s

The process:

  • Initializes (or fetches) a vtable with mono_class_vtable_checked
    • Checks if the class has a MonoClassRuntimeInfo and runtime_info->max_domain >= domain->domain_id && runtime_info->domain_vtables [domain->domain_id]).
      If I were to make a guess, I think it’s some kind of cache where we cache the vtable for each domain
      • If it has, then return the runtime class’ vtable for domain->domain_id
      • Else, ``mono_class_create_runtime_vtable`
    • Deep down the vtable process, mono_class_init_internal is called. I’ll explain later what are vtables
  • calls mono_object_new_alloc_specific_checked with the class’ vtable and returns the MonoObject* created
    • calls mono_gc_alloc_obj which allocates the necessary memory

Used functions:

Basically, that’s a memory-safe malloc.

Anyway, we have our MonoObject* which is a base for any C# object. It contains a pointer to the vtable, which contains function pointers. It’s size is variable, and is a base for any C# object.

23 years old documentation: docs/object-layout