Post

How to protect my code from reversing - Rust Binary

.so 파일을 리버싱으로부터 보호하는 방법에 대한 연구 (Rust)

How to protect my code from reversing - Rust Binary

Rust 기반 .so 적용 시 UnsatisfiedLinkError 크래시

이 글은 .so 를 보호하는 방법 에서 다룬 ELF 보호 도구를 실제로 운영하면서 마주친 버그의 원인 분석과 해결 과정을 기록한다.

현상

NDK C/C++ 로 작성된 라이브러리는 정상 동작하는 보호 도구를 Rust 기반 Android 라이브러리(libnative.so)에 적용했더니 앱이 즉시 크래시됐다.

1
2
3
4
5
6
7
E REDACTED.Sample: No implementation found for void com.REDACTED.REDACTED.init()
  (tried Java_com_REDACTED_REDACTED_init
   and Java_com_REDACTED_REDACTED_init__)
  - is the library loaded, e.g. System.loadLibrary?

E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: java.lang.UnsatisfiedLinkError: ...

라이브러리 로드 자체는 성공했고, 재배치도 문제없었다. 그러나 REDACTED.init() 를 호출하는 순간 찾을 수 없다는 것이다.

원인 분석

logcat 에서 핵심 라인을 뽑아보면:

1
2
3
4
5
I P4CKER: [diag] reloc tables: relr_count=0 android_rel_size=0 android_rela_size=0 rel_count=0 rela_count=6471
I P4CKER: embedded library embedded-payload does not export JNI_OnLoad; constructors only
I P4CKER: loaded embedded library embedded-payload (..., size=3756032)
I P4CKER: calling constructors for embedded-payload (..., init_array_count=0)
→ (crash)

두 가지 관찰이 눈에 띈다:

  1. JNI_OnLoad 없음
  2. init_array_count=0 으로 생성자도 없음

Rust 로 Android JNI 바인딩을 작성하는 방식은 C/C++ 과 다르다.

C/C++ 일반적인 패턴:

1
2
3
4
5
6
7
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    // RegisterNatives 또는 그냥 버전 반환
    return JNI_VERSION_1_6;
}

// JVM이 JNI_OnLoad 이후 dlsym으로 이 함수를 직접 찾음
JNIEXPORT void JNICALL Java_com_example_Foo_bar(JNIEnv *env, jobject thiz) { ... }

Rust 일반적인 패턴:

1
2
3
4
5
6
7
8
// JNI_OnLoad 없음
// #[no_mangle]로 C ABI 명명 규칙에 따라 직접 export
#[no_mangle]
pub extern "system" fn Java_com_REDACTED_REDACTED_init(
    env: JNIEnv, _class: JClass
) {
    // ...
}

Rust 는 JNI_OnLoad 없이 Java_* 심볼을 직접 export 하는 것이 관용적인 패턴이다. JNI_OnLoad 가 있더라도 내부에서 별도의 RegisterNatives 를 호출하지 않고 명명 규칙에만 의존하는 경우가 많다.

문제는 기존 로더 코드의 분기 구조에 있었다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    if (ensure_library_loaded() != 0) return JNI_ERR;
    if (g_payload_jni_loaded) return g_cached_jni_version;

    // ↓ JNI_OnLoad 가 없으면 여기서 바로 return → auto_register 호출 안 됨!
    if (g_library.jni_onload == NULL) {
        g_cached_jni_version = JNI_VERSION_1_6;
        g_payload_jni_loaded = 1;
        return g_cached_jni_version;   // ← 문제의 조기 반환
    }

    // JNI_OnLoad 있을 때만 여기까지 도달
    g_cached_jni_version = g_library.jni_onload(vm, reserved);
    ...

    // Java_* 심볼 등록 → JNI_OnLoad 있는 경우에만 실행됨
    auto_register_jni_symbols(env, &g_library);
    ...
}

auto_register_jni_symbols() 는 payload 심볼 테이블에서 Java_* 형식의 STT_FUNC 심볼을 스캔하여 JNI Reflection 으로 RegisterNatives 를 호출하는 함수다. 익명 메모리에 로드된 payload 는 JVM 의 dlsym 기반 탐색에서 보이지 않으므로, 이 함수로 강제 등록하지 않으면 JVM 이 절대 찾을 수 없다.

