2. Frida-gum - Interceptor (Native 훅)

2. Frida-gum - Interceptor (Native 훅)

2024년 11월 4일

서론 #

출처: https://bbs.kanxue.com/thread-278423.htm
Frida interceptor 그림

예전에 정말 가고싶던 회사의 면접에서 Frida의 원리에 대해 어떻게 동작하는지 질문이 나왔었는데, 대답을 하지 못했던 기억이 있다.
여러가지 일이 많아서 미뤄뒀지만 이후에 취업하게된 회사에서 Frida 탐지를 위해 연구하던 중 좋은 글을 발견해서 이 글을 베이스로 Frida 소스코드를 보며 정리하려 한다.


frida-gum #

Repo: https://github.dev/frida/frida-gum/tree/16.5.6
Version: 16.5.6

1 에서도 잠깐 말했다시피 frida-gum은 특정 아키텍쳐의 타겟앱에 껌처럼 붙어서 네이티브 후킹관련 코드들을 추상화한 API를 제공한다.
레포 링크를 들어가보면 각 코드에 대한 테스트 코드들이 만들어져 있고, 출처의 글도 레포에 공개된 테스트 코드를 따라가면서 분석이 진행된다.

어차피 코드가 너무 길어져서 순서대로 보기는 어렵기 때문에 테스트코드, 공통 인터페이스 코드, 아키텍쳐 코드 이렇게 분류해서 최대한 순서대로 작성했다.

이 추상화된 API를 gumjs 가 js로 래핑하고, gumpp가 c++로 래핑(?) 해서 편하게 리스너 코드를 주입할 수 있도록 도와준다.


용어 #

  • 리스너 : 후킹할 때 실행하게 하는 onEnter, onLeave 함수.
  • thunk : 원래 의미는 작은 코드조각이지만, b 명령 등을 사용한 간접 호출용 함수를 의미한다.
  • PAC : 코드를 서명해두고 CPU가 해당 포인터로 이동할 때 검증해서 변경됐는지 확인하는 arm 의 보호 방식. ptrauth_sign_unauthenticated 함수를 호출해서 서명한다.
  • deflector : 먼 거리를 점프하기 위한 트램폴린

구조 #

frida-gum devkit #

직접 주입한 frida-gum 사용하기

frida-gum을 devkit으로 빌드하면 <빌드경로>/gum/devkit/libfrida-gum.a 정적 라이브러리와 frida-gum.h 파일이 생기는데 이걸 포함시켜 so 등의 바이너리를 빌드한 후 타겟 프로세스에 주입하게 되면 원하는 후킹 코드를 올리거나 실행시킬 수 있게된다. frida-gum만 사용하기 때문에 네이티브 후킹만 가능하다.


frida server에 포함되는 형태 #

frida-server를 실행하면 default로 /data/local/tmp/re.frida.server/ 경로에 여러 파일들을 풀어두게되는데, 여기에는 frida-agent-64.so 라는 파일이 포함되며
frida-server는 특정 포트를 열고 대기하다가 frida client 명령에 의해 frida-agent.so 를 타겟 앱에 주입하는 so_injector의 역할과 앱에 주입된 frida-agent.so 에 명령을 전달하는 메시징 서버 역할을 수행한다.


Interceptor #

테스트 코드 #

TESTLIST_BEGIN은 그냥 테스트 이름을 말하는거고, TESTENTRY 를 실행시키면서 테스트 결과를 확인한다.

왠진 모르겠지만 arm 모바일에 대한 테스트 케이스만 포함되어 있다.

c89ffe67-3239-4e44-97da-795d26b80c61

arm64는 총 두개의 테스트를 진행하는데, thunk 함수에 후킹할때와 일반 함수에 후킹할때 두가지이다.
결국 이 테스트들은 frida-gum 라이브러리에서 export 된 API를 테스트코드로 감싸 테스트 하는것이고, 번외 글에서 처럼 직접 인젝트 하는 방식에서는 이 API들을 사용할 수 있다.
frida-server에서는 분석가가 작성한 js를 agent에 요청하며 이 API를 호출해주는 방식으로 후킹요청을 할것이다

1TESTLIST_BEGIN (interceptor_arm64)
2  TESTENTRY (attach_to_thunk_reading_lr)
3  TESTENTRY (attach_to_function_reading_lr)
4TESTLIST_END ()

1. TESTENTRY #

간접호출 함수도 잘 후킹되는지, 일반함수도 잘 후킹되는지를 테스트하는 코드이다.
thunk 형식의 함수 테스트 코드도 있지만, 차이점은 테스트중 후킹할 테스트함수가 thunk 형식인지, 일반 함수 코드인지의 차이이다.

 1TESTCASE (attach_to_function_reading_lr)
 2{
 3  const gsize code_size_in_pages = 1;
 4  gsize code_size;
 5  GumEmitLrFuncContext ctx;
 6
 7  code_size = code_size_in_pages * gum_query_page_size ();
 8  ctx.code = gum_alloc_n_pages (code_size_in_pages, GUM_PAGE_RW);
 9  ctx.run = NULL;
10  ctx.func = NULL;
11  ctx.caller_lr = 0;
12
13  // 1. 메모리 패치 함수. code의 위치에 size만큼 보호를 해제하고
14  // code 주소와 변경할 데이터를 ctx로 전달받아 gum_emit_lr_func을 적용한다.
15  // gum_emit_lr_func: 테스트용 코드를 패치하는 함수
16  gum_memory_patch_code (ctx.code, code_size, gum_emit_lr_func, &ctx);
17
18  // 2. 함수가 잘 패치됐는지 확인. 
19  // ctx.run() 을 실행한 리턴값 (x0)와 패치 당시 ctx.caller_lr 값이 동일한지 검사
20  // 아래에서 gum_emit_lr_func 함수를 보면 왜 이런 작업을 하는지 알 수 있다. 
21  g_assert_cmphex (ctx.run (), ==, ctx.caller_lr);
22
23  // 3. ctx.func을 후킹해서 테스트용 onEnter, onLeave 리스너 연결
24  interceptor_fixture_attach (fixture, 0, ctx.func, '>', '<');
25  // 함수를 실행해보고 함수 실행 결과가 정상적인지 확인한다. 후킹했기 때문에 달라진다. 
26  g_assert_cmphex (ctx.run (), !=, ctx.caller_lr);
27  // 테스트용 리스너가 정상적으로 동작했는지 확인한다. 
28  g_assert_cmpstr (fixture->result->str, ==, "><");
29
30  // 4. 테스트 완료 후 인터셉터에 모든 리스너 detach
31  interceptor_fixture_detach (fixture, 0);
32  gum_free_pages (ctx.code);
33}

1-1. test_case > gum_emit_lr_func #

패치용 코드도 테스트용 코드이고, 전달받은 code 주소(mem)에 분기나 주소 서명 등 테스트용 명령어를 작성한다.

  • ctx->run = mem
  • ctx->func = "func_start" 레이블 이후
  • ctx->caller_lr = 패치 중 writer가 가리키는 aw.pc = run() 함수의 리턴값(x0)
 1static void
 2gum_emit_lr_func (gpointer mem,
 3                  gpointer user_data)
 4{
 5  GumEmitLrFuncContext * ctx = user_data;
 6  GumArm64Writer aw;
 7  const gchar * func_start = "func_start";
 8
 9// arm64 writer 초기화. mem 부터 writing 한다는 의미
10  gum_arm64_writer_init (&aw, mem);
11  aw.pc = GUM_ADDRESS (ctx->code);
12// ctx->run에 현재 명령어를 쓰고있는 함수주소 서명후 추가 
13// (내부에서 ptrauth_sign_unauthenticated 호출)
14  ctx->run = gum_sign_code_pointer (GSIZE_TO_POINTER (aw.pc));
15// push fp, lr
16  gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X19, ARM64_REG_LR);
17// bl "func_start" (아래에서 label 푸시함)
18// bl로 점프해서 런타임에는 이 다음 명령어 주소가 lr이 된다.
19  gum_arm64_writer_put_bl_label (&aw, func_start);
20// pc의 주소(현재 쓰고있는 메모리영역)을 저장한다. 
21  ctx->caller_lr = aw.pc;
22// pop fp, lr; ret;
23  gum_arm64_writer_put_pop_reg_reg (&aw, ARM64_REG_X19, ARM64_REG_LR);
24  gum_arm64_writer_put_ret (&aw);
25
26// ctx->func 에 현재 writer의 pc 주소를 저장
27// "func_start" label 부터 함수가 시작된다는 뜻
28  ctx->func = GSIZE_TO_POINTER (aw.pc);
29// "func_start" 레이블 추가 및 레지스터 저장, nop, 복원 후 리턴
30  gum_arm64_writer_put_label (&aw, func_start);
31  gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X19, ARM64_REG_X20);
32  gum_arm64_writer_put_nop (&aw);
33  gum_arm64_writer_put_nop (&aw);
34// mov X0, LR; pop fp, lr; ret;
35// X0에 현재 LR을 넣고 리턴한다. 
36// ctx->run() 의 리턴값은 LR(위에서 bl점프 전 명령어 다음 주소)
37// ctx->run() == ctx->caller_lr
38  gum_arm64_writer_put_mov_reg_reg (&aw, ARM64_REG_X0, ARM64_REG_LR);
39  gum_arm64_writer_put_pop_reg_reg (&aw, ARM64_REG_X19, ARM64_REG_X20);
40  gum_arm64_writer_put_ret (&aw);
41
42  gum_arm64_writer_clear (&aw);
43}

1-3. test_cast > interceptor_fixture_attach #

ctx에 테스트용 enter, leave 콜백 리스너를 등록하는데, 얘네는 그냥 onEnter, onLeave가 호출될 때 인자로 전달받았던 enter_char>, leave_char<를 결과에 이어 붙이기만 할 뿐이다.

