[악성앱 분석 보고서] 안드로이드 피싱앱 행위 분석

1. 개요

디지털 환경이 발전할수록, 악성 앱과 피싱 앱은 그 기법을 점차 고도화하며 사용자들을 노리고 있다. 특히 스마트폰 사용이 일상화된 지금, 안드로이드 기기를 표적으로 한 악성 앱들이 빠르게 증가하고 있다. 이 포스팅에서는 이러한 악성 앱들이 어떻게 작동하며, 어떤 방식으로 사용자에게 위협을 가하는지 실제 사례와 함께 분석한다.

이 글을 통해 일상에서 자주 접할 수 있는 악성 앱의 흔적을 알아차리는 방법과, 안전하게 모바일 기기를 사용하는 팁을 제공하여 사용자의 정보 보안 의식을 높이는 데 도움을 주고자 한다.

 

DISCLAIMER

이 분석은 특정 시점에서 관찰된 악성 행위에 대해 서술하고 있으며, 해당 앱의 전체 기능이나 미래에 발생할 수 있는 모든 변동 사항을 다루지 않습니다. 따라서 이후에 앱이 업데이트되어 새로운 악성 행위가 추가되거나, 다른 유사한 앱에서 유사한 악성 행위가 발견될 수 있습니다. 이 포스팅은 참고 자료로서 제공되는 것이며, 특정 앱의 악성 여부를 완전히 보증하거나 모든 가능성을 포괄하지 않음을 알려드립니다.

 

 

2. 분석앱

이 분석의 주요 대상은 'OO톡' 앱으로 안드로이드 플랫폼에서 작동하는 스피어피싱을 목적으로 한 피싱 앱이다.

 

 

3. 분석방법

악성행위를 위주로 분석하며, 두 가지 방법론을 이용하여 분석하였다.

단계 내용
정적분석 - 앱 디컴파일 및 코드레벨에서의 위험요소 식별
- 앱의 코드, 구조, 권한, 액티비티 등 기능 식별
동적분석 - 실제 기기 및 에뮬레이터에서 앱 실행
- 고객 요구사항 및 진단 제약사항 파악

 

 

4. 분석 요약

대상 앱은 사용자의 개인정보를 무단으로 수집하고, 이를 악의적 목적으로 활용하는 다양한 행위를 수행한다. 연락처 접근, 문자 목록 접근, 저장된 미디어 파일 접근  안드로이드 위험 권한을 요청하고 사용자의 문자 송수신 목록  민감한 데이터를 탈취하며, 공격자가 2 공격에 활용할  있는 다양한 정보를 공격자의 서버로 전송한다.

 

악성 행위 개요도

 

5. 분석 상세

 

5.1 정적 분석

 

5.1.1. 권한(Permission)

 

앱이 요청하는 권한은 단말에서 수행하는 행위의 범위를 나타낸다. ‘위험 권한 요청은 사용자의 개인 정보의 접근이나 단말 시스템의 주요 기능과 연관되어 있어, 악용될 경우 사용자의 보안에 위협이   있다. 권한의 선언만으로는 앱의 악성 행위 여부를 판단하기 어렵지만, 앱의 전반적인 행위와 결합하여 평가할  있는 지표이다. 대상 앱에서 선언하는 권한과 설명은 다음과 같다.

 

권한 선언 설명 위험 권한 여부
android.permission.INTERNET 인터넷 연결 일반
android.permission.READ_CONTACTS 연락처 정보 읽기 위험
android.permission.READ_MEDIA_IMAGES 저장된 이미지 읽기 위험
android.permission.READ_PHONE_NUMBERS 단말 휴대전화번호 수집 위험
android.permission.READ_PHONE_STATE 단말 상태 수집 위험
android.permission.READ_SMS 문자(SMS) 수신 목록 열람 위험
android.permission.RECEIVE_SMS 문자(SMS) 수신 위험

 

5.1.2. 액티비티(Activity)

 

액티비티는 안드로이드 앱의 화면 구성 단위로, 사용자와 상호작용을 위한 인터페이스를 제공한다. 액티비티 분석은 앱의 기능적 구성요소와 사용자 인터페이스 설계를 파악할 수 있다. 예를 들어, 앱 내의 로그인 화면, 메인 메뉴, 설정 화면 등은 각각 별도의 액티비티로 구현된다. 악성 앱의 경우, 액티비티를 통해 사용자를 속이기 위한 화면을 제공하고 숨겨진 기능을 통해 악성 행위를 수행할 수 있다. 대상 앱에 선언된 액티비티는 다음과 같다.

 

