본문 바로가기

카테고리 없음

모바일 진단을 위한 안드로이드 구조

안드로이드 아키텍처

 

 

안드로이드 아키텍처는 다섯 계층으로 나눌 수 있다.

  1. 리눅스 커널 계층
  2. 하드웨어 추상화 (HAL) 계층
  3. Native 라이브러리/ 안드로이드 런타임 계층
  4. 프레임워크 계층
  5. 어플리케이션 계층

모바일 진단 시 가장 관련있다고 생각하는 곳은 네이티브와 런타임, 프레임워크이므로 세 계층에 대해서 중점적으로 보려고 한다.

 

 

안드로이드 런타임

안드로이드 런타임(Android RunTime, ART)은 안드로이드 OS에서 애플리케이션을 실행하기 위한 환경을 말한다.

 

자바는 소스코드 작성 후 컴파일 시 바이트코드로 변환된다.

바이트코드를 실행하는 가상 머신은 JVM(Java Virtual Machine) 인데 라이선스 문제로 인하여 구글에서는 Dalvik VM을 따로 개발하여 안드로이드에서 사용한다.

Dalvik VM은 안드로이드 5.0 롤리팝 버전 이후로 ART로 완전히 대체되게 된다.

DVM과 ART는 자바 바이트코드를 DEX(Dalvik Executable)로 변환 후 실행하게 된다.

 

모바일 진단 시 루팅 탐지 우회를 위해서 디컴파일된 자바 코드와 보안 솔루션이 있을 경우 .so 파일을 보면서 진단을 하게 된다. 자바 코드 디컴파일 시 주로 JADX라는 도구를 이용하게 되는데 apk 파일로부터 바이트코드인 dex 파일을 디컴파일하여 진단자가 자바 코드를 읽을 수 있게 해준다.

 

 

Native 라이브러리

네이티브 라이브러리는 Java로 작성된 애플리케이션과 직접적으로 상호작용할 수 있도록 하는 저수준의 라이브러리이다.

저수준에서 동작하기 때문에 C나 C++, 어셈블리 같은 언어로 작성되었으며, libc, SQLite 등 안드로이드에 필수적인 라이브러리가 포함되어있다.

 

안드로이드 핵심 구조 및 서비스를 구현할 때 네이티브 라이브러리를 사용(libc, SQLite 등)하며 네이티브 라이브러리는 메모리 손상에 취약하다는 단점이 있다.

그렇기 때문에 Frida를 이용하여 네이티브 라이브러리 등에 후킹을 걸어 변조가 가능하다.

 

애플리케이션 실행 시 보안 솔루션이 동작하여 악성 앱 탐지, 중간자 공격 방지, 루팅 탐지, 무결성 검사 등등의 기능을 수행하게 되는데  동적 파일 로드 과정을 숙지함으로써 모바일 보안 솔루션 우회 시 후킹 포인트와 시점을 파악하기 용이해진다. 대부분의 보안 솔루션은 .so 파일로 애플리케이션 내 내장되어있고 .so 파일은 네이티브 라이브러리로 만들어져 있기 때문에 IDA와 같은 리버스 엔지니어링 도구를 통하여 보안 솔루션에 후킹이 가능하게 된다.

 

 

Native  라이브러리의 동적 로딩 과정

네이티브 라이브러리를 이용하여 작성된 파일은 .so(Shared Object, 동적 라이브러리) 파일 형식으로 저장된다.(Window는 .dll)

so 파일은 네이티브 라이브러리와 C, C++ 언어를 이용하여 작성된다.

네이티브 라이브러리와 C, C++로 작성된 후 .c 확장자를 가지게 된 소스코드는 컴파일 후 .so 파일로 패키징되어 apk 내 lib 디렉터리에 배치된다.

 

 

