Mono/.NET Injection Under Linux
Learning
mono
orC#
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 Unity3D
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 mitigateanti-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