# unity game hack
## from mono to il2cpp
#### by asef18766
---
## outline
- quick intro to unity
- production apk & loader structure
- main logic rev
- mono
- il2cpp
---
## $man unity
---

---
almost all games made by unity
(especially in TW)
---
## [GGJ 2023](https://globalgamejam.org/2023/games?title=&country=All&city=&tools=unity&diversifier=All&platforms=All)
* unreal: 716
* unity: 4679

---
### for example

---
### also unity :P

---
### our objective
* explore internel structures & workaround
* hook
* explore defense solutions
* add more generic hook(?)
---
## unity game arch
---
* object in scence: GameObject
* elements in GameObject: Component

---
### componet
```csharp=1
public class User : MonoBehaviour
{
private Rigidbody2D _rigidbody2D;
[SerializeField] private float moveSpeed;
[SerializeField] private GameObject bullet;
...
// unity event functions
private void Start()
{...}
private void Update()
{...}
}
```
---
### Attribute
```csharp=4
[SerializeField] private float moveSpeed;
```

---
### how does SerializeField work?
---
### reflection
[ref](https://blog.kkbruce.net/2017/01/reflection-method-invoke-7-ways.html)
---
```csharp=
public class MyClass
{
public string Foo(int value)
{
return $"Foo value: {value}";
}
public string Bar(string value)
{
return $"Bar value: {value}";
}
}
```
---
#### normal invocation
```csharp=
internal void Way1_DirectMethodCall(object targetObject)
{
var baseName = MethodBase.GetCurrentMethod().Name;
var myClass = ((MyClass)targetObject);
var fooResult = myClass.Foo(1);
var barResult = myClass.Bar("Bruce");
PrintResult(baseName, fooResult, barResult);
}
private void PrintResult(string baseName, string foo, string bar)
{
Console.WriteLine($"{baseName}: {foo}, {bar}");
}
```
---
#### reflection invocation
(this will be used later on)
```csharp=
internal void Way3_UsingMethodInvoke(object targetObject)
{
var baseName = MethodBase.GetCurrentMethod().Name;
var foo = targetObject.GetType().GetMethod("Foo");
var fooResult = foo.Invoke(targetObject, new object[] { 3 }) as string;
var bar = targetObject.GetType().GetMethod("Bar");
var barResult = bar.Invoke(targetObject, new object[] { "Happy" }) as string;
PrintResult(baseName, fooResult, barResult);
}
```
---
### production
* runtime
* mono
* il2cpp

* export to Android Studio Project
---
#### $man mono
* open source .Net Framework
* [github](https://github.com/mono/mono)

---
#### $man il2cpp
* *AOT* compiler
* transform IL to Cpp

---
#### mono only support 32 bit

---
#### il2cpp can support all

---
### pros & cons in il2cpp
* partial reflection feature support
* performance
* a little bit more secure ...?
---
## apk structure
---
### mono
---
#### apk structure

---
#### AndroidManifest.xml
```xml=
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.unity3d.player" xmlns:tools="http://schemas.android.com/tools">
<application android:extractNativeLibs="true">
<activity android:name="com.unity3d.player.UnityPlayerActivity" android:theme="@style/UnityThemeSelector" android:screenOrientation="landscape" android:launchMode="singleTask" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density" android:resizeableActivity="false" android:hardwareAccelerated="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
<meta-data android:name="android.notch_support" android:value="true" />
</activity>
<meta-data android:name="unity.splash-mode" android:value="0" />
...
</application>
<uses-feature android:glEsVersion="0x00030000" />
...
</manifest>
```
---
#### UnityPlayerActivity
```java
@Override protected void onCreate(Bundle savedInstanceState)
{
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
String cmdLine = updateUnityCommandLineArguments(getIntent().getStringExtra("unity"));
getIntent().putExtra("unity", cmdLine);
mUnityPlayer = new UnityPlayer(this, this);
setContentView(mUnityPlayer);
mUnityPlayer.requestFocus();
}
```
---
#### UnityPlayer
##### note: il2cpp version & mono version almost the same
---
[preloadJavaPlugins](https://docs.unity3d.com/2017.2/Documentation/Manual/AndroidJARPlugins.html)
```java
private static void preloadJavaPlugins() {
try {
Class.forName("com.unity3d.JavaPluginPreloader");
} catch (ClassNotFoundException unused) {
} catch (LinkageError e2) {
com.unity3d.player.f.Log(6, "Java class preloading failed: " + e2.getMessage());
}
}
```
---
load native libraries(`libmain.so`) & invoke native defined `NativeLoader.load`
```java
String loadNative = loadNative(getUnityNativeLibraryPath(this.mContext));
```
```java=1
private static String loadNative(String str) {
String str2 = str + "/libmain.so";
try {
try {
System.load(str2);
} catch (UnsatisfiedLinkError unused) {
System.loadLibrary("main");
}
if (NativeLoader.load(str)) {
m.a();
return "";
}
com.unity3d.player.f.Log(6, "NativeLoader.load failure, Unity libraries were not loaded.");
return "NativeLoader.load failure, Unity libraries were not loaded.";
} catch (SecurityException e2) {
return logLoadLibMainError(str2, e2.toString());
} catch (UnsatisfiedLinkError e3) {
return logLoadLibMainError(str2, e3.toString());
}
}
```
---
invoke native defined `initJni`
```java
private final native void initJni(Context context);
```
---
start thread & render with `nativeRender`
```java
this.m_MainThread.start();
```
```java
e m_MainThread;
```
```java
private class e extends Thread {
//...
public final void run() {
this.setName("UnityMain");
Looper.prepare();
this.a = new Handler(new Handler.Callback() {
// ...
public final boolean handleMessage(Message var1) {
//...
if ((var2 = (d)var1.obj) == UnityPlayer.d.h) {
--e.this.e;
UnityPlayer.this.executeGLThreadJobs();
//...
if (!UnityPlayer.this.isFinishing() && !UnityPlayer.this.nativeRender()) {
UnityPlayer.this.finish();
}
//...
});
Looper.loop();
}
}
```
---
#### libmain.so(mono ver)
---
trigger `JNI_Onload` in `libmain.so` in `UnityPlayer` `loadNative` line 5
```c
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
jclass v2; // r0
JNIEnv *v4; // [sp+4h] [bp-Ch] BYREF
v4 = 0;
(*vm)->AttachCurrentThread(vm, &v4, 0);
v2 = (*v4)->FindClass(v4, "com/unity3d/player/NativeLoader");
if ( (*v4)->RegisterNatives(v4, v2, (const JNINativeMethod *)&off_6000, 2) > -1 )
return 0x10006;
(*v4)->FatalError(v4, "com/unity3d/player/NativeLoader");
return -1;
}
```
---
register two method with `off_6000`
* `jboolean NativeLoader_load(JNIEnv *env, jobject thiz, jstring path)`
* `jboolean NativeLoader_unload(JNIEnv *env, jobject thiz)`
```
.data:00006000 off_6000 DCD aLoad ; DATA XREF: JNI_OnLoad+2A↑o
.data:00006000 ; JNI_OnLoad+2C↑o ...
.data:00006000 ; "load"
.data:00006004 DCD aLjavaLangStrin ; "(Ljava/lang/String;)Z"
.data:00006008 DCD sub_1240+1
.data:0000600C DCD aUnload ; "unload"
.data:00006010 DCD aZ ; "()Z"
.data:00006014 DCD sub_12E4+1
```
---

---
reinvoke defined native method `NativeLoader.load`
```c
jboolean __fastcall NativeLoader_load(JNIEnv *env, jobject thiz, jstring path)
{
jboolean ret; // r5
size_t v6; // r9
char *base_path; // r8
const char *v8; // r6
ret = 0;
if ( (sub_1D74() & 4) != 0 )
{
v6 = (*env)->GetStringUTFLength(env, path) + 1;
base_path = (char *)malloc(v6);
v8 = (*env)->GetStringUTFChars(env, path, 0);
qmemcpy(base_path, v8, v6);
(*env)->ReleaseStringUTFChars(env, path, v8);
loadLib(env, base_path, "libunity.so", &h_libunity, 0);
loadLib(env, base_path, "libmonobdwgc-2.0.so", &h_libmonobdwgc, 1);
free(base_path);
ret = h_libunity;
if ( h_libunity )
return 1;
}
return ret;
}
```
---
invoke loaded lib's `JNI_Onload` and return handle
```c
v9 = dlopen(s, 1);
if ( v9 || (v9 = dlopen(libname, 1)) != 0 )
{
jni_onload = (int (__fastcall *)(JavaVM *, _DWORD))dlsym(v9, "JNI_OnLoad");
if ( !jni_onload || jni_onload(vm, 0) < 65543 )
{
*libhandle = v9;
return _stack_chk_guard - v18;
}
jenv = *env;
error_msg = "Unsupported VM version";
}
```
---
#### libunity.so(mono ver)
---
register `initJni`, `nativeRender` and tons of other native funcs in `JNI_Onload`
```
.data:010F5344 ; JNINativeMethod stru_10F5344[25]
.data:010F5344 stru_10F5344 JNINativeMethod <aInitjni, aLandroidConten, initJni+1>
.data:010F5344 ; DATA XREF: sub_3B032C+20↑o
.data:010F5344 ; sub_3B032C+22↑o ...
.data:010F5344 JNINativeMethod <aNativedone, aZ_4, sub_3AFB44+1> ; "(Ljava/lang/String;)V" ...
.data:010F5344 JNINativeMethod <aNativepause, aZ_4, sub_3AFB9C+1>
.data:010F5344 JNINativeMethod <aNativerecreate, aIlandroidViewS, sub_3AFC90+1>
.data:010F5344 JNINativeMethod <aNativesendsurf, aV, sub_3AFCC8+1>
.data:010F5344 JNINativeMethod <aNativerender, aZ_4, sub_3AFCFC+1>
.data:010F5344 JNINativeMethod <aNativeresume, aV, sub_3AFBCE+1>
.data:010F5344 JNINativeMethod <aNativelowmemor, aV, sub_3AFBF8+1>
.data:010F5344 JNINativeMethod <aNativeapplicat, aV, sub_3AFC20+1>
.data:010F5344 JNINativeMethod <aNativefocuscha, aZV, sub_3AFC4E+1>
.data:010F5344 JNINativeMethod <aNativesetinput, aIiiiV, sub_3AFE30+1>
.data:010F5344 JNINativeMethod <aNativesetkeybo, aZV, sub_3AFE6A+1>
.data:010F5344 JNINativeMethod <aNativesetinput_0, aLjavaLangStrin_8, sub_3AFE9C+1>
.data:010F5344 JNINativeMethod <aNativesetinput_1, aIiV, sub_3AFF10+1>
.data:010F5344 JNINativeMethod <aNativesoftinpu, aV, sub_3AFFCA+1>
.data:010F5344 JNINativeMethod <aNativesoftinpu_0, aV, sub_3AFF46+1>
.data:010F5344 JNINativeMethod <aNativereportke, aV, sub_3AFFA2+1>
.data:010F5344 JNINativeMethod <aNativesoftinpu_1, aV, sub_3AFF74+1>
.data:010F5344 JNINativeMethod <aNativeinjectev, aLandroidViewIn, sub_3AFD2C+1>
.data:010F5344 JNINativeMethod <aNativeunitysen, aLjavaLangStrin_26, sub_3AFFF8+1>
.data:010F5344 JNINativeMethod <aNativeisautoro, aZ_4, sub_3B010C+1>
.data:010F5344 JNINativeMethod <aNativemutemast, aZV, sub_3B0144+1>
.data:010F5344 JNINativeMethod <aNativerestarta, aV, sub_3B0188+1>
.data:010F5344 JNINativeMethod <aNativesetlaunc, aLjavaLangStrin_8, sub_3B01B0+1>
.data:010F5344 JNINativeMethod <aNativeorientat, aIiV, sub_3B0294+1>
```
---
import DLL with mono with `nativeRender`


---
#### libmonobdwgc.so
[fork of open source m$ mono](https://github.com/Unity-Technologies/mono)
---
#### libmain.so(il2cpp ver)
load differ libs

---
#### libunity.so(il2cpp ver)
* also register tons of methods
* trigger `libil2cpp.so` `il2cpp_init` in `nativeRender` sub function

---

---
## Mono reverse
* pretty easy
* throw `assets/bin/Data/Managed/Assembly-CSharp.dll` to dnSpy
---

---
## il2cpp reverse
* [ref](https://www.hebunilhanli.com/wonderland/mobile-security/analysis-of-il2cpp/)
---
### il2cpp_init walk graph

---
```
il2cpp_init
└── Runtime::Init
├── g_CodegenRegistration(s_Il2CppCodegenRegistration)
│ └── il2cpp_codegen_register # initialize global variable ptr
│ └── MetadataCache::Register
│ └── GlobalMetadata::Register
└── MetadataCache::Initialize
└── GlobalMetadata::Initialize
└── MetadataLoader::LoadMetadataFile("global-metadata.dat") # map file to memory
```
---
#### GlobalMetadata::Initialize

---
#### MetadataLoader::LoadMetadataFile

---
#### il2cpp_codegen_initialize_runtime_metadata

---
```
il2cpp_codegen_initialize_runtime_metadata
└── MetadataCache::InitializeRuntimeMetadata
└── GlobalMetadata::InitializeRuntimeMetadata
```
---
#### GlobalMetadata::InitializeRuntimeMetadata

---
#### type & ptr register in memory

---
exposed API

---
init
`il2cpp_domain_get`
`il2cpp_domain_get_assemblies`
`il2cpp_assembly_get_image`
---
obtain classes & their methods
`il2cpp_image_get_class`
`il2cpp_class_get_methods`
---
obtain object fields
`il2cpp_class_get_fields`
---
</slide>
{"metaMigratedAt":"2023-06-18T02:37:17.456Z","metaMigratedFrom":"YAML","title":"unity game hack","breaks":true,"contributors":"[{\"id\":\"1460deee-ae3a-4243-97db-66a1b58b48b2\",\"add\":14964,\"del\":479}]"}