.init_array 후킹하기
2024년 11월 19일
.init_array? #
보통 라이브러리를 후킹할 때 android_dlopen_ext 함수를 이용하는데, onEnter 리스너에서는 라이브러리가 로드되지 않았기 때문에 라이브러리 경로밖에 알 수 없고, onLeave 리스너에서 Module.findBaseAddress() 등으로 모듈의 주소를 얻고 오프셋으로 후킹하게 된다.
라이브러리 호출 과정 #
AOSP에서 libcore, art, bionic 코드를 클론하면 따라갈 수 있다.
1# android15-qpr2-release로 확인한 결과
2System.loadLibrary("abc.so") # (boot.art) APK에서 호출
3 └─ getRuntime().loadLibrary0() # (boot.art) 앱 메모리에서 싱글턴으로 관리하던 런타임
4 └─ Runtime_nativeLoad() # (libcore::libopenjdk.so) 네이티브 함수 호출
5 └─ JVM_NativeLoad() # (art::libart.so)
6 └─ vm->LoadNativeLibrary() # (art::libart.so)
7 ├─ android::OpenNativeLibrary() # (art::libart.so)
8 │ └─ android_dlopen_ext() # (bionic::libdl.so) 이후에는 전부 동적 로더 코드이다.
9 │ └─ do_dlopen()
10 │ ├─ find_library()
11 │ └─ call_constructors()
12 │ ├─ .init # 현대 컴파일러에선 추가해주지 않는다.
13 │ └─ .init_array # 초기화 함수 리스트 1..n 까지 호출
14 └─ dlsym("JNI_Onload()") # dlsym 으로 로드한 라이브러리에서 찾아서 직접 호출
15
16
17# call_constructors() 에서...
18 call_function("DT_INIT", init_func_, get_realpath());
19 call_array("DT_INIT_ARRAY", init_array_, init_array_count_, false, get_realpath());
20
21# LoadNativeLibrary() 에서...
22 using JNI_OnLoadFn = int(*)(JavaVM*, void*);
23 JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
24 int version = (*jni_on_load)(this, nullptr);
안드로이드 앱 코드에서 System.loadLibrary() 를 호출하면 앱 가상메모리 상에서 싱글톤으로 들고있던 Runtime의 객체를 얻어와 네이티브 함수를 호출한다.
.init_array는 라이브러리가 로드될 때 가장먼저 실행되는 함수 배열로, android_dlopen_ext 호출 중에 실행되기 때문에 이 코드들의 후킹은 onLeave에서 후킹한다 하더라도 이미 초기화 함수들은 호출된 이후라는게 한계점이다.
라이브러리에서 확인하기 #
readelf -S <elfname> 로 확인할 수 있다.
배열의 size는 0x998 바이트이기 때문에 0x998/8 = 0x133 (307) 로 307 개의 함수가 초기화 함수배열 안에 들어있다는 의미이다.
1$ readelf -S libflutter.so
2Section Headers:
3 [Nr] Name Type Address Offset
4 Size EntSize Flags Link Info Align
5 [18] .init_array INIT_ARRAY 0000000000964918 00944918
6 0000000000000998 0000000000000008 WA 0 0 8
call_constructors 후킹 #
init, init_array는 결국 call_constructors 에서 호출되는데, 이 전에 이미 메모리 로드 과정은 완료됐지만 내부에서 init 함수들이 호출되기 때문에 call_constructors 의 onEnter에서 init 함수들의 후킹을 성공시킬 수 있게 된다.
이 call_constructors 는 동적링커의 영역이며, 외부에 노출된(export) 함수가 아니기 때문에 Module.findExportByName 으로 찾을수가 없다.
1. 동적 링커에서 오프셋 찾기 #
동적링커가 안드로이드에서는 linker64 라는 이름의 시스템 프로그램으로 존재하는데, 이 안에 있는 call_constructors 함수의 오프셋을 구해야한다.
Ghidra 같은 디스어셈블러로 열어봐도 오프셋을 확인할 수 있고,
objdump -t linker64 | grep call_constructors 명령어를 사용할수도 있다. 둘다 같은 오프셋(0x4a258)을 가리키고 있다.
2. call_constructors 후킹 스크립트 #
Module.getBaseAddress 로 linker64의 베이스 주소를 확인하고 오프셋을 더해 후킹을 하면 된다.
이후 onEnter 에서 원하는 라이브러리의 init_array 함수를 후킹하면, 함수가 호출되기 전에 후킹을 걸 수 있게 된다.
1function hook_linker_call_constructors() {
2 let linker64_base_addr = Module.getBaseAddress('linker64')
3 let offset = 0x4a258 // __dl__ZN6soinfo17call_constructorsEv
4 let call_constructors = linker64_base_addr.add(offset)
5 let listener = Interceptor.attach(call_constructors,{
6 onEnter:function(args){
7 console.log('hook_linker_call_constructors onEnter')
8 let secmodule = Process.findModuleByName("libmsaoaidsec.so")
9 if (secmodule != null){
10 hook_target()
11 listener.detach()
12 }
13 }
14 })
15}