naroSEC
article thumbnail

개요

안드로이드에서 C/C++ 언어로 작성된 코드를 네이티브(Native) 코드라고 하며, 이러한 네이티브 코드는 ".so" 파일로 컴파일되어 라이브러리 형태로 제공된다. 특히나, 보안이 잘 된 안드로이드 앱을 진단하다 보면 Java 코드를 분석하는 상황보다 네이티브 코드로 작성된 라이브러리 파일을 분석하는 일이 더욱 빈번하다. 이번 포스팅에서는 네이티브 코드로 작성된 라이브러리(.so) 파일 분석 시 알아야 할 기초 지식에 관하여 기술하고자 한다.


라이브러리 로드 프로세스

[그림 1]의 도식도는 안드로이드 앱에서 라이브러리가 호출되어 로드되는 과정을 표현한 것으로 해당 프로세스를 통해 앱이 어떠한 함수를 사용하여 라이브러리를 불러와 Java로 작성된 코드와 C/C++ 로 작성된 네이티브 코드를 연결하여 상호 운용을 하는지 알 수 있다.

[그림 1] 라이브러리가 로드되는 과정

[그림 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] 공통 레지스터리 설명


 

profile

naroSEC

@naroSEC

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...