Post

Hacking Mono Games With Frida

INTRODUCTION

Recently, i’ve been practicing game hacking frida. Frida is an excellent tool for hacking. It allows us to make scripts in javascript, and is very flexible, a script, we made can be run on any architecture. Making it great for android game hacking. But there is one problem i encountered often. Games are compiled in mono and i dont know how to hack mono games and there is almost no resource about it except for the documentation. Mono games works differently than native games. So, i decided to make a writeup about it after hours of reading the documentation and reading the source code. Lets get started.

Game Plan

Before we start, make sure you have an ide and frida installed. The game plan on hacking mono games is simple. Here is a diagram i made summarizing it.

First, we will get the mono library which we will use. Then, we will need to set our thread to the root domain. So when we later compile functions using mono_compile_method (we will talk about this later.), it will work. Next, we will get all the assemblies. Assemblies are the inidividual dll’s that are loaded by the mono library.

Then, we will get the class that we need from the assembly. Then, we will get the function in the class, and get its address. Then, we can do whatever we want. Lets get started

Coding

For this writeup, i will be hacking ultrakill once again since i already worked with it before and know its classes and functions already. First, we will get the mono library. In ultrakill, the mono library is named mono-2.0-bdwgc.dll. We can get it using getModuleByName which will give us a module object.

1
var hMono = Process.getModuleByName("mono-2.0-bdwgc.dll")

Next, like i said in the flowchart, we need to set our thread to the root domain. The function for getting the root domain is mono_get_root_domain and the function for setting our thread is mono_thread_attach. We can get the address of these functions using the getExportByName. Since these are functions, we will make a new NativeFunction out of them.

1
2
3
4
5
6
var hMono = Process.getModuleByName("mono-2.0-bdwgc.dll") //Hook the mono module

//Domain Stuffs
var mono_get_root_domain = new NativeFunction(hMono.getExportByName("mono_get_root_domain"), 'pointer', [])
var mono_thread_attach = new NativeFunction(hMono.getExportByName("mono_thread_attach"), 'pointer', ['pointer'])
mono_thread_attach(mono_get_root_domain())

Next, to get the assemblies, we will use the function mono_assembly_foreach to loop through each assemblies, and check get the assembly if it is named GameAssembly, which is the dll that contains all the logics for unity. Then, we will need to convert this Assembly object to image object, since we cant really do anything with an Assembly object. According to the documentation, mono_assembly_foreach, first argument should be a function, which will be called, on every iteration of assemblies found. The Assembly object will get passed as the first argument to the function. The second argument of mono_assembly_foreach is useless and can be ignored.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var hMono = Process.getModuleByName("mono-2.0-bdwgc.dll") //Hook the mono module

//Domain Stuffs
var mono_get_root_domain = new NativeFunction(hMono.getExportByName("mono_get_root_domain"), 'pointer', [])
var mono_thread_attach = new NativeFunction(hMono.getExportByName("mono_thread_attach"), 'pointer', ['pointer'])
mono_thread_attach(mono_get_root_domain())

//Assemblies stuff
var mono_assembly_foreach = new NativeFunction(hMono.getExportByName("mono_assembly_foreach"), 'void', ['pointer', 'pointer']) //List all assembly
var mono_assembly_get_name  = new NativeFunction(hMono.getExportByName("mono_assembly_get_image"), 'pointer', ['pointer']) //Get image from assembly
var AssemblyCsharpAssembly
function GetAssemblyCsharpCallback(MonoAssemblyObject, user_data){ //Function to be called on every assemblies found
    var MonoAssemblyImageObject = mono_assembly_get_name(MonoAssemblyObject)
    var ImageName = mono_image_get_name(MonoAssemblyImageObject)
    if(ImageName.readUtf8String() == "Assembly-CSharp"){ //Check if it is the Assembly-CSharp
        console.log("AssemblyCsharp Found. Assembly object at :" + MonoAssemblyImageObject)
        AssemblyCsharpAssembly = MonoAssemblyImageObject
    }
}

mono_assembly_foreach(new NativeCallback(GetAssemblyCsharpCallback, 'void', ['pointer', 'pointer']), ptr(0))

