Post

Exploring the fundamentals of hacking Unity games

Introduction

In this post I’ll be talking about how I tackled a Unity game, specifically compiled with Mono, for the first time to make wall hack and surprised how it was easy to accomplish in comparison to the motherfucker, Unreal Engine.

The contents go on in such order

  • Include game’s files to our project to gain access to them
  • Implementing loader class
  • Implementing hack class
  • Take a look at game assemblies using dnSpy
  • How I update the game entities in real time
  • Wall hack a.k.a ESP code
  • How to inject our dll into Mono game

I’ve also read public articles about Unity hacking and tried some myself, but there are parts that I’m still not sure how they work. Apporogies

I recommend to see this post and vxcall/lethal_to_company side by side so that you can catch up with code that I may not mention in the article.

Table of Contents

Introduction to Lethal Company

In this post, I’ll take the game Lethal Company as an target (I enjoyed it recently with my friends). And it’s worth knowing a bit about the target game before reading this article, let me briefly introduce you to it.

This sentences are quorted from its Steam page.

You are a contracted worker for the Company. Your job is to collect scrap from abandoned, industrialized moons to meet the Company’s profit quota. You can use the cash you earn to travel to new moons with higher risks and rewards–or you can buy fancy suits and decorations for your ship. Experience nature, scanning any creature you find to add them to your bestiary. Explore the wondrous outdoors and rummage through their derelict, steel and concrete underbellies. Just never miss the quota.

In short, it’s a FPS game where you collect scraps in the planets that monsters are crawling around and sell them to your boss for the company.

[+] Setting up Visual Studio project

Why it’s a cinch to develop a hack of Unity? Essentially, by using C# as a language you’re allowed to use all resources which the game uses in your hack code like classes, functions even member variables too as long as you configure your Visual Studio right. It almost feels like you’re using dynamic library of the game lol.

Right off the bat, create a C# Class Library project. Remember we’re making internal hack. Then, right click References on the solution explorer and click Add Reference -> Browse and go to root directory of the Lethal Company and find folder called “Lethal Company_Data/Managed”. The “Managed” folder contains all the managed dll the game uses and is typically located under GAME_Data directory in case of other game too.

In the folder, there should be bunch of .dlls yet the ones we’re interested in is what’s called Assenbly-CSharp.dll, Assembly-CSharp-firstpass.dll and all the files starts their name with Unity and UnityEngine. I know it’s tremendous amount, but add them all anyway.

references References window after added dlls mentioned above

By now we added all we need which allow us to use all the fun stuff inside the game. The magic word using UnityEngine; gives us the power from now on.

[+] Probably an equivalent to dllmain…if you ask me

To perform its functionality after injection, define what’s equivalent to dllmain in C++. Following code is making GameObject, and adding component which is the body of our hack. Remember the namespace, class name and function name will be required when we inject the produced dll. In our case, lethal_to_company, loader, load. I’ll remind you about this in the later part.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;

namespace lethal_to_company
{
    public class loader
    {
        private static readonly GameObject MGameObject = new GameObject();

        public static void load()
        {
            MGameObject.AddComponent<hack>();
            Object.DontDestroyOnLoad(MGameObject);
        }
        public static void unload()
        {
            Object.Destroy(MGameObject);
        }
    }
}

[+] Hack code body

Apparently OnGUI function is Unity’s rendering function which runs at the end of the each frame and we override this function to render our esp.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void OnGUI()
{
  foreach (var go in grabbable_objects)
  {
    esp(go.transform.position, Color.green);
  }
  foreach (var enemy in enemies)
  {
    esp(enemy.transform.position, Color.red);
  }
}

private EnemyAI[] enemies;
private PlayerControllerB local_player;
private GrabbableObject[] grabbable_objects;
private Camera camera;

I’ll show you the esp function and how I update the entity in real time later but let me show you Lethal Company’s in-game entities GrabbableObject[] and EnemyAI[] first. Let’s fire up dnSpy and take a look at in-game objects statically.

On dnSpy, hit Edit -> Search Assembly to search text from entire Unity assemblies of the game (I believe it technically means every managed dlls in same folder). When I look up the word “enemy”, one class appeared which stands out, EnemyAI which sounds promising. Click it and look at its overview.

enemy search result “enemy” search result

Indeed, the convincing symbol names are scattered around in the class such as SetEnemyStunned or this.isEnemyDead.

enemy class first view of EnemyAI class

Ok EnemyAI’s been found, then let’s look for local player class. When you seach “Localplayer”, 2 pure localplayer text come up which both of them seems the member variables of HUDManager and SoundManager class. This means you are able to obtain local player by HUDManager.localPlayer or SoundManager.localPlayer.

local player the localPlayer member in HUDManager class

Alright, now that you understand how to get the local player pointer, let’s focus on the camera object. It’s essential for calculating the object position for ESP. While it’s commonly believed that Camera.main is the valid object used by many games, in this case, it’s different. Neither Camera.main nor Camera.current are applicable.

After some test I found that the local player has an attached Camera class named gameplayCamera which seems promising. Turns out this is a real camera used in the game.

gameplay camera gameplayCamera class in the PlayerControllerB class (class of the local player)

[+] How I update entities

In the last section we’ve found 3 entities we need (except grabbable object but it’s same tedious thing). Now we have to update entity’s info within a each few frame to update position of the entity and such.

