직접 주입한 frida-gum 사용하기
서론
출처: Frida-gum을 이용한 Android Hook - 라온
출처의 글이 4~5년 정도 되기도 했고 지금까지는 frida를 이렇게까지 쓴적이 없었지만, 이해도와 숙련도를 높이기 위해 정리해보려고 한다.
frida-gum 라이브러리
일반적인 프리다 사용 방식은 frida 프로젝트를 빌드하면 생성되는 frida-server 를 실행하고 cli와의 통신으로 frida-agent가 라이브러리 형태로 앱에 주입되어 내부의 frida-gum 모듈을 통해 C/C++ 함수의 후킹이 가능하게 된다.
그래서 이런 방법이 있는지도 모르고 사용하고 있었는데, frida-gum을 라이브러리 형태로 빌드하고 이걸 주입해서 후킹코드를 작성하여 클라이언트처럼 사용하는 방식이 있었다.
한번 해보자
예제 테스트
테스트 환경
- wsl ubuntu 24.04
- Android 13 (sdk 33) arm64
- android-ndk-r25c (현재 지원하는 최신버전)
- frida-gum 16.5.6
libfrida-gum.a 라이브러리 빌드
devkit으로 빌드하면 라이브러리와 예제 파일을 얻을 수 있다.
meson 빌드 시스템을 사용하며, configure -> releng/meson_configure.py -> meson setup 로 연결되고 기본적으로 meson.options 파일에 정의된 기본 옵션들과 인자로 전달한 추가 옵션을 파싱해서 빌드 디렉터리를 생성하게 된다.
이후 make -> Makefile -> releng/meson_make.py -> meson compile 순서로 호출되어 빌드되며 빌드디렉터리 내에 정의된 옵션과 meson.build 에 정의된 규칙에 따라 frida가 빌드되는 구조를 갖고 있다.
git clone --recursive https://github.com/frida/frida-gum.git
git checkout tags/16.5.6
make
mkdir android-arm64
cd android-arm64
../configure --host=android-arm64 --with-devkits=gum
make
cd ./gum/devkit
예제 파일 빌드 및 실행
예제 코드를 보면 맨위에 주석으로 어떻게 빌드하는지도 적혀있다. 하지만 나는 크로스플랫폼 타겟(안드로이드)으로 작성할 것이기 때문에 clang을 ndk에 있는걸로 써야한다.
~/android-ndk-r25c/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang -DANDROID -ffunction-sections -fdata-sections frida-gum-example.c -o frida-gum-example -L. -lfrida-gum -llog -ldl -lm -pthread
빌드된 파일을 실행하면 이렇게 보인다.
구조 분석
example 코드는 간단하다. 그냥 리스너 attch 후 open, close를 후킹하고 호출하고, 리스너 detach 후 호출하기만 한다.
int
main (int argc,
char * argv[])
{
// ... 인터셉터를 생성하고 리스너를 opem, close 함수에 attach 한다.
interceptor = gum_interceptor_obtain ();
gum_interceptor_begin_transaction (interceptor);
gum_interceptor_attach (interceptor,
GSIZE_TO_POINTER (gum_module_find_export_by_name (NULL, "open")),
listener,
GSIZE_TO_POINTER (EXAMPLE_HOOK_OPEN));
gum_interceptor_attach (interceptor,
GSIZE_TO_POINTER (gum_module_find_export_by_name (NULL, "close")),
listener,
GSIZE_TO_POINTER (EXAMPLE_HOOK_CLOSE));
gum_interceptor_end_transaction (interceptor);
// 테스트 1. 오픈, 클로즈 테스트
close (open ("/etc/hosts", O_RDONLY));
close (open ("/etc/fstab", O_RDONLY));
// 리스너를 detach 한다.
gum_interceptor_detach (interceptor, listener);
// 잘 detach 됐는지 확인. 이때는 로그출력이 안된다.
close (open ("/etc/hosts", O_RDONLY));
close (open ("/etc/fstab", O_RDONLY));
// detach 이후에 리스너가 호출됐는지 확인하는 코드.
// 위에서 볼 수 있듯 여전히 4 call 이다.
g_print ("[*] listener still has %u calls\n", EXAMPLE_LISTENER (listener)->num_calls);
// ...
}
// on_enter 리스너이다.
static void
example_listener_on_enter (GumInvocationListener * listener,
GumInvocationContext * ic)
{
ExampleListener * self = EXAMPLE_LISTENER (listener);
ExampleHookId hook_id = GUM_IC_GET_FUNC_DATA (ic, ExampleHookId);
switch (hook_id)
{
case EXAMPLE_HOOK_OPEN:
g_print ("[*] open(\"%s\")\n", (const gchar *) gum_invocation_context_get_nth_argument (ic, 0));
break;
case EXAMPLE_HOOK_CLOSE:
g_print ("[*] close(%d)\n", GPOINTER_TO_INT (gum_invocation_context_get_nth_argument (ic, 0)));
break;
}
// 호출될 때마다 호출 카운트 증가
self->num_calls++;
}
직접 인젝트해서 사용
예제코드는 자기 자신을 후킹하는 것이기 때문에 api 테스트는 가능하지만 의미 없는 코드이다.
테스트
공유 라이브러리 형태로 빌드
main 함수를 라이브러리 로드하면서 실행할 수 있도록 .init_array 등록 코드로 변경한다.
- int main(int argc, char * argv[])
+ __attribute__((constructor))
+ int init(void *arg)
빌드할때는 공유 라이브러리 형태로 빌드되도록 -shared 옵션을 추가하면 된다.
~/android-ndk-r25c/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang -DANDROID -ffunction-sections -fdata-sections frida-gum-example.c -o frida-gum-example -L. -lfrida-gum -llog -ldl -lm -pthread -shared
인젝터 빌드
~ 이 깃허브에서 빌드해서 사용하면 된다.
~ 방식으로 ~ 에러가 발생하기 때문에 이동시켜서 인젝트 한다.
Comments