[frida] frida memo

[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}
  1. static 필드 초기화
  2. 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 : 인스턴스를 접근하는것
  • 스택 트레이스 확인

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에서 호출해주는 부분이 있는데, 이 부분을 후킹해야한다. 6b7f5f18-a879-45ac-92f5-d87db4d352b0
    • 그냥 후킹하면 잘 안되니 오프셋을 가져와서 직접 메모리 주소에서 후킹하면 잘된다
      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);

152afc47-6cdb-49fd-a088-51bca6aeba21

ebebc0f3-55b1-4999-b9d8-48e6f0d47572

  • 익명 객체는 컴파일 시 숫자 이름이 생긴다. (Lcom/example/ClassName$1;)
comments powered by Disqus