질문 정리

안드로이드 앱 난독화 알아보기

five2week 2024. 7. 14. 09:46

문제 상황

팀 개발자분이 회사 안드로이드 앱이 난독화가 필요할 것 같다고 하셔서, 준비를 하게 되었습니다. 이전에 했던 개인 프로젝트들을 스토어에 올릴 때, abb 파일을 올리면 Progard 혹은 R8을 사용해서 난독화된 코드를 사용하라는 경고 메세지를 본 적이 있습니다. 이번 기회에 앱 난독화는 왜 해야하는지, 어떻게 하는 것인지 정리하려고 합니다.

 

난독화

난독화는 코드의 가독성을 떨어뜨려, 소스 코드를 분석하거나 역공학하는 것을 어렵게 만드는 기법입니다. 난독화를 통해 코드의 변수명, 메서드명, 클래스명을 의미 없는 문자열로 변환하거나, 코드의 구조를 복잡하게 만들어서 코드를 읽기 어렵게 만듭니다. 이는 주로 소스 코드의 보안을 강화하고, 소스 코드의 무단 복제나 악의적인 사용을 방지하기 위해 사용됩니다.

 

R8 적용 시 장점

  1. 축소
    • 사용되지 않는 코드를 제거하여 애플리케이션의 크기를 줄입니다. 이는 불필요한 클래스, 메서드, 필드 등을 제거함으로써 가능합니다.
  2. 최적화
    • 바이트코드를 최적화하여 애플리케이션의 성능을 개선합니다. 불필요한 명령어를 제거하고, 코드의 실행 경로를 최적화합니다.
  3. 난독화
    • 클래스명, 메서드명, 변수명을 의미 없는 문자열로 변경하여 소스 코드를 읽기 어렵게 만듭니다. 이는 역공학을 어렵게 만들어 코드의 보안을 강화합니다.
  4. 사후 검증
    • 애플리케이션을 실행하기 전에 바이트코드를 검증하여 런타임 오류를 방지합니다.

 

R8 적용 시 단점

  1. 디버깅의 어려움
    • ProGuard가 코드를 난독화하면 메소드 이름과 변수가 변경됩니다. 이로 인해 크래시 로그가 읽기 어려워지고, 문제를 진단하거나 디버깅하는 것이 더 복잡해집니다. 이는 특히 외부 라이브러리나 프레임워크에서 발생하는 문제를 해결할 때 더욱 도전적일 수 있습니다.
  2. 설정의 복잡성
    • ProGuard를 효과적으로 사용하려면 세심한 설정이 필요합니다. 잘못된 설정은 앱의 정상적인 기능을 방해할 수 있으며, 중요한 클래스나 메소드가 난독화 과정에서 제외되지 않아야 하는 경우 이를 정확히 지정해야 합니다. 설정을 잘못하면 런타임 오류가 발생할 수 있습니다.
  3. 빌드 시간 증가
    • ProGuard를 프로젝트에 적용하면, 난독화, 최적화, 그리고 축소 과정이 추가되어 앱의 빌드 시간이 길어질 수 있습니다. 이는 개발 과정에서 시간이 더 많이 소요될 수 있음을 의미합니다.

 

R8 사용법

  1. R8 활성화
    • R8은 안드로이드 build.gradle 버전 3.4.0 이상에서 기본적으로 활성화되어 있습니다. 별도로 활성화할 필요는 없으며, build.gradle 파일에 필요한 설정을 추가하면 됩니다.
  2. ProGuard 설정 파일 구성
    • ProGuard 설정 파일 (proguard-rules.pro)을 사용하여 R8의 동작을 제어합니다.
    • 이 파일에서 난독화, 축소, 최적화와 관련된 규칙을 정의합니다. 예를 들어:
    • -keep class com.example.myapp.** { *; } -dontwarn com.example.myapp.**
  3. 빌드 파일 설정
    • build.gradle 파일에서 릴리즈 빌드 구성에 R8을 사용하도록 설정합니다. R8은 기본적으로 활성화되어 있으므로, ProGuard 파일을 지정하기만 하면 됩니다:
    • buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }
  4. 빌드 및 배포
    • R8 설정이 완료되면 애플리케이션을 빌드할 때 자동으로 R8이 실행되어 코드가 축소, 최적화 및 난독화됩니다.
    • 릴리즈 빌드를 실행하려면 다음 명령어를 사용할 수 있습니다:
    • ./gradlew assembleRelease

 