Here, i made a new function, GetAssemblyCsharpCallback, the first argument is the Mono Assembly Object. It will get the name of the Assembly using mono_assembly_get_name, and if it is equals to Assembly-CSharp, set our AssemblyCsharpAssembly variable to the image of it. AssemblyCsharp contains all the game logic. Next, we are gonna get our target class on it, the NewMovement class.

This is the class of our player. We will use mono_class_from_name to get our Mono class object

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
var hMono = Process.getModuleByName("mono-2.0-bdwgc.dll") //Hook the mono module

//Domain Stuffs
var mono_get_root_domain = new NativeFunction(hMono.getExportByName("mono_get_root_domain"), 'pointer', [])
var mono_thread_attach = new NativeFunction(hMono.getExportByName("mono_thread_attach"), 'pointer', ['pointer'])
mono_thread_attach(mono_get_root_domain())

//Assemblies stuff
var mono_assembly_foreach = new NativeFunction(hMono.getExportByName("mono_assembly_foreach"), 'void', ['pointer', 'pointer']) //List all assembly
var mono_assembly_get_name  = new NativeFunction(hMono.getExportByName("mono_assembly_get_image"), 'pointer', ['pointer']) //Get image from assembly
var AssemblyCsharpAssembly
function GetAssemblyCsharpCallback(MonoAssemblyObject, user_data){
    var MonoAssemblyImageObject = mono_assembly_get_name(MonoAssemblyObject)
    var ImageName = mono_image_get_name(MonoAssemblyImageObject)
    if(ImageName.readUtf8String() == "Assembly-CSharp"){
        console.log("AssemblyCsharp Found. Assembly object at :" + MonoAssemblyImageObject)
        AssemblyCsharpAssembly = MonoAssemblyImageObject
    }
}

mono_assembly_foreach(new NativeCallback(GetAssemblyCsharpCallback, 'void', ['pointer', 'pointer']), ptr(0))

//Class Stuff
var mono_class_from_name = new NativeFunction(hMono.getExportByName("mono_class_from_name"), 'pointer', ['pointer', 'pointer', 'pointer'])

var NewMovementClass = mono_class_from_name(ptr(AssemblyCsharpAssembly), Memory.allocUtf8String(""), Memory.allocUtf8String("NewMovement"))

The mono_class_from_name takes 3 parameter, the first is the Assembly image, the next is the namespace, and the last is the name of the class. It accepts the second and third parameter as const char*, so we will use Memory.allocUtf8String, to alloc a const char to memory, and return a pointer to it. Now, we will get the Update method. we can do that with the function mono_class_get_method_from_name. It will return a mono method object, then we will pass it to mono_compile_method to get it’s address

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
var hMono = Process.getModuleByName("mono-2.0-bdwgc.dll") //Hook the mono module

//Domain Stuffs
var mono_get_root_domain = new NativeFunction(hMono.getExportByName("mono_get_root_domain"), 'pointer', [])
var mono_thread_attach = new NativeFunction(hMono.getExportByName("mono_thread_attach"), 'pointer', ['pointer'])
mono_thread_attach(mono_get_root_domain())

//Assemblies stuff
var mono_assembly_foreach = new NativeFunction(hMono.getExportByName("mono_assembly_foreach"), 'void', ['pointer', 'pointer']) //List all assembly
var mono_assembly_get_name  = new NativeFunction(hMono.getExportByName("mono_assembly_get_image"), 'pointer', ['pointer']) //Get image from assembly
var AssemblyCsharpAssembly
function GetAssemblyCsharpCallback(MonoAssemblyObject, user_data){
    var MonoAssemblyImageObject = mono_assembly_get_name(MonoAssemblyObject)
    var ImageName = mono_image_get_name(MonoAssemblyImageObject)
    if(ImageName.readUtf8String() == "Assembly-CSharp"){
        console.log("AssemblyCsharp Found. Assembly object at :" + MonoAssemblyImageObject)
        AssemblyCsharpAssembly = MonoAssemblyImageObject
    }
}

mono_assembly_foreach(new NativeCallback(GetAssemblyCsharpCallback, 'void', ['pointer', 'pointer']), ptr(0))

//Class Stuff
var mono_class_from_name = new NativeFunction(hMono.getExportByName("mono_class_from_name"), 'pointer', ['pointer', 'pointer', 'pointer'])

