Mono/.NET Injection Under Linux
Learning mono library injection through a Robocraft exploit. The method used in this publication can be used to modify a wide range of Unity games.
Mono Overview
Mono is an open source port of the .NET framework which runs on a variety of operating systems (including Linux).
The mono build chain compiles C# source code (.cs files) down to IL (immediate language) spec’d byte code which is then executed by the CLR (Common Language Runtime) layer provided by mono.
Due to the translation down to IL, module decompilation as well as modification/reverse engineering is relatively straightforward and a variety of C# IL decompilers/recompilers already exist (dnSpy, ILSpy).
The focus of this journal is on managed library injection, more specifically the ability to inject c# code of our own and interact with/modify a target host.
Exploiting Robocraft
Robocraft is an online MMO game developed by freejam games. It features futuristic robotic battles and is an example of an application we wish to tamper with.
Robocraft uses the Unity3D engine, which is a high level c# component based game engine.
World entities in Unity3D derive from class UnityEngine::GameObject
and may
have a number of components attached to them such as: rigidbodies, mesh
renderers, scripts, etc.
UnityEngine::GameObject
has many useful
properties such as a
name (string), tag, transform (position), etc. as well as static methods for
finding objects by name, tag, etc. These methods become useful when injecting
our own code as they provide a facility for interfacing with the game engine
from an external context (our c# script).
Browsing the Robocraft root directory (installed via steam) revealed a few directories that seemed interesting:
- Robocraft_Data
- lib64
- lib32
- EasyAntiCheat.
Upon further inspection of the Robocraft_Data directory, we find the folders containing the managed (C#/Mono) portion of the application. In particular, the Managed folder contains the C# libraries in DLL form of the Unity Engine as well as other proprietary modules from the game developer.
However at his point it’s worth noting the presence of the EasyAntiCheat folder in the root game directory which confirms the presence of an anti-cheat client.
After some research I found out a few interesting details about the game’s anti-cheat client EasyAntiCheat:
- The client computes hashes of all binary images during startup (including managed libraries) and is cross-referenced to prevent modification to game binaries.
- Uses a heartbeat mechanism to ensure presence of the anti-cheat client (To mitigate anti-cheat removal)
- Works with an online service known as RoboShield to monitor server side parameters such as position, velocity, damage, etc and assigns each user with a trust score. The lower the score the higher the chance of getting kicked from subsequent matches. This score seems to be persistent.
Nonetheless, nothing seemed to prevent us from injecting our own c# library at runtime and this was the vector employed with Robocraft. The advantage of this method was that no modification to the game binaries would be required and therefore any client side anti-tamper protection could be bypassed.
In order to inject our own c# code we need to somehow force the client to load
our own .NET/mono library at runtime. This may be accomplished by a stager
payload which is essentially a shared library that makes internal calls to
libmono.so
Some interesting symbols found in libmono.so
include:
- mono_get_root_domain - get handle to primary domain
- mono_thread_attach - attach to domain
- mono_assembly_open - load assembly
- mono_assembly_get_image - get assembly image
- mono_class_from_name - get handle to class
- mono_class_get_method_from_name - get handle to class method
- mono_runtime_invoke - invoke class method
The function signatures for these symbols are shown below:
1
2
3
4
5
6
7
typedef void* (*mono_thread_attach)(void* domain);
typedef void* (*mono_get_root_domain)();
typedef void* (*mono_assembly_open)(char* file, void* stat);
typedef void* (*mono_assembly_get_image)(void* assembly);
typedef void* (*mono_class_from_name)(void* image, char* namespacee, char* name);
typedef void* (*mono_class_get_method_from_name)(void* classs, char* name, DWORD param_count);
typedef void* (*mono_runtime_invoke)(void* method, void* instance, void* *params, void* exc);
In order to perform code injection, firstly a handle to the root application
domain must be retrieved using mono_get_root_domain
. The primary application
thread must then be binded to the root domain using mono_thread_attach
and the
assembly image loaded with mono_assembly_open
and mono_assembly_get_image
Next the assembly class and class method to execute may be found by name using
mono_class_from_name
and mono_class_get_method_from_name
Finally the class method may be executed using mono_runtime_invoke
. It should
be noted that the class method to execute should be declared as static.
The resulting starger payload is shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <link.h>
#include <fstream>
using namespace std;
typedef unsigned long DWORD;
typedef void* (*mono_thread_attach)(void* domain);
typedef void* (*mono_get_root_domain)();
typedef void* (*mono_assembly_open)(char* file, void* stat);
typedef void* (*mono_assembly_get_image)(void* assembly);
typedef void* (*mono_class_from_name)(void* image, char* namespacee, char* name);
typedef void* (*mono_class_get_method_from_name)(void* classs, char* name, DWORD param_count);
typedef void* (*mono_runtime_invoke)(void* method, void* instance, void* *params, void* exc);
mono_get_root_domain do_mono_get_root_domain;
mono_assembly_open do_mono_assembly_open;
mono_assembly_get_image do_mono_assembly_get_image;
mono_class_from_name do_mono_class_from_name;
mono_class_get_method_from_name do_mono_class_get_method_from_name;
mono_runtime_invoke do_mono_runtime_invoke;
mono_thread_attach do_mono_thread_attach;
int __attribute__((constructor)) init()
{
void* library = dlopen("./Robocraft_Data/Mono/x86_64/libmono.so", RTLD_NOLOAD | RTLD_NOW);
do_mono_thread_attach = (mono_thread_attach)(dlsym(library, "mono_thread_attach"));
do_mono_get_root_domain = (mono_get_root_domain)(dlsym(library, "mono_get_root_domain"));
do_mono_assembly_open = (mono_assembly_open)(dlsym(library, "mono_assembly_open"));
do_mono_assembly_get_image = (mono_assembly_get_image)(dlsym(library, "mono_assembly_get_image"));
do_mono_class_from_name = (mono_class_from_name)(dlsym(library, "mono_class_from_name"));
do_mono_class_get_method_from_name = (mono_class_get_method_from_name)(dlsym(library, "mono_class_get_method_from_name"));
do_mono_runtime_invoke = (mono_runtime_invoke)(dlsym(library, "mono_runtime_invoke"));
do_mono_thread_attach(do_mono_get_root_domain());
void* assembly = do_mono_assembly_open("./Robocraft_Data/Managed/Client.dll", NULL);
void* Image = do_mono_assembly_get_image(assembly);
void* MonoClass = do_mono_class_from_name(Image, "Test", "Test");
void* MonoClassMethod = do_mono_class_get_method_from_name(MonoClass, "Load", 0);
do_mono_runtime_invoke(MonoClassMethod, NULL, NULL, NULL);
return 0;
}
void __attribute__((destructor)) shutdown()
{
};
The stager payload shown above loads the mono assembly located in
<root>/Robocraft_Data/Managed/Client.dll
into memory and executes the class
method Load within the namespace Test and class Test (Test::Test::Load).
Load has the following signature: public static void Load()
The stager may
be compiled with: gcc -fpic -shared stager.cpp -o stager.so
In order to inject the stager into the target process you may use any standard linux shared library injector.
With the capability of loading our own mono code into the target process, we need to ensure that our injected c# code stays persistent, i.e to prevent de-allocation due to garbage collection.
For Unity3D this is typically achieved using the following pattern:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Exploit : MonoBehaviour
{...}
public static class Test
{
private static GameObject loader;
public static void Load()
{
loader = new GameObject();
loader.AddComponent<Exploit>();
UnityEngine.Object.DontDestroyOnLoad(loader);
}
}
It is also worth keeping track of the mono/.NET assembly versions used in the original application. Ideally you would want to use an identical .NET version as compiling your c# exploit with the wrong .NET version can cause your exploit to fail.
For Robocraft .NET v2.0 was required. Finding support for an older version of .NET can be difficult as most modern C# IDE’s do not support such an old target. A simple solution to this problem is to download an older version of mono.
At this point the second stage payload (our c# exploit) can be developed. I chose to implement three simple functionalities:
- Increase/decrease game speed
1
2
3
4
5
6
7
8
if(Input.GetKeyDown(KeyCode.F2)){
speedhack = !speedhack;
if(speedhack == true){
Time.timeScale = 3;
}else{
Time.timeScale = 1;
}
}
- Clip through walls/obstacles
1
2
3
4
5
6
7
8
9
10
if(Input.GetKeyDown(KeyCode.F3)){
collision = !collision;
GameObject obj = GameObject.Find("Player Machine Root");
Rigidbody rb = obj.GetComponent<Rigidbody>();
if(collision == true){
rb.detectCollisions = false;
}else{
rb.detectCollisions = true;
}
}
- Place all network entites near player
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if(Input.GetKeyDown(KeyCode.F1))
{
salt = !salt;
GameObject obj = GameObject.Find("Player Machine Root");
position = obj.transform.position;
foreach(GameObject gameObj in GameObject.FindObjectsOfType<GameObject>())
{
if(gameObj.name == "centerGameObject")
{
GameObject parent = gameObj.transform.parent.gameObject;
if(parent.name != "Player Machine Root"){
MonoBehaviour[] comp = parent.GetComponents<MonoBehaviour>();
foreach (MonoBehaviour c in comp){
c.enabled = !salt;
}
Vector3 myposition = position;
parent.transform.position = myposition;
}
}
}
}
In order to find the names of the game objects for the main player as well as network players you can simply iterate through all the global game objects and dump the corresponding names to a text file.
Source Code
All source code for this journal is hosted at https://github.com/lunarjournal/robocraft