프로가드 파일 문법

1. 클래스, 메서드 및 필드 유지

특정 클래스, 메서드 또는 필드를 난독화 및 제거하지 않도록 유지할 수 있습니다. 이는 주로 반사(reflection)를 사용하는 경우, 또는 특정 API를 외부에서 호출하는 경우에 필요합니다.

# 모든 공용 클래스와 메서드를 유지합니다.
-keep public class * {
    public *;
}

# 특정 패키지의 모든 클래스와 메서드를 유지합니다.
-keep class com.example.myapp.** { *; }

# 특정 클래스의 이름을 유지합니다.
-keepnames class com.example.myapp.MyClass

 

2. 경고 무시

ProGuard는 코드 축소 및 최적화 중 경고를 발생시킬 수 있습니다. 이러한 경고를 무시하도록 설정할 수 있습니다.

# 특정 패키지의 경고를 무시합니다.
-dontwarn com.example.myapp.**

 

3. 코드 축소 방지

특정 클래스나 메서드를 제거하지 않도록 설정할 수 있습니다. 이는 주로 외부 라이브러리와의 호환성을 유지하기 위해 사용됩니다.

# 모든 애노테이션을 유지합니다.
-keep @interface **

# 특정 클래스의 모든 메서드를 유지합니다.
-keepclassmembers class com.example.myapp.MyClass {
    *;
}

 

4. 라이브러리 모드 설정

ProGuard를 라이브러리 모드로 설정하여, 프로젝트가 아닌 라이브러리의 코드를 난독화할 때 사용할 수 있습니다.

# 라이브러리 모드 활성화합니다.
-libraryjars /path/to/retrofit.jar
-libraryjars /path/to/okhttp.jar

*라이브러리 모드(Library Mode)

ProGuard가 라이브러리 코드를 처리할 때 사용하는 특별한 설정입니다. 이 모드는 라이브러리의 코드가 다른 애플리케이션에 의해 사용될 수 있음을 가정하고, 이를 고려하여 코드 축소 및 난독화를 수행합니다. 즉, 라이브러리 모드는 ProGuard가 "이 코드는 다른 앱에서도 사용될 거야, 중요한 부분은 손대지 말고 잘 보호해줘"라고 인식하게 만드는 설정입니다.

 

5. 인라인 및 코드 제거 제어

ProGuard가 인라인 및 코드 제거를 수행하는 방식을 제어할 수 있습니다.

# 인라인을 비활성화합니다.
-dontoptimize

*인라인 최적화 (Inlining Optimization)

인라인 최적화는 컴파일러가 성능 향상을 위해 메서드 호출을 메서드의 실제 코드로 대체하는 최적화 기법입니다. 즉, 작은 메서드의 호출을 제거하고 그 메서드의 본문을 호출 지점에 직접 삽입하는 과정입니다. 이를 통해 함수 호출의 오버헤드를 줄이고 실행 속도를 높일 수 있습니다.

즉, 이유가 없다면 그대로 활성화 시켜두면 될 것 같습니다.

 

만들어본 내 프로가드 설정 파일

# 서버 통신 관련 클래스와 인터페이스를 유지합니다.
-keep class com.example.app.data.model.** { *; }
-keep interface com.example.app.data.remote.ApiService.** { *; }

# 라이브러리와 관련된 클래스와 메서드를 유지합니다.
# Keep Compose runtime Composable functions
-keepclassmembers class * {
    @androidx.compose.runtime.Composable *;
}

-keepclasseswithmembers class * {
    @androidx.compose.runtime.Composable *;
}

# Keep Dagger Hilt classes and annotations
-keep class dagger.hilt.** { *; }

-keepclassmembers class * {
    @dagger.hilt.android.qualifiers.ApplicationContext *;
    @dagger.hilt.android.lifecycle.HiltViewModel *;
}

# Keep Retrofit classes and annotations
-keep class retrofit2.** { *; }

# Keep attributes for Retrofit
-keepattributes Signature, Exceptions, RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations

