# 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 --- ![](https://i.imgur.com/OpCrsf1.png) --- 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 ![](https://i.imgur.com/2DmELEG.png) --- ### for example ![](https://i.imgur.com/URwtm05.png) --- ### also unity :P ![](https://i.imgur.com/wDuz60h.png) --- ### 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 ![](https://i.imgur.com/9A60bUV.png) --- ### 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; ``` ![](https://i.imgur.com/uUcXqy0.png) --- ### 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 ![](https://i.imgur.com/v8lvVro.png) * export to Android Studio Project --- #### $man mono * open source .Net Framework * [github](https://github.com/mono/mono) ![](https://i.imgur.com/QYJsKIR.png) --- #### $man il2cpp * *AOT* compiler * transform IL to Cpp ![](https://i.imgur.com/lw9fhZf.png) --- #### mono only support 32 bit ![](https://i.imgur.com/xKEktCB.png) --- #### il2cpp can support all ![](https://i.imgur.com/16Yb8Wo.png) --- ### pros & cons in il2cpp * partial reflection feature support * performance * a little bit more secure ...? --- ## apk structure --- ### mono --- #### apk structure ![](https://i.imgur.com/qHSnd7l.png) --- #### 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 ``` --- ![](https://i.imgur.com/2l2degU.png) --- 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` ![](https://i.imgur.com/8oC5em7.png) ![](https://i.imgur.com/dT1mBti.png) --- #### libmonobdwgc.so [fork of open source m$ mono](https://github.com/Unity-Technologies/mono) --- #### libmain.so(il2cpp ver) load differ libs ![](https://i.imgur.com/z5gZkbc.png) --- #### libunity.so(il2cpp ver) * also register tons of methods * trigger `libil2cpp.so` `il2cpp_init` in `nativeRender` sub function ![](https://i.imgur.com/5bQ74EU.png) --- ![](https://i.imgur.com/M4VvnLi.png) --- ## Mono reverse * pretty easy * throw `assets/bin/Data/Managed/Assembly-CSharp.dll` to dnSpy --- ![](https://i.imgur.com/LSIXyCI.png) --- ## il2cpp reverse * [ref](https://www.hebunilhanli.com/wonderland/mobile-security/analysis-of-il2cpp/) --- ### il2cpp_init walk graph ![](https://i.imgur.com/WjGMcGS.png) --- ``` 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 ![](https://i.imgur.com/9EGQooQ.png) --- #### MetadataLoader::LoadMetadataFile ![](https://i.imgur.com/CJxY0iY.png) --- #### il2cpp_codegen_initialize_runtime_metadata ![](https://i.imgur.com/5SQeP99.png) --- ``` il2cpp_codegen_initialize_runtime_metadata └── MetadataCache::InitializeRuntimeMetadata └── GlobalMetadata::InitializeRuntimeMetadata ``` --- #### GlobalMetadata::InitializeRuntimeMetadata ![](https://i.imgur.com/33xNfUm.png) --- #### type & ptr register in memory ![](https://i.imgur.com/PafkvgJ.png) --- exposed API ![](https://i.imgur.com/ySG6O0V.png) --- 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}]"}
    415 views