액티비티 선언 구현 기능 비고
bfaefadc.jcedeaed.bhcfhaie.loading 앱 로딩  
bfaefadc.jcedeaed.bhcfhaie.MainActivity 앱 메인 화면 메인 액티비티

 

5.2.1. 기타 컴포넌트

서비스(Service)나 리시버(Receiver) 등 기타 선언된 컴포넌트는 존재하지 않음을 분석하였다.

 


 

5.2 동적 분석

5.2.1. 사용자 직접 실행

 

  대상 앱의 동적 분석 과정에서 사용자에 의한 직접 실행을 기반으로 하는 앱의 동작을 분석한다. 앱이 사용자의 실행으로 어떻게 구동되고, 사용자와 상호작용에 반응하는지 분석한다. 분석의 시작점은 사용자가 앱을 실행하는 순간으로 설정한다. 앱의 초기 로딩, 권한 요청, 주요 기능 실행 등을 어떻게 처리하는지 분석한다. 앱의 실행 흐름을 따라가면서, 네트워크 통신, 데이터 저장  처리  앱의 핵심 동작을 실시간으로 모니터링한다. 특히, 의심스러운 네트워크 활동, 민감한 정보의 무단 수집  전송, 비정상적 시스템 자원 사용 등의 행위를 식별한다.

 

앱의 시작 지점인 메인 액티비티의 onCreate() 콜백 함수부터 분석하였다.

 

@Override  // android.app.Application
    public void onCreate() {
        super.onCreate();
        StubApp.m(this);
        StubApp.c = StubApp.d;
        this.k();
    }

 

StubApp 클래스의 m() 메소드를 호출하므로 이 함수를 분석한다.

 

    public static void m(Application application0) {
        String s = StubApp.efahfbbc("w7phxJXDvGXEg2FuZHJvaWQuYXBwLkxvYWRlZEFwa8O8ZcSDx5pixIE=");
        String s1 = StubApp.efahfbbc("Y8O8xIPDvGXEg2FuZHJvaWQuYXBwLkFjdGl2aXR5VGhyZWFkw7xlxIPDuu6fiMSD");
        Application application1 = StubApp.d;
        if(application1 != null && !StubApp.efahfbbc("YceaxIXDvGXEg2NvbS5zdHViLlN0dWJBcHDDvGXEg2TFq8SF").equals(application1.getClass().getName())) {
            try {
                Context context0 = application0.getBaseContext();
                StubApp.l(context0.getClass(), context0, new Object[]{StubApp.d}, StubApp.efahfbbc("ZWPEh8O8ZcSDc2V0T3V0ZXJDb250ZXh0w7xlxINh7p+IxIc="), new Class[]{Context.class});
                Object object0 = StubApp.g(context0.getClass(), context0, StubApp.efahfbbc("w7zFq8SJw7xlxINtTWFpblRocmVhZMO8ZcSDx5TDvMSJ"));
                StubApp.p(s1, object0, StubApp.efahfbbc("ZMO5xIvDvGXEg21Jbml0aWFsQXBwbGljYXRpb27DvGXEg2bHmsSL"), StubApp.d);
                ArrayList arrayList0 = (ArrayList)StubApp.h(s1, object0, StubApp.efahfbbc("x5RixI3DvGXEg21BbGxBcHBsaWNhdGlvbnPDvGXEg8ec7p+IxI0="));
                arrayList0.add(StubApp.d);
                arrayList0.remove(application0);
                Object object1 = StubApp.g(context0.getClass(), context0, StubApp.efahfbbc("YWXEj8O8ZcSDbVBhY2thZ2VJbmZvw7xlxIPHnMecxI8="));
                StubApp.p(s, object1, StubApp.efahfbbc("Y8O6xJHDvGXEg21BcHBsaWNhdGlvbsO8ZcSDx5rHmsSR"), StubApp.d);
                ApplicationInfo applicationInfo0 = (ApplicationInfo)StubApp.h(s, object1, StubApp.efahfbbc("ZsWrxJPDvGXEg21BcHBsaWNhdGlvbkluZm/DvGXEg8O57p+IxJM="));
                applicationInfo0.className = StubApp.d.getClass().getName();
                StubApp.d.onCreate();
            }
            catch(Exception exception0) {
                exception0.printStackTrace();
            }
            return;
        }
    }

 

