"IL2CPP로 빌드하면 안전하다"는 오해와 진실
유니티 게임 개발팀과 보안 이야기를 하다 보면 자주 듣게 되는 말이 있습니다. "저희는 IL2CPP로 빌드하고 있어서 코드 보안은 어느 정도 안심하고 있습니다." IL2CPP가 Mono보다 코드 보호에 유리한 건 사실입니다. 하지만 "안전하다"는 결론은 절반만 맞습니다.
IL2CPP는 C# 바이트코드를 네이티브 코드로 변환해 C# IL을 직관적으로 읽기 어렵게 만듭니다. 문제는 공격자가 IL을 굳이 읽을 필요가 없다는 점입니다. 빌드 산출물에 필연적으로 남는 메타데이터 파일 하나만으로 게임의 주요 클래스·메서드 구조를 상당 부분 복원할 수 있으며, 이를 이정표 삼아 런타임 후킹이나 바이너리 패치를 시도합니다.
이 글에서는 IL2CPP 역분석이 실제로 어떤 흐름으로 전개되는지, 왜 IL2CPP만으로는 부족한지, 그리고 빌드 무결성 검증이 왜 필요한지 살펴봅니다.
IL2CPP 빌드의 구조 — 무엇이 남는가
IL2CPP 빌드 프로세스는 다음과 같이 동작합니다.
[C# 소스 코드]
└─> IL2CPP 변환기 → C++ 소스 생성
└─> 네이티브 컴파일러 → libil2cpp.so / GameAssembly.dll (네이티브 바이너리)
+ global-metadata.dat (메타데이터 파일)
네이티브 바이너리(libil2cpp.so / GameAssembly.dll)는 기계어로 컴파일되어 있어 단순 디스어셈블만으로는 C# 원본 구조를 바로 파악하기 어렵습니다.
그러나 이 과정에서 global-metadata.dat가 반드시 함께 생성되어 패키징됩니다. 이 파일에는 클래스 이름, 메서드 이름, 필드 명칭, 문자열 리터럴, 타입 정보 등 게임 코드의 뼈대가 고스란히 담겨 있습니다. IL2CPP 엔진이 리플렉션·직렬화 등의 기능을 수행하려면 이 메타데이터가 필수이기 때문에, 상용 배포 빌드에도 반드시 포함됩니다.
공격자 입장에서 이 파일은 거대한 네이티브 바이너리의 미로를 파악하게 해주는 지도(Map)입니다.
IL2CPP 역분석의 실제 흐름
Il2CppDumper(libil2cpp.so + global-metadata.dat) ──> DummyDll(구조 복원) ──> 오프셋 특정 ──> 후킹·패치
1단계: 메타데이터 추출 및 더미 어셈블리 복원
Il2CppDumper 같은 공개 도구는 libil2cpp.so와 global-metadata.dat를 입력받아 DummyDll을 생성합니다. 내부 구현은 없지만 클래스·메서드·필드 이름과 시그니처가 복원된 어셈블리로, dnSpy 같은 .NET 디컴파일러로 열면 원래 프로젝트 구조를 트리 형태로 탐색할 수 있습니다.
2단계: 타깃 메서드 위치(Offset) 특정
덤프 과정에서는 이름 복원과 함께 각 메서드가 실제 바이너리의 어느 위치(RVA)에 있는지를 매핑한 정보(script.json)도 함께 출력됩니다. CheckSpeedHack, ValidateInventory, VerifyIAP처럼 보안·재화와 관련된 핵심 메서드의 정확한 위치를 손쉽게 알아낼 수 있게 됩니다.
3단계: 런타임 후킹 또는 바이너리 패치
위치를 알면 두 가지 경로가 열립니다.
- 런타임 후킹(동적 변조): Frida 등 동적 계측 도구로 해당 메서드의 진입점을 가로채 탐지 로직이 항상 정상을 반환하도록 조작합니다.
- 바이너리 패치(정적 변조): APK를 압축 해제한 뒤
libil2cpp.so의 해당 오프셋에서 분기 명령(B, BL 등)을 NOP으로 덮어써 검사 로직을 건너뜁니다. 수정한 APK는 재서명 후 재배포됩니다.
이 모든 과정이 고도화된 해킹 기술 없이도 공개 도구 조합만으로 가능하다는 것이 가장 큰 맹점입니다.
IL2CPP가 막지 못하는 것
IL2CPP의 보호 효과는 "C# 코드를 직접 읽고 수정하기 번거롭게 만드는 것"에 그칩니다. 다음은 IL2CPP가 채워주지 못하는 영역입니다.
- 메타데이터 기반 구조 복원:
global-metadata.dat가 보호 없이 동봉되는 한 클래스·메서드 구조는 언제든 복원될 수 있습니다. - 런타임 동적 후킹: C#이든 네이티브 코드든, 메모리에 로드된 함수를 가로채는 런타임 후킹 앞에서 컴파일 방식의 차이는 무의미합니다.
- 바이너리 패치 및 리패키징: 수정된 바이너리를 재서명해 재배포하는 것 자체를 IL2CPP가 막아주지는 않습니다. 이는 곧 MOD APK 확산으로 이어집니다.
빌드 무결성 검증 — 왜 필요하고 어떻게 동작하나
IL2CPP의 구조적 한계를 보완하려면 "현재 실행 중인 앱이 개발사가 서명해 배포한 원본과 동일한가"를 런타임에 능동적으로 검증하는 체계가 필요합니다.
(1) 서명 정합성 검증 정상 빌드에는 개발사의 고유 인증서 서명이 있습니다. 변조 후 재배포된 MOD 빌드는 이 서명이 달라질 수밖에 없습니다. 앱 실행 시 현재 서명 정보(Fingerprint)를 원본과 대조해 불일치하면 변조 빌드로 판단합니다.
(2) 바이너리 무결성 검증(해시 체크)
libil2cpp.so 같은 핵심 바이너리 영역의 해시를 빌드 시점에 계산해두고, 런타임에 같은 영역을 다시 해싱해 비교합니다. 단 한 바이트의 패치가 있어도 즉각 탐지됩니다.
(3) 검증 로직의 Native 격리 무결성 검증 코드 자체가 C# 계층에 있으면 앞서 본 메타데이터 덤프와 후킹으로 가장 먼저 표적이 됩니다. 검증 로직을 별도로 난독화된 Native C++ 계층에 분리해야 공격자가 감시자의 위치를 파악하고 우회하는 난이도를 실질적으로 높일 수 있습니다.
OZero Security는 이 원칙에 따라 서명·빌드 무결성 검증과 메타데이터 변조 탐지를 Native C++ 계층에 구현합니다. 검증 로직은 C# 레이어와 분리된 난독화된 바이너리에서 동작하며, Plus Add-on의 앱별 Native Variant를 적용하면 빌드마다 바이너리 구조가 달라져 한 게임의 분석 결과가 다른 게임에 재사용되는 것을 방지합니다. Pro Add-on의 텔레메트리로 서버 측에서 비정상 클라이언트를 실시간으로 식별·대응하는 것도 가능합니다.
요약 및 정리
- IL2CPP는 유니티 게임의 보안을 강화하는 유효한 수단이지만,
global-metadata.dat로 인해 구조가 복원되고 런타임 후킹·바이너리 패치로 검사 로직이 우회됩니다. - 대부분의 변조 빌드는 재서명을 수반하므로, 서명·바이너리 무결성 검증이 가장 강력한 1차 탐지 신호입니다.
- 단, 이 검증 로직이 C# 계층에 노출되면 역분석으로 패치될 수 있습니다. Native 계층으로 격리해야 공격 비용이 실질적으로 올라갑니다.
IL2CPP를 신뢰하는 것과 IL2CPP에만 의존하는 것은 다릅니다. 컴파일 방식 전환을 출발점으로 삼되, 메타데이터 보호·빌드 무결성 검증·검증 로직의 Native 격리를 함께 구성해야 실효성 있는 방어가 완성됩니다.