Why should I know this?

Android Hooking - Java layer 편 #1 본문

Knowledge/Android

Android Hooking - Java layer 편 #1

die4taoam 2022. 12. 7. 02:53

Frida 같은 도구가 Hooking을 통해 여러가지 인터페이스를 제공하죠.
이번 글에서는 Android의 Java Layer에서 어떻게 Hooking이 벌어지는지 아주 간단히 살펴보고자 합니다.

 

>Class 가 LOAD 되는 과정 입니다.

 

위 그림처럼 Class의 설계도는 .dex=안드로이드 파일에 보관되어 있습니다. class를 생성하는 과정에서 해당 설계도를 기반으로 ClassObject를 만듭니다. Class는 Variable/Method 로 이루어져있고 Variable은 ClassObject에, Method들은 Method 를 보관하는 테이블에 보관되며 해당 Method들에 대한 Index가 vtable에 보관됩니다.

 

위에서 언급한 과정을 좀더 자세히 도식화하면 위와 같습니다.

 

1. 첫 번째 Hooking 포인트는 ART에 존재하는 vtable입니다.

vTable을 만드는 과정은 간단합니다.
1. 생성하려는 자식 class의 부모 class의 vtable을 순서대로 자식 class의 vtable에 채워넣는다.
2. 채워진 vtable에서 만약 override 된 메소드가 있으면 자식 class의 method 를 채워넣는다.
3. 자식 class에 남은 method를 vtable에 채워넣는다.

 

