보안정보
전문화된 보안 관련 자료, 보안 트렌드를 엿볼 수 있는
차세대 통합보안관리 기업 이글루코퍼레이션 보안정보입니다.
ANDITER를 활용한 안드로이드 위협 탐지 및 우회 방안 PART 2 (디버그, 에뮬레이터)
2023.05.31
9,040
1. 디버그(Debug) 개요 및 우회 방안
1) 디버그의 개요
‘ANDITER를 활용한 안드로이드 위협 탐지 및 우회 방안 : PART 1’에서 애플리케이션 디바이스의 루팅탐지방안과 우회방안에 대해서 살펴보았다. PART2에서는 동적분석인 디버깅과 에뮬레이터의 보안위협 및 우회방안에 대해서 알아보도록 하자.
먼저 디버그(Debug)란, 프로그래밍 과정에서 발생하는 코드 단의 오류나 비정상적인 연산으로 인해 프로그램이 오작동 되는 경우 이를 해결하고자 버그(오류)를 찾고 수정하는 행위를 의미하며 디버그 수행과정을 디버깅(Debugging)이라고 한다. 모바일 애플리케이션의 오류가 개발 당시 모두 발견되지 못할 수 있기 때문에 서비스 운영 중에 발견될 수도 있다. 운영 중에 확인된 오류는 테스팅 및 검증 조직에서 환경을 구성하여 트러블 슈팅을 진행하게 되는데 이때 디버그 모드에서 코드 상의 오류를 발견하고 수정하게 된다.
디버그는 개발자 입장에서 코드 상의 오류를 찾을 수 있는 기능이기 때문에, 공격자에게 악용되면 동적 분석을 통해 애플리케이션의 데이터 흐름과 동작원리가 파악될 수 있어 반드시 탐지하고 차단돼야 한다. 이와 같은 디버그를 이용한 분석과정을 모바일 동적 분석이라고 한다.
[그림 1]은 JEB의 디버그 기능을 이용해 ANDITER의 isCheckRootingBinary() 함수를 동적 분석하는 과정으로 if 구문의 실제 비교되는 데이터 값을 확인할 수 있다.
2) 디버그 사용을 위한 사전 조건
애플리케이션을 디버그 하기 위해서는 두 가지 전제조건이 필요하다. 첫 번째는 분석 대상의 AndroidManifest.xml 파일에 디버그 허용 옵션(debuggable)이 설정되어 있거나, 두 번째는 모바일 디바이스 자체 내에서 디버그 모드 지원 관련 시스템 설정이 되어 있어야 한다.
먼저 첫 번째 ‘AndroidManifest.xml 파일 디버그 허용 옵션(debuggable)여부’에 대해서 살펴보고자 한다. AndroidManifest.xml 파일은 해당 애플리케이션에 대한 서비스, Intent, broadcast receiver 등의 구성요소, 액세스 권한 등의 정보 이외에도 디버그 모드 허용여부 설정도 포함되어 있다. 설정정보는 실행 시 사용한 빌드 도구, Android OS, Google Play에 제공하게 된다.
[그림 2]는 AndroidManifest.xml 파일 내용 중 일부로 애플리케이션 자체에서 디버그 모드 사용을 허용할 경우 application 태그의 debuggable 속성이 true로 설정되어 있어야 한다.
두 번째 ‘모바일 디바이스 자체 내에서 디버그 모드 지원 관련 시스템 설정’은 Android 시스템 파일인 build.prop의 빌드 속성 중 ro.debuggable 값을 1로 변경해 사용하는 경우이다. build.prop은 Android OS가 부팅 시 참조하는 빌드 정보 및 시스템 속성이 정의된 파일로 ro.debuggable 값이 1로 설정되어 있을 경우 모바일 디바이스 자체 내에서 모든 애플리케이션에 대한 디버그 모드 사용이 지원된다. 이 경우 [그림 2]와 같이 debuggable 속성이 true로 설정되어 있지 않아도 디버그 사용이 가능해진다.
3) Bypass TracerPID
지금까지 디버그에 관해서 간략히 살펴봤다. 이제 실습으로 넘어가 애플리케이션에서 어떤 방안을 통해 디버그를 탐지하는지 알아보도록 하겠다. TracerPid는 실행 중인 프로세스의 상태 정보를 나타내는 상태 필드 중 하나로 해당 프로세스를 디버깅하고 있는 프로세스의 아이디(Pid)를 표시한다. 만약 디버깅하고 있는 프로세스가 존재하지 않는다면 0으로 표기된다.
[그림 5]는 애플리케이션이 디버그 되고 있지 않을 때의 모습으로 TracertPid 값이 0으로 표기된다. 반면, 디버그 중일 때는 [그림 6]과 같이 디버그 하고 있는 프로세스의 아이디 값을 표시한다. 동작 중인 프로세스 정보는 /proc 디렉터리에 저장되며, 디바이스 콘솔에서 “cat /proc/[PID]/status” 명령으로 확인하고자 하는 애플리케이션에 관한 프로세스 상태 정보 열람이 가능하다. Bypass TracerPID 탐지 항목은 이와 같은 특징을 이용해 실행 중인 애플리케이션의 TracerPid 값을 확인해 0이 아닌 경우 디버그가 동작인 것으로 판단해 탐지하게 된다.
원활한 실습 진행을 위해 탐지를 원하는 경우 IDA, GDA, Ghidra와 같은 디버그를 지원하는 도구를 사용해 TracerPid 값을 변형시킬 수 있다. 다만, 이러한 도구들은 별도의 설치가 필요하고 추가적인 설정을 해줘야 하기 때문에, 해당 문서에서는 디바이스 콘솔에서 바로 사용이 가능한 strace 도구를 사용해 TracerPid 값을 변형시켜 보겠다.
strace는 애플리케이션이 실행될 동안 시스템 콜 함수들과 시그널 처리를 추적할 수 있는 디버그 도구로 [그림 7]과 같이 실행 중인 애플리케이션 대상으로 사용이 가능하며, 사용 시 [그림 8]처럼 TracerPid 값이 strace 프로세스 Pid 값으로 변경된 것을 확인할 수 있다.
[그림 9]의 isCheckDebggerState() 함수는 Bypass TracerPID 탐지 결과를 반환해 주는 역할을 한다. 코드를 살펴보면 ①에서 File 클래스를 사용해 TracerPid 상태 필드가 저장되어 있는 /proc/self/status 파일 객체를 생성한다. ② ①에서 생성한 파일 객체를 읽어와 contains() 함수를 사용해 TracerPid 문자열이 포함되어 있는지 확인한다. ③ 해당 문자열이 포함되어 있다면 split() 함수를 통해 TracerPid 값을 가져와 0과 비교해 Pid 값이 0이 아니라면 디버그가 동작 중인 것으로 판단해 탐지하게 된다.
Bypass TracerPID 탐지를 우회하기 위한 후킹 포인트는 세 가지가 있다. 첫 번째는 함수 결과와 상관없이 무조건 false를 반환하도록 isCheckDebggerState() 함수를 재 작성하는 방법이고 두 번째는 File 클래스가 객체를 생성할 때 인자로 전달하는 문자열을 더미 값으로 변조하는 방법과 exists() 함수를 후킹하는 방법이다. 마지막 세 번째는 contains() 함수의 인자 값을 변조하는 방법이다. 여기서는 두 번째 방법을 사용해 탐지를 우회해 보겠다. 다만, exists() 함수 후킹 방법의 경우 PART 1 루팅 Bypass Binaries 항목에서 다뤄봤으니, 객체 생성 시 인자로 전달되는 문자열을 변조하는 방법을 사용해 보겠다.
[그림 10]은 File 클래스 생성자를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 File 클래스 사용을 위해 클래스 객체를 반환받고 File 클래스 생성자를 오버로딩으로 구현한다.
②에서는 File 클래스 객체 생성 시 인자로 전달되는 문자열을 검사해 들어오는 매개 변숫값이 /proc/self/status일 경우 더미 값으로 변조하고 변조한 더미 값을 인자로 File 클래스 객체를 생성해 반환한다.
Frida를 통해 작성한 스크립트를 ANDITER 애플리케이션에 어태치 하고 Bypass TracerPID 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.
4) Bypass Debuggable
앞서 애플리케이션에서 디버그 사용을 위해서는 두 가지 선택적 전제 조건이 필요하다고 설명했었다. 첫 번째는 AndroidManifest.xml 파일에 debuggable 속성이 true 설정되어 있을 때고, 두 번째는 Android 시스템 파일인 build.prop의 빌드 속성 중 ro.debuggable 값을 1로 변경해 사용할 경우이다.
Bypass Debuggable 탐지 항목은 두 번째 방법에서 사용되는 ro.debuggable 빌드 속성 값을 확인해 값이 1인 경우 애플리케이션이 디버그 모드로 동작 중인 것으로 판단해 탐지하게 된다.
build.prop 파일은 Android OS 운영체제에서 사용되는 빌드 정보 및 시스템 속성이 포함된 파일로 디바이스 부팅 시 로드 된다. 파일은 /system 디렉터리 내에 위치하고 있으며 디바이스 콘솔에서 getprop 명령어를 통해 확인이 가능하다.
추가로 ro.debuggable 빌드 속성은 사용자 임의 변경이 불가능하므로 실습 진행을 위해 값을 변경하고자 하는 경우 PART 1 루팅 Bypass System Propery 탐지 항목에서 ro.adb.secure 속성 값 변경 시 사용한 방법을 사용해 변경하면 된다.
[그림 14]의 isCheckDebuggable() 함수는 Bypass Debuggable 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 getSystemProperty 클래스의 prop() 함수가 사용됐으며, 해당 함수의 인자로 전달되는 시스템 속성 값을 반환한다. 그 후 equals() 함수를 통해 반환받은 ro.debuggable 빌드 속성 값이 1인지 검증하게 되며 값이 1이라면 애플리케이션을 디버그 중인 것으로 판단해 탐지하게 된다.
Bypass Debuggable 탐지를 우회하기 위한 후킹 포인트는 세 가지가 있다. 첫 번째는 함수 결과와 상관없이 무조건 False를 반환하도록 isCheckDebuggable() 함수를 재 작성하는 방법이고 두 번째는 getSystemProperty 클래스의 prop() 함수로 전달되는 인자 값을 변조하는 방법이다. 그리고 세 번째는 속성 값 검증을 위해 equals() 함수의 인자로 전달되는 문자열을 변조하는 방법이다. 여기서는 세 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.
[그림 15]는 equals() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 equals() 함수 사용을 위해 StringsJVMKt 클래스 객체를 생성했고 isCheckDebuggable()에서 equals() 함수를 호출할 때 전달되는 인자의 데이터 타입에 맞춰 equals() 함수를 오버로딩으로 구현했다. ②에서는 두 번째 매개 변수(str2) 값이 1일 경우 더미 값으로 변조하고 변조한 더미 값을 인자로 원본 equals() 함수를 호출해 해당 결괏값을 반환하게 된다.
추가로 문자열 비교를 위해 equals() 함수로 들어오는 첫 번째 매개 변수(str1)가 getSystemProperty 클래스의 prop() 함수를 통해 반환받은 ro.debuggable 속성 값이고, 두 번째 매개 변수(str2)가 첫 번째 매개 변수와 값 비교 시 사용되는 문자열이다.
Frida를 통해 작성한 스크립트를 ANDITER 애플리케이션에 어태치하고 Bypass Debuggable 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.
5) Bypass Debug Tools
모바일 애플리케이션 디버그 시 해당 운영체제 및 아키텍처를 지원하는 모바일 전용 디버그 도구를 사용한다. 대표적으로 디버그 모드를 지원하는 도구로는 JEB, IDA, GDA, Ghidra가 있으며 디버그 시 별도의 에이전트 파일을 이용해 대상 애플리케이션과 상호작용하며 스레드 작업을 진행하게 된다.
그리고 이러한 도구 사용 방지를 위한 탐지 방안으로는 [1.3 Bypass TracerPID] 항목에서 살펴본 바와 같이 디버그 하고 있는 프로세스의 Pid를 표시하는 상태 필드인 TracerPid 값을 확인하거나 각 도구들이 사용하는 에이전트들의 특징을 캐치하는 방법이 있다. 다만, 위에서 언급한 도구들 중 JEB의 경우 디버그 시 에이전트를 사용해 통신하는 방식이 아닌 VM(Virtual Machine) 어태치 방식을 사용하기 때문에 기존의 알려진 방법으로는 탐지가 불가해 별도의 방법을 사용해야 한다.
Bypass Debug Tools 탐지 항목은 동작 중인 애플리케이션의 프로세스 정보를 확인해 JEB에서 디버그 시 참조하는 특정 라이브러리 파일이 사용 중인지 확인한다. 해당 라이브러리 파일이 메모리에 올라와 있다면 애플리케이션이 디버그 모드로 동작 중인 것으로 판단해 탐지하게 된다.
[그림 18]은 IDA 도구를 이용한 디버그 모드 사용 예시로 그림과 같이 애플리케이션의 상태 필드인 TracerPid 값이 디버그 하고 있는 에이전트의 프로세스 Pid 값으로 변경된 것을 확인할 수 있다.
반면, JEB를 이용한 디버그 모드 사용 시 [그림 19]과 같이 TracerPid 값이 디버그를 사용하지 않았을 때와 동일한 것을 볼 수 있다. 이는 각 도구가 디버그 모드를 지원하는 방식에 따른 차이점에서 나타나는 것으로 대다수의 도구가 별도의 에이전트와 통신을 이용한 디버그를 지원한다면 JEB의 경우 에이전트 없이 자체 기술인 VM 방식을 통한 디버그 모드를 사용하기 때문이다.
따라서, 앞서 언급한 것과 같이 기존의 TracerPid 값을 확인해 탐지하는 방법이 아닌 별도의 방식을 사용해야 하며 JEB 경우 디버그 시 ART 플러그인 파일인 libopenjdkjvmti 참조한다는 점을 이용해 애플리케이션 메모리 주소에 해당 라이브러리 파일이 올라와 있는지 검증하는 방법으로 탐지할 수 있다.
[그림 20]와 [그림 21]은 디버그 모드를 사용했을 때와 사용하지 않았을 때의 차이점을 보여주고 있으며, 디버그 사용 시에는 애플리케이션 메모리 주소에 ART 플러그인 libopenjdkjvmti.so 파일이 참조되고 있는 것을 알 수 있다.
[그림 22]의 isCheckCallPPID() 함수는 Bypass Debug Tools 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 ①에서 프로세스 참조 주소가 저장되어 있는 /proc/self/maps 파일을 객체로 생성하고 ② 해당 파일의 존재 유무를 확인한다. ③에서는 객체로 생성한 /proc/self/maps 파일 내용을 읽어와 파일 내용에 libopenjdkjvmti.so 문자열이 포함되어 있는지 검사하고 문자열 존재 시 디버그 모드가 동작 중인 것으로 판단해 탐지하게 된다.
Bypass Debug Tools 탐지를 우회하기 위한 후킹 포인트는 세 가지가 있다. 첫 번째는 File 클래스 생성자를 후킹해 파일 객체 생성 시 들어오는 인자 값이 /proc/self/maps일 경우 더미 값으로 변조하는 방법이고 두 번째는 파일이 존재하는 검사일 때 사용하는 exists() 함수의 결괏값을 변조하는 방법이다. 마지막 세 번째는 문자열 비교 시 사용한 contains() 함수를 후킹 해 전달되는 인자 값이 libopenjdkjvmti.so일 경우 더미 값으로 변조하는 방법이다. 여기서는 첫 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.
[그림 23]은 File 클래스 생성자를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 File 클래스 사용을 위해 객체를 생성하고 생성자를 오버로딩으로 구현한다. ②에서는 File 객체 생성 시 들어오는 매개 변숫값 중 /proc/self/maps일 경우 더미 값으로 변조하고 변조한 더미 값을 인자로 File 객체를 생성해 결과를 반환한다.
Frida를 통해 작성한 스크립트를 ANDITER 애플리케이션에 어태치(Attach) 하고, Bypass Debug Tools 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.
6) Bypass Develop Mode
Android OS 기반의 모바일 디바이스에는 개발자 옵션이라는 히든 설정이 존재한다. 해당 설정은 개발자들이 애플리케이션 제작 시 하드웨어 렌더링, 애니메이션, 블루투스 등 관련 기능을 테스트할 때 주로 사용되며 설정 중에는 디버그 사용을 위한 관련 항목도 존재한다.
[그림 26]은 개발자 옵션에서 디버깅 항목을 보여주고 있으며 이 중 USB 디버깅 설정은 애플리케이션 개발 시 사용되는 Android 스튜디오와 SDK 도구가 USB를 통해 디바이스를 인식할 수 있도록 도와주는 기능이다. 또한, 동적 분석을 위한 디버그 도구 사용 시에도 도구와 디바이스 연결을 위해서 사용된다.
Bypass Develop Mode 탐지 항목은 애플리케이션 기능 테스트를 위해 사용되는 개발자 옵션이 디바이스
에서 사용 중인지 확인하고 사용 중일 경우 애플리케이션이 디버그 모드로 동작 중인 것으로 판단해 탐지
하게 된다. 다만, 개발자 옵션의 경우 디버그 목적 외에도 다른 용도로 사용할 수 있기 때문에 실제 애플리
케이션에서는 디버그를 탐지할 때 개발자 옵션 사용 유무만으로 판단하지 않고 추가로 다른 설정들과 함께 참고해 디버그 모드를 탐지한다.
개발자 옵션은 히든 메뉴로 별도의 방법을 통해서만 활성화시킬 수 있다. 활성화 방법은 사용 중인 디바이스 기종과 제조사 별로 조금씩 차이가 있겠으나 큰 흐름은 똑같으며, [표 1]을 참고해 개발자 모드를 활성화시키면 된다. 만약 사용 중인 디바이스가 아래 목록에 포함되어 있지 않다면 디바이스 제조사 홈페이지의 매뉴얼을 통해 확인하면 된다.
[그림 28]의 isCheckDevelopMode() 함수는 Bypass Develop Mode 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 Settings 클래스가 사용되었는데 해당 클래스는 디바이스에서 사용하는 설정에 관련된 정보들을 제공해 주며 내부 클래스인 Global를 통해 디바이스에 공통적으로 적용되는 설정값을 참조할 수 있다. 그리고 인자로 전달되는 development_settings_enabled 설정값을 getInt() 함수를 통해 반환받게 된다. 값이 0이면 개발자 옵션이 비활성화 상태를 뜻하고 1이면 활성화 상태를 뜻한다. 따라서, 값이 1인 경우 디버그 모드가 동작 중인 것으로 판단해 탐지하게 된다.
Bypass Develop Mode 탐지를 우회하기 위한 후킹 포인트는 두 가지가 있다, 첫 번째는 함수 결과와 상관없이 무조건 False를 반환하도록 isCheckDevelopMode() 함수를 재 작성하는 방법이고 두 번째는 Settings.Global 클래스 getInt() 함수의 인자로 전달되는 설정값을 변조하는 방법이다. 여기서는 두 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.
[그림 29]은 getInt() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 getInt() 함수 사용을 위해 Settings 클래스 객체를 생성했고 isCheckDevelopMode()에서 getInt() 함수를 호출할 때 전달되는 인자의 데이터 타입에 맞춰 getInt() 함수를 오버로딩으로 구현한다. ②에서는 함수 호출 시 매개 변수 들어오는 설정값이 development_settings_enabled일 경우 더미 값으로 변조하고 변조한 더미 값을 인저로 원본 getInt() 함수를 호출해 해당 결괏값을 반환하게 된다.
Frida를 통해 작성한 스크립트를 ANDITER 애플리케이션에 어태치 하고 Bypass Develop Mode 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.
7) Bypass Debugging Mode
로컬 PC에서 Android 스튜디오, 기타 SDK 도구 등을 사용할 때 도구가 디바이스를 인식하고 연결하기 위해서는 개발자 옵션의 USB 디버깅 옵션이 활성화되어 있어야 한다. 이는 모바일 애플리케이션 디버그 도구를 사용하는 경우에도 마찬가지로 해당 옵션이 활성화되어 있어야 디버그 도구에서 디바이스를 인식할 수 있다.
Bypass Debugging Mode 탐지 항목은 개발자 옵션의 USB 디버깅 설정이 활성화되어 있는지 확인하고 사용 중일 경우 애플리케이션이 디버그 모드로 동작 중인 것으로 판단해 탐지하게 된다. 다만, USB 디버깅의 경우 제조사 펌웨어 수동 업데이트 또는 디바이스 데이터 이동 시에도 사용되기 때문에 실제 애플리케이션에서는 디버그를 탐지할 때 USB 디버깅 옵션 사용 유무만으로 판단하지 않고 추가로 다른 설정들과 함께 참고해 디버그 모드를 탐지하게 된다.
USB 디버깅은 개발자 옵션을 통해서 사용할 수 있으며, 이전 파트인 [Bypass Develop Mode] 항목에서 개발자 옵션을 활성화 시켰다면 설정 > 개발자 옵션 > 디버깅 > USB 디버깅에서 설정을 해주면 된다.
[그림 33]의 isCheckUSBDebuggingMode() 함수는 Bypass Debugging Mode 탐지 결과를 반환해주는 역할을 한다. 코드를 보면 [Bypass Develop Mode] 탐지 파트에서 사용되었던 Settings 클래스가 해당 함수에서도 동일하게 사용된 것을 볼 수 있으며 내부 클래스인 Global을 통해 adb_enabled 설정 값을 참조 후 getInt() 함수를 사용해 값을 가져오고 있다. 값이 0이면 USB 디버깅 설정이 비활성화 상태를 뜻하고 1이면 활성화 상태를 뜻한다. 따라서, 값이 1인 경우 디버깅 모드가 동작 중인 것으로 판단해 탐지하게 된다.
Bypass Debugging Mode 탐지를 우회하기 위한 후킹 포인트는 두 가지로 [Bypass Develop Mode] 탐지 파트에서 언급했던 우회 방법과 동일하다. 첫 번째는 함수 결과와 상관없이 무조건 False를 반환하도록 isCheckDevelopMode() 함수를 재 작성하는 방법이고 두 번째는 Settings.Global 클래스 getInt() 함수의 인수로 전달되는 옵션 값을 변조하는 방법이다. 이전 파트에서 두 번째 방법을 사용해 우회해 봤으니 여기서는 첫 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.
[그림 34]은 isCheckUSBDebuggingMode() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 isCheckUSBDebuggingMode() 함수 사용을 위해 DebuggerDetector 클래스 객체를 생성했고 ②에서는 함수 호출 시 무조건 false 값을 반환하도록 재 작성했다. 참고로 이와 같이 무조건 특정 값을 반환하도록 코드를 작성하는 경우 사전에 재 작성하려는 함수의 코드에 대한 정밀 분석이 이뤄져야 한다. 만약 해당 함수에서 다른 액티비티를 호출하거나 Context 수정 등의 작업을 하는데 단순 결괏값만 반환하도록 재 작성할 경우 원본 함수에서 수행하던 작업 프로세스가 깨지게 되며 오류가 발생해 애플리케이션이 비정상 종료될 수 있다.
Frida를 통해 작성한 스크립트를 ANDITER 애플리케이션에 어태치 하고 Bypass Develop Mode 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.
8) Bypass Connect USB
일반적으로 모바일 디버깅 및 기타 분석 도구들은 로컬 PC에서 실행시킨 후 USB를 통해서 디바이스와 연결해 사용한다. 물론, 몇몇 도구들 경우에는 공용 와이파이를 이용한 연결도 지원하지만 USB를 사용하는 게 보편적이다.
Bypass Connect USB 탐지 항목은 디바이스에 USB가 연결되었는지 확인하고 연결되어 있다면 애플리케이션이 디버그 모드로 동작 중인 것으로 판단해 탐지하게 된다. 하지만, USB 연결의 경우 PC와의 연결을 위해서 사용하기도 하지만 디바이스 배터리 충전 시에도 사용된다. 그래서 실제 애플리케이션에서는 USB 연결만으로 디버그 모드가 동작 중이라고 판단하지 않고 추가로 다른 설정들과 함께 참고해 디버그 모드를 탐지한다.
[그림 37]의 isCheckConnectUSB() 함수는 Bypass Connect USB 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 ①에서 IntentFilter를 통해 USB 연결 정보와 관련된 컴포넌트 유형을 지정해 주고 있으며 ②에서는 디바이스에 USB가 연결됐을 때 발생하는 이벤트를 수신하기 위해 ①에서 지정한 인텐트를 브로드캐스트 리시버에 등록해 준다. 그리고 ③에서는 getBooleanExtra() 함수를 사용해 인수로 전달된 데이터가 인텐트에 포함되어 있는지 확인한다. 인텐트에 connected 데이터가 존재할 경우 true를 반환하고 존재하지 않을 경우 두 번째 인수로 지정한 디폴트 값 false를 반환하게 된다.
Bypass Connected USB 탐지를 우회하기 위한 후킹 포인트는 두 가지가 있다. 첫 번째는 함수 결과와 상관없이 무조건 False를 반환하도록 isCheckConnectUSB() 함수를 재 작성하는 방법이고 두 번째는
getBooleanExtra() 함수 호출 시 전달되는 첫 번째 인수 connected를 더미 값으로 변조해 두 번째 인수 값 false를 반환하도록 만드는 방법이다. 위에서도 설명했듯이 getBooleanExtra() 함수는 데이터를 가져올 때 지정된 데이터가 존재하지 않는다면 함수 호출 시 전달된 두 번째 인수로 지정한 디폴트 값을 반환하게 된다. 여기서는 두 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.
[그림 38]는 getBooleanExtra() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 getBooleanExtra() 함수 사용을 위해 Intent 객체를 생성했고 isCheckConnectUSB()에서 getBooleanExtra() 함수를 호출할 때 전달되는 인자의 데이터 타입에 맞춰 getBooleanExtra() 함수를 오버로딩으로 구현했다. ②에서는 함수 호출 시 들어오는 첫 번째 매개 변수(str) 값이 connecnted와 같다면 더미 값으로 변조하고 변조한 더미 값을 인자로 원본 getBooleanExtra() 함수를 호출해 해당 결괏값을 반환하게 된다.
Frida를 통해 작성한 스크립트를 ANDITER 애플리케이션에 어태치 하고 Bypass Connect USB 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.
02. 에뮬레이터(Emulator) 개요 및 우회 방안
1) 에뮬레이터의 개요
Android 에뮬레이터란 컴퓨터에서 Android 디바이스를 시뮬레이션하기 위해 사용되는 응용 프로그램의 일종으로 실제 디바이스가 없어도 다양한 Android API 수준에서 애플리케이션을 테스트할 수 있다. 먼저, 에뮬레이터의 첫 등장은 Android_x86 프로젝트로 데스크톱 PC에서 네이티브로 Android를 구동하는 방안에 대해 연구되었고 그 후 앱플레이어라는 모바일 가상 전용 프로그램이 등장해 어디서든 PC만 있다면 실제 디바이스가 없어도 간편하게 애플리케이션을 구동 시킬 수 있게 됐다.
에뮬레이터는 애플리케이션 개발 시 간단한 테스트 목적을 위해 활용되었으나 현재는 게임이 주 목적을 이루고 있으며 이에 제작사들도 키 매핑, 매크로 등 게임에 특화된 강력한 기능을 추가해 선보이고 있는 추세이다. 하지만, 악의적인 사용자의 경우 에뮬레이터를 편의성 및 테스트 용도로 사용하는 게 아닌 외부 응용 프로그램들과 연동해 분석 자동화 또는 애플리케이션을 디버그 하고 메모리를 조작하는 등 데이터 위·변조 목적으로 사용한다.
에뮬레이터 탐지 기법으로는 Build 정보, USIM 정보 및 디바이스 센서 등 하드웨어 정보를 수집해 에뮬레이터에서 발견되는 특징들을 탐지하는 방법과 그 외 제조사 별로 설치되는 에뮬레이터 전용 애플리케이션들을 수집해 설치 유무를 탐지하는 방법이 있다. 다만, 에뮬레이터 특성상 루팅이 자유롭고 주요 시스템 설정이 프로그램 및 파일을 통해 변경이 가능해 탐지가 쉽지 않아 지속적으로 신규 버전 및 모델이 출시될 때마다 실제 디바이스와 차이점을 비교해서 탐지 정책을 업데이트해줘야 한다.
추가로 에뮬레이터 탐지 및 차단 정책을 적용하기 전에 제작한 애플리케이션이 에뮬레이터 환경에서의 구동 여부 허용에 대한 사전 평가가 먼저 이뤄져야 한다. 실제로 게임 제작사의 경우 게임 제작 시 에뮬레이터 사용자들도 고려해서 제작하며, 디바이스 성능 상의 이유로 에뮬레이터를 사용하는 이용자들도 있기 때문이다.
이번 챕터부터는 실제 애플리케이션에서 Android 에뮬레이터(Emulator) 탐지 및 차단 방법을 기술하고 이를 우회하는 방안을 다뤄보도록 하겠다.
2) Bypass Build Setting
에뮬레이터에는 몇 가지 특징이 존재한다. 현재 대다수 출시되고 있는 모바일 디바이스는 ARM 플랫폼의 arm64-v8a(64비트) 아키텍처 기반을 사용한다. 반면 에뮬레이터의 경우 PC 운영체제와의 호환성을 위해 x86 플랫폼의 32비트 아키텍처 또는 ARM 플랫폼의 armeabi-v7a(32비트) 아키텍처를 사용하는 경우가 많다. 물론, 모든 에뮬레이터가 32비트 기반의 아키텍쳐를 사용하는 것은 아니며, 최근 출시되고 있는 에뮬레이터 경우 64비트 기반의 모델도 존재한다.
Bypass Build Setting 탐지 항목은 디바이스의 Build 정보를 확인해 위와 같이 에뮬레이터에서만 발견되
는 알려진 특징들이 있을 경우 에뮬레이터 사용자로 판단해 탐지한다. 에뮬레이터 탐지 파트에서는 모바일 디바이스가 아닌 Android 7.0 버전의 Nox 에뮬레이터를 사용해 실습이 진행된다.
[그림 41]의 isCheckBuildSet() 함수는 Bypass Build Setting 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 디바이스의 아키텍처(Architecture) 및 Build 정보를 참조해 에뮬레이터에서 발견되는 특징들이 존재하는지 조건식을 통해 확인하고 있으며, 모두 or 논리 함수가 적용된 상태로 조건식 중 하나가 true로 반영될 경우 에뮬레이터 사용자로 탐지된다.
Bypass Build Setting 탐지를 우회하기 위한 후킹 포인트는 두 가지가 있다. 첫 번째는 함수 결과와 상관없이 무조건 false를 반환하도록 isCheckBuildSet() 함수를 재 작성하는 방법이고 두 번째는 아키텍처 및 Build 정보를 비교할 때 공통적으로 사용되는 contains() 함수를 후킹 해 매개 변수로 들어오는 비교 문자열을 변조하는 방법이다. 여기서는 두 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.
[그림 42]은 contains() 호출 타입과 디바이스에서 탐지되는 문자열을 확인하고자 필자가 작성한 Frida 스크립트이다. contains() 함수의 경우 호출 시 전달되는 인수 타입에 따라 오버로딩을 각각 구현해 줘야 한다. ①에서는 ANDITER 애플리케이션에서 사용되는 두 가지 타입 유형의 contains() 함수를 오버로딩으로 구현했고 ②, ③에서는 isCheckBuildSet() 함수에서 어떤 타입 유형의 contains()를 호출하는지 확인하고자 로그를 출력하도록 작성했다.
Frida를 통해 작성한 스크립트를 ANDITER 애플리케이션에 어태치 후 확인 시 contains2 유형의 contains() 함수가 호출된 것을 볼 수 있으며 로그가 2번 출력된 것을 통해 해당 함수에서 총 2번의 호출이 발생했고 2번째 호출 시 탐지된 것으로 추측할 수 있다. 이와 같이 출력되는 로그를 통해 클래스 및 함수를 추적하는 기법을 Frida Trace라고 하는데 네이티브 코드 분석 시 자주 사용되는 기법이다.
[그림 44]의 코드는 [그림 43]에서 확인된 contains() 함수의 타입 유형 및 탐지 시점 정보를 토대로 작성했으며, 문자열 비교를 위해 들어오는 두 번째 매개 변수(str2) 값이 i868일 경우 더미 값으로 변조하고 변조한 더미 값을 인자로 원본 contains() 함수를 호출해 해당 결괏값을 반환하게 된다.
Frida를 통해 작성한 스크립트를 ANDITER 애플리케이션에 어태치 하고 Bypass Build Setting 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.
3) Bypass Default Files
에뮬레이터에는 각 제조사 별 프로그램 호환성과 가상 환경 설정을 위한 라이브러리 파일, 명령어 파일 등 전용 설정 파일들이 존재한다. 예로 국내에서 많이 사용되고 있는 Nox 에뮬레이터의 경우 명령어 파일인 nox-prop, nox-vbox-sf, noxd와 가상환경 공유 라이브러리 파일인 libnoxd.so, libnoxspeedup.so가 있다.
Bypass Default Files 탐지 항목은 에뮬레이터 환경에서 발견되는 대표적인 파일들이 디바이스에 존재하는지 검사하고 파일이 존재할 경우 에뮬레이터 사용자로 판단해 탐지하게 된다.
[그림 47]의 isCheckEmulatorFile() 함수는 Bypass Default Files 탐지 결과를 반환해주는 역할을 한다. 코드를 보면 isFileCheck() 함수를 통해 [그림 48]과 같이 리스트로 정의된 genyFiles, andyFiles, noxFiles, pipeFiles 파일 목록이 디바이스에 설치되어 있는지 검사하며 파일이 디바이스에 존재할 경우 에뮬레이터 사용자로 판단해 탐지한다.
[그림 49]는 [그림 47]에서 에뮬레이터 파일 설치 유무를 확인할 때 사용됐던 함수이다. 코드를 보면 리스트 타입으로 넘어오는 매개 변수를 반복문을 통해 확인하고 있으며 이때, 파일 존재 여부 확인을 위해 File 클래스의 exists() 함수가 사용된 것을 볼 수 있다.
Bypass Default Files 탐지를 우회하기 위한 후킹 포인트는 세 가지가 있다. 첫 번째는 함수 결과 상관없이 무조건 false를 반환하도록 isCheckEmulatorFile() 함수를 재 작성하는 방법이고 두 번째는 isFileCheck 함수 호출 시 전달되는 인자를 더미 값으로 변조하는 방법이다. 마지막 세 번째는 파일 존재의 유무에 대한 결과를 반환 해주는 exists() 함수를 후킹해 반환 값을 변조하는 방법이다. 여기서는 두 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.
[그림 50]은 isFileCheck() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 isFileCheck() 함수 사용을 위해 EmulatorDectector 클래스 객체를 반환받고 isCheckEmulatorFile()에서 isFileCheck() 함수를 호출할 때 전달되는 인자의 데이터 타입에 맞춰 isFileCheck() 함수를 오버로딩으로 구현했다. ②에서는 isCheckEmulatorFile() 함수에서 검사하는 파일 목록을 동일하게 가져왔으며 ③ isFileCheck() 함수 호출 시 전달되는 인자 값이 checkList 목록에 포함되어 있다면 목록에서 제외해 새로운 fakeList 배열을 생성한다. 그 후 새로운 배열을 인자로 원본 isFileCheck() 함수를 호출해 해당 결괏값을 반환하게 된다.
Frida를 통해 작성한 스크립트를 ANDITER 애플리케이션에 어태치 하고 Bypass Default Files 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.
에뮬레이터에는 각 제조사 별 전용 마켓들과 디바이스 설정 관리를 위한 애플리케이션들이 설치되어 있다. Android 디바이스 사용자의 경우 애플리케이션 설치 시 일반적으로 Play Store, One Store 등 공식 마켓을 이용하는 반면 에뮬레이터의 경우 Play Store 및 에뮬레이터 전용 마켓을 이용해 애플리케이션을 설치하게 된다. 별도로 전용 마켓이 존재하는 이유는 회사 운영 방침, 광고성 수익 등이 있겠지만, 대표적인 이유로 호환성 문제 해결을 위함에 있다. 일반적으로 애플리케이션 제작 시 에뮬레이터보다는 모바일 하드웨어 성능에 맞춰 개발되기 때문에 에뮬레이터와 호환이 되지 않는 경우가 발생한다. 따라서, 이를 해결하기 위해 제작사 측에서 별도의 전용 마켓을 통해 호환되는 버전을 제공하는 것이다.
Bypass Packages 탐지 항목은 에뮬레이터에 설치되어 있는 전용 마켓 및 관리 애플리케이션의 패키지가 존재하는지 검사하고 존재 시 에뮬레이터 사용자로 판단해 탐지한다.
[그림 53]의 isCheckEumlatorPackages() 함수는 Bypass Packages 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 ①에서 에뮬레이터 패키지 목록을 배열 형태로 받아오고 있으며 ②에서 getPackageInfoCompat() 함수를 통해 ①에서 받아온 패키지가 디바이스에 설치되어 있는지 검사한다.
[그림 54] 에뮬레이터 패키지 목록이 저장되어 있는 리스트
[그림 55] getPackageInfoCompat() 함수는 루팅 Bypass Packages 탐지 항목에서도 사용되었던 함수로 매개 변수로 들어오는 패키지가 디바이스에 설치되어 있다면 해당 패키지 정보를 반환해 주는 기능을 가지고 있다. 코드를 보면 ①에서는 Android OS 버전에 따른 동작 코드를 나눴으며 Android API Level이 33(Android OS 13) 이상인지 확인하고 있다. 필자의 에뮬레이터 버전의 경우 Android 7.0으로 ② 구문의 코드가 동작하게 된다. ③ getPackageInfo()는 패키지 정보를 가지고 오는 실질적인 함수로써 [그림 54]의 emulatorPackages 리스트 저장되어 있는 데이터를 하나씩 인자 값으로 전달해 패키지 정보를 가지고 온다. 이때, 반환되는 패키지 정보가 있다면 에뮬레이터 사용자로 판단해 탐지하게 된다.
Bypass Packages 탐지를 우회하기 위한 후킹 포인트는 두 가지가 있다. 첫 번째는 함수 결과 상관없이 무조건 false를 반환하도록 isCheckEumlatorPackages() 함수를 재 작성하는 방법이고 두 번째는 getPackageInfoCompat() 함수로 호출 시 전달되는 인자를 더미 값으로 변조하는 방법이다. 여기서는 두 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.
[그림 56]은 getPackageInfoCompat() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 getPackageInfoCompat() 함수 사용을 위해 EmulatorDetector 클래스 객체를 반환받고 isCheckEumlatorPackages()에서 getPackageInfoCompat() 함수를 호출할 때 전달되는 인자의 데이터 타입에 맞춰 getPackageInfoCompat() 함수를 오버로딩으로 구현했다. ②에서는 isCheckEumlatorPackages() 함수에서 검사하는 emulatorPackages 배열 목록을 동일하게 가져왔으며 ③ getPackageInfoCompat() 함수 호출 시 전달되는 인자 값이 packages 목록에 포함되어 있다면 더미 값으로 변조하고 변조한 더미 값을 인자로 원본 getPackageInfoCompat() 함수를 호출해 해당 결괏값을 반환하게 된다.
Frida를 통해 작성한 스크립트를 ANDITER 애플리케이션에 어태치 하고 Bypass Packages 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.
03. 마무리
지금까지 실제 애플리케이션에서 디버그와 에뮬레이터를 어떠한 방법으로 탐지하고 공격자는 또 이것을 어떻게 우회하는지에 대해 살펴보았다. 공격자는 데이터를 변조하기 앞서 반드시 애플리케이션을 디 컴파일 후 코드에 대한 정적 분석 과정을 거쳐 변조 포인트 진입 점을 찾게 된다. 다만, 정적 분석은 간단한 테스트 목적 또는 연습용 애플리케이션이 아닌 이상 대다수 상용 애플리케이션은 코드 양이 방대하기 때문에 분석하는데 시간이 오래 걸리고 코드만으로는 실제 데이터의 이동 흐름을 정확하게 파악할 수 없다. 그래서 공격자는 정적 분석을 통해 엔트리 포인트(Entry Point)를 정한 후 동적 분석으로 넘어가 실제 데이터의 흐름과 동작 방식을 분석하는 방법을 선호한다. 애플리케이션에서 디버그 사용을 탐지하지 않는다는 것은 이는 공격자의 분석 시간을 줄여주는 것과 같으며 문서에서 언급한 탐지 방안을 포함한 여러 대응 방안을 마련해 디버그 사용을 탐지하고 차단해야 한다.
디버그와 더불어 에뮬레이터도 공격자가 선호하는 분석 도구 중 하나로 문서에서는 언급하지 않았지만 단순 게임 및 개발 테스트 목적으로 제작된 에뮬레이터 외에 모바일 침투 환경으로 만들어진 에뮬레이터가 있으며, 최근에는 공격 서버로도 활용되고 있는 추세이다. 또한, 에뮬레이터 특성상 로컬 PC에서 동작하기에 외부 응용 프로그램과 연동이 쉬우며 이를 이용한 애플리케이션 자동화 분석도 이뤄지고 있다. 다만, 무작정 에뮬레이터를 차단하는 것은 좋은 방안은 아니며 애플리케이션 사용 목적과 주 사용자들의 실행 환경 등을 고려해 탐지 정책 여부를 결정해야 한다.
PART 3에서는 애플리케이션 데이터 위·변조 목적으로 많이 사용되는 후킹 도구인 Frida에 대한 탐지 방안과 중간자 공격(Man In The Middle)을 예방하기 위해서 사용하는 Pinning 기법을 다뤄보고 공격자의 관점에서 이를 어떠한 방식으로 우회하는지 살펴보겠다.
04. 참고자료
[1] 나무위키
https://namu.wiki/w/%EB%94%94%EB%B2%84%EA%B7%B8
[2] ART TI
https://source.android.com/docs/core/runtime/art-ti?hl=ko
[3] strace
https://source.android.com/docs/core/tests/debug/strace?hl=ko
https://strace.io/
[4] Android ABI
https://developer.android.com/ndk/guides/abis?hl=ko
[5] 모바일 게임 보안을 위한 안드로이드 에뮬레이터 탐지방법에 관한 연구
https://scienceon.kisti.re.kr/srch/selectPORSrchArticle.do?cn=JAKO201533678768383&dbt=NART