안드로이드 So 파일 보호 기법: 섹션 및 해시 기반 암호화

  1. 개요 안드로이드 앱의 네이티브 코드 보호를 위한 소 파일(소, .so) 강화 기술에 대해 다룹니다. 주로 두 가지 접근 방식을 탐구합니다: 특정 섹션 내 함수를 암호화하는 방법과 동적 심볼 테이블 기반으로 함수 위치를 찾아 암호화하는 방법입니다. 이러한 기법은 라이브러리 내 핵심 로직을 보호하고 역공학을 어렵게 만듭니다.

  2. 섹션 기반 암호화 암호화 원리: 사용자가 정의한 섹션(.mytext 등)에 특정 함수를 배치하고, 이 섹션의 내용을 암호화하여 저장합니다. 실행 시, __attribute__((constructor)) 특성을 가진 초기화 함수가 메모리에 로드되기 전에 실행되어 암호화된 섹션을 복호화합니다.

구현 절차

  1. 네이티브 코드 작성 함수 선언에 __attribute__((section(".mytext")))를 추가하여 해당 함수를 사용자 정의 섹션에 포함합니다.
jint JNICALL native_Add(JNIEnv* env, jobject obj, jdouble num1, jdouble num2)
    __attribute__((section(".mytext")));
  1. 복호화 함수 생성 __attribute__((constructor))를 적용해 main() 실행 전에 자동 실행되도록 설정합니다. 이 함수는 /proc/<pid>/maps를 통해 로드된 .so 파일의 기준 주소를 확인하고, 섹션 정보를 추출하여 암호화된 코드를 복호화합니다.
void init_native_Add() __attribute__((constructor));
void init_native_Add() {
    unsigned long base = getLibAddr();
    Elf32_Ehdr* ehdr = (Elf32_Ehdr*)base;
    Elf32_Shdr* shdr = (Elf32_Shdr*)(base + ehdr->e_shoff);
    // 섹션 이름 비교 후 암호화된 데이터 복호화
    for (int i = 0; i < nblock; i++) {
        ((char*)text_addr)[i] ^= 0xFF;
    }
}
  1. 외부 암호화 프로그램 개발 Visual Studio 2010 기반 프로그램으로 .so 파일을 읽어 .mytext 섹션을 찾고, 간단한 비트 반전(~) 알고리즘으로 암호화합니다. 이후 암호화된 데이터를 e_entrye_shoff 필드에 저장하여 복호화 시 필요한 정보를 포함합니다.

  2. 실행 결과 검증 암호화된 .so 파일을 애플리케이션에 통합하여 로딩하고, 함수 호출 결과를 확인합니다. 원본 값에 상수를 더한 결과가 반환됨으로써 암호화/복호화가 성공적으로 수행되었음을 확인할 수 있습니다.

핵심 지식

  • ELF 파일 구조 이해: 섹션 테이블, 문자열 테이블, 프로그램 헤더의 역할.
  • e_shoff, e_entry 등의 필드는 로드 시 무시되므로, 암호화 목적에 맞게 재사용 가능.
  • mprotect()를 이용해 메모리 권한을 일시적으로 PROT_READ | PROT_WRITE | PROT_EXEC로 변경하여 복호화 수행.
  1. 해시 기반 함수 직접 암호화 이 기법은 함수 이름을 기반으로 .hash 섹션에서 심볼 위치를 탐색하여 암호화하는 방식입니다.

원리

  • .dynsym 섹션에는 함수 심볼 정보가 포함되고, .dynstr에는 함수 이름이 저장됩니다.
  • .hash 섹션은 해시 테이블이며, 함수 이름의 해시값을 기반으로 bucket[]chain[] 구조를 통해 심볼 인덱스를 찾습니다.
  • 해시 함수는 일반적으로 elf_hash()로 구현되며, 입력 이름에 따라 고정된 해시 값을 산출합니다.

구현 단계

  1. 동적 섹션 파싱: PT_DYNAMIC 프로그램 헤더를 통해 .dynsym, .dynstr, .hash 섹션의 오프셋과 크기를 추출합니다.
  2. 함수 위치 탐색: 주어진 함수 이름을 elf_hash()로 해시하고, bucket[hash % nbucket]를 통해 시작 인덱스를 얻습니다. 이후 chain 링크를 따라 실제 심볼을 찾습니다.
  3. 암호화 수행: st_valuest_size 정보를 바탕으로 함수 코드 영역을 선택하고, 비트 반전 또는 기타 암호화 방식으로 처리합니다.
static char getTargetFuncInfo(char* szFileData, const char *funcName, funcInfo *info) {
    unsigned funHash = elfhash(funcName);
    int bucketIdx = funHash % nbucket;
    int index = *(int*)(szFileData + dyn_hash + 8 + bucketIdx * 4);
    // chain 탐색
    while (index != 0) {
        Elf32_Sym* sym = (Elf32_Sym*)(szFileData + dyn_symtab + index * sizeof(Elf32_Sym));
        if (strcmp(dynstr + sym->st_name, funcName) == 0) {
            info->st_value = sym->st_value;
            info->st_size = sym->st_size;
            return 0;
        }
        index = *(int*)(szFileData + dyn_hash + 4*(2+nbucket+index));
    }
    return -1;
}
  1. 복호화: 로드 시 __attribute__((constructor))로 등록된 함수가 동일한 절차로 함수 위치를 찾아 복호화합니다.

주의사항

  • static으로 선언된 함수는 .dynsym에 포함되지 않으므로, 해시 기반 탐색이 불가능합니다.
  • 암호화된 코드는 메모리에 로드된 상태에서만 복호화 가능하므로, mprotect()를 사용해 실행 및 쓰기 권한을 부여해야 합니다.
  1. 결론 안드로이드의 .so 파일 강화는 기본적인 ELF 포맷 지식과 메모리 조작 능력만으로도 실현 가능합니다. 섹션 기반과 해시 기반 두 가지 방법은 각각 장단점이 있으며, 실제 보안 환경에서는 병행 사용이 효과적입니다. 특히, 해시 기반 기법은 함수명을 기반으로 하므로, 코드 구조 변화에 유연하게 대응할 수 있습니다.

이 연구는 안드로이드 네이티브 보안에 대한 깊은 이해를 제공하며, 역분석 저항력을 높이는 데 실질적인 기여를 합니다.

태그: ELF Android NDK so file obfuscation dynamic linking symbol table

5월 28일 22:46에 게시됨