Frida mono hook(feat. 유니티 모노, 자마린 안드로이드)
몇일 전에 Android CTF 문제를 풀었는데, 자마린으로 제작된 앱 이었습니다.
자마린 앱은 모노 환경에서 구동되더군요.
2021년에 유니티 모노 게임 진단하면서, il2cpp와는 다르게 프리다 후킹이 안되어서 유니티 모노 환경이 무결성 솔루션만 있으면 더 안전한 것 아닌가?(모노 게임 해킹은 주로 소스코드 변조를 하기 때문) 라고 생각하고 넘어갔었는데요...아니네요.
환경구성이 번거롭다 뿐이지, 전체 소스코드를 확보한 상태에서 후킹까지 할 수 있으니 확실히 il2cpp보다 분석이 수월합니다.
다만 유니티 모노 게임은 직접 빌드하거나, 고전 모바일 게임 아니면 이제 볼일이 없다는 점과, Xamarin 역시 다른 크로스플랫폼 개발 언어(ex. Flutter, React Native)에 밀려 만날 일이 많지 않다는 점에서 간단히 정리하고 넘어가려고 합니다.
모노 후킹의 핵심은 모노 api 중 "mono_compile_method" 를 이용하여 메서드를 강제 컴파일시켜서 후킹을 하는 것입니다.
◼︎mono hook 환경 구성
npm, frida, frida-compile 설치되어 있어야 합니다.
git clone https://github.com/GoSecure/frida-xamarin-unpin.git
cd frida-xamarin-unpin
git clone https://github.com/GoSecure/frida-mono-api mono-api
cd mono-api && git switch extra
cd ..
npm install
https://github.com/hypn/unity-frida-hacks/blob/master/enumerator.js 에서 enumerator.js 파일 받아서 mono-api\src\ 하위에 위치시켜줍니다. 해당 js 파일은 모노 환경에서 좀 더 분석을 쉽게 하기 위한 인터페이스를 포함하고 있습니다.
◼︎유니티 모노 게임 후킹
연습할 대상은 "실루엣 걸"이라는 게임인데요, 최신버전은 il2cpp 환경이지만, 2019년 버전은 mono 환경입니다. apkpure 사이트에서 다운받을 수 있습니다.
https://apkpure.com/silhouettegirl/com.luckypunchstream.silhouettegirl/versions
apk 파일 디컴파일해서 lib 폴더를 보니 libmono.so 파일에서 mono api를 제공해주는 것으로 보입니다.
mono-api\src\mono-module.js의 KNOWN_RUNTIMES에 'libmono.so'를 추가해줍니다.
dnSpy를 통해 Assembly-CSharp.dll 파일을 분석합니다. Assembly-CSharp.dll 파일은 보통 디컴파일한 apk 파일의 "assets\bin\Data\Managed" 디렉터리에 위치하고 있습니다. 저는 "MoreMountains.InfiniteRunnerEngine.Jumper" 클래스의 "SetPlayerData" 메서드를 후킹하려고 합니다.
src\main.js에 후킹코드를 작성합니다.
import { MonoApi, MonoApiHelper, Enumerator } from 'frida-mono-api';
// MonoAssembly* mono_assembly_load_with_partial_name (const char *name, MonoImageOpenStatus *status)
// 설명: Loads a Mono Assembly from a name, 반환값: NULL on failure, or a pointer to a MonoAssembly on success.
let status = Memory.alloc(0x1000);
let AssemblyCSharp = MonoApi.mono_assembly_load_with_partial_name(Memory.allocUtf8String('Assembly-CSharp'), status);
// MonoImage* mono_assembly_get_image (MonoAssembly *assembly)
let img = MonoApi.mono_assembly_get_image(AssemblyCSharp);
// MonoClass* mono_class_from_name (MonoImage *image, const char* name_space, const char *name)
// 설명: Obtains a MonoClass with a given namespace and a given name which is located in the given MonoImage.
let kHandler = MonoApi.mono_class_from_name(img, Memory.allocUtf8String('MoreMountains.InfiniteRunnerEngine'), Memory.allocUtf8String('Jumper'));
// MonoApiHelper.ClassFromName을 이용해 class 탐색.
// let kHandler2 = MonoApiHelper.ClassFromName(img, 'GameManagerScript');
// 탐색한 클래스 내 필드 enumerate 및 print.
// var enumClassField = Enumerator.getFields(kHandler2)
// Enumerator.prettyPrint(enumClassField);
// 특정 필드값 출력.
// var test = Enumerator.getFieldValue(0xbe44ea90, 'boolean')
// console.log('test: ', test)
// Intercept the method.
// mono_compile_method 를 이용해 메서드를 강제 컴파일시켜서 후킹진행.
MonoApiHelper.Intercept(kHandler, 'SetPlayerData', {
onEnter: function(args) {
// args[0] 인자값은 self
let self = args[0];
// args[1] = ptr(0xa)
console.log("jump_ct: ", args[1]);
// args[2] = ptr(0x100)
console.log("damage: ", args[2]);
// args[3] = ptr(0x1);
console.log("gun_kankaku: ", args[3]);
// args[4] = ptr(0x1000);
console.log("gun_time: ", args[4]);
// args[5] = ptr(0x0);
console.log("firest: ", args[5]);
// this.instance = args[0];
// console.log("yay");
},
onLeave: function(retval) {
}
});
npm run build 명령어로 프리다 스크립트를 빌드합니다. 빌드된 스크립트는 dist\mono-hook.js 에 위치합니다.
※ npm run build 시의 동작을 변경하려면 package.json 파일을 수정해주면 됩니다.
빌드된 스크립트를 앱에 붙여서 분석을 시작합니다.
아무런 변조 없이 값만 찍은 경우 입니다. jump_ct 변수값이 0x1 이어서 한번밖에 점프를 하지 못합니다. 한번밖에 점프를 하지 못하니 초반에 게임난이도가 상당히 높게 느껴졌습니다.
점프 갯수가 게임플레이에 크리티컬해서인지 제일 높은 가격에 업그레이드 할 수 있군요.
jump_ct 값을 0xa(10)으로 변조하였습니다. 10번 연속해서 점프를 할 수 있기 때문에 높은 위치에 캐릭터가 떠 있는 것을 확인할 수 있습니다.
◼︎Xamarin Android 후킹
src\main.js 파일에 후킹 코드를 작성합니다.
아래 코드는 Hack The Box: SeeTheSharpFlag CTF의 Xamarin.Android 앱(https://app.hackthebox.com/challenges/seethesharpflag)에 대하여 "mscorlib.dll" 의 "System.IO.StreamReader.ReadToEnd" 메서드를 후킹하는 frida script 입니다.
- mscorlib.dll 파일의 "System.IO.StreamReader.ReadToEnd" 메서드
- main.js
import { MonoApiHelper, MonoApi } from 'frida-mono-api'
let status = Memory.alloc(0x1000);
// let hooked = false;
let mscorlib = MonoApi.mono_assembly_load_with_partial_name(Memory.allocUtf8String('mscorlib'), status);
console.log("loaded dll: ", mscorlib);
let img = MonoApi.mono_assembly_get_image(mscorlib);
console.log("img: ", img);
let kHandler = MonoApi.mono_class_from_name(img, Memory.allocUtf8String('System.IO'), Memory.allocUtf8String('StreamReader'));
console.log("kHandler: ", kHandler);
if(kHandler) {
MonoApiHelper.Intercept(kHandler, 'ReadToEnd', {
onEnter: (args) => {
console.log("[*] mscorlib.System.IO.StreamReader.ReadToEnd got called!");
},
onLeave: (retval) => {
var size = 0;
console.log("return string size: ", size = retval.add(2*Process.pointerSize).readInt());
console.log("[*] return: ", retval.add(3*Process.pointerSize).readUtf16String(size));
}
})
}
npm run build 명령어로 프리다 스크립트를 빌드합니다. 빌드된 스크립트는 dist/xamarin-unpin.js 에 위치합니다.
빌드된 스크립트를 이용하여 Target App에 attach하여 동적 분석을 진행하면 됩니다. "CLICK ON ME" 버튼 클릭 시 return 값에 flag가 찍힌 것을 확인할 수 있습니다.
※ 출처
https://github.com/freehuntx/frida-mono-api
https://www.hypn.za.net/blog/2020/04/19/hacking-unity-games-part-2-manipulating/