grep

iOS

frida-gum을 활용한 iOS 앱 Hook

NHN

2022년 10월 11일

원문에서 보기 ↗

Meetup! - frida-gum을 활용한 iOS 앱 Hook_섬네일_221007.jpg

1. 개요

Frida(https://github.com/frida/frida)의 핵심 로직은 모듈화되어 있습니다. 그래서 후킹(Hooking) 모듈만 가져다 프로그램에 포함시켜 개발할 수 있으며, Frida JavaScript API 외에 C/C++ API를 이용해 Frida의 강력한 후킹 기능을 사용할 수 있습니다. 따라서 좀 더 유연하게 iOS 앱 분석이 가능하다는 이점이 있습니다.

frida-gum(https://github.com/frida/frida-gum)이 바로 이 후킹 모듈입니다. 이 글에서는 해당 모듈을 이용해 기본적인 method trace 트윅(Tweak)을 구현해 보려 합니다.

2. 환경 구성

트윅 작성을 위해 THEOS를 설치해야 합니다. https://theos.dev/docs/installation-macos

THEOS를 설치한 뒤에는 frida 공식 깃허브에서 frida-gum-devkit을 다운로드합니다. 01.png

압축을 풀면 예시 소스 코드(frida-gum-example.c), 헤더 파일(frida-gum.h) 및 static library 파일(libfrida-gum.a)이 들어 있습니다. 02.png

이 중 헤더 파일 및 static library 파일을 THEOS 설치 경로 하위로 이동합니다.

cp libfrida-gum.a <THEOS 설치 경로>/lib/
cp frida-gum.h <THEOS 설치 경로>/include/

3. 테스트

DVIA-v2(https://github.com/prateek147/DVIA-v2) 앱에 대해서 Objective-C 메서드 콜을 추적하는 트윅을 작성해 보려 합니다. THEOS 프로젝트를 하나 생성하고 Makefile을 다음과 같이 작성합니다.

FINALPACKAGE := 1
ARCHS := arm64
TARGET := iphone:clang:latest:12.4
INSTALL_TARGET_PROCESSES = SpringBoard

include $(THEOS)/makefiles/common.mk

TWEAK_NAME = fridagumtest

$(TWEAK_NAME)_FILES = Tweak.xm
$(TWEAK_NAME)_CFLAGS = -fobjc-arc -w 
$(TWEAK_NAME)_LDFLAGS = -lm -lresolv
$(TWEAK_NAME)_LIBRARIES = frida-gum

include $(THEOS_MAKE_PATH)/tweak.mk

frida-gum API를 사용하기 위한 헤더 파일을 하나 생성합니다. frida-gum example(https://gist.github.com/oleavr/3edc47c9f69eb048de9d70ed45998f9c)을 참고하여 작성하였습니다.

#import "frida-gum.h"

#include <fcntl.h>
#include <unistd.h>
#include <Foundation/Foundation.h>

enum _FridaHookId
{
  FRIDA_HOOK_OBJC
};

typedef enum _FridaHookId FridaHookId;

struct _FridaListener
{
  GObject parent;

  FridaHookId hookid;
  NSString* fullname;
  guint num_calls;
  guint paramscount;
};

typedef struct _FridaListener FridaListener;

static void frida_listener_iface_init (gpointer g_iface, gpointer iface_data);

#define FRIDA_TYPE_LISTENER (frida_listener_get_type ())
G_DECLARE_FINAL_TYPE (FridaListener, frida_listener, FRIDA, LISTENER, GObject)
G_DEFINE_TYPE_EXTENDED (FridaListener,
                        frida_listener,
                        G_TYPE_OBJECT,
                        0,
                        G_IMPLEMENT_INTERFACE (GUM_TYPE_INVOCATION_LISTENER,
                            frida_listener_iface_init))

// Interceptor onEnter
static void
frida_listener_on_enter (GumInvocationListener * listener,
                           GumInvocationContext * ic);

// Interceptor onLeave
static void
frida_listener_on_leave (GumInvocationListener * listener,
                           GumInvocationContext * ic);

static void
frida_listener_class_init (FridaListenerClass * klass)
{
  (void) FRIDA_IS_LISTENER;
  (void) glib_autoptr_cleanup_FridaListener;
}

static void
frida_listener_iface_init (gpointer g_iface,
                             gpointer iface_data)
{
  GumInvocationListenerInterface * iface = static_cast<GumInvocationListenerInterface *>(g_iface);

  iface->on_enter = frida_listener_on_enter;
  iface->on_leave = NULL;
}

static void
frida_listener_init (FridaListener * self)
{
}

다음으로 frida-gum을 이용한 메인 로직을 Tweak.xm 파일에 작성합니다. 대소문자 구분 없이 *[dvia* *] regex 패턴을 가진 Objective-C 메서드를 추적하려 합니다.

#import "fridagumlistener.h"

#include <mach-o/dyld.h>
#include <Foundation/Foundation.h>

static BOOL imageLoaded = NO;
static char* loadedImagename;
static uint64_t image_base = 0x0;

// Interceptor onEnter
static void
frida_listener_on_enter (GumInvocationListener * listener,
                           GumInvocationContext * ic)
{
  FridaListener * self = FRIDA_LISTENER (listener);
  FridaHookId hook_id = self->hookid;
  NSString* full_name = self->fullname;
  gpointer inptr;

  switch (hook_id)
  {
    case FRIDA_HOOK_OBJC:
      // print objc method arguments
      NSLog(@"[nhncloud] [+] %@", full_name);
      if(self->paramscount > 0){
        for(int i = 0; i < self->paramscount; i++){
          	inptr = gum_invocation_context_get_nth_argument (ic, i + 2);
        	NSLog(@"[nhncloud] \t\targs%d: %p", (i + 2), inptr);
        }
      }

      break;
  }

  self->num_calls++;
}

static gboolean
instrument_objc_method (const GumApiDetails *details,
                        gpointer user_data)
{
  // gum interceptor set up
  GumInterceptor * interceptor;
  GumInvocationListener * listener;

  interceptor = gum_interceptor_obtain ();
  listener = static_cast<GumInvocationListener *>(g_object_new (FRIDA_TYPE_LISTENER, NULL));

  const char* name = details->name;
  GumAddress address = details->address;
  FRIDA_LISTENER(listener)->hookid = FRIDA_HOOK_OBJC;
  FRIDA_LISTENER(listener)->fullname = [NSString stringWithUTF8String:name];
  NSString *fullname = FRIDA_LISTENER(listener)->fullname;

  // check if objc method has arguments. then how many params it has
  if([fullname containsString:@":"]){
    NSArray *arrString= [[[fullname componentsSeparatedByString: @" "][1] componentsSeparatedByString:@"]"][0] componentsSeparatedByString:@":"];
    FRIDA_LISTENER(listener)->paramscount = (int) ([arrString count] - 1);
  }

  gum_interceptor_begin_transaction (interceptor);
  // gum interceptor attach to OBJC method
  gum_interceptor_attach (interceptor,
      GSIZE_TO_POINTER (address),
      listener,
      NULL);
  gum_interceptor_end_transaction (interceptor);

  return TRUE; // keep enumerating
}

void objcTrace(const char* objc_pattern){
  // find objc methods with pattern using "gum_api_resolver_enumerate_matches". then instrument it
  if(image_base != NULL){
    g_autoptr(GumApiResolver) resolver = gum_api_resolver_make("objc");
    gum_api_resolver_enumerate_matches (resolver,
                                        objc_pattern,
                                        instrument_objc_method,
                                        NULL,
                                        NULL);
  }
}

// check if wanted image loaded. then trace objc
void checkImageLoaded(){
  char* target_img = "DVIA-v2";
  while(true){
    image_base = gum_module_find_base_address(target_img);
    if(!image_base){
      continue;
    }
    else if(image_base){
      loadedImagename = target_img;
      NSLog(@"[nhncloud] wanted image %s loaded!", loadedImagename);
      objcTrace("*[dvia* *]/i");
      break;
    }
  }
}

%ctor {
  // initialize frida-gum
  gum_init_embedded ();
  NSLog(@"[nhncloud] frida-gum initialized!");

  // check image loaded asynchronously
  dispatch_async(dispatch_get_main_queue(), ^{
    checkImageLoaded();
  });
}

이제 트윅을 빌드하여 단말기에 설치하고, 타깃 앱인 DVIA-v2를 실행해 보면 Objective-C 메서드 콜 추적 로그가 잘 기록되는 것을 확인할 수 있습니다. 03.png

4. 느낀점

iOS 앱 분석을 하다 보면 기본 frida만으로는 한계에 부딪히는 경우가 많았습니다. 예컨대 구조체 접근이나 각종 Objective-C 문법을 Frida의 JavaScript API로 구현하기 힘든 경우가 있습니다.

frida-gum을 활용하여 트윅을 작성하면 C/C++/Objective-C 문법 사용이 가능해 이와 같은 한계 극복할 수 있었습니다. 기본 frida에서 제공하는 후킹 기능도 frida-gum을 이용해 대부분 구현이 가능했습니다. 다만 소스 코드 작성에 시간이 오래 걸린다는 점과, 매번 빌드가 필요하다는 단점도 있습니다.

그래서 기본 frida와 frida-gum을 활용한 프로그램을 동시에 사용한다면 iOS 앱 분석에 있어 상호 보완이 가능할 것으로 보입니다.

참고