[안드로이드 모의해킹] 메모리 내 중요정보 평문 노출

직접 연구하여 작성한 자료입니다. 공식 출처가 명시되지 않은 자료의 무단 복제, 사용을 금지합니다.
공격 기법은 학습용, 허가된 환경에서 실습 바랍니다. 실 운영망 대상 공격은 처벌받습니다. (정보통신망법 제48조 1항)

 

개요

 

안드로이드 앱은 실행 시 사용할 메모리 영역을 할당받는다. 메모리에는 앱의 실행 코드와 사용할 자원(함수, 변수, 리소스 등)이 저장된다. 안드로이드 런타임은 '페이징' '메모리 매핑'을 이용해서 메모리를 관리한다. 메모리 자원을 관리하기 위해 자바에서는 동적으로 할당했던 메모리 영역 중 사용하지 않는 메모리를 주기적으로 삭제하는 가비지 컬렉션(Garbage Collection, GC)을 수행한다. 운영체제 수준에서 메모리를 관리하기 때문에 개발자는 기능 개발에 집중할 수 있다. 하지만 앱에서 메모리를 할당하고 해제하는 시점과 위치를 예상할 수 없고 시스템에서 판단하기 때문에 메모리에 사용하지 않는 데이터가 계속해서 남아있을 수 있다.

사용자가 앱을 닫더라도 앱의 프로세스는 종료되지 않고 캐시에 저장된다. 또는 백그라운드에서 동작하는 앱은 화면이 표시되지 않는 상태에서도 실행을 유지할 수 있도록 메모리가 관리된다.  앱에서 사용하는 메모리는 앱의 화면 표시 여부에 관계 없이 앱의 생명주기에 따라 관리된다.

 

💡 더 알아보기
프로세스와 앱의 생명주기에 관한 설명은 공식문서를 참고한다.
https://developer.android.com/guide/components/activities/process-lifecycle?hl=ko

 

앱에서 사용된 변수는 메모리에 저장되며, 저장된 정보는 앱의 다양한 상태 변화에 따라 관리된다. 대부분의 정적 데이터는 프로세스 메모리에 매핑되어, 이를 통해 데이터를 프로세스 간에 공유할 수 있고 필요할 때 메모리 공간을 확보할 수 있다. 정적 데이터의 예시로는 달빅 코드, 앱 리소스, 네이티브 코드 파일(.so) 등이 있다.

 

자바의 정적 변수(static variable)는 클래스 수준에서 선언되며, 클래스 인스턴스의 생성 여부와 관계없이 하나의 복사본이 존재한다. 앱이 실행되는 동안 이 변수들은 계속해서 메모리에 남아 있으며, 해당 클래스에 접근 가능한 위치와 관계없이 사용할 수 있다. 만약 민감 정보가 정적 변수에 저장되는 경우, 해당 정보는 앱 실행 동안 메모리에 남아 있게 된다. 디버깅 도구를 통한 앱의 메모리 덤프 또는 스냅샷 생성 시 포함될 수 있다. 스냅샷은 현재의 메모리 상태를 사진을 찍듯이 그대로 복사해오는 기능으로, 자원의 할당 상태나 데이터 처리 과정에서의 메모리 구조를 포함한 복사본을 제공한다.

 

public class StaticReferenceLeakActivity extends AppCompatActivity {
    /*  
     * 정적으로 선언된 textView, activity 
     */
    private static TextView textView;
    private static Activity activity;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);
        
        textView = findViewById(R.id.activity_text);
        textView.setText("민감정보 예시");
        
        activity = this;
    }
}

 

 

정적으로 선언된 변수나 액티비티는 한번이라도 직접 또는 간접적으로 참조되면 액티비티가 종료된 이후에도 가비지 콜렉터에서 사용하지 않는 메모리를 회수하지 않을 수 있다. 또한 최적화되지 않은 외부 라이브러리를 사용도 문제를 일으킬 수 있는데, 특히 모바일 환경을 예상하지 않고 작성된 코드일 경우가 해당된다. C C++로 개발된 외부 라이브러리, NDK를 사용하는 경우 개발자가 직접 메모리 관리 함수를 사용하여 직접 메모리를 할당하고 사용해야 한다. 다음은 JNI를 사용하여 자바에서 C로 데이터를 전달하고, C 라이브러리에서 해당 데이터를 처리한 후 결과를 다시 자바로 변환하는 과정을 보여준다. 메모리 관리에 주의하며 예제를 살펴본다.

 

