[frida] frida memo

frida 정리 전 메모

앱 실행 순서 관련

클래스 로드 시 순서

class Example {
    static final String LIB_LOADER_NAME = "loader";   // 이건 컴파일타임에 값이 정해져서 초기화 되기 때문에 별도의 초기화는 없다. 

    static int a = 1;    // --- 1.
    static int b;        // --- 2.
    
    static {
        b = 2;           // --- 3.
    }
    
    static {
        a = 3;           // --- 4.
    }
}
  1. static 필드 초기화
  2. static 초기화블록 순서대로 실행
    smali로 변경하면 아래와 같다.
.method static constructor <clinit>()V
    .registers 1

    ; static int a = 1;
    const/4 v0, 0x1
    sput v0, LExample;->a:I

    ; static int b; (초기화되지 않은 static 필드는 기본값 0으로 초기화됩니다)

    ; b = 2;
    const/4 v0, 0x2
    sput v0, LExample;->b:I

    ; a = 3;
    const/4 v0, 0x3
    sput v0, LExample;->a:I

    return-void
.end method

Frida 관련

  • Java 연결
    • Java와 상호작용(Java.use, Java.choose 등)을 하기위해 무조건 실행해야 한다.
    • Java.perform : 프로세스의 java코드와 상호작용을 위해 java runtime과 연결함
      • spawn 방식에서는 Java 런타임이 초기화되고 준비되면 실행된다.
      • attach 방식에서는 이미 런탕미이 초기화된 상태기 때문에 즉시 실행된다.
    • Java.performNow : 런타임 초기화와 상관 없이 동기적으로 즉시 실행되고, 런타임이 준비되지 않은 경우 예외가 발생한다.
      • spawn 방식에서는 초기화되지 않은 상태에서 호출 시 예외가 발생할 수 있음.
      • attach 방식은 perform과 동일하다.
// 스크립트 내에서 지연 연결
setTimeout(function() {
    Java.perform(function() {
        // Java 코드 조작
    });
}, 1000); // 1초 후 실행

// 라이브러리 로드 후 
Interceptor.attach(Module.findExportByName(null, "dlopen"), {
    onLeave: function(retval) {
        Java.perform(function() {
            // 특정 라이브러리 로드 후 실행되는 코드
        });
    }
});
  • Java.use 를 사용하면 클래스가 강제로 로드된다

    • 아직 클래스가 로드되지 않은 경우 강제로 clinit도 호출된다. (항상 보장되는건 아님)
      • lazyLoading 클래스의 경우 실제로 사용하기 전까지 clinit이 지연된다.
        (static 메소드나 필드가 사용될때, 인스턴스가 생성될때)
    • Java.use : 클래스를 접근하는것
    • Java.choose : 인스턴스를 접근하는것
  • 스택 트레이스 확인

var stackTrace = currentThread.getStackTrace();
for (var i = 0; i < stackTrace.length; i++) {
    console.log(stackTrace[i].toString());
}
  • 스토커 API를 사용하면 이론상 어셈 수준에서 팔로우가 가능하다
  • 후킹하고 onEnter에서 this 객체에 데이터를 저장해두면, onLeave에서 꺼낼 수 있고 그것은 한번의 후킹 (onEnter->onLeave) 에서 유효하다.
  • frida로 스레드 생성 부분을 후킹하는 방법.
    • pthread_create를 후킹한다고 새로운 스레드의 tid를 알아올 순 없다. 기존 스레드에서 그냥 스레드 생성하는 함수를 호출한 것이다.
    • pthread_create의 스택을 확인해보면, libc에서 호출해주는 부분이 있는데, 이 부분을 후킹해야한다.
      6b7f5f18-a879-45ac-92f5-d87db4d352b0
      6b7f5f18-a879-45ac-92f5-d87db4d352b0
    • 그냥 후킹하면 잘 안되니 오프셋을 가져와서 직접 메모리 주소에서 후킹하면 잘된다
      const threadStartAddress = libcModule.base.add(0xC21A0);
      
      Interceptor.attach(threadStartAddress, {
          onEnter: function (args) {
              const tid = Process.getCurrentThreadId();
              console.log('New thread started -> tid: ', tid );
              console.log('Thread argument:', args[0]);
          }
      });
      
    • 스레드의 시작 부분에서 후킹했으니 Process.getCurrentThreadId도 pthread에서 생성한 ID이다.

기타정보

  • 스레드풀을 사용하면, 스레드를 재사용하기 때문에 같은 tid를 갖는다. 스레드 이름을 출력해서 확인할 수 있다.
PID: 19535, TID: 19650
Thread Name: Thread-49
l208b01bc.$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

PID: 19535, TID: 19650
Thread Name: Thread-52
l208b01bc.$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를 출력해야한다.

Interceptor.attach(Module.findExportByName(null, 'pthread_create'), {
    onEnter: function(args) {
        console.log('[Native] New thread created, Thread ID:', this.threadId);
        // this.context의 내용을 출력
        // console.log('[Native] Context:');
        // for (let key in this.context) {
        //     console.log(`  ${key}: ${this.context[key]}`);
        // }

        // 특정 레지스터 값을 출력하고 싶다면 (예: PC, SP)
        // console.log('[Native] Specific registers:');
        // console.log('  PC:', this.context.pc);
        // console.log('  SP:', this.context.sp);

        // 스택 포인터 주변의 메모리 내용을 출력하고 싶다면
        // console.log('[Native] Memory around SP:');
        // console.log(hexdump(this.context.sp, { length: 32 }));


        // pthread_create를 호출한 주소 얻기
        var callerAddress = this.returnAddress;
        console.log('Caller Address:', callerAddress);

    }
});
  • 프리다에서 라이브러리(모듈)의 base 위치를 확인했을때 안드로이드 다이나믹 피쳐에서 메모리에 앱이름으로 로드된것 처럼 보이지만, 결국엔 라이브러리이다.
function getMaps() {
    var maps = [];
    Process.enumerateModules({
        onMatch: function(module) {
            if (module.name == "libloader.so") {
                maps.push({
                    name: module.name,
                    base: module.base,
                    size: module.size,
                    path: module.path
                });
            }
        },
        onComplete: function() {
            console.log(JSON.stringify(maps, null, 2));
        }
    });
}

// 라이브러리 로드를 기다림
setTimeout(function() {
    getMaps();
}, 2000);

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

ebebc0f3-55b1-4999-b9d8-48e6f0d47572
ebebc0f3-55b1-4999-b9d8-48e6f0d47572

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

Comments

ESC
Type to search...