frida-core devkit 에 Bridge 추가하기
2025년 10월 16일
frida-core #
frida-core는 프로세스에 frida-agent.so 를 주입하는 제어 담당 코드이다.
frida-agent 안에는 frida-gum을 내장해서 C로 작성된 스크립트를 프로세스에서 실행시키며 함수를 후킹하거나 프로세스의 정보를 획득할 수 있다.
frida-gumjs 는 frida-gum을 통해서 js 스크립트를 이용해 후킹하는 인터페이스를 제공한다.
frida-core 를 빌드할때 devkit 옵션을 지정하면 C를 이용해 ELF 바이너리의 실행으로 frida 스크립트를 주입할 수 있도록 위의 모든게 포함돼서 libfrida-core.a 파일로 빌드된다.
1git clone --recurse-submodules https://github.com/frida/frida-core.git
2cd frida-core
3git checkout tags/17.4.0
4mkdir android-arm64 && cd android-arm64
5../configure --host=android-arm64 --with-devkits=core
6
7cd ../android-arm64
8make
9cd src/devkit
아래처럼 라이브러리와 헤더, 예시 파일이 만들어진다.
1kdh@DESKTOP-MHEA7GE:~/frida-core/android-arm64/src/devkit$ ls
2frida-core-example.c frida-core.gir frida-core.h libfrida-core.a
불편하니까 쉽게 빌드되도록 만든 스크립트이다.
1# clang -v -DANDROID -ffunction-sections -fdata-sections frida-core-example.c -o frida-core-example -L. -lfrida-core -llog -ldl -lm -pthread -Wl,--export-dynamic
2
3export CC="/opt/android-ndk-r25c/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang"
4export STATIC="-L. -lfrida-core -llog -ldl -lm -pthread -Wl,--export-dynamic,-z,relro,-z,noexecstack,--gc-sections"
5export CFLAGS="-DANDROID -ffunction-sections -fdata-sections"
6export FILES="frida-core-example.c"
7export TARGET="frida-core-example"
8
9${CC} ${CFLAGS} -o ${TARGET} ${FILES} ${STATIC}
frida-core 가 17버전에서 변경된점 #
이전 버전에는 frida-gumjs 안에 다양한 플랫폼을 후킹할 수 있게 도와주는 bridge 들이 포함되어 빌드되고 있었다. (그래서 frida-core에도 자동으로 포함되어 있었음)
js 스크립트에서 Java.perform() 을 사용하거나 ObjC.available, Swift.String 등 각 플랫폼에 해당하는 환경에 따라 달라질 수 있는 부분들을 위한 객체들을 말하는데, 17 버전부터는 frida-gumjs 에서 분리되었다.
에이전트에서 로드할 스크립트들에 직접 import 후 번들링해서 로드하도록 변경됐는데 이런 작업을 하지 않으면 Java 라는 객체를 찾을 수 없어서 'Java' is not defined 과 같은 에러를 만나볼 수 있을 것이다.
사실 frida CLI에서는 아직도 포함되어있기 때문에 그런 에러는 만나볼 수 없다.
java-bridge의 역할 #
주로 사용하는 java-bridge 에서 어떤 역할을 하는지 설명하려 한다.
frida에서 Java 라는 객체는 JVM에 붙어서 Java 함수들을 후킹할 수 있도록 도와주는 역할을 한다.
안드로이드에서 실행되는 Java코드들은 JVM에서 관리되고, JVM은 art에서 실행되기 때문에 art에서 JVM의 오프셋을 찾고 원하는 함수를 후킹하게된다.
이때 java-bridge가 libart.so 에서 ClassLinker, JavaVM, JNIEnv, thread list 등의 오프셋을 시그니쳐나 바이트패턴 등으로 휴리스틱하게 직접 찾고 그 오프셋을 기준으로 함수의 오프셋이나 획득할 정보의 오프셋을 계산하게 된다.
안드로이드는 google play system 이 업데이트 되면서 art의 버전이 변경될 때가 있는데, 이때 java-bridge의 휴리스틱한 탐지를 벗어나는 art가 되는 경우가 있고 모든 오프셋이 틀어져 후킹이 불가능하게 된다.
그렇기 때문에 최신기기를 대응하기 위해서는 최신 bridge를 사용하는건 매우 중요하고, art를 삭제해서 버전을 초기 버전으로 내리거나, java-bridge의 버전을 올리거나, bridge에서 지원이 되지 않으면 직접 수정해서 art를 찾아내도록 패치할 수 있을 것이다.
ex)
https://github.com/MehmetEfeFriday/FRIDA-ART-FIX
1adb shell pm uninstall com.google.android.art
2adb shell reboot
기존 스크립트처럼 사용하고 싶은데? #
이렇게 변경된 상태에서는 바이너리 하나로 여러개의 스크립트 파일을 분석하려고 하는데, 몇가지 문제가 생긴다.
- 모든 의존성 bridge들을 매번 번들링 해야한다.
- 스크립트 하나하나에 번들된 브릿지들이 포함돼서 사이즈가 커진다.
- CLI에서 사용한 스크립트를 바로 사용할 수 없다.
해법은 의외로 간단했다.
번들링 스크립트 분석 #
번들링 방법 #
1mkdir frida-agent && cd frida-agent
2npm init -y
3npm i -D frida-compile frida-java-bridge
4
5cat script.js
6import Java from "frida-java-bridge";
7
8Java.perform(() => {
9 const m = Process.enumerateModules()[11];
10 console.log(JSON.stringify({
11 name: m.name,
12 base: m.base.toString(),
13 size: m.size,
14 path: m.path
15 }, null, 2));
16 console.log("in Java.perform");
17});
18
19npx frida-compile script.js -o agent.js
스크립트 분석 #
기존에 사용하던 스크립트를 번들하면 아래와 같이 agent.js 파일이 만들어진다.
위쪽에 이모지와 문자열이 보이고, __commonJS 안에서 내가 작성했던 코드가 포함되는 것을 볼 수 있다.
일단 스크립트 시작 전까지 헤더로 볼 수 있고, 숫자는 헤더를 제외한 스크립트의 전체 크기인 것을 알 수 있다.
만들어진 코드 #
헤더 + JavaBridge 코드 + __commonJS 시작 + 로드할 스크립트 + __commonJS 끝
이런 식으로 로드할 스크립트를 번들링할때처럼 묶으면 된다.
CLI에서 사용하던 스크립트는 Java. 으로 접근하기 때문에 __commonJS 내부에서는 frida_java_bridge_default 를 가리키도록 수정해야한다.
나머지는 한번 bridge로 번들링해보면 보인다. 만약 포맷이 맞지 않거나 헤더에서 표현된 사이즈가 안맞으면 Occurred core error Malformed package 라는 에러나 발생하거나 크래시가 발생한다.
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5char *make_header_with_number(size_t number, const char* filename, size_t *out_len) {
6 const unsigned char emoji_bytes[] = { 0xF0, 0x9F, 0x93, 0xA6 };
7 const unsigned char scissors_bytes[] = { 0xE2, 0x9C, 0x84 };
8
9 char numbuf[32];
10 int n = snprintf(numbuf, sizeof(numbuf), "%zu", number);
11 if (n < 0) return NULL;
12 size_t ndigits = (size_t)n;
13
14 // emoji(4) + \n(1) + digits(ndigits) + " "(1) + filename(flen) + \n(1) + scissors(3) + \n(1)
15 // = 11 + ndigits + flen
16 size_t flen = strlen(filename);
17 size_t payload_len = 11 + ndigits + flen;
18 size_t total_alloc = payload_len + 1;
19
20 unsigned char *buf = malloc(total_alloc);
21 if (!buf) return NULL;
22
23 size_t off = 0;
24 memcpy(buf + off, emoji_bytes, sizeof(emoji_bytes)); off += sizeof(emoji_bytes); // +4
25 buf[off++] = '\n'; // +1
26 memcpy(buf + off, numbuf, ndigits); off += ndigits; // +ndigits
27 buf[off++] = ' '; // +1
28 memcpy(buf + off, filename, flen); off += flen; // +flen
29 buf[off++] = '\n'; // +1
30 memcpy(buf + off, scissors_bytes, sizeof(scissors_bytes)); off += sizeof(scissors_bytes); // +3
31 buf[off++] = '\n'; // +1
32
33 if (out_len) *out_len = payload_len;
34 return (char*)buf;
35}
36
37char* wrap_script(char* script, size_t script_len) {
38 FILE* load_brdige = fopen("/data/local/tmp/load_bridge.js", "rb");
39
40 fseek(load_brdige, 0, SEEK_END);
41 int bridge_len = ftell(load_brdige);
42 fseek(load_brdige, 0, SEEK_SET);
43
44 const char* prologue =
45 "var require_script = __commonJS({"
46 " \"script.js\"() {"
47 " init_node_globals();"
48 " init_frida_java_bridge();"
49 " var Java = frida_java_bridge_default;"
50 "";
51 const char* epilogue =
52 ""
53 " var throwableClass;"
54 " var throwable;"
55 " var innerExceptionClass;"
56 " var innerException;"
57 " }"
58 "});"
59 "export default require_script();"
60 "";
61
62 size_t p_len = strlen(prologue);
63 size_t e_len = strlen(epilogue);
64
65 size_t total = bridge_len + p_len + script_len + e_len;
66
67 size_t header_len = 0;
68 char* header = make_header_with_number(total, "./script.js", &header_len);
69 char *out = (char*)malloc(total + header_len + 1);
70
71 size_t offset = 0;
72 memcpy(out + offset, header, header_len);
73 offset += header_len;
74 free(header);
75
76 fread(out + offset, 1, bridge_len, load_brdige);
77 offset += bridge_len;
78 fclose(load_brdige);
79
80 // 나머지도 전부 복사
81 memcpy(out + offset, prologue, p_len);
82 offset += p_len;
83 memcpy(out + offset, script, script_len);
84 offset += script_len;
85 memcpy(out + offset, epilogue, e_len);
86 offset += e_len;
87 out[offset] = '\0';
88 return out;
89}