-keepclasseswithmembers class * {
    @retrofit2.http.* <methods>;
}

# Keep Firebase and Google Play Services classes
-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }

# Keep OkHttp Callback interface
-keep class okhttp3.Callback { *; }

# Keep AndroidX Lifecycle classes
-keep class androidx.lifecycle.ViewModel { *; }
-keep class androidx.lifecycle.LiveData { *; }

# Keep all classes extending Fragment
-keep class * extends androidx.fragment.app.Fragment { *; }

# Keep Gson classes and annotations
-keep class com.google.gson.** { *; }

-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.TypeAdapter
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer

-keepclassmembers,allowobfuscation class * {
    @com.google.gson.annotations.SerializedName <fields>;
}

# Keep Kotlin coroutines methods
-keepclassmembers class kotlinx.coroutines.** {
    public <methods>;
}

 

발생할 수 있는 오류 리스트

1. Missing classes detected while running R8.

에러 메시지에서 제공한 경로(C:\\...\\build\\outputs\\mapping\\debug\\missing_rules.txt)를 확인하여 어떤 클래스나 메소드가 누락되었는지 파악합니다. 추가적으로 필요한 keep 규칙이 있을 수 있습니다. 하지만 OkHttp 4.11.0 버전 이전이라면, 라이브러리 오류일 수 있습니다.

https://github.com/square/okhttp/issues/6258

 

Proguard Rules (OkHttp 4.7.2) · Issue #6258 · square/okhttp

Hi, I am getting a few warnings when building with DexGuard. Gradle: 5.6.4 Android Gradle Plugin: 3.6.3 OkHttp: 4.7.2 compile_sdk = 30 build_tools = "30.0.2" Current Proguard rules for OkHttp: # ==...

github.com

 

2. 빌드는 되는데, 서버 통신이 안되는 경우

요청까지는 잘 하는지 리턴으로 200이 돌아오는데 제가 만든 데이터 클래스를 사용하는 부분에서 못찾는 문제가 있었습니다. 데이터 클래스 또는 모델이 제대로 보존되지 않은 경우, Gson을 사용하여 JSON을 파싱할 때 필드 이름이 변경되어 올바르게 매핑되지 않을 수 있는 것 같습니다.

-keep class com.example.app.data.model.** { *; }
-keep interface com.example.app.data.remote.ApiService.** { *; }

 

3. e$default: java.lang.ClassCastException이 발생하는 경우

ClassCastException은 타입 캐스팅 문제이므로, 관련 클래스의 타입 정보가 제대로 보존되도록 ProGuard 규칙을 조정해야 합니다. Gson은 내부적으로 리플렉션을 사용하여 객체를 생성하고 데이터를 할당합니다. 리플렉션을 사용하는 경우, 클래스의 생성자, 메소드, 필드가 난독화 과정에서 이름이 변경되거나 제거되지 않도록 보존해야 합니다.

-keepclassmembers class * {
    <init>(...);
    private <fields>;
    private <methods>;
}

 

4. AGP가 8.0보다 높은 경우

8.0부터는 R8이 프로가드와 호환모드가 아닌 풀모드로 동작하기 때문에 규칙을 더 자세히 작성해야합니다.

https://developer.android.com/build/releases/gradle-plugin#default-changes

 

Android Gradle 플러그인 8.5 출시 노트  |  Android Studio  |  Android Developers

Android 스튜디오 빌드 시스템은 Gradle을 기반으로 하며 Android Gradle 플러그인에는 Android 앱을 빌드하는 데 사용하는 몇 가지 추가 기능이 있습니다.

developer.android.com

 

 

참고 사이트

앱 축소, 난독화 및 최적화  |  Android Studio  |  Android Developers

 

앱 축소, 난독화 및 최적화  |  Android Studio  |  Android Developers

사용하지 않는 코드와 리소스를 삭제하기 위해 출시 빌드에서 코드를 축소하는 방법을 알아보세요.

developer.android.com

Kotlin 버전에 필요한 D8 및 R8 컴파일러 버전  |  Android Studio  |  Android Developers

 

Kotlin 버전에 필요한 D8 및 R8 컴파일러 버전  |  Android Studio  |  Android Developers

각 Kotlin 버전의 최소 D8/R8 버전을 찾습니다.

developer.android.com