var NewMovementClass = mono_class_from_name(ptr(AssemblyCsharpAssembly), Memory.allocUtf8String(""), Memory.allocUtf8String("NewMovement"))

//Method Stuff
var mono_class_get_method_from_name  = new NativeFunction(hMono.getExportByName("mono_class_get_method_from_name"), 'pointer', ['pointer', 'pointer', 'int'])
var mono_compile_method = new NativeFunction(hMono.getExportByName("mono_compile_method"), 'pointer', ['pointer'])

//Get Update Method address
var NewMovementUpdateMethod = mono_class_get_method_from_name(NewMovementClass, Memory.allocUtf8String("Update"), 0)
var NewMovement_Update = mono_compile_method(NewMovementUpdateMethod)

mono_class_get_method_from_name accepts 3 parameter, the first is the Mono Class object, the second is the name of the function, and the third is the number of parameters of the function. Then, we passed the Mono Method Object to mono_compile_method. Now that we have those, we can start making a hack

Making the hack

Now that we know how to get the address of the functions, we can now make our hack. To demonstrate the power of frida, i will be showing you three of the most important aspect of game hacking, memory manipulation, function hooking, and function calling

Function Hooking.

First, we will hook the update method to get our player object. Class methods always pass the object instance as the first variable, so we can get our NewMovement instance in the first argument of the Update method. We can do that in frida, using Interceptor

1
2
3
4
5
6
7
8
9
10
11
...

var LocalPlayer
Interceptor.attach(NewMovement_Update, {
  onEnter(args) {
    if(!LocalPlayer || LocalPlayer.toString() != args[0].toString()){
      console.log("LocalPlayer Found: " + args[0].toString())
      LocalPlayer = args[0]
    }
  }
});

Now we have the LocalPlayer instance

Memory Manipulation

Now that we have the local player. Now, we will manipulate its data. Specifically, the hp field. To get the offset of a field. We will use mono_class_get_field_from_name to get the Mono field object and mono_field_get_offset to get its offset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...

//Field Stuff
var mono_class_get_field_from_name = new NativeFunction(hMono.getExportByName("mono_class_get_field_from_name"), 'pointer', ['pointer', 'pointer'])
var mono_field_get_offset = new NativeFunction(hMono.getExportByName("mono_field_get_offset"), 'int', ['pointer'])

function ManipulateHealth(Player){
  var ClassHpField = mono_class_get_field_from_name(NewMovementClass, Memory.allocUtf8String("hp"))
  var HpOffset = mono_field_get_offset(ClassHpField)
  console.log("Health: " + Player.add(HpOffset).readInt())
  Player.add(HpOffset).writeInt(200)
}

var LocalPlayer
Interceptor.attach(NewMovement_Update, {
  onEnter(args) {
    if(!LocalPlayer || LocalPlayer.toString() != args[0].toString()){
      console.log("LocalPlayer Found: " + args[0].toString())
      LocalPlayer = args[0]
      ManipulateHealth(args[0])
    }
  }
});

Function Calling

Now for function calling, lets call the jump function. To get the address of it, we will use again mono_class_get_method_from_name and mono_compile_method. For this example, we will get rid of the ManipulateHealth method and instead, call the SuperCharge function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
function SuperCharge(Player){
    var SuperChargeMethod = mono_class_get_method_from_name(NewMovementClass, Memory.allocUtf8String("SuperCharge"), 0)
    var SuperChargeAddress = mono_compile_method(SuperChargeMethod)
    var SuperChargeFunction = new NativeFunction(SuperChargeAddress, 'void', ['pointer'])
    SuperChargeFunction(Player)
}

var LocalPlayer
Interceptor.attach(NewMovement_Update, {
  onEnter(args) {
    if(!LocalPlayer || LocalPlayer.toString() != args[0].toString()){
      console.log("LocalPlayer Found: " + args[0].toString())
      LocalPlayer = args[0]
      SuperCharge(args[0])
    }
  }
});

Conclusion

As you can see, frida is a powerful tool. The hardest part of this is learning and understanding the documentation. I hope this writeup will be a guide to future hackers that are also finding problems in hacking mono games. Thanks for reading.

This post is licensed under CC BY 4.0 by the author.