본문 바로가기

Android/Tech

[안드로이드] 유니티 게임 메모리 덤프 방법

 이번 편은 안드로이드 전체 이미지에 대한 덤프가 아닌 특정 프로세스의 메모리 영역을 덤프 하여 유니티 게임에서 사용되는 "Assembly-CSharp.dll" 또는 "libil2cpp.so" 파일을 추출하는 방법을 설명드리려고 합니다. 해당 방법은 게임 보호 솔루션에 의해 암호화된 파일을 복원하기 위해 복호화 로직을 일일이 직접 분석하기보다는 덤프를 이용하여 쉽게 원본 파일을 확보할 수 있습니다. 

1. gdb gcore
 우선 가장 보편적으로 알려져 있는 방법으로는 gdb의 gcore 명령어를 이용하여 해당 프로세스의 모든 메모리를 덤프한 뒤 WinHex의 "File Recovery by Type" 기능을 이용하여 PE 구조인 파일을 리스트업하고 원본 "Assembly-CSharp.dll" 추출하는 방법이 있습니다. 자세하게 설명드리고 싶지만 이미 친절하게 설명된 튜토리얼 글들이 구글에 굉장히 많습니다. 구글에 "gcore Assembly-CSharp" 검색어로 쉽게 확인할 수 있습니다.

2. fridump
 해당 방법도 대부분이 이미 알고 있는 방법으로 생각됩니다. 먼저 frida 설치 및 기본적인 세팅을 마친 뒤, 깃허브에서 fridump 스크립트를 받습니다. (링크 : https://github.com/Nightbringer21/fridump)
다운로드하였다면  
"fridump.py -u -s 패키지명" 다음과 같은 커맨드를 이용하여 손쉽게 프로세스 내 메모리를 덤프 할 수 있습니다. 위와 마찬가지로 WinHex를 이용하여 덤프된 데이터 중 원본 DLL 파일 또는 so 파일을 추출할 수 있습니다.

3. libmono.so의 mono_image_open_from_data_with_name 함수 후킹하기
MonoImage* (*mono_image_open_from_data_with_name) (char *data, unsigned int data_len, bool need_copy, MonoImageOpenStatus *status, bool refonly, const char * name);
 가장 확실하게 dll 파일을 덤프 할 수 있는 방법으로 libmono.so의 mono_image_open_from_data_with_name 함수를 후킹하여손 쉽게 원본 파일을 추출할 수 있습니다. 해당 함수는 mono 이미지를 로드할 때 사용되는 함수입니다. 유니티 모듈은 dll 파일을 로드할때 해당 함수를 호출하게 되는데 이때 인라인 후킹을 이용하여 추출할 수 있습니다. 먼저 함수를 호출하고 name 인자값이  "Assembly-CSharp.dll"일 경우 실제 사용되는 dll코드가 복사되어 있는 첫번째 인자값 data를 파일로 만들도록 코드를 구현해주면 됩니다. 밑에 예제 코드와 같이 원본 파일을 얻을 수 있습니다. 

1
2
3
4
5
6
7
8
9
10
int mono_image_open_from_data_with_name_mod(char *data, int data_len, int need_copy, void *status, int refonly, const char *name) {
    LOGD("mono_image_open_from_data_with_name, name: %s, len: %d, buff: %s", name, data_len, data);
    int ret = mono_image_open_from_data_with_name_original(data, data_len, need_copy, status, refonly, name);
    if(strstr(name,"Assembly-CSharp.dll")){
        LOGD("mono_image_open_from_data_with_name, buff: %s", data);
        saveFile(data, data_len,"Assembly-CSharp.dll");
    }
    return ret;
}

또한 모드앱 또는 보호 솔루션들은 해당 함수 내에 복호화 로직이 포함되어 있는 경우가 많습니다. 덤프를 시도하기 전에 해당 함수를 디컴파일 하여 어떠한 방법으로 복화화되는지 확인하는 것도 분석 방법 중 하나입니다. 소개해드린 방법 외에도 해당 함수를 반환하기 전에 복호화된 데이터를 가로채는 방법이 있습니다. memcpy 함수를 후킹하여 앞에 2바이트가 0x4D5A 이면 파일을 생성하여 떨구도록 하는 방법도 있어 후킹 프레임워크(Frida, Xposed 등)로 쉽게 원본 파일을 확보할 수 있습니다. 그 외에도 반환값인 image의 raw_data 필드를 가져와서 파일을 만들어도 됩니다. 이렇듯 해당 함수 로직만 분석한다면 쉽게 원본 파일을 확보할 수 있습니다.

mono_image_open_from_data_with_name [1]
mono_image_open_from_data_with_name [2]

본문에 소개해드린 방법 외에도 다양한 방법들이 존재하며 각각 환경에 맞는 덤프방법을 이용하시면 됩니다. 
 대부분의 방법은 ptrace 디버깅을 이용하여 메모리를 추출하는 방법을 사용 할 텐데 대다수의 보호솔루션은 실제 메모리가 로드되기 이전에 안티디버깅(ptrace) 또는 안티덤프(inotify)가 적용 되어있어 추출이 까다로워질 수 있습니다. 이런경우 직접 보호솔루션을 코드 패치하거나 해당 함수들을 후킹하여 정상적인 작동이 안 되도록 조작한다면 실제 원본 데이터를 얻을 수 있을 것입니다. 
또한 원본 파일(데이터)를 얻는다고 하더라도 요즘에는 암호화 + 메타데이터 조작 + 스톨른바이트 + 가상화 기법이 사용되어 코드 변조가 힘들며 우회하기 위한 동작 로직 분석과 변조가 까다로울 수 있습니다. 그렇기 때문에 최근에는 DLL 파일을 코드 패치 하기보다는 unity 동작 함수를 후킹 하여 실제 게임 코드들을 변조하는 사례들이 늘어나고 있습니다. 유니티 동작 함수를 후킹 하는 코드를 구현해놓으면 번거로운 코드 패치 없이 변조가 가능할 것입니다. 모든 분야의 해킹이 그렇듯 공격 기법은 항상 다양해지지만 방어 기법은 안정성 등 필드에 배포되기 전 여러 상황도 고려해야 되기 때문에 더욱 어려운 듯합니다.