초기화 이후 gum_interceptor_attach 함수를 호출하면서 attach 성공 실패 결과 반환 후 리턴한다.

 1static void
 2android_listener_context_on_enter (AndroidListenerContext * self,
 3                                   GumInvocationContext * context)
 4{
 5  g_assert_cmpuint (gum_invocation_context_get_point_cut (context), ==,
 6      GUM_POINT_ENTER);
 7// enter_char 를 문자열에 붙이기 
 8  g_string_append_c (self->fixture->result, self->enter_char);
 9
10  self->last_seen_argument = (gsize)
11      gum_invocation_context_get_nth_argument (context, 0);
12  self->last_on_enter_cpu_context = *context->cpu_context;
13
14  self->last_thread_id = gum_invocation_context_get_thread_id (context);
15}
16
17static void
18android_listener_context_on_leave (AndroidListenerContext * self,
19                                   GumInvocationContext * context)
20{
21  g_assert_cmpuint (gum_invocation_context_get_point_cut (context), ==,
22      GUM_POINT_LEAVE);
23
24  g_string_append_c (self->fixture->result, self->leave_char);
25
26  self->last_return_value = gum_invocation_context_get_return_value (context);
27}
28
29static GumAttachReturn
30interceptor_fixture_try_attach (TestInterceptorFixture * h,
31                                guint listener_index,
32                                gpointer test_func,
33                                gchar enter_char,
34                                gchar leave_char)
35{
36  GumAttachReturn result;
37  ListenerContext * ctx;
38
39  ctx = h->listener_context[listener_index];
40  if (ctx != NULL)
41  {
42    listener_context_free (ctx);
43    h->listener_context[listener_index] = NULL;
44  }
45
46  ctx = g_slice_new0 (ListenerContext);
47
48  ctx->listener = test_callback_listener_new ();
49  ctx->listener->on_enter =
50      (TestCallbackListenerFunc) listener_context_on_enter;
51  ctx->listener->on_leave =
52      (TestCallbackListenerFunc) listener_context_on_leave;
53  ctx->listener->user_data = ctx;
54
55  ctx->fixture = h;
56  ctx->enter_char = enter_char;    // '>'
57  ctx->leave_char = leave_char;    // '<'
58
59  result = gum_interceptor_attach (h->interceptor, test_func,
60      GUM_INVOCATION_LISTENER (ctx->listener), NULL);
61  if (result == GUM_ATTACH_OK)
62  {
63    h->listener_context[listener_index] = ctx;
64  }
65  else
66  {
67    listener_context_free (ctx);
68  }
69
70  return result;
71}

1-4. test_cast > interceptor_fixture_detach #

1static void
2interceptor_fixture_detach (TestInterceptorFixture * h,
3                            guint listener_index)
4{
5  gum_interceptor_detach (h->interceptor,
6      GUM_INVOCATION_LISTENER (h->listener_context[listener_index]->listener));
7}

추상화된 코드 #

아키텍쳐 특성에 따라 어셈블리 코드나 함수 호출 방식 등 바이너리가 다르기 때문에 frida-gum에서 함수를 추상화해서 하나의 인터페이스로 구현했다.

gum_memory_patch_code #

address 에 apply 함수를 적용해서 코드를 패치하는 함수이다.

 1gboolean
 2gum_memory_patch_code (gpointer address,
 3                       gsize size,
 4                       GumMemoryPatchApplyFunc apply,
 5                       gpointer apply_data)
 6{
 7  gsize page_size;
 8  guint8 * start_page, * end_page;
 9  gsize page_offset, range_size;
10  gboolean rwx_supported;
11
12  address = gum_strip_code_pointer (address);
13// 메모리 페이지 사이즈 계산
14  page_size = gum_query_page_size ();
15  start_page = GSIZE_TO_POINTER (GPOINTER_TO_SIZE (address) & ~(page_size - 1));
16  end_page = GSIZE_TO_POINTER (
17      (GPOINTER_TO_SIZE (address) + size - 1) & ~(page_size - 1));
18  page_offset = ((guint8 *) address) - start_page;
19  range_size = (end_page + page_size) - start_page;
20// rwx 메모리 권한이 가능하거나, 서는 mprotect로 권한 설정 후 apply 적용
21// 일부 플랫폼에서는 WX를 같이 설정하지 못하게 하는 W^X 정책이 있는데, 
22// 코드영역은 RX 권한이라 RW 권한으로 패치 후 나중에 다시 RX로 복원한다. 
23  rwx_supported = gum_query_is_rwx_supported ();
24  if (rwx_supported || !gum_code_segment_is_supported ())
25  {
26    GumPageProtection protection;
27
28    protection = rwx_supported ? GUM_PAGE_RWX : GUM_PAGE_RW;
29
30    if (!gum_try_mprotect (start_page, range_size, protection))
31      return FALSE;
32
33    apply (address, apply_data);
34
35    gum_clear_cache (address, size);
36// rwx가 지원되지 않으면 RX 권한으로 복원
37    if (!rwx_supported)
38      if (!gum_try_mprotect (start_page, range_size, GUM_PAGE_RX))
39        return FALSE;
40  }
41  else
42  {
43// 코드 세그먼트 방식으로 패치한다. (주로 탈옥되지 않은 darwin 플랫폼 환경)
44// 임시 메모리 페이지 생성 후 기존 명령어를 복사하고 그 위치에 apply 를 적용한다
45// 이후 새로만든 임시 페이지를 원래 메모리에 리매핑 한다.
46    GumCodeSegment * segment;
47    guint8 * scratch_page;
48
49    segment = gum_code_segment_new (range_size, NULL);
50    scratch_page = gum_code_segment_get_address (segment);
51    memcpy (scratch_page, start_page, range_size);
52
53    apply (scratch_page + page_offset, apply_data);
54
55    gum_code_segment_realize (segment);
56    gum_code_segment_map (segment, 0, range_size, start_page);
57
58    gum_code_segment_free (segment);
59
60    gum_clear_cache (address, size);
61  }
62
63  return TRUE;
64}

1-3-1. (공용) test_cast > interceptor_fixture_attach > gum_interceptor_attach #

함수 후킹, 리스너 attach 작업을 진행하는 frida-gum의 인터페이스

 1GumAttachReturn
 2gum_interceptor_attach (GumInterceptor * self,
 3                        gpointer function_address,
 4                        GumInvocationListener * listener,
 5                        gpointer listener_function_data)
 6{
 7  GumAttachReturn result = GUM_ATTACH_OK;
 8  GumFunctionContext * function_ctx;
 9  GumInstrumentationError error;
10
11  gum_interceptor_ignore_current_thread (self);
12  GUM_INTERCEPTOR_LOCK (self);
13  gum_interceptor_transaction_begin (&self->current_transaction);
14  self->current_transaction.is_dirty = TRUE;
15// 1. 후킹하려는 함수의 실제 포인터를 구한다.
16// sdk 29 (AOS 10) 이상 일때만 후킹 대상 함수 주소에 페이지단위로 RWX 권한 추가.
17// gum_softened_code_pages : 권한이 변경된 코드페이지 관리용 테이블 (중복방지)
18// 만약 인자로 전달한 function_address 가 가리키는 코드가 간접참조 코드라면, 
19// 참조하는 주소도 재귀적으로 적용해서 실제 function_address 리턴
20  function_address = gum_interceptor_resolve (self, function_address);
21// 2. DEFAULT 타입으로 함수 후킹을 설정하고 타겟 함수 컨텍스트 반환
22// 아래에서 자세히 설명
23  function_ctx = gum_interceptor_instrument (self, GUM_INTERCEPTOR_TYPE_DEFAULT,
24      function_address, &error);
25  if (function_ctx == NULL)
26    goto instrumentation_error;
27
28// 3. 같은 리스너가 이미 있는지 확인하고, 없으면 추가
29  if (gum_function_context_has_listener (function_ctx, listener))
30    goto already_attached;
31  gum_function_context_add_listener (function_ctx, listener,
32      listener_function_data);
33
34  goto beach;
35
36instrumentation_error:
37  {
38    switch (error)
39    {
40      case GUM_INSTRUMENTATION_ERROR_WRONG_SIGNATURE:
41        result = GUM_ATTACH_WRONG_SIGNATURE;
42        break;
43      case GUM_INSTRUMENTATION_ERROR_POLICY_VIOLATION:
44        result = GUM_ATTACH_POLICY_VIOLATION;
45        break;
46      case GUM_INSTRUMENTATION_ERROR_WRONG_TYPE:
47        result = GUM_ATTACH_WRONG_TYPE;
48        break;
49      default:
50        g_assert_not_reached ();
51    }
52    goto beach;
53  }
54already_attached:
55  {
56    result = GUM_ATTACH_ALREADY_ATTACHED;
57    goto beach;
58  }
59beach:
60  {
61// 4. 예약했던 작업을 적용하고, 데이터 정리하는 등 트랜젝션을 끝낸다.
62    gum_interceptor_transaction_end (&self->current_transaction);
63    GUM_INTERCEPTOR_UNLOCK (self);
64    gum_interceptor_unignore_current_thread (self);
65
66    return result;
67  }
68}

