-
개요 안드로이드 앱의 네이티브 코드 보호를 위한 소 파일(소, .so) 강화 기술에 대해 다룹니다. 주로 두 가지 접근 방식을 탐구합니다: 특정 섹션 내 함수를 암호화하는 방법과 동적 심볼 테이블 기반으로 함수 위치를 찾아 암호화하는 방법입니다. 이러한 기법은 라이브러리 내 핵심 로직을 보호하고 역공학을 어렵게 만듭니다.
-
섹션 기반 암호화 암호화 원리: 사용자가 정의한 섹션(.mytext 등)에 특정 함수를 배치하고, 이 섹션의 내용을 암호화하여 저장합니다. 실행 시,
__attribute__((constructor))특성을 가진 초기화 함수가 메모리에 로드되기 전에 실행되어 암호화된 섹션을 복호화합니다.
구현 절차
- 네이티브 코드 작성
함수 선언에
__attribute__((section(".mytext")))를 추가하여 해당 함수를 사용자 정의 섹션에 포함합니다.
jint JNICALL native_Add(JNIEnv* env, jobject obj, jdouble num1, jdouble num2)
__attribute__((section(".mytext")));
- 복호화 함수 생성
__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;
}
}
-
외부 암호화 프로그램 개발 Visual Studio 2010 기반 프로그램으로
.so파일을 읽어.mytext섹션을 찾고, 간단한 비트 반전(~) 알고리즘으로 암호화합니다. 이후 암호화된 데이터를e_entry와e_shoff필드에 저장하여 복호화 시 필요한 정보를 포함합니다. -
실행 결과 검증 암호화된
.so파일을 애플리케이션에 통합하여 로딩하고, 함수 호출 결과를 확인합니다. 원본 값에 상수를 더한 결과가 반환됨으로써 암호화/복호화가 성공적으로 수행되었음을 확인할 수 있습니다.
핵심 지식
- ELF 파일 구조 이해: 섹션 테이블, 문자열 테이블, 프로그램 헤더의 역할.
e_shoff,e_entry등의 필드는 로드 시 무시되므로, 암호화 목적에 맞게 재사용 가능.mprotect()를 이용해 메모리 권한을 일시적으로PROT_READ | PROT_WRITE | PROT_EXEC로 변경하여 복호화 수행.
- 해시 기반 함수 직접 암호화
이 기법은 함수 이름을 기반으로
.hash섹션에서 심볼 위치를 탐색하여 암호화하는 방식입니다.
원리
.dynsym섹션에는 함수 심볼 정보가 포함되고,.dynstr에는 함수 이름이 저장됩니다..hash섹션은 해시 테이블이며, 함수 이름의 해시값을 기반으로bucket[]→chain[]구조를 통해 심볼 인덱스를 찾습니다.- 해시 함수는 일반적으로
elf_hash()로 구현되며, 입력 이름에 따라 고정된 해시 값을 산출합니다.
구현 단계
- 동적 섹션 파싱:
PT_DYNAMIC프로그램 헤더를 통해.dynsym,.dynstr,.hash섹션의 오프셋과 크기를 추출합니다. - 함수 위치 탐색: 주어진 함수 이름을
elf_hash()로 해시하고,bucket[hash % nbucket]를 통해 시작 인덱스를 얻습니다. 이후chain링크를 따라 실제 심볼을 찾습니다. - 암호화 수행:
st_value와st_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;
}
- 복호화: 로드 시
__attribute__((constructor))로 등록된 함수가 동일한 절차로 함수 위치를 찾아 복호화합니다.
주의사항
static으로 선언된 함수는.dynsym에 포함되지 않으므로, 해시 기반 탐색이 불가능합니다.- 암호화된 코드는 메모리에 로드된 상태에서만 복호화 가능하므로,
mprotect()를 사용해 실행 및 쓰기 권한을 부여해야 합니다.
- 결론
안드로이드의
.so파일 강화는 기본적인 ELF 포맷 지식과 메모리 조작 능력만으로도 실현 가능합니다. 섹션 기반과 해시 기반 두 가지 방법은 각각 장단점이 있으며, 실제 보안 환경에서는 병행 사용이 효과적입니다. 특히, 해시 기반 기법은 함수명을 기반으로 하므로, 코드 구조 변화에 유연하게 대응할 수 있습니다.
이 연구는 안드로이드 네이티브 보안에 대한 깊은 이해를 제공하며, 역분석 저항력을 높이는 데 실질적인 기여를 합니다.