Frida와 Go 통합 경험담

Frida와 Go 통합 경험담

Android 앱을 동적으로 분석하는 도구를 만들면서, Frida를 Go에서 네이티브로 사용하게 됐다. CLI subprocess 방식에서 frida-go 바인딩으로 전환한 과정을 기록한다.


왜 Go + Frida인가

Python이 Frida의 주력 언어지만, 배포 편의성 때문에 Go를 선택했다.

처음에는 Go에서 Frida CLI를 subprocess로 호출했다. 작동은 했지만 문제가 많았다.

// 초기 방식 - subprocess
cmd := exec.Command("frida", "-U", "-l", "hook.js", "-f", packageName)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()

문제점:


frida-go 바인딩

frida-go는 Frida의 공식 Go 바인딩이다. CGO를 사용해서 frida-core C 라이브러리를 직접 호출한다.

import "github.com/frida/frida-go/frida"

// 디바이스 연결
mgr := frida.NewDeviceManager()
device, err := mgr.DeviceByType(frida.DeviceTypeUsb)

// 프로세스에 attach
session, err := device.Attach(pid, nil)

// 스크립트 주입
script, err := session.CreateScript(hookScript)
script.On("message", func(message string) {
    // Go 함수로 메시지 처리
})
script.Load()

훨씬 깔끔하다. 에러 핸들링도 Go 방식으로 할 수 있고, 메시지도 콜백으로 받을 수 있다.


CGO 빌드 설정

frida-go는 CGO를 사용하므로 빌드가 복잡하다. Frida devkit을 다운로드해서 링크해야 한다.

1. Frida Devkit 다운로드

# macOS arm64 예시
FRIDA_VERSION="17.0.5"
curl -LO "https://github.com/frida/frida/releases/download/${FRIDA_VERSION}/frida-core-devkit-${FRIDA_VERSION}-macos-arm64.tar.xz"
tar xf frida-core-devkit-*.tar.xz -C ~/.kyobo/frida-devkit/

2. Makefile 설정

FRIDA_DEVKIT_DIR := $(HOME)/.kyobo/frida-devkit

build:
    CGO_ENABLED=1 \
    CGO_CFLAGS="-I$(FRIDA_DEVKIT_DIR)" \
    CGO_LDFLAGS="-L$(FRIDA_DEVKIT_DIR) -lfrida-core -lm -ldl -lpthread" \
    go build -o bin/app main.go

3. macOS 링커 경고 억제

macOS에서 중복 라이브러리 경고가 발생한다.

CGO_LDFLAGS="... -Wl,-no_warn_duplicate_libraries"

Spawn vs Attach

Frida에는 두 가지 연결 방식이 있다.

Spawn 모드

앱을 Frida가 직접 실행하고 attach.

pid, err := device.Spawn(packageName, nil)
session, err := device.Attach(pid, nil)
device.Resume(pid)

문제: jailed Android에서는 Gadget이 필요하다.

Error: Need Gadget to attach on jailed Android

Launch + Attach 모드

ADB로 앱을 먼저 실행하고, 실행 중인 프로세스에 attach.

// 1. ADB로 앱 실행
exec.Command("adb", "shell", "am", "start", "-n",
    "com.example.app/.MainActivity").Run()

// 2. PID 획득
output, _ := exec.Command("adb", "shell", "pidof", packageName).Output()
pid, _ := strconv.Atoi(strings.TrimSpace(string(output)))

// 3. Attach
session, err := device.Attach(pid, nil)

Rooted 에뮬레이터에서는 이 방식이 Gadget 없이 작동한다.


스크립트 임베딩

Go의 //go:embed를 사용해서 Frida 스크립트를 바이너리에 포함시켰다.

//go:embed hook.js
var hookScript string

//go:embed java.js
var javaBridge string

func injectHook(session *frida.Session) error {
    // java.js는 frida-tools의 Java 브릿지
    fullScript := javaBridge + "\n" + hookScript

    script, err := session.CreateScript(fullScript)
    if err != nil {
        return err
    }

    return script.Load()
}

java.jsfrida-tools/bridges에서 빌드한 Java API 브릿지다. Java.perform() 등의 API를 제공한다.


메시지 핸들링

Frida 스크립트에서 send()로 보낸 메시지를 Go에서 받는다.

// hook.js
send({type: 'file_created', path: '/sdcard/Download/output.pdf'});
script.On("message", func(message string) {
    var msg struct {
        Type    string `json:"type"`
        Payload struct {
            Type string `json:"type"`
            Path string `json:"path"`
        } `json:"payload"`
    }

    json.Unmarshal([]byte(message), &msg)

    if msg.Payload.Type == "file_created" {
        // 파일 다운로드 처리
        downloadFile(msg.Payload.Path)
    }
})

Cleanup 최적화

종료 시 cleanup이 5-10초 이상 걸리는 문제가 있었다.

// 문제 코드
func (m *Manager) cleanup() {
    m.script.Unload()  // 느림
    m.session.Detach() // 느림
}

해결: 타임아웃 + 병렬 실행

func (m *Manager) cleanup() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        m.script.Unload()
    }()

    go func() {
        defer wg.Done()
        m.session.Detach()
    }()

    done := make(chan struct{})
    go func() {
        wg.Wait()
        close(done)
    }()

    select {
    case <-done:
        // 정상 완료
    case <-ctx.Done():
        // 타임아웃
    }
}

이제 최대 2초 내 종료.


바이너리 크기

최종 바이너리는 약 122MB.

총 122 MB
├── 108 MB - frida-core 정적 라이브러리 + 의존성
├──   8 MB - Go 런타임 + 코드
└──   6 MB - Java 브릿지 + 후킹 스크립트

크긴 하지만, Python + Frida 환경 설정하는 것보다 낫다.


결론

frida-go 전환 후:

CGO 빌드가 복잡하지만, 배포 편의성이 이를 상쇄한다.

Frida를 Go에서 쓰고 싶다면 frida-go를 추천한다. 다만 CGO 빌드에 대한 이해가 필요하다.

Back