위 과정의 결과로 Android에서 Method의 호출은 vtable의 index를 참조하는 것을 통해 수행될 수 있습니다.
일반적인 함수의 호출은 위의 과정을 통해 이뤄집니다.

 

	
	// art/runtime/mirror/object.h
	// C++ mirror of java.lang.Object
	class MANAGED LOCKABLE Object {
	 public:
	  // The number of vtable entries in java.lang.Object.
	  static constexpr size_t kVTableLength = 11;
	
	
	// art/runtime/mirror/class.h
	// Virtual method table (vtable), for use by "invoke-virtual".  The vtable from the superclass is
	// copied in, and virtual methods from our class either replace those from the super or are
	// appended. For abstract classes, methods may be created in the vtable that aren't in
	// virtual_ methods_ for miranda methods.
	HeapReference<PointerArray> vtable_;

	// The size of java.lang.Class.class.
	static uint32_t ClassClassSize(PointerSize pointer_size) {
	  // The number of vtable entries in java.lang.Class.
	  uint32_t vtable_entries = Object::kVTableLength + 67;
	  return ComputeClassSize(true, vtable_entries, 0, 0, 4, 1, 0, pointer_size);
	}


vtable은 export되지 않는 것으로 보입니다. 하지만 **class.h**에 존재하는 함수를 통해 offset을 역산할 수 있습니다. 
- Hooking 함수를 구현한 Object의 vtable entry를 검색하여 함수 주소 확보
- Hooking 하고자 하는 method의 vtable entry 를 찾아 교체

 

ART에서는 이렇게 class offset을 계산해서 참조하는 코드들이 많습니다. (이렇게 해도 되는거 맞지?)

 

2. 두 번째 Hooking 포인트는 code_item_offset 입니다

Android의 경우 Runtime으로 불리는 Interpreter가 실행을 제어합니다. JNI를 통해 Call로 시작되는 API를 실행하면 다음처럼 ArtMethod의 Invoke를 호출하는 과정을 거치게 됩니다. Android API들을 제공하는 DEX들은 예외없이 최적화 되어 있으므로 다음처럼 interpreting 을 직접 호출합니다.

void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
                   const char* shorty) {
  ...                 
  if (!IsStatic()) {
    (*art_quick_invoke_stub)(this, args, args_size, self, result, shorty);
  } else {
    (*art_quick_invoke_static_stub)(this, args, args_size, self, result, shorty);
  }

 

art_quick_invoke 들은 asm으로 구현되어 있습니다.

    /*
     * Quick invocation stub.
     * On entry:
     *   [sp] = return address
     *   rdi = method pointer
     *   rsi = argument array that must at least contain the this pointer.
     *   rdx = size of argument array in bytes
     *   rcx = (managed) thread pointer
     *   r8 = JValue* result
     *   r9 = char* shorty
     */
DEFINE_FUNCTION art_quick_invoke_stub
#if defined(__APPLE__)
    int3
    int3
#else
    // Set up argument XMM registers.
    leaq 1(%r9), %r10             // R10 := shorty + 1  ; ie skip return arg character.
    leaq 4(%rsi), %r11            // R11 := arg_array + 4 ; ie skip this pointer.
    LOOP_OVER_SHORTY_LOADING_XMMS xmm0, .Lxmm_setup_finished
    LOOP_OVER_SHORTY_LOADING_XMMS xmm1, .Lxmm_setup_finished
    LOOP_OVER_SHORTY_LOADING_XMMS xmm2, .Lxmm_setup_finished
    LOOP_OVER_SHORTY_LOADING_XMMS xmm3, .Lxmm_setup_finished
    LOOP_OVER_SHORTY_LOADING_XMMS xmm4, .Lxmm_setup_finished
    LOOP_OVER_SHORTY_LOADING_XMMS xmm5, .Lxmm_setup_finished
    LOOP_OVER_SHORTY_LOADING_XMMS xmm6, .Lxmm_setup_finished
    LOOP_OVER_SHORTY_LOADING_XMMS xmm7, .Lxmm_setup_finished
    .balign 16
.Lxmm_setup_finished:
    PUSH rbp                      // Save rbp.
    PUSH r8                       // Save r8/result*.
    PUSH r9                       // Save r9/shorty*.
    PUSH rbx                      // Save native callee save rbx
    PUSH r12                      // Save native callee save r12
    PUSH r13                      // Save native callee save r13
    PUSH r14                      // Save native callee save r14
    PUSH r15                      // Save native callee save r15
    movq %rsp, %rbp               // Copy value of stack pointer into base pointer.
    CFI_DEF_CFA_REGISTER(rbp)

    movl %edx, %r10d
    addl LITERAL(100), %edx        // Reserve space for return addr, StackReference<method>, rbp,
                                   // r8, r9, rbx, r12, r13, r14, and r15 in frame.
    andl LITERAL(0xFFFFFFF0), %edx // Align frame size to 16 bytes.
    subl LITERAL(72), %edx         // Remove space for return address, rbp, r8, r9, rbx, r12,
                                   // r13, r14, and r15
    subq %rdx, %rsp                // Reserve stack space for argument array.

#if (STACK_REFERENCE_SIZE != 4)
#error "STACK_REFERENCE_SIZE(X86_64) size not as expected."
#endif
    movq LITERAL(0), (%rsp)       // Store null for method*

    movl %r10d, %ecx              // Place size of args in rcx.
    movq %rdi, %rax               // rax := method to be called
    movq %rsi, %r11               // r11 := arg_array
    leaq 8(%rsp), %rdi            // rdi is pointing just above the ArtMethod* in the stack
                                  // arguments.
    // Copy arg array into stack.
    rep movsb                     // while (rcx--) { *rdi++ = *rsi++ }
    leaq 1(%r9), %r10             // r10 := shorty + 1  ; ie skip return arg character
    movq %rax, %rdi               // rdi := method to be called
    movl (%r11), %esi             // rsi := this pointer
    addq LITERAL(4), %r11         // arg_array++
    LOOP_OVER_SHORTY_LOADING_GPRS rdx, edx, .Lgpr_setup_finished
    LOOP_OVER_SHORTY_LOADING_GPRS rcx, ecx, .Lgpr_setup_finished
    LOOP_OVER_SHORTY_LOADING_GPRS r8, r8d, .Lgpr_setup_finished
    LOOP_OVER_SHORTY_LOADING_GPRS r9, r9d, .Lgpr_setup_finished
.Lgpr_setup_finished:
    call *ART_METHOD_QUICK_CODE_OFFSET_64(%rdi) // Call the method.
    movq %rbp, %rsp               // Restore stack pointer.
    POP r15                       // Pop r15
    POP r14                       // Pop r14
    POP r13                       // Pop r13
    POP r12                       // Pop r12
    POP rbx                       // Pop rbx
    POP r9                        // Pop r9 - shorty*
    POP r8                        // Pop r8 - result*.
    POP rbp                       // Pop rbp
    cmpb LITERAL(68), (%r9)       // Test if result type char == 'D'.
    je .Lreturn_double_quick
    cmpb LITERAL(70), (%r9)       // Test if result type char == 'F'.
    je .Lreturn_float_quick
    movq %rax, (%r8)              // Store the result assuming its a long, int or Object*
    ret
.Lreturn_double_quick:
    movsd %xmm0, (%r8)            // Store the double floating point result.
    ret
.Lreturn_float_quick:
    movss %xmm0, (%r8)            // Store the floating point result.
    ret
#endif  // __APPLE__
END_FUNCTION art_quick_invoke_stub

구시대 인사로서 x86 이 보기 더 편해 x86 asm을 가져왔습니다.

asm 코드는 인자로 받은 ART Method에서 특정 offset의 호출하는게 주요 코드입니다.

 

해당 코드는 다음과 같이 정의되어 있습니다.

ASM_DEFINE(ART_METHOD_QUICK_CODE_OFFSET_64,
           art::ArtMethod::EntryPointFromQuickCompiledCodeOffset(art::PointerSize::k64).Int32Value())

 

 돌고 돌아 ART_METHOD 입니다.

 protected:
  // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
  // The class we are a part of.
  GcRoot<mirror::Class> declaring_class_;

  // Access flags; low 16 bits are defined by spec.
  // Getting and setting this flag needs to be atomic when concurrency is
  // possible, e.g. after this method's class is linked. Such as when setting
  // verifier flags and single-implementation flag.
  std::atomic<std::uint32_t> access_flags_;

  /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */

  // Index into method_ids of the dex file associated with this method.
  uint32_t dex_method_index_;

  /* End of dex file fields. */

  // Entry within a dispatch table for this method. For static/direct methods the index is into
  // the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
  // interface's method array in `IfTable`s of implementing classes.
  uint16_t method_index_;

  union {
    // Non-abstract methods: The hotness we measure for this method. Not atomic,
    // as we allow missing increments: if the method is hot, we will see it eventually.
    uint16_t hotness_count_;
    // Abstract methods: IMT index.
    uint16_t imt_index_;
  };

  // Fake padding field gets inserted here.

  // Must be the last fields in the method.
  struct PtrSizedFields {
    // Depending on the method type, the data is
    //   - native method: pointer to the JNI function registered to this method
    //                    or a function to resolve the JNI function,
    //   - resolution method: pointer to a function to resolve the method and
    //                        the JNI function for @CriticalNative.
    //   - conflict method: ImtConflictTable,
    //   - abstract/interface method: the single-implementation if any,
    //   - proxy method: the original interface method or constructor,
    //   - other methods: during AOT the code item offset, at runtime a pointer
    //                    to the code item.
    void* data_;

    // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
    // the interpreter.
    void* entry_point_from_quick_compiled_code_;
  } ptr_sized_fields_;
  static constexpr MemberOffset EntryPointFromQuickCompiledCodeOffset(PointerSize pointer_size) {
    return MemberOffset(PtrSizedFieldsOffset(pointer_size) + OFFSETOF_MEMBER(
        PtrSizedFields, entry_point_from_quick_compiled_code_) / sizeof(void*)
            * static_cast<size_t>(pointer_size));
  }

 

과정을 살펴보는게 기네요. 이런 과정을 통해 Java Method가 호출되는걸 종종 뜨는 에러코드로도 확인할 수 있습니다.

    #05 pc 0000000000559f88  /system/lib64/libart.so (art_quick_invoke_stub+584)
    #06 pc 00000000000ced40  /system/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+200)
    #07 pc 0000000000280cf0  /system/lib64/libart.so (art::interpreter::ArtInterpreterToCompiledCodeBridge(art::Thread*, art::ArtMethod*, art::ShadowFrame*, unsigned short, art::JValue*)+344)
    #08 pc 000000000027acac  /system/lib64/libart.so (bool art::interpreter::DoCall<false, false>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, art::JValue*)+948)
    #09 pc 000000000052abc0  /system/lib64/libart.so (MterpInvokeDirect+296)
    #10 pc 000000000054c614  /system/lib64/libart.so (ExecuteMterpImpl+14484)

 

 

art_method는 method의 native의 구현체입니다. jni에서 reflect로 쉬이 참조 가능하구요. 해당 주소에서 EntryPointFromQuickCompiledCodeOffset 에 해당하는 곳에 원본 코드 주소가 보관되는 것이죠.
그러므로 이 값을 변경하는 식으로 hooking 을 할 수 있죠.

 

준비할 것은 컴파일된 코드겠네요.

 

 

3. 세 번째 Hooking 포인트는 method inline patch 입니다.

Hooking 을 할 때 가장 까다롭고 번거로운 방식은, 패치하고 싶은 곳의 Code를 직접 수정하는 겁니다.

 

대충 이런식입니다. (이미지 재탕 죄송;;;)

 

Instruction을 수정해서 Instrumentation 가능한 포인트를 만들고 따로 실행하는 방법입니다.

예를 들어 art_quick_invoke_stub 의  "call *ART_METHOD_QUICK_CODE_OFFSET_64(%rdi) // Call the method." 아래에 Instrumetation을 하면 결과를 수정할 수 있겠죠. 일반적으로는 호출되는 Code의 Entry를 수정하면 될 것 같습니다. 위에서 이미 EntryPointFromQuickCompiledCodeOffset 까지 다 계산할 수 있었으니까 말이에요.

 

이쯤오면 이미 범위를 한정하기 어렵기 때문에 의미가 없어집니다.

 

PLT/GOT 후킹을 막기 위한 숱한 방법이 있었겠지만 위처럼 code 자체를 수정해버리면 이를 탐지하거나 막는건 기술적으로 불가능한 상황에 와버리죠. 직접 만든 LIBRARY라 코드 전체의 변조 여부를 파악할 수 있는 경우를 제외하면요.

 

이상입니다.

 

 

위에서 나열한 방식들도 특정 조건만 충족된다면 모두 무력화 할 수 있는 방법은 있습니다.

다음에 생각이 나면 올려보도록 하겠습니다.

 

- 끗 -

 

ArtMethod hooking 관련 : https://github.com/PAGalaxyLab/YAHFA

 

GitHub - PAGalaxyLab/YAHFA: Yet Another Hook Framework for ART

Yet Another Hook Framework for ART. Contribute to PAGalaxyLab/YAHFA development by creating an account on GitHub.

github.com

ArtMethod code entry 를 후킹하는 코드.
ArtMethod hooking을 탐지하고 싶다면, 그러나 방지하고 싶다면 ArtMethod를 재구성하는 방법이 최선.

Comments