난독화된 많은 문자열을 확인할 수 있다. 정적 분석 시 악성행위를 하는 문자열의 식별으 어렵게 하고 분석 시간을 오래 소요하는 데 목적이 있다. 그러나 난독화된 문자열을 복호화하는 함수(efahfbbc())가 동일 클래스 내 선언되어 있음을 확인한다.

 

public static String efahfbbc(String s) {
        try {
            return new String(Base64.decode(s, 0)).split("üeă")[1].replace("\\\'", "\'");
        }
        catch(Exception exception0) {
            return s;
        }
    }

복호화 함수 코드

 

복호화 함수 분석 결과, ‘efahfbbc’ 함수의 인자로 전달된 문자열을 Base64 디코딩하고 특정 구분자(“üeă”) 분할하고  번째 문자열을 반환한다. 문자열 예시와 반환결과는 다음과 같다.

 

"w7rDvMSRw7xlxIMuamlhZ3XDvGXEg8ecZcSR" -> "üeădecoded stringüeăpart1"

 

1.       "üeă" 분할: ["", "decoded string", "part1"]

2.       [1] 선택: "decoded string"

3.       "'" 치환: "decoded string" (치환할 내용이 없으므로 변경 없음)

 

따라서 난독화된 문자열을 복호화한 결과는 “decoded string”이다.

 

이외에도 문자열이 많은데 GPT에게 암호화된 문자열과 복호화 함수 코드를 제공하여 복호화를 요청한다.

 

난독화 문자열(분석 전) 복호화 문자열(분석 후)
Y8eaxJPDvGXEgy8uamlhZ3Uvc3VjY2Vzcy5mbGFnw7xlxIPDvMeaxJM= absolute/path/success.flag
w7rHmsSFw7xlxIMuamlhZ3XDvGXEg2JmxIU= another/part
YcO6xIfDvGXEgy8uamlhZ3UvbGliamlhZ3XDvGXEg2bHmsSH absolute/path/libnative.so
Y8eUxIvDvGXEgy8uamlhZ3UvdHJ5LmZsYWfDvGXEg8WrZMSL absolute/path/try.flag
YcO6xInDvGXEgy8uamlhZ3Uvc3VjY2Vzcy5mbGFnw7xlxINlx5bEiQ== absolute/path/success.flag

 

문자열 ‘s’ ‘s1’ 할당된 문자열은 ‘Stub.efahfbbc’ 함수를 통해 Base64 디코딩된다. 디코딩 결과는 각각 ‘android.app.LoadedApk’, ‘android.app.ActivityThread’이다. 앱이 정상 실행 중인 경우 if 조건 절을 만족하므로 조건절 내부가 실행된다. 여러 메서드를 호출하여 컨텍스트를 가져오고, 리플렉션을 통해 클래스와 메서드에 접근한다. 따라서 재작성한 m() 함수 디컴파일 코드는 다음과 같다.

 

public static void m(Application application0) {
    String s = "android.app.LoadedApk";
    String s1 = "android.app.ActivityThread";
    Application application1 = StubApp.d;
    if (application1 != null && !"com.stub.StubApp".equals(application1.getClass().getName())) {
        try {
            Context context0 = application0.getBaseContext();
            StubApp.l(context0.getClass(), context0, new Object[]{StubApp.d}, "setOuterContext", new Class[]{Context.class});
            Object object0 = StubApp.g(context0.getClass(), context0, "MainThread");
            StubApp.p(s1, object0, "InitialApplication", StubApp.d);
            ArrayList arrayList0 = (ArrayList) StubApp.h(s1, object0, "AllApplications");
            arrayList0.add(StubApp.d);
            arrayList0.remove(application0);
            Object object1 = StubApp.g(context0.getClass(), context0, "PackageInfo");
            StubApp.p(s, object1, "Application", StubApp.d);
            ApplicationInfo applicationInfo0 = (ApplicationInfo) StubApp.h(s, object1, "ApplicationInfo");
            applicationInfo0.className = StubApp.d.getClass().getName();
            StubApp.d.onCreate();
        } catch (Exception exception0) {
            exception0.printStackTrace();
        }
        return;
    }
}

