Unity Mono 분석
.Net Framwork
Microsoft에서 개발한 Windows 프로그램 개발 및 실행 환경을 말합니다. 다양한 프로그래밍 언어와 라이브러리들을 이용하여 윈도우 기반 응용프로그램을 개발 및 실행할 수 있도록, MSIL(MicroSoft Intermediate Language)를 사용함으로써 통합된 프로그래밍 환경을 제공합니다. .NET Framework는 지원하는 언어들이 사용할 수 있는 API와 type으로 구성된 Class Library와 쓰레드 관리, garbage collection, 예외 handling 등을 제공하는 CLR(Common Language Runtime)이라는 실행 엔진(일종의 가상머신)으로 구성되어 있습니다. 이 구조를 CLI(Common Language Infrastructure)라고 하며 해당 구조는 다음과 같습니다.
.NET Framework를 사용하는 언어로 작성된 코드를 해당 언어에 맞는 컴파일러를 이용하여 CIL(Common Intermediate Language)로 컴파일시키고, 런타임 시 CLR(Common Language Runtime)에서 JIT 방식으로 읽어들여 native code로 변환하여 실행합니다. 장점으로는 플랫폼에 독립적이고 CLS(Common Language Specification)을 따르는 모든 언어로 개발이 가능하고, .NET Framework가 설치된 모든 환경에서 개발 및 실행이 가능하다는 점이 있습니다. 단점으로는 운영체제가 무거워지고 시스템 하드웨어 요구사항이 커진다는 점이 있습니다.
Mono
.NET Framework가 매우 유용한 개발환경이지만, 환경에 상관없이 Windows 프로그램을 구동시키자는 취지로 개발되었기 때문에 Windows 환경에만 제한되어 있었습니다. 이에 오픈 소스 개발자 그룹은 Linux/Mac 등의 Windows 환경 외에서도 .NET Framework를 사용하기 위해 Mono 프로젝트를 시작하였습니다. .NET Framework는 오픈 소스 프레임워크가 아니었기 때문에 ECMA International(국제 표준 기구)에 등록된 .NET Framework의 CLI를 분석하여 구현했다고 합니다.
현재 Microsoft는 .NET Framework를 오픈 소스로 공개했을 뿐만 아니라, Mono의 메인테이너인 Xamarin을 인수하고, .NET Core라는 Windows, macOS, Linux 환경에서 모든 .NET Framework 기반 언어를 이용하여 개발/실행이 가능한 오픈 소스 프레임워크를 공개했습니다.
Mono Android (Xamarin Android)
Mono는 다양한 플랫폼을 지원하며, 그 중 안드로이드 역시 포함됩니다. Xamarin Android 응용 프로그램은 Mono 실행 환경에서 실행되며, 안드로이드에서 사용하는 ART 환경과 side-by-side 방식으로 실행됩니다. ART와 Mono 모두 Linux Kernel 위에서 실행되며, 제공되는 API를 통해 사용자는 기본 시스템에 접근할 수 있습니다.
안드로이드의 Audio, Graphics, OpenGL, Telephony 등의 시스템 기능들은 native 응용 프로그램에서 직접적으로 사용이 불가능합니다. 오직 Java.XXX, Android.XXX 과 같은 이름을 가진 Android Runtime Java API을 통해 사용할 수 있습니다. 따라서 앱의 동작은 Mono와 ART가 서로 데이터를 교환하며 수행되는데, 해당 아키텍쳐는 다음과 같습니다.
Android runtime에서 managed code, 즉 C# 등의 코드를 호출해야 하는 경우에는 ACW (Android Callable Wrappers)를 사용하고, managed code에서 Android code를 호출해야 하는 경우에는 MCW (Managed Callable Wrappers)를 사용합니다. 두 방법 모두, JNI (Java Native Interface)를 사용하여 type을 조회하고 field를 읽고 쓰고, 메소드를 호출함으로써 Java의 type 및 멤버를 bind 시킵니다. MCW의 경우 클라이언트 코드가 Java global reference를 통해 Java 인스턴스와 managed 인스턴스 사이의 mapping을 수행할 수 있습니다.
다른 환경에서 Java 사용하는 방법
Java가 아닌 코드에서 Java를 사용하기 위한 방법은 세 가지 정도 존재합니다.
1.
Java Native Interface
JNI는 C++이나 C#과
같은 Java가 아닌 코드가 JVM 내에서
실행되고
있는 Java 코드를 호출하거나 혹은 Java 코드에
의해
호출되도록
만들어주는 Framework입니다.
2.
Java Binding Library 생성
JNI를
기반으로, C# 코드와 Java 코드를
서로 bind 시켜줌으로써 다른 코드에서 Java 코드를
사용할
수
있습니다.
3.
Port the Code
Java 코드를 C# 코드로 포팅함으로써 Java 코드를
사용할
수
있습니다.
Xamarin Android로 빌드된 앱의 동작은 다음과 같습니다.
1. activity나 service 등이 시작되었을 때, 안드로이드는 우선 해당 프로세스가 이미 존재하는지 확인합니다.
2. 만약 해당 프로세스가 기존에 존재하지 않았다면, 새로운 프로세스가 생성이 되며 AndroidManifest.xml에 존재하는 /manifest/application/@android:name에 명시된 속성이 로드되어 인스턴스화됩니다.
3. 그런 다음, /manifest/application/provider/@android:name에 지정된 모든 type의 속성 값이 인스턴스화되고 해당 contentProvider.attachInfo 메소드가 호출됩니다.
4. Xamarin.Android는 빌드 과정 중에 contentProvider.attachInfo 메소드를 후킹하여 프로세스에 Mono runtime을 로딩하는 메소드를 포함하는 mono.MonoRuntimeProvider ContentProvider를 AndroidManifest.xml에 추가합니다.
5. 프로세스 초기화가 완료되면, AndroidManifest.xml을 읽어 activity나 service 등의 클래스 이름을 찾습니다.
6. activity의 경우라면, android.app.Activity를 상속받습니다. 또 다른 type의 경우 Class.forName()에 의해 로드가 된 뒤 인스턴스화가 되는데, 이 type들은 Java type이여야 하기 때문에 ACW가 필요합니다.
7. ACW는 Java type들을 해당하는 C# type의 인스턴스를 생성합니다.
8. 이 후, 안드로이드에서 Java의 onCreate()를 호출하면 Xamarin의 onCreate()를 호출할 수 있게 됩니다.
Unity Mono
Unity는 사용자가 쉽고 편하게 개발을 진행할 수 있도록 Cross-compile 플랫폼을 제공합니다. 대표적으로 중간언어를 기반으로 하는 C#을 지원함으로써 사용자는 한 번의 코드 작성으로 다양한 플랫폼에 대해 컴파일 된 프로그램들을 얻을 수 있습니다. Unity는 크게 Mono와 IL2CPP 빌드 버전을 지원하는데, 다음에서는 Mono에 대해 설명하겠습니다.
Loading native libraries본 행에서는 App이 실행되었을 때, native library가 메모리에 로드되는 과정을 살펴보겠습니다.
C/C++ native의 경우 함수, Java의 경우 메소드라고 부르겠습니다.
1. AndroidManifest.xml에 등록된 MainActivity 클래스의 onCreate() 메소드(멤버 함수)가 호출되며 해당 클래스의 슈퍼 클래스(부모 클래스)인 UnityPlayerActivity 클래스의 onCreate() 메소드가 호출됩니다. (Unity App의 activity 요소는 항상 UnityPlayerActivity를 상속합니다.)
2. UnityPlayerActivity의 onCreate() 함수는 UnityPlayer의 인스턴스를 생성합니다.
3. 해당 인스턴스는 System.loadLibraryStatic() 메소드를 호출하여 libmain.so 파일을 메모리에 로드합니다.
4. 정상적으로 로드가 된 경우, libmain.so의 JNI_OnLoad() 함수를 호출하여 해당 라이브러리의 내부 native 함수들과 NativeLoader 클래스의 load() 메소드를 바인딩 시킵니다.
5. 바인딩 된 NativeLoader()의 load() 메소드를 호출하여 native 라이브러리 libunity.so와 libmono.so 를 메모리에 로드합니다.
6. 이 후, 로드된 libunity.so의 JNI_Onload() 함수를 통해, 해당 라이브러리 내의 NativeXXXX() 함수들과 UnityPlayer의 메소드들을 bind 시켜 사용하게 됩니다. 해당 동작은 RegisterNatives() 함수를 사용하여 native 함수들을 등록시킬 수 있습니다.
Loading DLL files다음은 사용자가 작성한 코드를 포함한 mono runtime에서 동작하는 Unity의 다양한 DLL 파일들을 메모리에 로드하는 과정을 살펴보겠습니다.
1. 앞서 설명했던 bind 과정에서 등록된 함수들 중 하나인 nativeRender() 함수(libunity.so에 존재.)가 UnityPlayer 인스턴스에 의해 호출됩니다.
2. 해당 함수는 libmono.so 내에 존재하는 mono_jit_init_version() 함수를 호출함으로써 libunity.so 내부에 선언되어 있는 MonoManager 인스턴스를 생성합니다.
3. 마지막으로 MonoManager::LoadAssemblies() 함수를 호출하여 /assets/bin/Data/Managed 디렉토리에 존재하는 DLL 파일들을 메모리에 로드합니다. 이 DLL 파일에는 CIL 코드가 포함되어 있습니다.
C#에서 Java 메소드를 사용하는 프로세스
1.
libunity.so에서 Java virtual machine을 생성합니다.
- 먼저, VM을 생성하기 위한 옵션 값들을 설정합니다.
- 이후, VM을 생성합니다.
2.
UnityEngine.dll에서
해당 Java 메소드를 찾아 실행합니다.
- Java class를
찾아
로드합니다.
- 호출하려는 Java 메소드의 ID 값을
가져옵니다.
- 해당 Java 메소드에 대한 인자 정보를 생성합니다.
- 해당
메소드를
호출합니다.
3. libunity.so에서 Java virtual machine을 제거합니다.
C# 환경에서는 Java virtual machine을 생성/삭제가 불가능하기 때문에, 해당 작업은 native domain 단에서 처리해야 합니다. 따라서, libunity.so에서 Java virtual machine을 생성/삭제 작업이 실행됩니다.
이제, UnityPlayer, libunity.so, libmono.so가 각각 메인 게임 내에서 서로 협력적으로 프로세스를 처리하게 됩니다.
libunity.so는 개발자의 CIL 코드를 JIT 컴파일 및 실행하기 위해서 libmono.so의 함수를 호출합니다.
왜 Mono에서는 ARM64를 지원하지 않나요?
공식 답변에 따르면, 과거 ARM64에 관한 Mono AOT engine이 오픈 소스가 아니었기 때문에, 적절한 라이센스를 소유하지 못한 Unity에서는 해당 기술을 사용할 수 없었다고 합니다. 현재에 와서는 해당 기술이 오픈 소스로 공개되었지만, Unity에서 자체 개발한 IL2CPP 빌드 방법이 존재하기 때문에, 추가적인 지원 계획은 없다고 합니다.
Ref.
J. Shim, K. Lim, S. Cho, S. Han and M. Park,
"Static and Dynamic Analysis of Android Malware and Goodware Written
with Unity Framework", Security and Communication Networks, vol. 2018, pp. 12.
https://source.android.com/devices/tech/dalvik/configure
https://www.mono-project.com/docs/advanced/runtime/#ahead-of-time-compilation
https://www.mono-project.com/docs/advanced/runtime/docs/
'좀 열심히 쓴 글' 카테고리의 다른 글
OpenJPEG Code Coverage 코드 작성 및 알고리즘 설명 (0) | 2020.06.14 |
---|---|
Android 동작 구조 (0) | 2020.06.08 |
Frida 및 nodejs 사용할 때 TMI (4) | 2020.06.04 |
Format String field width (0) | 2020.04.09 |
[Project Zero] Bad Binder: Android In-The-Wild Exploit 분석글 (0) | 2020.03.07 |