1-3-1-2. (공용) test_cast > interceptor_fixture_attach > gum_interceptor_attach > gum_interceptor_instrument #

 1static GumFunctionContext *
 2gum_interceptor_instrument (GumInterceptor * self,
 3                            GumInterceptorType type,
 4                            gpointer function_address,
 5                            GumInstrumentationError * error)
 6{
 7  GumFunctionContext * ctx;
 8
 9  *error = GUM_INSTRUMENTATION_ERROR_NONE;
10// 1. function_by_address 해시테이블에서 함수 컨텍스트 객체를 검색한다. 
11// 후킹이 처음인 경우 해시테이블에는 없을 것이다. 
12  ctx = (GumFunctionContext *) g_hash_table_lookup (self->function_by_address,
13      function_address);
14  if (ctx != NULL)
15  {
16    if (ctx->type != type)
17    {
18      *error = GUM_INSTRUMENTATION_ERROR_WRONG_TYPE;
19      return NULL;
20    }
21    return ctx;
22  }
23
24// 2. 후킹 백엔드가 아직 없는 경우 백엔드도 생성한다.
25// 이 백엔드는 아키텍쳐별로 구현된 _GumInterceptorBackend 구조체이며,
26// 코드 삽입을 위한 writer, relocator, allocator, mutex, thunk 등이 포함된 구조체이다. 
27  if (self->backend == NULL)
28  {
29    self->backend =
30        _gum_interceptor_backend_create (&self->mutex, &self->allocator);
31  }
32// 3. 후킹할 함수의 컨텍스트를 생성한다. 
33// 함수의 후킹 상태를 저장하는 컨텍스트이며,
34// 후킹이 완료될때 해시테이블에 저장하기 때문에 후킹 함수마다 컨텍스트는 하나만 생성된다. 
35  ctx = gum_function_context_new (self, function_address, type);
36// 4. 트램폴린을 생성하는데, 코드서명 강제화 여부에 따라 트램펄린 방식이 달라진다. 
37// 필요하다면 deflector 코드도 생성한다.
38// 자세한 내용은 아래의 함수 분석 글 참고
39  if (gum_process_get_code_signing_policy () == GUM_CODE_SIGNING_REQUIRED)
40  {
41    if (!_gum_interceptor_backend_claim_grafted_trampoline (self->backend, ctx))
42      goto policy_violation;
43  }
44  else
45  {
46    if (!_gum_interceptor_backend_create_trampoline (self->backend, ctx))
47      goto wrong_signature;
48  }
49// 5. 함수 컨텍스트를 해시테이블에 등록하고 
50// 다음부터 같은 함수를 후킹할땐 이 컨텍스트를 리턴해준다. 
51  g_hash_table_insert (self->function_by_address, function_address, ctx);
52
53// 6. 후킹 작업을 트랜잭션 단위로 관리하기 위한 스케줄링 함수
54// 후킹할 함수 컨텍스트에 gum_interceptor_activate 작업을 예약
55// gum_interceptor_activate 함수는 타겟 함수 코드를 변조하고 후킹을 활성화 하는 함수
56// 자세한 내용은 코드분석 확인
57  gum_interceptor_transaction_schedule_update (&self->current_transaction, ctx,
58      gum_interceptor_activate);
59
60  return ctx;
61
62policy_violation:
63  {
64    *error = GUM_INSTRUMENTATION_ERROR_POLICY_VIOLATION;
65    goto propagate_error;
66  }
67wrong_signature:
68  {
69    *error = GUM_INSTRUMENTATION_ERROR_WRONG_SIGNATURE;
70    goto propagate_error;
71  }
72propagate_error:
73  {
74    gum_function_context_finalize (ctx);
75
76    return NULL;
77  }
78}

1-3-1-2-6. test_cast > interceptor_fixture_attach > gum_interceptor_attach > gum_interceptor_instrument > gum_interceptor_transaction_schedule_update #

전달받은 func 를 예약하는 함수이다.
UpdateTask 객체에 함수 정보(ctx), addr을 설정하고 pending 배열에 추가한 뒤 pending_update_tasks 로 관리한다.
나중에 gum_interceptor_attach 함수 리턴 직전 트랜잭션을 마무리하며 한꺼번에 몰아서 처리하게 된다.

 1static void
 2gum_interceptor_transaction_schedule_update (GumInterceptorTransaction * self,
 3                                             GumFunctionContext * ctx,
 4                                             GumUpdateTaskFunc func)
 5{
 6  guint8 * function_address;
 7  gpointer start_page, end_page;
 8  GArray * pending;
 9  GumUpdateTask update;
10// 페이지 주소 및 경계 설정. 함수를 페이지 단위로 호출예약 하기 위해.
11// 메모리 보호모드 설정이 페이지 단위로 되기 때문임
12  function_address = _gum_interceptor_backend_get_function_address (ctx);
13  start_page = gum_page_address_from_pointer (function_address);
14  end_page = gum_page_address_from_pointer (function_address +
15      ctx->overwritten_prologue_len - 1);
16
17// start_page에 대한 update_tasks가 남아있는지 조회
18  pending = g_hash_table_lookup (self->pending_update_tasks, start_page);
19  if (pending == NULL)
20  {
21// 없으면 새롭게 삽입함 (첫 리스트임)
22    pending = g_array_new (FALSE, FALSE, sizeof (GumUpdateTask));
23    g_hash_table_insert (self->pending_update_tasks, start_page, pending);
24  }
25
26// 함수 정보 저장
27  update.ctx = ctx;
28  update.func = func;
29  g_array_append_val (pending, update);
30// 함수의 끝이 다음페이지로 넘어가는 경우 다음 페이지에도 걸쳐서 테이블에 넣어둠
31// 메모리 보호가 페이지 단위이기 때문에 다음페이지에 정보를 중복해서 넣어야됨 
32  if (end_page != start_page)
33  {
34    pending = g_hash_table_lookup (self->pending_update_tasks, end_page);
35    if (pending == NULL)
36    {
37      pending = g_array_new (FALSE, FALSE, sizeof (GumUpdateTask));
38      g_hash_table_insert (self->pending_update_tasks, end_page, pending);
39    }
40  }
41}

1-3-1-2-6 (2). test_cast > interceptor_fixture_attach > gum_interceptor_attach > gum_interceptor_instrument > gum_interceptor_activate #

실제 후킹 작업이 이뤄지는 함수. 후킹 타겟 함수에 필요한 트램폴린들을 삽입한다.

 1static void
 2gum_interceptor_activate (GumInterceptor * self,
 3                          GumFunctionContext * ctx,
 4                          gpointer prologue)
 5{
 6  if (ctx->destroyed)
 7    return;
 8
 9  g_assert (!ctx->activated);
10  ctx->activated = TRUE;
11
12  _gum_interceptor_backend_activate_trampoline (self->backend, ctx,
13      prologue);
14}

1-3-1-4. (공용) test_cast > interceptor_fixture_attach > gum_interceptor_attach > gum_interceptor_transaction_end #

위에서 attach를 위해 페이지 단위로 예약했던 작업들을 한꺼번에 적용한다.

 1static void
 2gum_interceptor_transaction_end (GumInterceptorTransaction * self)
 3{
 4  ...
 5  addresses = g_hash_table_get_keys (self->pending_update_tasks);
 6
 7// 위에서 예약했던 작업들을 전부 실행하는 로직이다.    
 8// 조건마다 코드가 조금씩 다르지만 큰 동작 자체는 동일하다. 
 9  if (gum_process_get_code_signing_policy () == GUM_CODE_SIGNING_REQUIRED)
10  {
11    for (cur = addresses; cur != NULL; cur = cur->next)
12    {
13      gpointer target_page = cur->data;
14      GArray * pending;
15      guint i;
16// 해시테이블에서 pending 리스트 가져옴
17      pending = g_hash_table_lookup (self->pending_update_tasks, target_page);
18
19      for (i = 0; i != pending->len; i++)
20      {
21// 실제 예약됐던 pending 태스크 전부 실행
22        GumUpdateTask * update;
23        update = &g_array_index (pending, GumUpdateTask, i);
24        update->func (interceptor, update->ctx,
25            _gum_interceptor_backend_get_function_address (update->ctx));
26      }
27    }
28  }
29// 이 이후엔 그냥 완료된 태스크들 정리하는 과정이 포함된다. 
30}

아키텍쳐 구현 (arm64) #

1. (arm64용) _gum_interceptor_backend_create #

아키텍쳐마다 후킹을 위한 backend 구조체를 생성 및 초기화하는 코드이다.
첫 후킹때만 백엔드가 생성되면서 호출된다.
코드 서명이 선택적인 경우 writer, relocator, thunk 까지 미리 설정한다.

 1GumInterceptorBackend *
 2_gum_interceptor_backend_create (GRecMutex * mutex,
 3                                 GumCodeAllocator * allocator)
 4{
 5  GumInterceptorBackend * backend;
 6
 7  backend = g_slice_new0 (GumInterceptorBackend);    // 동적할당 후 0으로 초기화함
 8  backend->mutex = mutex;
 9  backend->allocator = allocator;
10
11  if (gum_process_get_code_signing_policy () == GUM_CODE_SIGNING_OPTIONAL)
12  {
13    gum_arm64_writer_init (&backend->writer, NULL);
14    gum_arm64_relocator_init (&backend->relocator, NULL, &backend->writer);
15
16    gum_interceptor_backend_create_thunks (backend);
17  }
18  return backend;
19}

1-1. (arm64용) _gum_interceptor_backend_create > gum_interceptor_backend_create_thunks #

백엔드를 초기화할때 호출되며 트램폴린을 미리 작성하는 코드이다.
backend->thunks 구조체에 공간 할당 후 gum_emit_thunks 를 적용한다.

 1static void
 2gum_interceptor_backend_create_thunks (GumInterceptorBackend * self)
 3{
 4  gsize page_size, code_size;
 5  GumMemoryRange range;
 6
 7  page_size = gum_query_page_size ();
 8  code_size = page_size;
 9// 1. 백엔드 구조체의 thunk에 메모리 할당 (mmap)
10  self->thunks = gum_memory_allocate (NULL, code_size, page_size, GUM_PAGE_RW);
11
12  range.base_address = GUM_ADDRESS (self->thunks);
13  range.size = code_size;
14  gum_cloak_add_range (&range);
15// 2. 위에서 할당한 메모리 공간에 get_emit_thunks 적용
16  gum_memory_patch_code (self->thunks, 1024,
17      (GumMemoryPatchApplyFunc) gum_emit_thunks, self);
18}

1-1-2. (arm64용) _gum_interceptor_backend_create > gum_interceptor_backend_create_thunks > gum_emit_thunks #

전달받은 메모리 공간에 enter_thunk, leave_thunk 트램폴린 코드 블록을 메모리에 쓴다.
leave_thunk 는 enter_thunk + writer_offset 이기 때문에 두 코드는 붙어있게 된다.

 1static void
 2gum_emit_thunks (gpointer mem,
 3                 GumInterceptorBackend * self)
 4{
 5  GumArm64Writer * aw = &self->writer;
 6
 7  self->enter_thunk = self->thunks;
 8  gum_arm64_writer_reset (aw, mem);
 9  aw->pc = GUM_ADDRESS (self->enter_thunk);
10  // 1. onEnter 코드로 가기 위한 준비코드 작성
11  gum_emit_enter_thunk (aw);
12  gum_arm64_writer_flush (aw);
13
14  self->leave_thunk =
15      (guint8 *) self->enter_thunk + gum_arm64_writer_offset (aw);
16  // 2. onLeave 코드로 가기 위한 준비코드 작성
17  gum_emit_leave_thunk (aw);
18  gum_arm64_writer_flush (aw);
19}

