디버거를 붙이면 걸린다면, 실행을 흉내 내서 본다 — 안드로이드 ARM64 문자열 난독화 해제기

디버거를 붙이면 걸린다면, 실행을 흉내 내서 본다 — 안드로이드 ARM64 문자열 난독화 해제기

분류: Security · 모바일 · 리버싱/난독화해제 [정보성]

덱. NVISO의 모바일 보안 연구자 Jeroen Beckers는 실제 진단 업무 중 문자열을 암호화해 숨겨둔 안드로이드 ARM64(64비트 ARM 아키텍처) 앱을 분석하게 되면서, 디버거를 붙이지 않고도 리버스엔지니어링 도구(Ghidra) 안에서 코드를 직접 실행시켜(에뮬레이션) 암호화된 문자열을 복원하는 절차를 정리했습니다. 마지막 단계는 파이썬 스크립트로 자동화해, 앱 전체의 난독화 문자열을 한 번에 풀어내는 데까지 나아갑니다.

리드

분석을 방해하려는 앱은 코드 안의 문자열(URL, 명령어, 오류 메시지 같은 것들)을 그대로 두지 않고 암호화하거나 뒤섞어 저장해 둡니다. 정적으로 코드를 읽는 것만으로는 이 문자열의 실제 값을 알 수 없고, 실행 흐름을 따라가야만 원래 값이 드러납니다. 문제는 실제 실행 흐름을 보려고 디버거나 후킹 도구(Frida 등)를 붙이면, 이런 앱은 대개 런타임 자기보호(RASP, Runtime Application Self-Protection) 기능이 그 시도를 즉시 감지해 버린다는 점입니다. 직접 건드리면 걸리는 상황에서, 코드를 실제 기기 밖에서 흉내 내 실행시켜 보는 것 — 이것이 이번 사례의 핵심 아이디어입니다.

핵심 사실 요약

  • 연구자는 문제를 단순화하기 위해 먼저 작은 테스트 앱을 직접 만들었습니다. 이 앱은 XOR(각 바이트를 키와 배타적 논리합 연산하는 가장 단순한 형태의 암호화) 방식으로 암호화된 문자열 3개를 담고 있고, 각 문자열마다 “이미 복호화됐는지”를 나타내는 상태값을 원자적(atomic, 여러 스레드가 동시에 건드려도 값이 꼬이지 않도록 보장하는 방식)으로 관리합니다. 실제 현업 앱에서 흔히 보이는 패턴을 그대로 재현한 것입니다.
  • Ghidra로 이 바이너리를 열어 자동 분석을 돌리면, 디컴파일된 코드는 깔끔하게 나오지만 문자열 자체는 암호화된 바이트 나열로만 보입니다. 정적 분석만으로는 예상대로 아무것도 읽어낼 수 없습니다.
  • 여기서 디버거를 붙이는 대신 Ghidra에 내장된 에뮬레이터를 사용합니다. 분석 대상 함수 시작 지점에서 트레이스를 걸고, 복호화 함수 호출 직후 지점에 중단점을 찍어 실행을 관찰하는 방식입니다.
  • 에뮬레이터로 실행해 보면 곧바로 걸리는 문제가 있습니다. 문자열 길이를 구하는 라이브러리 함수(strlen) 호출이 외부 라이브러리에 있는 코드라, 에뮬레이터가 그 내부를 알지 못해 실행이 엉뚱한 주소로 튀어버립니다. 이를 해결하는 방법은 두 가지였습니다.
    • 수동 개입: 문제의 호출 명령어를 건너뛰고, 그 결과로 남아야 할 레지스터 값(문자열 길이)을 메모리를 직접 눈으로 세어 확인한 뒤 손으로 채워 넣는 방식.
    • 자동화: Ghidra의 저수준 스크립팅 언어(SLEIGH)로 “strlen처럼 동작하는 대체 코드”를 즉석에서 작성해, 해당 호출을 만날 때마다 자동으로 실행되도록 주입하는 방식.
  • 두 번째로 걸린 문제는 조금 더 근본적이었습니다. 여러 스레드가 동시에 상태값을 건드려도 안전하도록 쓰이는 원자적 연산(다른 스레드가 끼어들지 못하게 값을 배타적으로 잠갔다가 쓰는 저수준 명령어 조합)을 에뮬레이터가 아예 구현하지 못해 오류가 나며 멈춰 버렸습니다. 하지만 지금은 스레드가 하나뿐인 에뮬레이션 환경이라 이 배타성 보장 자체가 필요 없다는 점에 착안해, 문제의 명령어를 일반적인 “그냥 저장” 명령으로 바꿔치기(패치)하고, 그 뒤에 이어지는 재시도 분기 명령도 무력화하는 방식으로 우회했습니다.
  • 이 두 가지 우회를 마치고 나면, 에뮬레이터는 실제로 복호화 루틴을 끝까지 실행해 메모리에 평문 문자열을 만들어 냅니다. 이 시점에서 해당 메모리 위치를 문자열 데이터로 지정하고 이름표를 붙이면, Ghidra의 정적 리스팅과 디컴파일 결과에도 그 이름이 그대로 반영됩니다.
  • 마지막으로, 이 과정을 문자열 하나하나 손으로 반복하지 않기 위해 파이썬 스크립트로 자동화했습니다. 스크립트는 복호화 함수를 호출하는 모든 지점(교차 참조)을 찾아, 각 호출 직전 코드를 거슬러 올라가며 인자로 쓰인 레지스터 값(암호문 주소, 키 주소)을 알아낸 뒤, 그 위치의 바이트를 직접 읽어 파이썬 안에서 XOR 복호화를 수행하고, 결과를 다시 해당 주소의 이름표로 자동 등록합니다.

