Android NDK - C Language Build ( Native )
Android에서 독립적으로 실행할 수 있는 Native Binary를 빌드하기 위한 방법이다.
보통의 경우에는 AndroidStudio에서 Java/Kotlin+Native의 형태로 많이 사용되지만, Native Binary만 독립적으로 사용해야 하는 경우에는 이 방법으로 빌드하면 된다.
나 같은 경우에는 Android System Exploit 연구를 하면서 C언어로 Exploit Code를 작성하고 빌드해야 하기 때문에 이 방법을 사용한다.
Android NDK 환경 설정
먼저 아래 링크에서 최신버전의 Android NDK를 다운로드 한다.
https://developer.android.com/ndk/downloads
호스트 플랫폼에 맞는 패키지를 다운로드하고 적당한 위치에 압축을 풀어준다.
나는 Linux 64bit 환경에서 진행했다.
그리고 압축을 푼 경로를 환경변수에 등록해줘야 한다.
터미널에서 export해서 등록하는건 일회성이기 때문에, 영구적으로 등록해주려면 ~/.bashrc에 위와 같이 등록해줘야 한다.
vi ~/.bashrc
편집기로 ~/.bashrc파일을 열어준 뒤, 적당한 위치에 아래 구문을 입력해주고 저장한다.
export PATH="$PATH:[Android NDK PATH]"
[Android NDK PATH]는 자신이 압축을 해제한 경로로 대체해서 입력한다.
나는 export PATH="$PATH:/root/android-ndk-r21"이 된다.
입력을 마쳤으면 저장하고 아래 명령어로 ~/.bashrc를 동기화 해준다.
source ~/.bashrc
위 명령어로 동기화 해주면 Android NDK 환경설정은 끝났다.
정상적으로 등록됐는지 확인하려면, NDK경로 외에 위치에서 ndk-build를 실행해보면 된다.
Native Build
Native Binary를 빌드하려면 아래 3개의 파일이 반드시 필요하다.
1. Android.mk
2. Application.mk
3. Sourcefile (.c)
그리고 빌드의 편의성을 위해서 선택적으로 Makefile도 작성해줘야 한다.
Android.mk와 Application.mk에 대한 작성문법은 Android 공식 문서에 자세히 설명되어 있다.
https://developer.android.com/ndk/guides/android_mk
여기서 설명하는 방법은 가장 기본적인 사용법이다.
이 사용법을 베이스로 해서 본인 상황에 맞게 옵션들을 추가해서 사용하면 되고, 특별한 경우가 아니라면 이대로 따라서 빌드하면 된다.
1. Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := JNI_output
LOCAL_SRC_FILES := JNI_src.c
include $(BUILD_EXECUTABLE)
위에서 LOCAL_MODULE, LOCAL_SRC_FILES값만 자신에게 맞게 수정해서 사용하면 된다.
각 구문들의 의미는 다음과 같다.
◾ LOCAL_PATH : = $(call my-dir)
- 현재 작업 디렉토리의 경로를 설정한다. $(call my-dir)은 Android.mk가 위치한 경로를 자동으로 가져온다.
◾ include $(CLEAR_VARS)
- Android NDK에 맞는 특수한 GNU Makefile을 가리키도록 CLEAR_VARS를 선언한다.
Makefile에서 LOCAL_MODULE, LOCAL_SRC_FILES 등에 설정된 파일들을 자동으로 삭제할 수 있도록 해준다.
◾ LOCAL_MODULE := [OUTPUT]
- 빌드 결과로 저장된 Output파일의 이름을 설정한다.
위에서는 JNI_output으로 설정하였다.
◾ LOCAL_SRC_FILES := [SRC_FILES]
- 소스파일의 경로를 설정한다.
소스파일은 여러 개일 수 있고, 띄어쓰기로 구분해서 입력해주면 된다.
Ex) LOCAL_SRC_FILES := src/main.c src/source_1.c src/source_2.c
◾ include $(BUILD_EXECUTABLE)
- 빌드 형식을 지정한다.
$(BUILD_EXECUTABLE)은 일반적인 실행파일로 빌드되고, $(BUILD_SHARED_LIBRARY)은 라이브러리(.so)로 빌드된다.
2. Application.mk
APP_ABI := arm64-v8a
APP_PLATFORM := android-24
APP_BUILD_SCRIPT := Android.mk
Application.mk에서는 빌드된 Binary의 플랫폼을 정의해줄 수 있다.
특별한 경우가 아니라면 위 그대로 사용해도 된다.
각 구문들의 의미는 다음과 같다.
◾ APP_ABI := [ARCH]
- 빌드된 App의 아키텍처를 설정해준다.
arm64는 arm64-v8a, arm은 armeabi-v7a, x86은 x86, x86-64는 x86_64, 모든 아키텍처는 all로 설정해주면 된다.
기본 값은 all로 설정되며, 띄어쓰기로 구분해서 여러 아키텍처를 설정할 수도 있다.
◾ APP_PLATFORM := [API LEVEL]
- App의 최소 API 수준을 설정해준다.
값을 설정하지 않으면 NDK에 맞는 최소 수준으로 설정되며, android-24는 Android7.0 수준을 의미한다.
◾ APP_BUILD_SCRIPT := [Android.mk PATH]
- Android.mk의 경로를 설정해준다.
경로를 설정하지 않으면 기본적으로 현재 경로의 jni/Android.mk로 설정되기 때문에, jni/Android.mk에 위치해 있는게 아니라면 경로를 설정해줘야 한다.
이 외에도 변수들이 많이 존재한다.
gcc 빌드옵션을 설정할 수 있는 APP_CFLAGS 등 본인에게 필요한 변수를 사용하면 된다.
https://developer.android.com/ndk/guides/application_mk
3. Sourcefile
#include <stdio.h>
int main()
{
printf("HELLO py0zz1 w0r1d!\n");
return 0;
}
소스코드는 간단하게 printf( )로 "HELLO py0zz1 w0r1d!"를 출력하도록 작성했다.
이제 ndk-build로 소스코드를 빌드해주면 된다.
NDK_PROJECT_PATH=. ndk-build NDK_APPLICATION_MK=./Application.mk
위 명령어로 소스코드를 빌드하면 libs/[ARCH]/위치에 위에서 설정했던 LOCAL_MODULE 값으로 바이너리가 생성된다.
정상적으로 빌드된 모습은 위와 같으며, 나는 libs/arm64-v8a/JNI_output으로 빌드된 바이너리가 생성됐다.
빌드된 바이너리를 보면 설정한대로 빌드된 것을 확인할 수 있다.
4. Makefile
C언어 특성상 결과를 확인하려면 컴파일을 해줘야 하고, 그러려면 위 명령어를 그때 마다 계속 입력해줘야 한다.
좀 더 편리하게 빌드할 수 있도록 Makefile을 작성하도록 하자.
NDK_BUILD := NDK_PROJECT_PATH=. ndk-build NDK_APPLICATION_MK=./Application.mk
all: build
build:
$(NDK_BUILD)
clean:
$(NDK_BUILD) clean
Makefile을 위와 같이 작성하고 Application.mk와 같은 경로에 저장해준다.
이제 make 명령어로 바이너리를 간편하게 빌드할 수 있다.
make clean 명령어를 입력하면 빌드된 바이너리를 삭제할 수 있다.
이런 방법으로 필요한 작업을 매크로화 시킬 수 있다.
예를 들면, adb push하는 작업을 Makefile에 등록해서 더욱 간단하게 진행할 수 있다.
NDK_BUILD := NDK_PROJECT_PATH=. ndk-build NDK_APPLICATION_MK=./Application.mk
# Binary Name from Android.mk
BIN := $(shell cat Android.mk | grep LOCAL_MODULE | head -n1 | cut -d' ' -f3) # +
BIN_PATH := libs/arm64-v8a/$(BIN) # +
all: build
build:
$(NDK_BUILD)
#+
push: build
adb push $(BIN_PATH) /data/local/tmp/$(notdir $(BIN_PATH))
clean:
$(NDK_BUILD) clean
adb shell rm /data/local/tmp/$(notdir $(BIN_PATH)) # +
기존 Makefile에서 '+'주석 달아놓은 구문만 추가되었다.
만약 본인이 arm64로 빌드하지 않았다면, BIN_PATH에서 arm64-v8a부분은 본인에게 맞게 수정해주면 된다.
make push를 입력하면 위와 같이 바이너리가 빌드되고 adb push로 빌드된 바이너리가 /data/local/tmp로 복사된다.
[그림 9]에서 /data/local/tmp에 push된 것을 확인할 수 있고, 바이너리도 정상적으로 실행된다.
make clean을 하면 로컬에 있는 바이너리와 Android기기에 있는 바이너리를 삭제한다.
Refer. https://github.com/bluefrostsecurity/CVE-2020-0041/blob/master/lpe/Makefile