JNIEXPORT void JNICALL Java_com_example_MyClass_storeSensitiveData(JNIEnv *env, jobject obj, jstring j_name, jstring j_password) {
    struct SensitiveData {
        char name[64];
        char password[64];
    } data;
    // 자바에서 전달받은 문자열을 C 문자열로 변환
    const char *name = (*env)->GetStringUTFChars(env, j_name, 0);
    const char *password = (*env)->GetStringUTFChars(env, j_password, 0);
    // 문자열을 구조체에 복사
    strncpy(data.name, name, sizeof(data.name));
    strncpy(data.password, password, sizeof(data.password));
    // 데이터 처리 또는 다른 작업 수행
    // 데이터를 사용 후 초기화
    memset(&data, 0, sizeof(data));
    // 문자열 메모리를 해제
    (*env)->ReleaseStringUTFChars(env, j_name, name);
    (*env)->ReleaseStringUTFChars(env, j_password, password);
}

 

C언어에서는 malloc()함수와 free()함수를 사용하여 메모리를 할당하고 해제할 수 있다. 이 두 함수는 C언어 메모리 관리에 핵심적인 역할을 수행한다. malloc()함수는 필요한 메모리 크기를 인자로 받아 해당 크기의 메모리를 동적으로 할당하며, free()함수는 malloc()을 통해 할당된 메모리를 해제하는 데 사용한다. 예제 코드에서 구조체를 선언하고, malloc()함수를 사용하여 메모리를 할당한다. 데이터 사용이 끝난 이후에는 memset()함수를 사용하여 구조체를 초기화한다. memset()함수는 메모리 블록 내의 내용을 특정 값으로 설정하는 데 사용되며, 이를 통해 데이터를 초기화할 수 있다. 마지막으로, free()함수를 이용하여 할당된 메모리를 해제한다.

 

안드로이드 앱에서 사용자가 텍스트 입력 폼에 정보를 입력할 때, 입력 내용은 키보드 캐시에 저장되어 자동 완성 제안이나 예측 기능을 제공할 수 있다. 이는 사용자 편의에 도움을 주지만 비밀번호나 개인정보와 같은 민감 정보가 키보드 캐시를 통해 다른 앱으로 유출될 수 있는지 여부를 확인해야 한다. 액티비티의 레이아웃 파일에서 민감 정보를 입력 받는 폼 속성에 일반 텍스트(text)로 지정되어 있는 경우 사용자가 입력했을 때 키보드 캐시에 값이 저장된다. 이는 보안상 문제가 될 수 있으며, 민감한 정보를 입력 받는 필드에 대해서는 키보드 캐시에 저장되지 않도록 설정해야 한다.

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
     <EditText
        android:id="@+id/KeyBoardCache"
       <!-- 민감정보 입력 폼의 타입 변경 -->
        android:inputType="text" />
</LinearLayout>

 

진단방법

 

루팅된 단말에서는 사용자 또는 앱이 시스템 레벨에서 메모리에 접근할 수 있다. 루트 사용자는 메모리에 저장된 모든 데이터에 접근이 가능하기 때문에 민감한 정보가 평문으로 저장된 경우 유출될 수 있다. adb를 이용하여 단말의 메모리를 덤프하여 스냅샷을 기록할 수 있다. 메모리 스냅샷의 결과에서 평문으로 저장된 민감정보가 있는지 여부를 확인한다. 다음의 명령어를 사용한다.

 


adb 쉘 접속

PC > adb shell

 

메모리 덤프를 위해 프로세스 id(Process ID, PID)를 확인

ADB # ps | grep [패키지명]

 

‘dumpheap’ 명령으로 프로세스 메모리 덤프

ADB # ps | am dumpheap [PID] /data/local/tmp/memory_dump.hprof

 

메모리 덤프 결과 내 문자열만 추출하기 위해 strings 명령 사용

ADB # cd /data/local/tmp

ADB # chmod 777 memory_dump.hprof

ADB # strings memory_dump.hprof > memory_dump.txt

 

덤프 결과를 PC로 복사

PC > adb pull /data/local/tmp/memory_dump.txt C:\android

 