sequenceDiagram
    participant JVM
    participant Carrier as Carrier SO (링커에 등록됨)
    participant Anon as Payload (익명 메모리)

    JVM->>Carrier: System.loadLibrary("libnative")
    JVM->>Carrier: JNI_OnLoad 호출
    Carrier->>Carrier: ensure_library_loaded() → payload를 익명 메모리에 로드
    Note over Carrier: jni_onload == NULL 확인
    Carrier-->>JVM: JNI_VERSION_1_6 반환 (바로 return)
    Note over Anon: Java_*_initJni 심볼 존재하지만 미등록 상태

    JVM->>Carrier: dlsym(handle, "Java_*_initJni")
    Note over Carrier: 캐리어 심볼 테이블에 없음
    Carrier-->>JVM: NULL
    JVM->>JVM: UnsatisfiedLinkError 발생!

해결

auto_register_jni_symbols() 호출을 JNI_OnLoad 존재 여부와 무관하게 항상 실행하도록 분기 구조를 변경했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 변경 전: JNI_OnLoad 없으면 조기 return
if (g_library.jni_onload == NULL) {
    g_cached_jni_version = JNI_VERSION_1_6;
    g_payload_jni_loaded = 1;
    return g_cached_jni_version;   // ← 여기서 끝
}
// ... JNI_OnLoad 호출 ...
auto_register_jni_symbols(env, &g_library);  // JNI_OnLoad 있을 때만 실행

// 변경 후: if-else 로 구조 변경, auto_register는 항상 실행
if (g_library.jni_onload == NULL) {
    // JNI_OnLoad 없는 경우 (Rust 등)
    g_cached_jni_version = JNI_VERSION_1_6;
} else {
    // JNI_OnLoad 있는 경우 (C/C++, Unreal 등)
    g_cached_jni_version = g_library.jni_onload(vm, reserved);
    if (g_cached_jni_version == JNI_ERR) return JNI_ERR;
}

// JNI_OnLoad 유무 관계없이 항상 Java_* 심볼 등록
auto_register_jni_symbols(env, &g_library);

수정 후 흐름:

sequenceDiagram
    participant JVM
    participant Carrier as Carrier SO
    participant Anon as Payload (익명 메모리)

    JVM->>Carrier: JNI_OnLoad 호출
    Carrier->>Carrier: ensure_library_loaded()
    Note over Carrier: jni_onload == NULL 확인
    Carrier->>Carrier: g_cached_jni_version = JNI_VERSION_1_6
    Carrier->>Carrier: auto_register_jni_symbols() 실행
    loop payload symtab Java_* STT_FUNC 심볼 각각
        Carrier->>JVM: FindClass("com/...REDACTED/REDACTED") → cls
        Carrier->>JVM: getDeclaredMethods() → initJni 찾기
        Carrier->>JVM: RegisterNatives(cls, {name, sig, fn_ptr})
    end
    Carrier-->>JVM: JNI_VERSION_1_6 반환

    JVM->>Carrier: REDACTED.init() 호출
    Carrier->>Anon: 등록된 fn_ptr 로 직접 dispatch
    Note over Anon: 정상 실행!

수정 후 예상 로그

1
2
3
4
5
6
7
8
9
10
# 수정 전
I P4CKER: embedded library embedded-payload does not export JNI_OnLoad; constructors only
→ (auto_register_jni_symbols 호출 없이 반환)
E REDACTED.Sample: No implementation found for void ...REDACTED.init()
E AndroidRuntime: FATAL EXCEPTION

# 수정 후
I P4CKER: carrier JNI_OnLoad: payload has no JNI_OnLoad, scanning for Java_* symbols
I P4CKER: auto_register_jni_symbols: registered=N skipped=M
→ REDACTED.init() 정상 호출

영향 범위

이 수정은 Rust 에만 국한되지 않는다. JNI_OnLoad 없이 Java_* 명명 규칙만으로 JNI 함수를 export 하는 모든 언어/빌드시스템에 동일하게 적용된다.

케이스수정 전수정 후
C/C++ with JNI_OnLoad✅ 정상✅ 정상
C/C++ without JNI_OnLoad + Java_*❌ 크래시✅ 정상
Rust #[no_mangle] Java_*❌ 크래시✅ 정상
Kotlin/Native❌ 크래시✅ 정상
Unreal Engine (JNI_OnLoad 있음)✅ 정상✅ 정상
Go (gomobile)❌ 크래시..✅ 아마도..정상 - Next(?)

Conclusion

보호 도구의 목표는 모든 .so 에 대한 호환성이다. 내가 빌드하지 않은 것들 — Unreal, Unity, Flutter, 그리고 이번처럼 Rust — 이 오히려 더 다양한 엣지케이스를 가져온다. 각 언어/엔진의 ABI 관용패턴을 이해하는 것이 결국 호환성 있는 보호 도구를 만드는 핵심이다.

This post is licensed under CC BY 4.0 by the author.
If you find any errors, please let me know by comment or email. Thank you.

© Ruffalo. Some rights reserved.

I'm

Using the Chirpy theme for Jekyll.