1-1-2-1. (arm64용) _gum_interceptor_backend_create > gum_interceptor_backend_create_thunks > gum_emit_thunks > gum_emit_enter_thunk #

onEnter 직전의 트램폴린 코드를 작성하는 함수이며,
X1GUM_CPU_CONTEXT, X2GUM_CPU_CONTEXT.lr, X3NEXT_HOP 을 가리키게 하고, _gum_function_context_begin_invocation 함수를 호출하는 코드가 작성된다.

 1#define GUM_FRAME_OFFSET_CPU_CONTEXT 0
 2#define GUM_FRAME_OFFSET_NEXT_HOP \
 3    (GUM_FRAME_OFFSET_CPU_CONTEXT + sizeof (GumCpuContext))
 4#define G_STRUCT_OFFSET(struct_type, member) \
 5    ((glong) offsetof (struct_type, member))
 6
 7static void
 8gum_emit_enter_thunk (GumArm64Writer * aw)
 9{
10// 후킹용 함수 프롤로그 작성. 
11// NEXTHOP 포인터 공간, CPU_CONTEXT 공간 만큼 추가 스택 공간 할당 후
12// 호출 전 레지스터를 저장하고 후킹 스크립트에서 접근할 수 있도록 하는 역할을 한다. 
13// 프레임포인터 체인을 설정해서 콜스택 추적이 가능하도록 한다. 
14  gum_emit_prolog (aw);
15
16//1. add x1,sp,OFFSET_CPU_CONTEXT(0)
17// X1 레지스터가 CPU_CONTEXT가 저장될 공간을 가리키도록 한다. 
18  gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X1, ARM64_REG_SP,
19      GUM_FRAME_OFFSET_CPU_CONTEXT);
20
21//2. add x2,sp,G_STRUCT_OFFSET(GumCpuContext, lr)
22// X2 레지스터가 lr 주소를 가리키도록 한다. 
23  gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X2, ARM64_REG_SP,
24      GUM_FRAME_OFFSET_CPU_CONTEXT + G_STRUCT_OFFSET (GumCpuContext, lr));
25
26//3. add x3,sp,sizeof(GumCpuContext)
27// X3 레지스터가 스택의 CPUCONTEXT 이후 주소를 가리키도록 한다. 
28// NEXT_HOP 주소는 아래에서 call 하는 함수 인자로 전달돼서 함수 안에서 값이 세팅된다.
29  gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X3, ARM64_REG_SP,
30      GUM_FRAME_OFFSET_NEXT_HOP);
31
32//4. call _gum_function_context_begin_invocation(x17,x1,x2,x3)
33// _gum_function_context_begin_invocation 이 함수의 인자 레지스터와 함께 call 명령추가
34  gum_arm64_writer_put_call_address_with_arguments (aw,
35      GUM_ADDRESS (_gum_function_context_begin_invocation), 4,
36      GUM_ARG_REGISTER, ARM64_REG_X17,
37      GUM_ARG_REGISTER, ARM64_REG_X1,
38      GUM_ARG_REGISTER, ARM64_REG_X2,
39      GUM_ARG_REGISTER, ARM64_REG_X3);
40
41// 후킹용 함수 에필로그 작성.
42// onEnter 리스너 실행 후 프롤로그에서 저장했던 레지스터 상태나 스택들을 복원한다. 
43  gum_emit_epilog (aw);
44}

1-1-2-1-4. (공용) _gum_interceptor_backend_create > gum_interceptor_backend_create_thunks > gum_emit_thunks > gum_emit_enter_thunk > _gum_function_context_begin_invocation #

어떤 함수가 후킹됐을때 런타임에 onEnter 트램폴린에서 호출하는 함수.
중첩 호출을 방지하고, 후킹 방식에 따라 next_hop을 세팅한 후, onEnter 리스너들을 호출해준다.

  1gboolean
  2_gum_function_context_begin_invocation (GumFunctionContext * function_ctx,
  3                                        GumCpuContext * cpu_context,
  4                                        gpointer * caller_ret_addr,
  5                                        gpointer * next_hop) {
  6// 1. 재귀나 중첩호출로 인해 같은 인터셉터가 실행되지 않도록 가드 
  7  if (gum_tls_key_get_value (gum_interceptor_guard_key) == interceptor)
  8  {
  9// next_hop을 org함수의 트램폴린으로 세팅함.
 10// onEnter 리스너를 한 스레드에서 중첩 호출하지 않도록 가드한것
 11    *next_hop = function_ctx->on_invoke_trampoline;
 12    goto bypass;
 13  }
 14  gum_tls_key_set_value (gum_interceptor_guard_key, interceptor);
 15
 16// 2. 콜스택을 확인해서 가장 위에있는 함수가 replace 함수인 경우 트램폴린 세팅 후 bypass
 17// replace 함수가 호출된 상태에서 재귀적으로 호출되는 것을 방지
 18  interceptor_ctx = get_interceptor_thread_context ();
 19  stack = interceptor_ctx->stack;
 20
 21  stack_entry = gum_invocation_stack_peek_top (stack);
 22  if (stack_entry != NULL &&
 23      stack_entry->calling_replacement &&
 24      gum_strip_code_pointer (GUM_FUNCPTR_TO_POINTER (
 25          stack_entry->invocation_context.function)) ==
 26          function_ctx->function_address)
 27  {
 28    gum_tls_key_set_value (gum_interceptor_guard_key, NULL);
 29    *next_hop = function_ctx->on_invoke_trampoline;
 30    goto bypass;
 31  }
 32
 33// 특정 선택된 스레드만 리스너를 호출하거나 ignore_level 설정에 따라 리스너를 호출하도록 구현할 수 있다. 
 34  if (interceptor->selected_thread_id != 0)
 35  {
 36    invoke_listeners =
 37        gum_process_get_current_thread_id () == interceptor->selected_thread_id;
 38  }
 39
 40  if (invoke_listeners)
 41  {
 42    invoke_listeners = (interceptor_ctx->ignore_level <= 0);
 43  }
 44
 45// onLeave 시점에 작업이 필요한지에 대한 bool 값 
 46// Interceptor에서 replace(함수대체)를 사용했거나, onLeave가 있는 경우 세팅.
 47  will_trap_on_leave = function_ctx->replacement_function != NULL ||
 48      (invoke_listeners && function_ctx->has_on_leave_listener);
 49
 50  if (will_trap_on_leave)
 51  {
 52// 지금은 call로 _gum_function_context_begin_invocation 함수에 들어왔는데, 
 53// onLeave나 replace를 사용하면 원래 ret_addr로 frida에서 리턴해줘야 하기 때문에 X2(lr)를 푸시한다. 
 54    stack_entry = gum_invocation_stack_push (stack, function_ctx,
 55        *caller_ret_addr);
 56    invocation_ctx = &stack_entry->invocation_context;
 57  }
 58  else if (invoke_listeners)
 59  {
 60// onEnter만 있는 경우인데, onEnter 이후에 원본 함수를 호출해야 하기 때문에 원본함수 주소를 푸시한다.
 61    stack_entry = gum_invocation_stack_push (stack, function_ctx,
 62        function_ctx->function_address);
 63    invocation_ctx = &stack_entry->invocation_context;
 64  }
 65
 66// 프리다는 여러개의 리스너를 등록할 수 있는데 (onEnter만 여러개 등록할수도 있음)
 67// 먼저등록된 onEnter부터 순차적으로 호출되고 나중에 원본함수를 호출한다. 
 68  if (invoke_listeners)
 69  {
 70    listener_entries =
 71        (GPtrArray *) g_atomic_pointer_get (&function_ctx->listener_entries);
 72    for (i = 0; i != listener_entries->len; i++) {
 73      if (listener_entry->listener_interface->on_enter != NULL)
 74      {
 75        listener_entry->listener_interface->on_enter (
 76            listener_entry->listener_instance, invocation_ctx);
 77      }
 78    }
 79  }
 80
 81// 위에서 세팅한 주소 pop
 82  if (!will_trap_on_leave && invoke_listeners)
 83  {
 84    gum_invocation_stack_pop (interceptor_ctx->stack);
 85  }
 86
 87  if (will_trap_on_leave)
 88  {
 89// org함수 호출 후 onLeave 트램폴린으로 이동할 수 있도록 세팅
 90    *caller_ret_addr = function_ctx->on_leave_trampoline;
 91  }
 92
 93// 다음 함수 지정하는건데, replace인 경우 대체함수로 이동,    
 94// 그게 아니라면 org함수를 가리키는 트램폴린을 지정
 95  if (function_ctx->replacement_function != NULL)
 96  {
 97    stack_entry->calling_replacement = TRUE;
 98    stack_entry->cpu_context = *cpu_context;
 99    stack_entry->original_system_error = system_error;
100    invocation_ctx->cpu_context = &stack_entry->cpu_context;
101    invocation_ctx->backend = &interceptor_ctx->replacement_backend;
102    invocation_ctx->backend->data = function_ctx->replacement_data;
103
104    *next_hop = function_ctx->replacement_function;
105  }
106  else
107  {
108    *next_hop = function_ctx->on_invoke_trampoline;
109  }
110}

1-1-2-2. (arm64용) _gum_interceptor_backend_create > gum_interceptor_backend_create_thunks > gum_emit_thunks > gum_emit_leave_thunk #

onEnter 스텁코드 처럼 X1에는 CPU_CONTEXT, X2에는 NEXT_HOP 이 저장되고 _gum_function_context_end_invocation 를 호출한다.

 1static void
 2gum_emit_leave_thunk (GumArm64Writer * aw)
 3{
 4  gum_emit_prolog (aw);
 5
 6  gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X1, ARM64_REG_SP,
 7      GUM_FRAME_OFFSET_CPU_CONTEXT);
 8  gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X2, ARM64_REG_SP,
 9      GUM_FRAME_OFFSET_NEXT_HOP);