다음은 안드로이드 내에서 .so 파일의 로드 및 실행되는 전체적인 과정을 시간 순으로 나타낸 것이다.

  1. 네이티브 라이브러리 로딩
    • 애플리케이션 내 자바 코드에서 System.loadLibrary("example_name.so")을 호출하여 네이티브 라이브러리 파일을 로드
  2. dlopen() 호출
      • System.loadLibrary() 함수 호출 시 dlopen()을 통해 네이티브 라이브러리가 메모리에 로드된다.
        dlopen 후킹 프리다 코드
        dlopen이 로드한 네이티브 라이브러리 명 출력

    • dlopen
      • 매개변수
        • filename: 로드할 동적 라이브러리 파일 명 또는 경로
        • flag: .라이브러리 로드 시 동작 방식 지정
      • 반환값
        • NULL
          • .so 파일이 존재하지 않을 때
          • .so 파일 로딩이 실패하였을 때
          • 오류 발생 시 dlerror()를 호출
        • NULL이 아닐 경우
          • void *형태의 shared object에 대한 handle 반환
          • so 파일을 정상적으로 로딩
          • so 파일이 이미 로딩됨
  3. JNI 초기화
    • JNI(Java Native Interface): 자바로 작성된 프로그램이 C와 C++로 작성된 프로그램과 상호 작용할 수 있게 해주는 인터페이스이며, 네이티브 라이브러리가 JVM에 로드될 때 호출
    • JNI 초기화 작업은 애플리케이션과 네이티브 코드 간 상호 작용을 설정하는 작업
    • JNI는 JNI_Onload() 함수를 호출하여 초기화 작업을 수행
  4. 네이티브 메소드 호출
    • 자바 코드에서 네이티브 메소드 호출하여 JNI가 네이티브 메소드의 주소를 찾아 실행
    • 네이티브 메소드는 안드로이드 애플리케이션의 클래스에서 native 키워드를 사용하여 네이티브 메소드를 선언
  5. dlsym
    • .so 파일 내 함수를 사용 시 해당 함수의 주소를 검색한다.

 

 

자바 API 프레임워크

안드로이드 시스템 자원과 하드웨어 기능을 개발자들이 Java API를 이용하여 접근하여 사용할 수 있도록 도와주는 계층이다. JNI에 의해 런타임과 네이티브 라이브러리가 Java로 추상화 되기 때문에 시스템 자원에 접근이 가능하게 된다.

 

안드로이드 애플리케이션은 4대 컴포넌트로 구성되어있다.

  1. 액티비티(Activity)
  2. 서비스(Service)
  3. 브로드캐스트 리시버(Broadcast Receiver)
  4. 컨텐트 프로바이더(Content Provider)

네 개의 컴포넌트 간 상호작용을 관리하는 것이 Intent이다.

각 컴포넌트는 인텐트를 사용하여 컴포넌트 간 데이터를 전달하고 상호작용한다.

인텐트와 컴포넌트의 관계

 

모바일 진단을 진행하며 자주 보고 연관있는 컴포넌트는 액티비티와 서비스이므로 두 컴포넌트에 대해 알아보려고 한다.

 

 

애플리케이션 컴포넌트

1. 액티비티(Activity)

사용자와 애플리케이션 간 상호작용하는 애플리케이션 단일화면을 의미한다.

애플리케이션의 화면을 나타내기 때문에 사용자 인터페이스를 담당하며 안드로이드 애플리케이션은 반드시 하나 이상의 액티비티를 포함하고 있어야 한다.

 

액티비티 생명주기

액티비티가 생성되는 순간부터 종료되는 순간까지의 상태 변화를 관리하는 과정을 생명주기라 한다.

애플리케이션 사용 중 다른 애플리케이션으로 전환 시 기존에 실행하고 있던 애플리케이션은 활동 상태가 변경되고 사용자가 돌아왔을 때 기존에 작동하던 위치로 돌아갈 수 있도록 해준다.

 

JADX를 통해 모바일 진단 시 생명주기 메소드를 자주보게 되는데 생명주기 메소드 안에 루팅 탐지 로직이 있는 경우도 있으므로 이에 대해 숙지하는 것이 좋다고 생각한다.

 

 

액티비티 생명주기 콜백 메소드

