[안드로이드/iOS] SSL Pinning 구현과 Frida 우회 공격 방어 (Native Level)

금융 앱이나 핀테크 서비스를 개발하다 보면 모바일 보안(Mobile Security)은 선택이 아닌 생존의 문제입니다. 최근 우리 팀은 침투 테스트(Penetration Test) 과정에서 단순한 HTTPS 통신만으로는 Fiddler나 Charles Proxy를 이용한 중간자 공격(MITM)에 취약하다는 지적을 받았습니다. 더 심각한 문제는 루팅된 기기에서 Frida를 이용해 보안 로직 자체를 후킹(Hooking)하여 무력화시키는 공격 패턴이었습니다. 이 글에서는 안드로이드 보안iOS 보안을 위해 SSL Pinning을 강제하고, 네이티브 레벨(C/C++)에서 Frida 인젝션을 탐지하여 앱을 강제 종료시키는 실전 코드를 공유합니다.

공격 벡터 분석: 왜 HTTPS 만으로는 부족한가?

일반적인 HTTPS 통신은 시스템의 '신뢰할 수 있는 루트 인증서' 저장소(Trust Store)를 기반으로 동작합니다. 공격자가 사용자 기기에 '자신의 인증서'를 강제로 설치하면, 앱은 이를 신뢰하게 되고 모든 트래픽이 평문으로 노출됩니다. 이를 막기 위해 서버의 공개키 해시값을 앱 내부에 하드코딩하는 SSL Pinning이 필수적입니다.

하지만 단순히 Pinning만 적용해서는 안 됩니다. 공격자는 Frida와 같은 동적 분석 툴을 사용하여 okhttp3.CertificatePinner.check() 같은 검증 함수가 무조건 true를 반환하도록 메모리를 조작합니다. 따라서 우리는 Java/Swift 레이어가 아닌, 공격자가 분석하기 훨씬 까다로운 Native(C/C++) 레벨에서 Frida 방어 로직을 수행해야 합니다.

Critical Warning: 단순히 Java/Kotlin 단에서 PackageManager를 통해 'com.frida' 패키지명을 검색하는 방식은 이미 우회 방법이 널리 퍼져 있어 보안 효과가 거의 없습니다. 반드시 프로세스 메모리 맵이나 포트 스캔을 수행해야 합니다.

The Solution: Pinning 구현 및 Native 탐지

가장 강력한 방어 전략은 Network Layer에서의 PinningNative Layer에서의 탐지를 결합하는 것입니다. 아래는 OkHttp를 사용한 Pinning 구현과 JNI를 이용한 Frida 탐지 코드입니다.

1. Android SSL Pinning (OkHttp)

가장 대중적인 OkHttp 라이브러리를 사용한다면 CertificatePinner를 빌더에 추가하는 것만으로 구현 가능합니다. 해시값은 서버 인증서의 SPKI(Subject Public Key Info)를 SHA-256으로 해싱하여 추출합니다.

// Retrofit/OkHttp 설정
val certificatePinner = CertificatePinner.Builder()
    .add("api.yourdomain.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // Backup Key 1
    .add("api.yourdomain.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // Backup Key 2
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()

2. Native 레벨 Frida 탐지 (Android NDK)

Java 코드는 디컴파일이 쉽고 후킹이 용이합니다. 따라서 JNI(C/C++)를 사용하여 /proc/self/maps 파일을 스캔하고, Frida 관련 라이브러리(frida-agent, gum-js-loop)가 메모리에 로드되어 있는지 확인해야 합니다. 이 방식은 우회하기 위해 커널 레벨의 조작이 필요하므로 난이도가 급격히 상승합니다.

// native-lib.cpp
#include <jni.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <android/log.h>

#define LOG_TAG "SecurityCheck"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

// Frida가 사용하는 기본 포트 및 메모리 내 시그니처 확인
extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_security_NativeDetector_detectFrida(JNIEnv *env, jobject thiz) {
    
    // 1. TCP 포트 스캔 (Frida Server Default Port: 27042)
    FILE *fp = fopen("/proc/net/tcp", "r");
    if (fp != NULL) {
        char line[1024];
        while (fgets(line, 1024, fp)) {
            if (strstr(line, "69A2") != NULL) { // 27042 in Hex
                LOGD("Frida Server Port Detected!");
                fclose(fp);
                return JNI_TRUE;
            }
        }
        fclose(fp);
    }

    // 2. 메모리 맵 스캔 (/proc/self/maps)
    // Frida agent, gum-js-loop 등 의심스러운 라이브러리 로드 확인
    fp = fopen("/proc/self/maps", "r");
    if (fp != NULL) {
        char line[1024];
        while (fgets(line, 1024, fp)) {
            if (strstr(line, "frida") != NULL || strstr(line, "gum-js-loop") != NULL) {
                LOGD("Frida Agent Found in Memory!");
                fclose(fp);
                return JNI_TRUE;
            }
        }
        fclose(fp);
    }

    return JNI_FALSE;
}
Note: 위 C++ 코드는 JNI를 통해 Java단에서 주기적으로 호출하거나, 앱 시작 시점에 System.loadLibrary 직후 실행하여 탐지 시 exit(0) 또는 kill(getpid(), SIGKILL)로 앱을 즉시 종료시켜야 합니다.
보안 기법 방어 대상 구현 난이도 우회 난이도
HTTPS (Default) 단순 패킷 스니핑 하 (사용자 인증서 설치)
SSL Pinning (Java) MITM (Charles, Fiddler) 중 (Frida Hooking)
Native Frida Detection 동적 분석 및 후킹 상 (커널 레벨 조작 필요)

Conclusion

모바일 보안은 창과 방패의 싸움입니다. 완벽한 보안은 존재하지 않지만, SSL Pinning으로 네트워크 레이어를 잠그고 Native Level의 탐지 로직으로 분석 도구의 접근을 차단함으로써 공격 비용을 비약적으로 높일 수 있습니다. 특히 금융권 앱이라면 단순한 코드 난독화(Obfuscation)에 의존하지 말고, 위와 같은 능동적인 방어 체계를 반드시 구축해야 합니다. 공격자가 여러분의 앱을 포기하고 더 쉬운 먹잇감을 찾아 떠나게 만드는 것이 우리의 최종 목표입니다.

Post a Comment