10
11  gum_arm64_writer_put_call_address_with_arguments (aw,
12      GUM_ADDRESS (_gum_function_context_end_invocation), 3,
13      GUM_ARG_REGISTER, ARM64_REG_X17,
14      GUM_ARG_REGISTER, ARM64_REG_X1,
15      GUM_ARG_REGISTER, ARM64_REG_X2);
16
17  gum_emit_epilog (aw);
18}

1-1-2-2-3. (공용) _gum_interceptor_backend_create > gum_interceptor_backend_create_thunks > gum_emit_thunks > gum_emit_leave_thunk > _gum_function_context_end_invocation #

onEnter에 비해 onLeave 트램폴린은 리턴주소만 세팅하고 리스너 호출하기만 하면 돼서 하는 일이 적다.

 1void
 2_gum_function_context_end_invocation (GumFunctionContext * function_ctx,
 3                                      GumCpuContext * cpu_context,
 4                                      gpointer * next_hop)
 5{
 6// onLeave가 호출되면서도 TLS 가드를 세팅해서 onEnter 부터 못들어오도록 막는다. 
 7  gum_tls_key_set_value (gum_interceptor_guard_key, function_ctx->interceptor);
 8
 9// next_hop에 원본 함수의 리턴주소(원본함수 호출자)를 세팅한다. 
10  interceptor_ctx = get_interceptor_thread_context ();
11  stack_entry = gum_invocation_stack_peek_top (interceptor_ctx->stack);
12  *next_hop = gum_sign_code_pointer (stack_entry->caller_ret_addr);
13
14  invocation_ctx->backend = &interceptor_ctx->listener_backend;
15
16  gum_function_context_fixup_cpu_context (function_ctx, cpu_context);
17
18// onLeave 리스너 호출
19  listener_entries =
20      (GPtrArray *) g_atomic_pointer_get (&function_ctx->listener_entries);
21  for (i = 0; i != listener_entries->len; i++)
22  {
23    listener_entry = g_ptr_array_index (listener_entries, i);
24
25    gum_invocation_listener_on_leave (listener_entry->listener_instance,
26        invocation_ctx);
27  }
28
29  gum_thread_set_system_error (invocation_ctx->system_error);
30  gum_invocation_stack_pop (interceptor_ctx->stack);
31  gum_tls_key_set_value (gum_interceptor_guard_key, NULL);
32  g_atomic_int_dec_and_test (&function_ctx->trampoline_usage_counter);
33}

2. _gum_interceptor_backend_create_trampoline #

첫번째 onEnter, onLeave, onInvoke 트램폴린과 조건에 따라 deflector를 생성하는 함수.
후킹 타겟 함수의 시작부분에 트램폴린을 바로 삽입할 수 있는지에 따라 결정된다.

  1gboolean
  2_gum_interceptor_backend_create_trampoline (GumInterceptorBackend * self,
  3                                            GumFunctionContext * ctx)
  4{
  5// 1. deflector가 필요한지 찾고 need_deflector 값을 세팅
  6// 거리에 따라 필요한 점프명령 크기(4~16 바이트)만큼 안전하게 옮길 수 있는지 체크 한다.
  7// 코드 분석 참고 
  8  if (!gum_interceptor_backend_prepare_trampoline (self, ctx, &need_deflector))
  9    return FALSE;
 10
 11// 트램펄린 코드를 작성할 위치로 aw 초기화
 12  gum_arm64_writer_reset (aw, ctx->trampoline_slice->data);
 13
 14  if (ctx->type == GUM_INTERCEPTOR_TYPE_FAST)
 15  {
 16    deflector_target = ctx->replacement_function;
 17  }
 18  else
 19  {
 20// on_enter_trampoline 을 현재 writer위치로 설정해서 onEnter 트램폴린 작성 시작
 21    ctx->on_enter_trampoline =
 22        gum_sign_code_pointer (gum_arm64_writer_cur (aw));
 23    deflector_target = ctx->on_enter_trampoline;
 24  }
 25
 26// 2. 디플렉터가 필요한 경우 deflector 생성 후 ctx->trampoline_deflector 에 저장
 27  if (need_deflector)
 28  {
 29    ...
 30    dedicated = data->redirect_code_size == 4;
 31    ctx->trampoline_deflector = gum_code_allocator_alloc_deflector (
 32        self->allocator, &caller, return_address, deflector_target, dedicated);
 33    if (ctx->trampoline_deflector == NULL)
 34    {
 35      gum_code_slice_unref (ctx->trampoline_slice);
 36      ctx->trampoline_slice = NULL;
 37      return FALSE;
 38    }
 39// gum_insert_deflector 이 함수에서 push x0, lr 을 하고 dedicated_target을 호출하거나 dispatcher->thunk를 호출한다
 40// dispatcher->thunk 를 lookup 함수때문에 호출하면 x0과 lr이 변경될 수 있어서 복원하는 코드를 추가한다. 
 41    gum_arm64_writer_put_pop_reg_reg (aw, ARM64_REG_X0, ARM64_REG_LR);
 42  }
 43
 44// onEnter, onLeave 트램폴린 연속으로 작성 
 45// X17에 ctx 정보들을 넣고 X16에 리스너 thunk 주소를 담아 점프하는 코드이다.
 46  if (ctx->type != GUM_INTERCEPTOR_TYPE_FAST)
 47  {
 48    gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X17, GUM_ADDRESS (ctx));
 49    gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X16,
 50        GUM_ADDRESS (gum_sign_code_pointer (self->enter_thunk)));
 51    gum_arm64_writer_put_br_reg (aw, ARM64_REG_X16);
 52
 53    ctx->on_leave_trampoline = gum_arm64_writer_cur (aw);
 54
 55    gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X17, GUM_ADDRESS (ctx));
 56    gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X16,
 57        GUM_ADDRESS (gum_sign_code_pointer (self->leave_thunk)));
 58    gum_arm64_writer_put_br_reg (aw, ARM64_REG_X16);
 59
 60    gum_arm64_writer_flush (aw);
 61    ...
 62  }
 63// 타겟 함수 본체를 호출하기 위한 invoke 트램폴린 작성 시작
 64  ctx->on_invoke_trampoline = gum_sign_code_pointer (gum_arm64_writer_cur (aw));
 65
 66// 원본 함수의 명령어를 파싱한다. 
 67  gum_arm64_relocator_reset (ar, function_address, aw);
 68  signature = g_string_sized_new (16);
 69  do
 70  {
 71    const cs_insn * insn;
 72
 73    reloc_bytes = gum_arm64_relocator_read_one (ar, &insn);
 74    g_assert (reloc_bytes != 0);
 75
 76    if (signature->len != 0)
 77      g_string_append_c (signature, ';');
 78    g_string_append (signature, insn->mnemonic);
 79  }
 80  while (reloc_bytes < data->redirect_code_size);
 81
 82// LR을 재작성 해야하는 상황인지 확인
 83// dlopen() 등 호출자 주소에 따라 다른 동작 수행하는 로직을 사용하는 경우
 84// LR을 변경했을 때 문제가 발생할 수 있기 때문에 예외처리
 85  is_eligible_for_lr_rewriting = strcmp (signature->str, "mov;b") == 0 ||
 86      g_str_has_prefix (signature->str, "stp;mov;mov;bl");
 87
 88  if (is_eligible_for_lr_rewriting) 
 89  {
 90    ...
 91    if (insn->id == ARM64_INS_MOV &&
 92        insn->detail->arm64.operands[1].reg == ARM64_REG_LR)
 93      // LR 사용 시 호출자 주소를 대체하는 로직
 94    else
 95      gum_arm64_relocator_write_one (ar);
 96  }
 97  else
 98    gum_arm64_relocator_write_all (ar);
 99
100// 원래 함수(복사된 코드 이후 주소)로 점프하는 invoke_trampoline의 마지막 코드 작성
101  if (!ar->eoi) 
102  {
103    GumAddress resume_at =
104        gum_sign_code_address (GUM_ADDRESS (function_address) + reloc_bytes);
105    gum_arm64_writer_put_ldr_reg_address (aw, data->scratch_reg, resume_at);
106    gum_arm64_writer_put_br_reg (aw, data->scratch_reg);
107  }
108// 후킹 코드가 activate 되기 전이라 function_address는 아직 변조가 일어나지 않았다.
109// overritten_prologue에 원본 함수의 덮어써질 코드 바이트를 저장해둔다. 
110  ctx->overwritten_prologue_len = reloc_bytes;
111  gum_memcpy (ctx->overwritten_prologue, function_address, reloc_bytes);
112
113  return TRUE;
114}

2-1. _gum_interceptor_backend_create_trampoline > gum_interceptor_backend_prepare_trampoline #

