[frida] frida memo
2024년 6월 26일
frida 정리 전 메모
앱 실행 순서 관련 #
클래스 로드 시 순서 #
1class Example {
2 static final String LIB_LOADER_NAME = "loader"; // 이건 컴파일타임에 값이 정해져서 초기화 되기 때문에 별도의 초기화는 없다.
3
4 static int a = 1; // --- 1.
5 static int b; // --- 2.
6
7 static {
8 b = 2; // --- 3.
9 }
10
11 static {
12 a = 3; // --- 4.
13 }
14}
- static 필드 초기화
- static 초기화블록 순서대로 실행 smali로 변경하면 아래와 같다.
1.method static constructor <clinit>()V
2 .registers 1
3
4 ; static int a = 1;
5 const/4 v0, 0x1
6 sput v0, LExample;->a:I
7
8 ; static int b; (초기화되지 않은 static 필드는 기본값 0으로 초기화됩니다)
9
10 ; b = 2;
11 const/4 v0, 0x2
12 sput v0, LExample;->b:I
13
14 ; a = 3;
15 const/4 v0, 0x3
16 sput v0, LExample;->a:I
17
18 return-void
19.end method
Frida 관련 #
- Java 연결
- Java와 상호작용(Java.use, Java.choose 등)을 하기위해 무조건 실행해야 한다.
- Java.perform : 프로세스의 java코드와 상호작용을 위해 java runtime과 연결함
- spawn 방식에서는 Java 런타임이 초기화되고 준비되면 실행된다.
- attach 방식에서는 이미 런탕미이 초기화된 상태기 때문에 즉시 실행된다.
- Java.performNow : 런타임 초기화와 상관 없이 동기적으로 즉시 실행되고, 런타임이 준비되지 않은 경우 예외가 발생한다.
- spawn 방식에서는 초기화되지 않은 상태에서 호출 시 예외가 발생할 수 있음.
- attach 방식은 perform과 동일하다.
1// 스크립트 내에서 지연 연결
2setTimeout(function() {
3 Java.perform(function() {
4 // Java 코드 조작
5 });
6}, 1000); // 1초 후 실행
7
8// 라이브러리 로드 후
9Interceptor.attach(Module.findExportByName(null, "dlopen"), {
10 onLeave: function(retval) {
11 Java.perform(function() {
12 // 특정 라이브러리 로드 후 실행되는 코드
13 });
14 }
15});
-
Java.use 를 사용하면 클래스가 강제로 로드된다
- 아직 클래스가 로드되지 않은 경우 강제로 clinit도 호출된다. (항상 보장되는건 아님)
- lazyLoading 클래스의 경우 실제로 사용하기 전까지 clinit이 지연된다. (static 메소드나 필드가 사용될때, 인스턴스가 생성될때)
- Java.use : 클래스를 접근하는것
- Java.choose : 인스턴스를 접근하는것
- 아직 클래스가 로드되지 않은 경우 강제로 clinit도 호출된다. (항상 보장되는건 아님)
-
스택 트레이스 확인
1var stackTrace = currentThread.getStackTrace();
2for (var i = 0; i < stackTrace.length; i++) {
3 console.log(stackTrace[i].toString());
4}
- 스토커 API를 사용하면 이론상 어셈 수준에서 팔로우가 가능하다
- 후킹하고 onEnter에서 this 객체에 데이터를 저장해두면, onLeave에서 꺼낼 수 있고 그것은 한번의 후킹 (onEnter->onLeave) 에서 유효하다.
- frida로 스레드 생성 부분을 후킹하는 방법.
- pthread_create를 후킹한다고 새로운 스레드의 tid를 알아올 순 없다. 기존 스레드에서 그냥 스레드 생성하는 함수를 호출한 것이다.
- pthread_create의 스택을 확인해보면, libc에서 호출해주는 부분이 있는데, 이 부분을 후킹해야한다.
- 그냥 후킹하면 잘 안되니 오프셋을 가져와서 직접 메모리 주소에서 후킹하면 잘된다
1const threadStartAddress = libcModule.base.add(0xC21A0); 2 3Interceptor.attach(threadStartAddress, { 4 onEnter: function (args) { 5 const tid = Process.getCurrentThreadId(); 6 console.log('New thread started -> tid: ', tid ); 7 console.log('Thread argument:', args[0]); 8 } 9}); - 스레드의 시작 부분에서 후킹했으니 Process.getCurrentThreadId도 pthread에서 생성한 ID이다.
기타정보 #
- 스레드풀을 사용하면, 스레드를 재사용하기 때문에 같은 tid를 갖는다. 스레드 이름을 출력해서 확인할 수 있다.
1PID: 19535, TID: 19650
2Thread Name: Thread-49
3l208b01bc.$init is called: msg={app_display_name} detected malware in use, like Magisk or similar tool. To protect you, the app will close. Support REF: 6908, shortMsg=Magisk Detected by App, shouldExit=true, restart=false, notifyViaUrl=false, urlToOpen=, localeKey=, messageSuffix=7144:E10C 1900000
4
5PID: 19535, TID: 19650
6Thread Name: Thread-52
7l208b01bc.$init is called: msg={app_display_name} detected use of Frida or similar tool. To protect you, the app will close. Support REF: 6905-1, shortMsg=Frida Detected by App, shouldExit=true, restart=false, notifyViaUrl=false, urlToOpen=, localeKey=, messageSuffix=4513:E10C
-
스레드가 호출되면서 특정함수를 호출한다면, 스택트레이스의 맨 위는 해당 스레드라서 더이상 참조할 수 없게된다.
-
스택트레이스 연결할때 앱이 멈추는 경우도 있는데, 그냥 스택을 가져와서 마음대로 볼 수 있고, 리턴주소도 읽을 수 있기 때문에 위치를 찾기 쉽다. 현재 예시에선 onEnter라서 함수 호출 직후이기 때문에 pc를 출력해봤자 의미없고, returnAddress를 출력해야한다.
1Interceptor.attach(Module.findExportByName(null, 'pthread_create'), {
2 onEnter: function(args) {
3 console.log('[Native] New thread created, Thread ID:', this.threadId);
4 // this.context의 내용을 출력
5 // console.log('[Native] Context:');
6 // for (let key in this.context) {
7 // console.log(` ${key}: ${this.context[key]}`);
8 // }
9
10 // 특정 레지스터 값을 출력하고 싶다면 (예: PC, SP)
11 // console.log('[Native] Specific registers:');
12 // console.log(' PC:', this.context.pc);
13 // console.log(' SP:', this.context.sp);
14
15 // 스택 포인터 주변의 메모리 내용을 출력하고 싶다면
16 // console.log('[Native] Memory around SP:');
17 // console.log(hexdump(this.context.sp, { length: 32 }));
18
19
20 // pthread_create를 호출한 주소 얻기
21 var callerAddress = this.returnAddress;
22 console.log('Caller Address:', callerAddress);
23
24 }
25});
- 프리다에서 라이브러리(모듈)의 base 위치를 확인했을때 안드로이드 다이나믹 피쳐에서 메모리에 앱이름으로 로드된것 처럼 보이지만, 결국엔 라이브러리이다.
1function getMaps() {
2 var maps = [];
3 Process.enumerateModules({
4 onMatch: function(module) {
5 if (module.name == "libloader.so") {
6 maps.push({
7 name: module.name,
8 base: module.base,
9 size: module.size,
10 path: module.path
11 });
12 }
13 },
14 onComplete: function() {
15 console.log(JSON.stringify(maps, null, 2));
16 }
17 });
18}
19
20// 라이브러리 로드를 기다림
21setTimeout(function() {
22 getMaps();
23}, 2000);
- 익명 객체는 컴파일 시 숫자 이름이 생긴다. (Lcom/example/ClassName$1;)