단말의 파일을 PC로 가져오는 'adb pull' 명령어를 사용한다. PC에 덤프 파일이 저장되었으므로 파일 편집기를 사용하여 분석하고 민감정보가 평문으로 저장된 부분이 있는지 확인할 수 있다. 앱에서 사용하는 메모리 영역 전체를 덤프한 결과이므로 파일의 크기가 크고 많은 양의 데이터가 존재할 수 있다. 파일 전체에 대한 전수조사는 어렵기 때문에 앱 내에서 사용자가 입력하거나 처리하고 있는 민감정보를 직접 검색하거나, 정규표현식(regular expression, regex) 등을 사용하여 규칙을 가진 문자열을 찾을 수 있다.

 

진단 시 주의사항으로는 앱의 기능을 충분히 사용한 후에 메모리를 확인하도록 한다. 사용자의 입력 또는 앱 내에서 처리되는 데이터가 메모리 영역에 저장되기 위해서는 기능의 사용이 필수적이며, 단순히 파일 시스템에 저장된 데이터는 기능을 사용하지 않는 경우 앱 사용중에도 메모리에 저장하지 않을 수 있다. 기능 사용 시 메모리 할당, 해제에서 발생하는 취약점이므로 기능 사용 전후로 여러번 덤프를 실행해볼 수 있다. 취약점 발견 사례는 사용자 계정정보 입력, 2차 비밀번호 입력, 네트워크 전송을 위한 버퍼 데이터 준비 관련 기능 등에서 발생한다.

 

취약여부 설명
취약 메모리 영역 내 단말 또는 사용자 민감정보가 평문으로 노출되고 있는 경우
양호 메모리 영역에 단말 또는 사용자 민감정보가 평문으로 노출되지 않는 경우

 

Fridump