짧은 논평

이 글이 보여주는 원리는 특정 도구나 아키텍처에 갇힌 이야기가 아닙니다. 직접 관찰하는 통로가 막혀 있을 때, 그 통로를 흉내 낸 별도의 환경에서 같은 실행을 재현해 관찰한다는 발상은 저희가 다른 우회 사례에서도 반복해서 마주치는 구조입니다 — 예를 들어 필터의 내부 로직을 직접 볼 수 없을 때 응답 차이를 관찰해 규칙을 역으로 추론하는 방식과 같은 결의 접근입니다. 여기서 주목할 점은, RASP나 안티디버깅이 막는 것은 “기기 위에서의 직접 개입”이지 “그 코드가 어떻게 동작하는지 이해하는 것” 자체는 아니라는 사실입니다. 실행 환경을 도구 안으로 옮겨 오면, 대상 앱은 자신이 관찰당하고 있다는 사실을 알 방법이 없습니다. 방어하는 입장에서 이 사실이 주는 시사점도 뚜렷합니다 — 안티디버깅·안티후킹만으로 분석을 완전히 막을 수는 없고, 문자열 암호화 같은 장치는 어디까지나 분석 시간을 늘리는 것이지 막는 것은 아니라는 점을 전제로 방어를 설계해야 합니다.

References / 참고자료

  1. Jeroen Beckers (NVISO), “Deobfuscating Android ARM64 strings with Ghidra: Emulating, Patching, and Automating” — 이 글이 재구성한 원 분석. Ghidra 에뮬레이터로 XOR 난독화 문자열을 복호화하고 파이썬으로 자동화하는 전 과정을 다룸. https://blog.nviso.eu/2024/01/15/deobfuscating-android-arm64-strings-with-ghidra-emulating-patching-and-automating/

이 글은 위 원 분석을 우리 관점에서 다시 쓰고 짧은 논평을 더한 정보성 초안입니다. 특정 문장·스크립트·SLEIGH 코드를 그대로 옮기지 않았으며, 작동하는 재현 절차는 싣지 않았습니다. ⚠️미확인 표시 항목은 발행 전 독립 팩트체크에서 확증·정정되어야 합니다.