액티비티 생명주기

  1. onCreate()
    • 필수적으로 구현해야하는 메소드로 액티비티가 처음 생성될 때 호출된다.
    • 초기화 설정을 하는 메소드이며 전체 수명 주기 중 한 번만 발생한다.
    • 액티비티 최초 실행 시 루팅 탐지 관련 로직이 들어가기에 적합하다.
  2. onStart()
    • 액티비티가 사용자에게 보여지기 직전에 호출된다.
    • 화면 진입 시 UI를 그리기 직전에 필요한 작업을 수행한다.
  3. onResume()
    • 액티비티가 사용자와 상호작용 가능한 상태가 되었을 때 호출, 즉 액티비티가 화면에 보여지고나서 호출된다.
    • 다른 액티비티가 포커스를 가져가기 전까지(다른 액티비티가 화면에 나타나기 전까지) 이 상태를 유지한다.
  4. onPause()
    • 다른 액티비티가 포커스를 가져가려고 할 때 호출된다.
      • 액티비티가 화면에 있지 않지만 곧 다시 실행될 액티비티일 때 호출된다.
      • 기존 액티비티가 다른 액티비티에 가려도 화면에 기존 액티비티가 일부 나타나면 onPause() 상태가 된다. (대화 상자, 반투명 액티비티 등등)
        기존 액티비티를 다른 액티비티가 가려 onPause 상태가 되는 예시


      • 다시 액티비티가 활성화되면 onResume()이 호출된다.
  5. onStop()
    • 액티비티가 다른 액티비티에게 완전히 가려지는 경우나 액티비티의 활동이 종료될 시점에 호출된다.
    • 다른 애플리케이션을 실행할 메모리 부족 시 메소드 호출이 되지 않을 수도 있다.
    • 기존 액티비티를 다른 액티비티가 완전히 가려 보이지 않을 경우 호출된다.
  6. onRestart()
    • onStop() 상태의 액티비티가 다시 시작하기 전에 호출된다.
  7. onDestroy()
    • 액티비티가 완전히 종료되기 전에 호출된다.
      • 액티비티가에서 finish()가 호출되어 활동이 종료되는 경우
      • 사용자가 액티비티를 종료하는 경우

 

2. 서비스(Service)

사용자 인터페이스가 존재하지 않고 백그라운드에서 작업을 처리하기 위한 컴포넌트이기 때문에 사용자와 직접 상호작용하지 않는다.

다른 앱으로 전환되어 액티비티가 종료된 상태에서도 앱을 백그라운드에서 동작 시키기 위한 컴포넌트이다. 서비스가 동작하고 있는 상태일 때 메모리 부족 등과 같은 경우를 제외하고 프로세스가 종료되지 않게 해준다.

 

Service 유형

  1. 포그라운드 서비스
    • 사용자가 직접 볼 수 있는, 서비스의 동작을 인지할 수 있는 작업을 수행한다. 예를 들면 상단바에 띄워지는 알람(카카오톡 등)은 가시적이므로 포그라운드라고 한다.
  2. 백그라운드 서비스
    • 사용자가 볼 수 없는 작업을 수행한다. 서비스 우선순위가 낮기에 리소스가 부족할 경우 강제 종료될 수 있다.
  3. 바인드 서비스
    • bindService()를 호출하여 액티비티 같은 애플리케이션 컴포넌트가 서비스와 바인딩(연결)하여 서로 상호작용한다.
    • 개별적으로 운영이 가능한 액티비티와 서비스를 바인딩을 통해 서로 통신하게 할 수 있다.

 

Service 생명주기

서비스 생명주기

 

 

 

서비스 생명주기 콜백 메소드

 

startService()

  1. startService()
    • 서비스 실행
  2. onCreate()
    • 서비스 최초 생성 시 한 번만 호출
    • onCreate()가 호출되어 한 번 실행된 후 onStartCommand()가 호출됨
  3. onStartCommand()
    • 서비스를 시작하도록 요청하는 메소드
    • 해당 메소드가 실행되면 종료 메소드 호출 전까지 무한히 실행됨
    • 서비스의 중요한 내용들이 이곳에 작성됨
    • 메소드 중단 시 stopSelf()나 stopService() 메소드를 사용하여 중단
  4. onDestroy()
    • 서비스를 더 이상 사용하지 않거나 서비스 제거 시 호출
  5. stopService()
    • 다른 구성 요소가 서비스를 중단시킴

 

bindService()

  1. bindService()
    • 서비스에 바인딩
  2. onCreate()
    • 바인딩 서비스가 생성되지 않았으면 onCreate()를 호출
  3. onBind()
    • 다른 컴포넌트에서 bindService() 호출 시 실행됨
    • IBinder를 호출하여 클라이언트와 서비스가 상호작용할 수 있는 인터페이스 제공
  4. onUnbind()
    • unbindService() 실행 시 onUnbind() 호출
  5. UnbindService()
    • 서비스 언바인딩 시 호출