The main method is introduced by this Guided Hacking post. Apparently it’s not performant way but frankly speaking I dont care. Let’s be lazy and take the easiest way. The function FindObjectsOfType automatically looks up every instances of the given type at runtime. Use this for EnemyAI and GrabbableObject. But, in case of local player and camera, we can obtain them by HUDManager.Instance.localPlayer and local_player.gameplayCamera.

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
using UnityEngine;

namespace lethal_to_company
{
  partial class hack : MonoBehaviour
  {
    // Setup a timer and a set time to reset to
    private readonly float entity_update_interval = 5f;
    private float entity_update_timer;

    private void EntityUpdate()
    {
      if (entity_update_timer <= 0f)
      {
        enemies = FindObjectsOfType<EnemyAI>();
        grabbable_objects = FindObjectsOfType<GrabbableObject>();

        // You have to open menu to get local player lol
        local_player = HUDManager.Instance.localPlayer;

        assign_camera();

        clear_update_timer();
      }

      entity_update_timer -= Time.deltaTime;
    }

    private void clear_update_timer()
    {
      entity_update_timer = entity_update_interval;
    }
    private void assign_camera()
    {
      camera = local_player.gameplayCamera;
    }
  }
}

[+] Wall hack a.k.a ESP function

The code below is the esp function and some other utilities it uses.

  • world_to_screen func calculates and translate world 3D position to screen 2D coordinate.
  • distance func calculates distance between 2 objects.
  • esp func draws box and line on your screen.

The sole function in which absurd things are going here is world_to_screen. In terms of world to screen mechanism, people typically use camera.WorldToScreenPoint function which is predefined by Unity, but somewhat this game’s WorldToScreenPoint function produces a bit off result from expecting coordinates. The reason why it’s being useless is because this game is purposely rendered at a very small resolution which is 860 x 520 for some reason. The game stretches its resolution to your window size I think for performance or to be look low poly game. Anyway this weird method turning WorldToScreenPoint function completely garbage.

Fortunately, there’s a function called camera.WorldToViewportPoint which produces normalized coordinates on the screen and return value in a range from 0 to 1. Official document states:

Transforms position from world space into viewport space. Viewport space is normalized and relative to the camera. The bottom-left of the camera is (0,0); the top-right is (1,1). The z position is in world units from the camera.

Note that z axis refers to the depth from the camera. If z axis is positive value it means the object is in front of you and while not it’s behind you.

Anyways, for this game WorldToViewportPoint works well as opposed to WorldToScreenPoint. Don’t forget to multiply screen width and height to fit your resolution.

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
55
56
57
using UnityEngine;
using System;

namespace lethal_to_company
{
  partial class hack : MonoBehaviour
  {
    private Vector3 world_to_screen(Vector3 world)
    {
      Vector3 screen = camera.WorldToViewportPoint(world);

      screen.x *= Screen.width;
      screen.y *= Screen.height;

      screen.y = Screen.height - screen.y;

      return screen;
    }

    private float distance(Vector3 world_position)
    {
      return Vector3.Distance(camera.transform.position, world_position);
    }

    private void esp(Vector3 entity_position, Color color)
    {
      if (camera == null)
      {
        console.write_line("camera is null");
        return;
      }

      Vector3 entity_screen_pos = world_to_screen(entity_position);

      if (entity_screen_pos.z < 0 || Math.Abs(entity_position.y - local_player.transform.position.y) > 50)
      {
        return;
      }

      float distance_to_entity = distance(entity_position);
      float box_width = 300 / distance_to_entity;
      float box_height = 300 / distance_to_entity;

      float box_thickness = 3f;

      if (entity_screen_pos.x > 0 && entity_screen_pos.x < Screen.width && entity_screen_pos.y > 0 && entity_screen_pos.y < Screen.height)
      {
        render.draw_box_outline(
          new Vector2(entity_screen_pos.x - box_width / 2, entity_screen_pos.y - box_height / 2), box_width,
          box_height,
          color, box_thickness);
        render.draw_line(new Vector2(Screen.width / 2, Screen.height),
          new Vector2(entity_screen_pos.x, entity_screen_pos.y + box_height / 2), color, 2f);
      }
    }
  }
}

[+] Inject the dll

Once you built the dll, last thing you’d do is injecting it to the game. Because the dll is managed, you have to use correct injector, not the one you’ve been using with C++ hack. There’re several options out there of which injector to use, but the best one is SharpMonoInjector. Others don’t work well, but this one works nicely as of the date this article was written.

When you open it up, all the input should be blank at first. Let’s click “Refresh” button and let it find processes running under mono runtime.

If you’re opening processes like Valorant or dnSpy which have kind of comprehensive protection, SharpMonoInjector will terminate immediately when you press “Refresh”. Therefore make sure you close every those apps in advance.

Secondly, click “…” to select dll that you want to inject, in my case it’s called “lethal_to_company.dll”. Once you select the dll file it will automagically recognize the namespace you encapsulated your code in.

Lastly you fill the Class name and Method name which I as mentioned earlier part, are loader and load. There you go, by pressing “Inject” button at the bottom, it will do its job after that. You dont have to care about left pane of the injector UI when you inject.

SharpMonoInjector SharpMonoInjector filled with all informations

The final result looks like this.

hack visual resulting ESP. green indicates items and red indicates enemies

Conclusion

Honestly, I might not be gonna get along with C# any further. However I’ve been wanting to scratch the surface of Mono hacking once in my life. Indeed it was absolutely fresh experience from what I’ve been done with C++ and fun to manipulate game as if I’m modifying game’s source code directly.

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

© vxcall. Some rights reserved.