deflector의 필요 여부에 따라 need_deflector값을 true / false를 세팅한다.

 1static gboolean
 2gum_interceptor_backend_prepare_trampoline (GumInterceptorBackend * self,
 3                                            GumFunctionContext * ctx,
 4                                            gboolean * need_deflector)
 5{
 6  GumArm64FunctionContextData * data = GUM_FCDATA (ctx);
 7  gpointer function_address = ctx->function_address;
 8  guint redirect_limit;
 9
10  *need_deflector = FALSE;
11// 16바이트 크기만큼 relocate할 수 있는지 체크.
12// 인자로 전달한 redirect_limit에 몇바이트까지 안전하게 재배치가 가능한지 저장해준다.
13// 훅을 걸때 이 코드들이 invoke 트램폴린으로 이동돼야 하기 때문에 이동 후에도 정상적으로 동작 가능한지를 체크한다.
14//    1. 명령을 파싱해서 bl, blr, svc 같은 옮기기 까다로운 명령어 없이 n byte를 충족할 수 있는지 확인
15//       - SVC : 시스템 콜 명령을 재배치하다가 잘못된 주소에서 실행하면 문맥이 깨져 예외가 발생할 수 있다.
16//       - BL, BLR : 이 명령 실행 전에 LR을 저장하는데 다음 명령주소를 저장하게되니 재배치된 주소를 가리켜 문제가 발생한다. 
17//    2. 함수를 전부 분기명령까지 재귀적으로 확인해서 패치할 영역으로 다시 돌아오는지 확인
18//    3. 재배치한 코드에서 x16 or x17 레지스터를 사용하고 있는지 확인
19  if (gum_arm64_relocator_can_relocate (function_address, 16,
20      GUM_SCENARIO_ONLINE, &redirect_limit, &data->scratch_reg))
21  {
22    data->redirect_code_size = 16;
23    ctx->trampoline_slice = gum_code_allocator_alloc_slice (self->allocator);
24  }
25  else
26  {
27    GumAddressSpec spec;
28    gsize alignment;
29
30// 재배치 가능한 코드 사이즈에 따라 B로 패치할지, ADRP로 패치할지 결정된다.
31// 그리고 그 명령에 따라 max_distance가 결정되며, 이 값이 아래에서 near의 기준이 된다.
32// 현재 함수 주소로 바로 점프할 수 없다면 deflector를 사용하게 된다. 
33    if (redirect_limit >= 8)
34    {
35      data->redirect_code_size = 8;
36
37      spec.near_address = GSIZE_TO_POINTER (
38          GPOINTER_TO_SIZE (function_address) &
39          ~((gsize) (GUM_ARM64_LOGICAL_PAGE_SIZE - 1)));
40      spec.max_distance = GUM_ARM64_ADRP_MAX_DISTANCE;
41      alignment = GUM_ARM64_LOGICAL_PAGE_SIZE;
42    }
43    else if (redirect_limit >= 4)
44    {
45      data->redirect_code_size = 4;
46
47      spec.near_address = function_address;
48      spec.max_distance = GUM_ARM64_B_MAX_DISTANCE;
49      alignment = 0;
50    }
51    else
52    {
53      return FALSE;
54    }
55
56// 특정 주소 근처(위에서 지정한 max_distance의 범위 안)에서 할당 가능한 메모리가 있는지 할당 시도
57    ctx->trampoline_slice = gum_code_allocator_try_alloc_slice_near (
58        self->allocator, &spec, alignment);
59    if (ctx->trampoline_slice == NULL)
60    {
61// 근처에 할당 가능한 메모리가 없다면, 그냥 가능한대로 할당하고 need_deflector 를 TRUE로 설정
62      ctx->trampoline_slice = gum_code_allocator_alloc_slice (self->allocator);
63      *need_deflector = TRUE;
64    }
65  }
66
67  if (data->scratch_reg == ARM64_REG_INVALID)
68    goto no_scratch_reg;
69
70  return TRUE;
71
72no_scratch_reg:
73  {
74    gum_code_slice_unref (ctx->trampoline_slice);
75    ctx->trampoline_slice = NULL;
76    return FALSE;
77  }
78}

2-1-1. _gum_interceptor_backend_create_trampoline > gum_interceptor_backend_prepare_trampoline gum_arm64_relocator_can_relocate #

  1gboolean
  2gum_arm64_relocator_can_relocate (gpointer address,
  3                                  guint min_bytes,
  4                                  GumRelocationScenario scenario,
  5                                  guint * maximum,
  6                                  arm64_reg * available_scratch_reg)
  7{
  8// 1. 타겟함수의 명령어를 한개씩 파싱해서 bl, blr, svc를 만나지 않고 min_bytes가 넘는게 가능한지 체크한다
  9  gum_arm64_relocator_init (&rl, address, &cw);
 10  do
 11  {
 12    const cs_insn * insn;
 13// 만약 b(일부), br, ret 같은 명령을 만나서 함수가 끝나면 rl.eoi가 true로 세팅된다.
 14    reloc_bytes = gum_arm64_relocator_read_one (&rl, &insn);
 15    switch (insn->id)
 16    {
 17      case ARM64_INS_BL:
 18      case ARM64_INS_BLR:
 19      case ARM64_INS_SVC:
 20        safe_to_relocate_further = FALSE;
 21        break;
 22      default:
 23        safe_to_relocate_further = TRUE;
 24        break;
 25    }
 26  }
 27  while (reloc_bytes < min_bytes);
 28  
 29// 2. 만약 계속 읽을 명령어가 있는 경우 capstone 디스어셈블러를 사용해서 2차 분석.
 30// 2차 분석의 이유는 이후 코드에서도 수정된 16byte 안으로 다시 점프될 수 있기 때문에 체크한다.
 31// b, br, ret은 명령 실행 이후 다시 돌아오는 경우가 거의 없기 때문에 eoi로 예외처리된 것이다. 
 32  if (!rl.eoi)
 33  {
 34    csh capstone;
 35// 체크된 주소
 36    checked_targets = g_hash_table_new (NULL, NULL);
 37// 체크할 주소
 38    targets_to_check = g_hash_table_new (NULL, NULL);
 39
 40    do {
 41// 현재 주소를 체크된 주소로 넣고 다시 만나면 체크하지 않는다. 
 42      g_hash_table_add (checked_targets, (gpointer) current_code);
 43      gum_ensure_code_readable (current_code, current_code_size);
 44
 45// capstone 디스어셈블러로 현재 코드를 순차적으로 접근하고,
 46// 다시 초기 코드로 돌아갈 수 있는 분기를 만난 경우 targets_to_check 배열에 저장해서
 47// 더이상 분석할 코드가 없을때까지 재분석한다.
 48      while (carry_on && cs_disasm_iter (capstone, &current_code,
 49          &current_code_size, &current_address, insn)) {
 50        cs_arm64 * d = &insn->detail->arm64;
 51        switch (insn->id)
 52        { 
 53          case ARM64_INS_B:
 54          case ARM64_INS_CBZ:
 55          case ARM64_INS_CBNZ:
 56              g_hash_table_add (targets_to_check, target);
 57              ...
 58        }
 59      }
 60    } while (current_code != NULL);
 61
 62// 체크된 타겟들을 전부 확인해서 target과 address의 주소 차이가 0~16byte 사이인지 체크한다.
 63// 그 사이라면 n을 재설정 한다. 결국 가장 작은 offset으로 n을 설정하는 것이다. 
 64    g_hash_table_iter_init (&iter, checked_targets);
 65    while (g_hash_table_iter_next (&iter, &target, NULL))
 66    {
 67      gssize offset = (gssize) target - (gssize) address;
 68      if (offset > 0 && offset < (gssize) n)
 69      {
 70        n = offset;
 71        if (n == 4)
 72          break;
 73      }
 74    }
 75  }
 76
 77// 3. 재배치할 명령어에서 x16 또는 x17 레지스터를 사용하고 있는지 체크한다.
 78// 나중에 on_enter_trampoline으로 점프할때 사용하기 위해서이다.
 79  if (available_scratch_reg != NULL)
 80  {
 81    gboolean x16_used = FALSE, x17_used = FALSE;
 82
 83    for (insn_index = 0; insn_index != n / 4; insn_index++)
 84    {
 85      const cs_insn * insn = rl.input_insns[insn_index];
 86      const cs_arm64 * info = &insn->detail->arm64;
 87      uint8_t op_index;
 88
 89      for (op_index = 0; op_index != info->op_count; op_index++)
 90      {
 91        const cs_arm64_op * op = &info->operands[op_index];
 92
 93        if (op->type == ARM64_OP_REG)
 94        {
 95          x16_used |= op->reg == ARM64_REG_X16;
 96          x17_used |= op->reg == ARM64_REG_X17;
 97        }
 98      }
 99    }
100// 사용하지 않는 레지스터를 할당해준다. 
101    if (!x16_used)
102      *available_scratch_reg = ARM64_REG_X16;
103    else if (!x17_used)
104      *available_scratch_reg = ARM64_REG_X17;
105    else
106      *available_scratch_reg = ARM64_REG_INVALID;
107  }
108}

2-2. _gum_interceptor_backend_create_trampoline > gum_code_allocator_alloc_deflector #

need_deflector가 세팅된 경우 deflector를 생성하고 반환해주는 함수이다.

 1GumCodeDeflector *
 2gum_code_allocator_alloc_deflector (GumCodeAllocator * self,
 3                                    const GumAddressSpec * caller,
 4                                    gpointer return_address,
 5                                    gpointer target,
 6                                    gboolean dedicated)
 7{
 8  GumCodeDeflectorDispatcher * dispatcher = NULL;
 9  GSList * cur;
10  GumCodeDeflectorImpl * impl;
11  GumCodeDeflector * deflector;
12
13  if (!dedicated)
14  {
15// 1. max_distance보다 작은 거리안에 있는 디스패처가 있다면 재사용한다.
16    for (cur = self->dispatchers; cur != NULL; cur = cur->next)
17    {
18      GumCodeDeflectorDispatcher * d = cur->data;
19      gsize distance;
20
21      distance = ABS ((gssize) GPOINTER_TO_SIZE (d->address) -
22          (gssize) caller->near_address);
23      if (distance <= caller->max_distance)
24      {
25        dispatcher = d;
26        break;
27      }
28    }
29  }
30
31  if (dispatcher == NULL)
32  {
33// 2. 없으면 디스패처 생성 
34// iOS나 32bit 안드로이드에서만 생성되며, 그외의 경우 NULL이 리턴된다.
35// TODO: 왜 이렇게 구현됐는지 이해가 안됨. arm64가 아직 구현이 안된것으로도 판단됨..
36    dispatcher = gum_code_deflector_dispatcher_new (caller, return_address,
37        dedicated ? target : NULL);
38    if (dispatcher == NULL)
39      return NULL;
40    self->dispatchers = g_slist_prepend (self->dispatchers, dispatcher);
41  }
42
43// 3. deflector를 생성 후 초기화한 다음 dispatcher의 호출자 리스트에 등록한다. 
44// deflector에 target과 return_address를 담고, callers 에 추가한다.
45// 나중에 dispatcher->thunk 에서 gum_code_deflector_dispatcher_lookup 함수가 target을 x0에 리턴한 뒤 호출한다.
46// 이 target은 on_enter_trampoline 또는 replacement_function 이 된다. 
47  impl = g_slice_new (GumCodeDeflectorImpl);
48
49  deflector = &impl->parent;
50  deflector->return_address = return_address;
51  deflector->target = target;
52  deflector->trampoline = dispatcher->trampoline;
53  deflector->ref_count = 1;
54
55  impl->allocator = self;
56
57  dispatcher->callers = g_slist_prepend (dispatcher->callers, deflector);
58
59  return deflector;
60}

