ARM 기반의 네이티브(Native) 코드 분석에 관한 기초 지식
개요
안드로이드에서 C/C++ 언어로 작성된 코드를 네이티브(Native) 코드라고 하며, 이러한 네이티브 코드는 ".so" 파일로 컴파일되어 라이브러리 형태로 제공된다. 특히나, 보안이 잘 된 안드로이드 앱을 진단하다 보면 Java 코드를 분석하는 상황보다 네이티브 코드로 작성된 라이브러리 파일을 분석하는 일이 더욱 빈번하다. 이번 포스팅에서는 네이티브 코드로 작성된 라이브러리(.so) 파일 분석 시 알아야 할 기초 지식에 관하여 기술하고자 한다.
라이브러리 로드 프로세스
[그림 1]의 도식도는 안드로이드 앱에서 라이브러리가 호출되어 로드되는 과정을 표현한 것으로 해당 프로세스를 통해 앱이 어떠한 함수를 사용하여 라이브러리를 불러와 Java로 작성된 코드와 C/C++ 로 작성된 네이티브 코드를 연결하여 상호 운용을 하는지 알 수 있다.
[그림 1]의 각 단계에서 사용되는 함수들의 기능은 아래의 [표 1]과 같다.
함수 이름 | 기능 설명 |
android_dlopen_ext() dlopen() do_dlopen() |
이 세 가지 함수는 주로 라이브러리 파일을 로드하는 데 사용되며, android_dlopen_ext()의 경우 런타임에 공유 라이브러리를 동적으로 로드할 때 사용된다. dlopen() 함수를 사용하여 동적 라이브러리를 로드할 수 있지만, 안드로이드 플랫폼에서는 android_dlopen_ext() 함수를 사용하여 몇 가지 더 추가적인 기능을 수행할 수 있게 된다. 참고로 디바이스가 API 28 이상(Android 9.0)인 경우 android_dlopen_ext() 함수를 사용하고 그 미만인 경우 dlopen()을 사용한다. |
find_library() | find_library() 함수는 안드로이드 NDK에서 제공되는 함수로, 주로 동적 라이브러리 파일의 경로를 찾아서 반환해주는 역할을 한다. |
call_constructors() | call_constructors() 함수는 안드로이드 NDK에서 사용되는 함수 중 하나로, 네이티브 라이브러리의 생성자(constructor) 함수를 호출하는 역할을 한다. 생성자 함수는 네이티브 라이브러리가 로드될 때 실행되며, 초기화 작업을 수행하는 데 사용된다. |
init() | init() 함수는 네이티브 라이브러리가 로드되고 사용될 준비가 되었을 때 호출되는데, 주로 전역 변수 초기화, 리소스 로딩, 설정 등의 초기화 작업을 수행하기 위해 사용된다. |
init_array() | init_array() 함수는 컴파일러와 링커가 제공하는 기능으로, 개발자가 직접 호출하여 사용하는 것이 아닌 라이브러리나 프레임워크에서 사용된다. 이 배열에 등록된 함수들은 프로그램이 실행되기 전에 자동으로 호출되며, 주로 전역 변수 초기화나 라이브러리 초기화 등을 수행하는 데 사용된다. |
jni_onload() | Java Native Interface (JNI)를 사용하여 Java 언어와 C/C++ 언어 간의 상호 작용을 지원하한다. 즉, JNI는 Java 언어로 작성된 코드와 네이티브 언어로 작성된 코드를 연결하여 상호 운용성을 가능하게 한다. 주요 목적은 네이티브 라이브러리의 초기화 단계에서 필요한 작업을 수행하고, JNI 환경을 설정한다. |
[표 1] 모듈 로드 함수 설명
ARM 아키텍처 레지스터 공통 지식
아래의 [표 2]는 ARM 아키텍처 기반의 라이브러리 파일에서 사용되는 공통 레지스터리 정보이다.
레지스터 이름 | 설명 |
R0 ~ R12 | 임시 데이터를 저장하는 데 사용되는 범용 레지스터로 함수가 호출되면 R0-R3처음 네 개의 매개변수를 저장하는 데 사용되고 나머지 매개변수는 스택을 통해 전달된다. |
R13 (SP: Stack Pointer) | 스택 포인터는 현재 스택 프레임의 최상단을 가리키는 주소를 나타내며, 스택 영역에서 데이터의 추가 및 제거 작업에 사용된다. |
R14 (LR: Link Register) | 함수 호출과 반환을 관리하는 데 사용되는 레지스터로 호출된 함수에서 반환할 때 호출한 함수의 다음 명령어의 주소를 저장하는 역할을 한다. 이를 통해 함수가 종료되고 호출한 위치로 돌아갈 수 있게 된다. |
R15 (PC: Program Counter) | 다음에 실행할 명령어의 주소를 가리키는 레지스터로 현재 실행 중인 명령어의 주소를 저장하고, 다음에 실행할 명령어의 주소를 계산하여 업데이트 한다. 즉, 명령어의 순차적 실행을 제어하는 데 사용되며, 명령어의 실행이 끝나면 자동으로 다음 명령어의 주소로 증가하게 되는 방식이다. |
CPSR (Current Program Status Register) | CPSR 레지스터에는 다양한 상태 및 제어 비트가 포함되어 있으며, 프로세서의 동작을 제어하고 현재 실행 중인 프로그램의 상태를 나타낸다. 이 레지스터의 비트들에는 프로세서 모드 (User, Supervisor, IRQ, FIQ, 등), 프로세서 상태 (실행 중인 명령어 세트, 엔디안 등), 인터럽트 사용 여부, 프로세서 상태 플래그 등이 포함된다. |
FPSCR (Floating-Point Status and Control Register) | FPSCR 레지스터는 부동 소수점 연산에서 발생하는 상태와 제어 정보를 저장하며, 부동 소수점 연산이 발생하는 경우 이 레지스터의 값이 업데이트 된다. |
[표 2] 공통 레지스터리 설명
라이브러리 보안 기법
라이브러리 파일을 컴파일 하기 전에 빌드 도구를 이용하여 실행 코드를 보호할 수 있다. 대표적인 방법에는 패커, Linker, 안티 콜, ollvm 등의 기법이 있으며, 이를 통해 악의적인 사용자로의 리버스 엔지니어링과 같은 역공학 분석을 어렵게 만드는 것에 목적이 있다. [표 3]은 라이브러리에 적용되는 대표적인 보안 기법을 기술한 것이다.
모듈(라이브러리) 보안 기법 | 설명 |
라이브러리 패커 | 라이브러리 파일이 올바르게 디컴파일 및 디스어셈블되지 않도록 C/C++ 소스 코드에서 컴파일된 SO 파일을 압축한다. |
라이브러리 소스 코드 가상화 보호 매커니즘 | 공유 라이브러리 소스 코드를 가상화하는 기술을 사용하여 소스 코드를 보호한다. 이를 통해 소스 코드의 구조와 로직을 분석하기 어렵게 만들어 악의적인 사용자로부터 코드를 보호하거나 무단 접근을 방지할 수 있으며, 가상화된 코드는 원본 소스 코드와 다른 형태로 실행된다. |
라이브러리 Anti Call | 인증되지 않은 애플리케이션이 라이브러리 파일을 호출하고 실행하지 못하도록 라이브러리 파일에 권한을 부여하고 바인딩한다. |
라이브러리 링킹 | 코드 세그먼트, 기호 테이블 및 문자열을 포함한 전체 라이브러리 파일을 암호화 및 압축한 다음 런타임 시 메모리에서 해독 및 압축을 해제하여 라이브러리 소스코드 유출되는 것을 막는다. |
라이브러리 소스 코드 난독화 | 소스 코드의 가독성을 떨어뜨리고 분석을 어렵게 만든다. |
라이브러리 모니터링 | 대표적인 모니터링 모듈로 Anti-frida/xposed/root, anti-dynamic debugging, anti-simulator, anti-multi-opening 등이 있다. |
ollvm | ollvm은 Obfuscator-LLVM의 줄임말로, LLVM (Low-Level Virtual Machine) 프레임워크를 기반으로한 코드 난독화 및 변환 도구이다. LLVM은 컴파일러 및 코드 최적화 도구를 개발하는 데 사용되는 오픈 소스 프로젝트이며, ollvm은 LLVM을 확장하여 코드 난독화 기능을 제공한다. |
[표 3] 공통 레지스터리 설명