이 함수 코드는 앱을 분석하기 전 리플렉션을 통해 앱 내부 상태를 조정하고 초기화하는 것임을 확인한다.

이후에는 변조된 앱의 실행을 막기 위해 무결성 검증을 실시한다.

 

public static String e(Context context0) {
        Signature[] arr_signature1;
        try {
            PackageManager packageManager0 = context0.getPackageManager();
            Signature[] arr_signature = new Signature[0];
            try {
                arr_signature1 = packageManager0.getPackageInfo("bfaefadc.jcedeaed.bhcfhaie", 0x40).signatures;
            }
            catch(PackageManager.NameNotFoundException packageManager$NameNotFoundException0) {
                packageManager$NameNotFoundException0.printStackTrace();
                goto label_10;
            }
            arr_signature = arr_signature1;
        label_10:
            if(arr_signature != null && arr_signature.length > 0) {
                return StubApp.i(arr_signature[0]);
            }
        }
        catch(Exception exception0) {
            exception0.printStackTrace();
        }
        return null;
    }

 

PackageManager 통해 설치된 패키지에 대한 정보를 가져오고 서명을 저장한다. “bfaefadc.jcedeaed.bhcfhaie”  패키지 서명을 가져오려고 하는데 PackageManager.GET_SIGNATURES(0x40) 플래그를 사용하여 서명 정보를 저장한다. 만약 해당 패키지를 발견하지 못하면  배열로 유지한다.

Stub.this.r() 함수를 호출한다. 이 함수도 난독화되어 있으나 설명은 생략하고 복호화된 코드로 바로 분석한다.

if (!activity.getPackageName().equals("com.example.somepackage") || !"expectedSignatureValue".equals(e)) {
    StubApp.this.r(activity, "someStringForTrueCondition", true);
} else if (!new StringBuilder("444444448888888888").toString().contains("4")) {
    StubApp.this.r(activity, "someStringForFalseCondition", false);
}

 

대상앱의 패키지명이 “com.example.somepackage” 일치하지 않으므로 조건문의  조건으로 실행한다.

 

5.2.2. 앱 실행 및 화면

 

앱 실행 화면

 실행  아로마요가 메시지와 하단의 앨범 열기 버튼을 확인할  있다. “앨범 열기 버튼을 터치하면 사용자에게 시스템 접근 권한을 요청한다.

 

 

 

연락처 접근, 전화걸기, 문자(SMS) 접근 권한을 요청하며, 권한을 모두 허용한 경우 앱이 정상적으로 동작한다. 허용하지 않는 경우 경고창을 띄우며 앱을 종료한다.

  

권한을 허용한 이후에는 화면변화 없이 사용자가 인지하지 못한 채 악성 행위가 동작한다. 먼저, 새로운 피해 단말이 등록되었음을 알리는 패킷을 공격자의 서버로 전송한다.

 

공격자의 서버로 패킷 전송

분석 시점 현재, 공격 서버가 동작하지 않아 응답은 확인할  없다.

 번째 패킷을 전송한 이후에는 단말에 저장된 문자 송수신 기록 전체를 전송한다.

 

공격자 서버로 문자 송수신 기록 전송 패킷

 

송수신한 문자 전체를 공격자의 서버로 전송하고 있음을 패킷 및 단말 로그를 통해 확인한다.

 

연락처 접근, 저장된 사진 접근  추가 위험행위를 위한 권한을 요청하고 있지만, 공격자 서버가 동작하지 않는 사유 등으로 추가 악성 행위는 식별되지 않았다.

 

6. 결론

 

사용자는 Google Play 스토어 외부에서 앱을 설치할 때 신뢰할 수 있는 출처에서만 다운로드 해야 한다. 만약 앱이 의심스러운 동작을 보이거나 악성 행위가 우려되는 상황이라면, 보안 전문가의 도움을 받아 정확한 진단과 대응 조치를 받는 것이 중요하다. 안전한 모바일 환경을 유지하기 위해 신중한 접근과 정보 보안 의식이 필요하다.

 

 

반응형