2-2-2. _gum_interceptor_backend_create_trampoline > gum_code_allocator_alloc_deflector > gum_code_deflector_dispatcher_new #

TODO: 현재 버전 기준으로는 안드로이드 arm64의 경우 gum_code_deflector_dispatcher_new 에 구현이 없는데, 말이안된다. 그럼 deflector 로직이 없는건가?
그렇다고 하기엔 나머지 함수들에서 ARM64의 흔적들이 보인다…

 1static GumCodeDeflectorDispatcher *
 2gum_code_deflector_dispatcher_new (const GumAddressSpec * caller,
 3                                   gpointer return_address,
 4                                   gpointer dedicated_target)
 5{
 6#if defined (HAVE_DARWIN) || (defined (HAVE_ELF) && GLIB_SIZEOF_VOID_P == 4)
 7  GumCodeDeflectorDispatcher * dispatcher;
 8  GumProbeRangeForCodeCaveContext probe_ctx;
 9  GumInsertDeflectorContext insert_ctx;
10
11  probe_ctx.caller = caller;
12
13  probe_ctx.cave.base_address = 0;
14  probe_ctx.cave.size = 0;
15// 프로세스에 로드된 모듈(라이브러리, 실행파일 등)을 열거한 후 콜백함수를 호출한다.
16// gum_probe_module_for_code_cave 는 각 모듈에서 빈 메모리(코드케이브) 영역을 탐색하는 함수이다. 
17// 현재 코드와 max_distance 보다 짧은 거리에 있는것을 찾아준다. 
18  gum_process_enumerate_modules (gum_probe_module_for_code_cave, &probe_ctx);
19  if (probe_ctx.cave.base_address == 0)
20    return NULL;
21
22  dispatcher = g_slice_new0 (GumCodeDeflectorDispatcher);
23// cave의 base_address를 dispatcher->address로 설정한다.
24  dispatcher->address = GSIZE_TO_POINTER (probe_ctx.cave.base_address);
25// 혹시몰라 deflector를 제거할때 복원하기 위해 original_data를 저장해둔다.
26  dispatcher->original_data = g_memdup (dispatcher->address,
27      probe_ctx.cave.size);
28  dispatcher->original_size = probe_ctx.cave.size;
29
30// 전용 디스패처를 사용하는 경우(변경가능한 코드가 4byte인 경우) 별도의 트램폴린을 생성하지 않음
31  if (dedicated_target == NULL)
32  {
33    gsize thunk_size;
34    GumMemoryRange range;
35
36    thunk_size = gum_query_page_size ();
37
38    dispatcher->thunk =
39        gum_memory_allocate (NULL, thunk_size, thunk_size, GUM_PAGE_RW);
40    dispatcher->thunk_size = thunk_size;
41// 1. dispatcher의 트램폴린 코드를 작성한다.
42// 메모리에 레지스터를 푸시하고 gum_code_deflector_dispatcher_lookup -> br X0 순서로 호출하는 코드를 작성한다.
43// dispatcher_lookup 함수는 함수의 호출자를 찾아주는 함수이다. 
44    gum_memory_patch_code (dispatcher->thunk, GUM_MAX_CODE_DEFLECTOR_THUNK_SIZE,
45        (GumMemoryPatchApplyFunc) gum_write_thunk, dispatcher);
46
47    range.base_address = GUM_ADDRESS (dispatcher->thunk);
48    range.size = thunk_size;
49    gum_cloak_add_range (&range);
50  }
51
52  insert_ctx.pc = GUM_ADDRESS (dispatcher->address);
53  insert_ctx.max_size = dispatcher->original_size;
54  insert_ctx.return_address = return_address;
55  insert_ctx.dedicated_target = dedicated_target;
56
57  insert_ctx.dispatcher = dispatcher;
58// 2. 위에서 찾은 code cave에 deflector 코드를 작성해주는 함수이다. 
59// dedicated_target이 있다면 그쪽으로 점프하고, 없다면 dispatcher로 점프한다.
60// 그러니까 후킹타겟함수 호출 -> deflector -> dispatcher.thunk -> on_enter_trampoline 또는 replacement_function 형식이다. 
61  gum_memory_patch_code (dispatcher->address, dispatcher->original_size,
62      (GumMemoryPatchApplyFunc) gum_insert_deflector, &insert_ctx);
63
64  return dispatcher;
65#else
66  (void) gum_insert_deflector;
67  (void) gum_write_thunk;
68  (void) gum_probe_module_for_code_cave;
69
70  return NULL;
71#endif
72}

2-2-2-1. _gum_interceptor_backend_create_trampoline > gum_code_allocator_alloc_deflector > gum_code_deflector_dispatcher_new > gum_write_thunk #

공용 dispatcher의 thunk 코드를 작성하는 함수이다.

 1static void
 2gum_write_thunk (gpointer thunk,
 3                 GumCodeDeflectorDispatcher * dispatcher)
 4{
 5  GumArm64Writer aw;
 6
 7  gum_arm64_writer_init (&aw, thunk);
 8  aw.pc = GUM_ADDRESS (dispatcher->thunk);
 9
10  /* push {q0-q7} */
11  gum_arm64_writer_put_instruction (&aw, 0xadbf1fe6);
12  gum_arm64_writer_put_instruction (&aw, 0xadbf17e4);
13  gum_arm64_writer_put_instruction (&aw, 0xadbf0fe2);
14  gum_arm64_writer_put_instruction (&aw, 0xadbf07e0);
15
16  gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X17, ARM64_REG_X18);
17  gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X15, ARM64_REG_X16);
18  gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X13, ARM64_REG_X14);
19  gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X11, ARM64_REG_X12);
20  gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X9, ARM64_REG_X10);
21  gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X7, ARM64_REG_X8);
22  gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X5, ARM64_REG_X6);
23  gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X3, ARM64_REG_X4);
24  gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X1, ARM64_REG_X2);
25
26// 호출 정보를 기반으로 실행 흐름의 타겟 주소를 결정하는 역할을 한다.
27// lr 레지스터 기반으로 호출자 목록에서 누가 호출했는지 확인한다.
28  gum_arm64_writer_put_call_address_with_arguments (&aw,
29      GUM_ADDRESS (gum_code_deflector_dispatcher_lookup), 2,
30      GUM_ARG_ADDRESS, GUM_ADDRESS (dispatcher),
31      GUM_ARG_REGISTER, ARM64_REG_LR);
32
33  gum_arm64_writer_put_pop_reg_reg (&aw, ARM64_REG_X1, ARM64_REG_X2);
34  gum_arm64_writer_put_pop_reg_reg (&aw, ARM64_REG_X3, ARM64_REG_X4);
35  gum_arm64_writer_put_pop_reg_reg (&aw, ARM64_REG_X5, ARM64_REG_X6);
36  gum_arm64_writer_put_pop_reg_reg (&aw, ARM64_REG_X7, ARM64_REG_X8);
37  gum_arm64_writer_put_pop_reg_reg (&aw, ARM64_REG_X9, ARM64_REG_X10);
38  gum_arm64_writer_put_pop_reg_reg (&aw, ARM64_REG_X11, ARM64_REG_X12);
39  gum_arm64_writer_put_pop_reg_reg (&aw, ARM64_REG_X13, ARM64_REG_X14);
40  gum_arm64_writer_put_pop_reg_reg (&aw, ARM64_REG_X15, ARM64_REG_X16);
41  gum_arm64_writer_put_pop_reg_reg (&aw, ARM64_REG_X17, ARM64_REG_X18);
42
43  /* pop {q0-q7} */
44  gum_arm64_writer_put_instruction (&aw, 0xacc107e0);
45  gum_arm64_writer_put_instruction (&aw, 0xacc10fe2);
46  gum_arm64_writer_put_instruction (&aw, 0xacc117e4);
47  gum_arm64_writer_put_instruction (&aw, 0xacc11fe6);
48// 그리고 그 호출해준 녀석으로 다시 점프하는 코드
49  gum_arm64_writer_put_br_reg (&aw, ARM64_REG_X0);
50  gum_arm64_writer_clear (&aw);
51}

2-2-2-2. _gum_interceptor_backend_create_trampoline > gum_code_allocator_alloc_deflector > gum_code_deflector_dispatcher_new > gum_insert_deflector #

 1static void
 2gum_insert_deflector (gpointer cave,
 3                      GumInsertDeflectorContext * ctx)
 4{
 5
 6  GumCodeDeflectorDispatcher * dispatcher = ctx->dispatcher;
 7  GumArm64Writer aw;
 8
 9  gum_arm64_writer_init (&aw, cave);
10  aw.pc = ctx->pc;
11
12  if (ctx->dedicated_target != NULL)
13  {
14// 만약 4byte로 deflector를 구현해야 하는 경우 이쪽 로직을 탐
15// dedicated_target(on_enter_trampoline 혹은 replacement_function) 로 점프하는 코드를 작성
16    gum_arm64_writer_put_push_reg_reg (&aw, ARM64_REG_X0, ARM64_REG_LR);
17    gum_arm64_writer_put_ldr_reg_address (&aw, ARM64_REG_X0,
18        GUM_ADDRESS (ctx->dedicated_target));
19    gum_arm64_writer_put_br_reg (&aw, ARM64_REG_X0);
20  }
21  else
22  {
23// 디스패처를 사용하는 경우 dispatcher의 thunk로 점프하는 코드를 작성
24// deflector -> dispatcher.thunk -> gum_code_deflector_dispatcher_lookup -> br on_enter_trampoline 또는 replacement_function
25    gum_arm64_writer_put_ldr_reg_address (&aw, ARM64_REG_X0,
26        GUM_ADDRESS (dispatcher->thunk));
27    gum_arm64_writer_put_br_reg (&aw, ARM64_REG_X0);
28  }
29
30  gum_arm64_writer_flush (&aw);
31  g_assert (gum_arm64_writer_offset (&aw) <= ctx->max_size);
32  gum_arm64_writer_clear (&aw);
33
34  dispatcher->trampoline = GSIZE_TO_POINTER (ctx->pc);
35}

