.init_array 후킹하기

.init_array?

보통 라이브러리를 후킹할 때 android_dlopen_ext 함수를 이용하는데, onEnter 리스너에서는 라이브러리가 로드되지 않았기 때문에 라이브러리 경로밖에 알 수 없고, onLeave 리스너에서 Module.findBaseAddress() 등으로 모듈의 주소를 얻고 오프셋으로 후킹하게 된다.

라이브러리 호출 과정

AOSP에서 libcore, art, bionic 코드를 클론하면 따라갈 수 있다.

# android15-qpr2-release로 확인한 결과
System.loadLibrary("abc.so")                 # (boot.art) APK에서 호출
  └─ getRuntime().loadLibrary0()             # (boot.art) 앱 메모리에서 싱글턴으로 관리하던 런타임
      └─ Runtime_nativeLoad()                # (libcore::libopenjdk.so) 네이티브 함수 호출
          └─ JVM_NativeLoad()                # (art::libart.so)
              └─ vm->LoadNativeLibrary()     # (art::libart.so)
                  ├─ android::OpenNativeLibrary()  # (art::libart.so)
                  │  └─ android_dlopen_ext()       # (bionic::libdl.so) 이후에는 전부 동적 로더 코드이다.
                  │     └─ do_dlopen()
                  │        ├─ find_library()
                  │        └─ call_constructors()
                  │            ├─ .init            # 현대 컴파일러에선 추가해주지 않는다.
                  │            └─ .init_array      # 초기화 함수 리스트 1..n 까지 호출
                  └─ dlsym("JNI_Onload()")         # dlsym 으로 로드한 라이브러리에서 찾아서 직접 호출


# call_constructors() 에서...
  call_function("DT_INIT", init_func_, get_realpath());
  call_array("DT_INIT_ARRAY", init_array_, init_array_count_, false, get_realpath());

# LoadNativeLibrary() 에서...
  using JNI_OnLoadFn = int(*)(JavaVM*, void*);
  JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
  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 개의 함수가 초기화 함수배열 안에 들어있다는 의미이다.

$ readelf -S libflutter.so
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [18] .init_array       INIT_ARRAY       0000000000964918  00944918
       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
e7191245-a927-4e49-a4a9-e1cd19319d1e

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

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

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

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

9df4a339-ecb9-43ac-b774-fc7b5dd203a1
9df4a339-ecb9-43ac-b774-fc7b5dd203a1

2. call_constructors 후킹 스크립트

Module.getBaseAddress 로 linker64의 베이스 주소를 확인하고 오프셋을 더해 후킹을 하면 된다.

이후 onEnter 에서 원하는 라이브러리의 init_array 함수를 후킹하면, 함수가 호출되기 전에 후킹을 걸 수 있게 된다.

function hook_linker_call_constructors() {
    let linker64_base_addr = Module.getBaseAddress('linker64')
    let offset = 0x4a258           // __dl__ZN6soinfo17call_constructorsEv
    let call_constructors = linker64_base_addr.add(offset)
    let listener = Interceptor.attach(call_constructors,{
        onEnter:function(args){
            console.log('hook_linker_call_constructors onEnter')
            let secmodule = Process.findModuleByName("libmsaoaidsec.so")
            if (secmodule != null){
                hook_target()
                listener.detach()
            }
        }
    })
}

Comments

ESC
Type to search...