프리덤프는 프리다 프레임워크를 사용하는 오픈 소스 메모리 덤프 도구이다. 프리다가 지원하는 다양한 운영체제에서 실행되는 바이너리의 접근 가능한 메모리를 덤프할 수 있는 기능을 제공한다. 4.11 절에서 다루었던 메모리 내 중요정보 평문 노출 취약점을 프리덤프를 이용하여 동일하게 확인할 수 있다. 제작자의 깃허브 페이지(https://github.com/rootbsd/fridump3)에서 다운로드 받을 수 있으며 파이썬 스크립트로 제공하고 있어 별도의 설치 과정 없이 실행만으로 결과를 확인할 수 있다. 실행할 파이썬 파일(fridump3.py)200줄이 되지 않으니 직접 스크립트를 분석을 하는 것도 권장한다.

프리덤프 깃허브 페이지

 

 

분석할 앱 프로세스 식별자(PID) 확인

PC > frida-ps -Uai

 

프리덤프를 이용한 메모리 덤프

PC > python fridump3.py -u -r [PID] -s

 

PC(예시) > python fridump3.py -u -r 4340 -s

 


⚠️ fridump 실행 시 에러 발생(unable to find remote frida-server)

파이썬으로 작성된 프리다 스크립트가 동작할 때 PC에 연결된 단말의 연결상태를 대기하지 않도록 설정되어 있어 발생하는 에러이다. 대기 시간(초)을 지정하는 방법으로 해결할 수 있다. 다운로드 받은 파일 중 ‘fridump3.py’ 파일의 79번째 행의 코드를 다음과 같이 수정한다.

수정 전

-      session = frida.get_usb_device(0).attach(APP_NAME)

수정 후

-      session = frida.get_usb_device(1).attach(APP_NAME)

 

프리덤프 실행 결과 파일

 

프리덤프를 실행한 위치에서 dump 폴더가 생성되고 앱의 메모리 주소에 해당하는 덤프가 파일로 생성된 것을 확인할 수 있다. 실행 시 -s 옵션을 지정하였기 때문에 문자열로 식별될 수 있는 메모리 값을 추출하여 단일 파일인 strings.txt에서 확인할 수 있다. 다만 방대한 양의 데이터가 포함되어 있으므로, 앱에서 직접 입력한 민감정보나 단말의 화면에 보이는 데이터를 검색하여 전후 위치를 분석하는 방법으로 확인할 수 있다.

민감정보가 앱의 메모리 영역에 평문으로 저장되어 있는지 여부를 확인하기 위함이므로 앱에서 관련 기능(로그인, 회원정보 수정 등)충분히 사용한 후에 메모리 덤프를 수행해야 함에 유의한다. 사용자 패스워드, 민감정보 등이 기능 사용 이후에도 초기화되지 않고 메모리에 남아 있는 경우 또는 암호화된 값이더라도 암·복호화 과정에서 메모리에 평문으로 저장되는 경우 취약으로 판단한다.

 

 

대응방안

 

민감정보는 평문으로 처리하지 않는다. 암호화를 적용하여 메모리 스냅샷 내 데이터를 분석하더라도 알아볼 수 없는 문자열로 처리한다. 암호화 시 메모리 내 데이터 평문 저장을 방지하기 위해 안드로이드에서 제공하는 암호화 모듈을 사용하거나, 안드로이드 키스토어(android keystore)를 사용할 수 있다. 키스토어는 하드웨어 보안 모듈을 사용하여 암호화 키를 컨테이너에 저장하고 키 추출을 어렵게 하며, 암복호화되는 내용이 메모리에 평문으로 저장되지 않도록 한다.

민감정보를 할당하는 변수 선언 시 정적 변수 선언을 하지 않는다. 불필요한 데이터를 정적 변수 내에 저장하지 않도록 하고, 가능한 경우 인스턴스 변수를 사용하여 데이터를 관리한다. NDK를 사용하는 경우 메모리 할당 함수에 대한 호출의 반환값을 저장하는 마지막 포인터의 수명이 끝나기 전에 해당 포인터 값을 free() 함수를 이용해서 해당 포인터를 할당 해제해야 한다. 민감정보를 처리하는 클래스나 컴포넌트를 사용한 이후에 메모리의 재활용 발생 시 의도치 않게 공개될 수 있으므로 기능을 사용한 이후에는 메모리를 초기화하여 메모리 영역에 존재하는 데이터를 널(null) 값으로 설정하거나 무작위 값으로 덮어쓰기한 후 메모리 할당을 해제해야 한다. 다음은 민감정보를 포함하는 변수를 사용하고 사용 후 초기화하는 예제이다.

char ch []=new char[10];
ch [0] = 'P';
ch [1] = 'a';
ch [2] = 's';
ch [3] = 's';
ch [4] = 'W';
ch [5] = 'd';
ch [6] = '!';

//사용 후 삭제
java.util.Arrays.fill(ch, (char)0x20);

 

브로드캐스트 리시버 등 기능은 필요 시에만 선언하고 사용한 이후에는 할당 해제하여 불필요하게 메모리를 사용하지 않는다. 액티비티 생명주기에 따라 일시정지 시점(onPause) 또는 제거 시점(onDestroy) 콜백 함수에 등록하여 구현할 수 있다.

취약점 발견에 따른 대응방안을 안내할 때, 개발자의 조치방법에 대한 문의가 많이 발생하는 항목이다. 변수 선언 및 사용 후 할당 해제까지 완료했음에도 메모리 덤프 시 민감정보가 평문으로 노출되는 경우이다. 다양한 경우가 있지만 외부 라이브러리를 불러와서 사용하는 경우 변수가 메모리 내에 저장되어 사용 후에도 남아있는 경우가 있다. 민감정보를 처리하는 액티비티 전체 과정에서 메모리 사용 방법에 대한 보안대책 수립이 필요한 항목이므로, 메모리 사용에 대한 모니터링이 필요하다. 안드로이드 스튜디오에서는 '메모리 프로파일러' 도구를 제공한다. 개발 시 디버깅하는 과정에서 앱에서 시간 경과에 따라 메모리를 할당하는 과정, 자바 객체 등을 실시간으로 확인할 수 있다. 앱의 메모리 할당을 기록한 다음 할당된 객체를 모두 검사하고, 각 할당의 호출 기록을 확인하고 바로 소스코드로 이동할 수 있다. 이를 참조하여 보안대책을 적용한다.

 

💡 더 알아보기
메모리 프로파일러 도구의 상세 정보는 안드로이드 공식 문서에서 확인할 수 있다.
https://developer.android.com/studio/profile/memory-profiler?hl=ko

 

민감한 정보를 입력하는 입력 폼에서는 textNoSuggestions 또는 textPassword와 같은 입력 유형을 사용하여 키보드 입력이 캐시 메모리에 저장되는 것을 방지한다. 다음은 메모리에 저장되지 않는 안전한 입력 폼을 사용하는 예제이다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
     <EditText
        android:id="@+id/KeyBoardCache"
       <!-- 민감정보 입력 폼의 타입 변경 -->
        android:inputType="textNoSuggestions" />
      <EditText
        android:id="@+id/password_good"
       <!-- 민감정보 입력 폼의 타입 변경 -->
        android:inputType="textPassword"/>  
</LinearLayout>

 

 

반응형