3. (arm64용) _gum_interceptor_backend_activate_trampoline #

 1void
 2_gum_interceptor_backend_activate_trampoline (GumInterceptorBackend * self,
 3                                              GumFunctionContext * ctx,
 4                                              gpointer prologue)
 5{
 6  GumArm64Writer * aw = &self->writer;
 7  GumArm64FunctionContextData * data = GUM_FCDATA (ctx);
 8  GumAddress on_enter;
 9
10// 트램펄린 진입점을 결정한다. 
11// 보통 replace 방식으로 했을때 TYPE_FAST가 설정된다. 
12  if (ctx->type == GUM_INTERCEPTOR_TYPE_FAST)
13    on_enter = GUM_ADDRESS (ctx->replacement_function);
14  else
15    on_enter = GUM_ADDRESS (ctx->on_enter_trampoline);
16
17// writer를 트램펄린을 작성할 위치(타겟함수)로 초기화한다.
18  gum_arm64_writer_reset (aw, prologue);
19  aw->pc = GUM_ADDRESS (ctx->function_address);
20
21// 타겟 함수에 트램폴린으로 점프하는 코드를 작성한다. 
22// 후킹 코드에서 디플렉터를 사용하는 경우 deflector로 점프한다.
23// trampoline_deflector->trampoline == dispatcher->trampoline == code_cave
24  if (ctx->trampoline_deflector != NULL)
25  {
26    if (data->redirect_code_size == 8)
27    {
28      gum_arm64_writer_put_push_reg_reg (aw, ARM64_REG_X0, ARM64_REG_LR);
29      gum_arm64_writer_put_bl_imm (aw,
30          GUM_ADDRESS (ctx->trampoline_deflector->trampoline));
31    }
32    else
33    {
34      g_assert (data->redirect_code_size == 4);
35      gum_arm64_writer_put_b_imm (aw,
36          GUM_ADDRESS (ctx->trampoline_deflector->trampoline));
37    }
38  }
39  else
40  {
41// 디플렉터가 없는 경우. 직접 on_enter로 점프한다.
42// on_enter_trampoline 또는 replacement_function
43    switch (data->redirect_code_size)
44    {
45// 점프 거리에 따라서 사용해야되는 명령어가 다르다. 
46// 점프 옵코드까지 포함한 사이즈가 4인경우, 8인경우(최대 4gb 점프가능), 16인경우(그이상)
47      case 4:
48        gum_arm64_writer_put_b_imm (aw, on_enter);
49        break;
50      case 8:
51        gum_arm64_writer_put_adrp_reg_address (aw, data->scratch_reg, on_enter);
52        gum_arm64_writer_put_br_reg_no_auth (aw, data->scratch_reg);
53        break;
54      case 16:
55        gum_arm64_writer_put_ldr_reg_address (aw, data->scratch_reg, on_enter);
56        gum_arm64_writer_put_br_reg (aw, data->scratch_reg);
57        break;
58      default:
59        g_assert_not_reached ();
60    }
61  }
62
63  gum_arm64_writer_flush (aw);
64  g_assert (gum_arm64_writer_offset (aw) <= data->redirect_code_size);
65}

함수 호출 흐름 TODO #

후킹 코드 세팅 #

결국 이 테스트 코드가 타겟 process에 부착된 frida-gum 라이브러리가 지원하는 API 를 호출하는 것이기 때문에 이걸 호출하는 주체가 frida-server 일 수 도 있지만 libfrida-gum.a 을 포함시켜 devkit으로 구현한 process에 주입된 공유 라이브러리일 수 있다. →

테스트코드 #

  1. 테스트 함수 세팅
  2. (non hook) 테스트 함수 실행
  3. interceptor_fixture_attach
    • 테스트 리스너 등록
    • gum_interceptor_attach
  4. (hooked) 테스트 함수 실행
  5. interceptor_fixture_detach
    • gum_interceptor_detach

gum_interceptor_attach #

  1. 실제 함수 코드가 담긴 후킹 함수를 찾아온다. gum_interceptor_resolve
  2. 실제 함수 후킹 작업 진행 gum_interceptor_instrument
    1. function_by_address 해시테이블에서 함수 컨텍스트 검색. 있으면 바로 리턴, 없으면 테이블에 추가 후 다음작업 실행
    2. 백엔드(코드 삽입을 위한 구조체)를 생성하고 설정한다.
      • enter_thunk, leave_thunk 트램폴린 코드 생성. 실제 onEnter, onLeave를 호출하는 _gum_function_context_*_invocation 을 호출하는 트램폴린코드이다.
    3. 함수 컨텍스트 생성 및 해시테이블에 추가
    4. 트램폴린을 생성한다.
      • deflector 가 필요하면 생성
      • onEnter, onLeave, invoke 트램폴린 코드 생성. 여기서 생성하는 onEnter 트램폴린 코드는 enter_thunk 분기처리한다.
    5. gum_interceptor_activate 태스크를 예약한다.
  3. 트랜잭션 완료 gum_interceptor_transaction_end
    1. 펜딩 처리됐던 예약 태스크들 전부 실행
      • gum_interceptor_activate 함수가 실행되면서 후킹 타겟 함수의 앞부분 코드를 수정한다. 이렇게 타겟함수, 트램폴린, 리스너, 타겟원본함수가 연결된다.
    2. 태스크 리소스 정리

관련 함수들의 연결고리 #

원본함수 → deflector → onEnter 트램폴린 #

  • 예약 작업으로 _gum_interceptor_backend_activate_trampoline 코드에서 원본함수 코드를 수정한다.

     1if (ctx->type == GUM_INTERCEPTOR_TYPE_FAST)
     2  on_enter = GUM_ADDRESS (ctx->replacement_function);
     3else
     4  on_enter = GUM_ADDRESS (ctx->on_enter_trampoline);  
     5
     6// deflector가 있는 경우 쓰기 가능한 바이트 크기에 맞는 deflector->trampoline 점프 코드를 삽입한다.
     7if (ctx->trampoline_deflector != NULL)
     8{
     9    gum_arm64_writer_put_b_imm (aw,
    10        GUM_ADDRESS (ctx->trampoline_deflector->trampoline));
    11}
    12else
    13{
    14// 일반적인 후킹의 경우 쓰기 가능한 바이트 크기에 맞게 위에서 가져온 on_enter 주소로 점프하는 코드를 작성한다
    15    gum_arm64_writer_put_b_imm (aw, on_enter);
    16}
    
  • deflector는

onEnter 트램폴린 → enter_thunk #

enter_thunk → onEnter 리스너 #

onEnter 리스너 → on_invoke 트램펄린 #

e53cb46a-b1b1-447f-a767-eb86dfda5cfc


후킹된 코드 실행 #

함수 호출 맨 앞 코드 수정 -> (거리가 먼 경우 deflector -> dispatcher.thunk -> dispatcher_lookup -> onEnter 트램폴린 -> enter_thunks -> onEnter 호출 -> on_invoke 트램펄린 (트램폴린으로 덮인 코드 실행 + 타겟함수로 점프) -> 타겟함수 -> on_leave 트램펄린 -> leave_thunk -> on_leave 핸들러
return 하면서는 lr을 이용해서 직접 이동하기 때문에 deflector 로직이 필요없다.

replaece 인 경우 onEnter, onLeave가 없기 때문에 enter/leave_thunks가 없음

호출하는 함수 기준으로 코드를 작성하고 그림으로 레지스터까지 표현

각 트램폴린이 하는 역할을 적어주면 좋을듯

스택이나 레지스터들은 어떻게 유지되는지 확인해ㅔ봐야함 #

스택 구조는 어떻게 유지되는지, 깨질 수 있는 가능성은? 이런걸 적어줘도 좋음. 누구의 스택이 보이는건지..? caller, callee? 얘네의 위치상 관계 등
이걸 정리해봐야 어떤 구조로 되어있고 후킹하면서 어떻게 해야 메모리를 잘 읽어올 수 있는지 이해가 될듯
LR 관련해서도 문제가 생기는 것으로보임
64bit wecom에서 확인했는데,
어떤 함수 호출 후 lr 을 확인했더니 호출된 곳이 아래 코드의 위치로 확인됨 그런데 호출 후에 x29(fp).sub(0x60) 을 확인하려 했더니 x0 에 담긴 값이랑 다른 값이 출력된다.

아마 내부적으로 간접점프를 한번 더 하는데 프리다가 그걸 계속 내부참조해서 실제 함수를 찾아가는데 LR이나 FP를 이상한 값으로 건드려서 이런 문제가 발생할 수 있고, 그냥 lr은 미리 세팅해버리고 간접참조 점프 코드에서는 fp를 세팅해버린 이유일수도 있다. 레지스터 세팅 관점에서도 생각해봐야할듯 싶다.

1.text:0000000005BF8304                 LDUR            X0, [X29,#var_60]
2.text:0000000005BF8308                 LDR             X8, [X0,#8]
3.text:0000000005BF830C ;   try {
4.text:0000000005BF830C                 BLR             X8

5BF8308 이 위치를 후킹하니까 정상적으로 fp.sub(0x60)의 값이랑 x0이 맞는 것을 확인할 수 있다.

1[Pixel 6a::com.tencent.wework ]-> Function address: 0x73a5248c80
2x0: 0x74c773a4f0, x29: 0x73d55bb2a0, sp: 0x73d55bb160
3             0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
473d55bb240  f0 a4 73 c7 74 00 00 00                          ..s.t...
5             0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
673d55bb100  00 00 00 00 00 00 00 00                          ........

DEFLECTOR 로직 분석 필요 #

DEFLECTOR 로직 없이 분석해두고 여기에서 DEFLECTOR가 뭔지 분석

comments powered by Disqus