.init_array 후킹하기

.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 으로 찾을수가 없다.

e7191245-a927-4e49-a4a9-e1cd19319d1e

1. 동적 링커에서 오프셋 찾기 #

동적링커가 안드로이드에서는 linker64 라는 이름의 시스템 프로그램으로 존재하는데, 이 안에 있는 call_constructors 함수의 오프셋을 구해야한다.

c24bbc6b-86c6-4319-848b-3c47afb3f7d6

Ghidra 같은 디스어셈블러로 열어봐도 오프셋을 확인할 수 있고,
objdump -t linker64 | grep call_constructors 명령어를 사용할수도 있다. 둘다 같은 오프셋(0x4a258)을 가리키고 있다.

9df4a339-ecb9-43ac-b774-fc7b5dd203a1

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}
comments powered by Disqus