보안정보
전문화된 보안 관련 자료, 보안 트렌드를 엿볼 수 있는
차세대 통합보안관리 기업 이글루코퍼레이션 보안정보입니다.
ANDITER를 활용한 안드로이드 위협 탐지 및 우회 방안 : PART 4-1 (무결성, 동적 로딩)
2023.08.02
11,560
01. 무결성 개요와 탐지 및 우회 방안
1) 무결성(Integrity)
'ANDITER를 활용한 안드로이드 위협 탐지 및 우회 방안 : PART 3'에서는 공격자들이 모바일 애플리케이션의 코드와 데이터를 위·변조하고자 많이 사용하는 프리다(Frida) 도구에 대한 탐지 방안과 우회 기법을 살펴보았다. 또한 중간자 공격(Man in the Middle) 방지를 위해 애플리케이션 코드 단에서 적용할 수 있는 피닝(Pinning)에 대해서도 살펴봤다.
PART 4에서는 공격자들의 무분별한 코드 변조에 대응하기 위한 무결성(Integrity) 검증 방안과 악의적인 사용자와 보안 솔루션에서 중요 코드를 숨기고자 사용하는 동적 로딩(Dynamic Loading) 기법에 대해서 살펴보도록 하겠으며, 추가적으로 네이티브 코드를 이용한 탐지방안에 대해서도 설명하고자 한다. 관련 내용이 많은 관계로 PART4의 1편에서는 ‘무결성, 동적로딩’에 대해서 설명하고 PART4의 2편에서는 나머지 네이티브 코드를 이용한 탐지방안에 대해 설명한다.
무결성(또는 데이터 무결성)은 사전적인 의미로 데이터를 보호하고 항상 정상 데이터 상태를 유지하는 것으로, 주로 데이터베이스 관련 분야에서 정확성을 보장한다는 의미로 통용된다. 그리고 현재는 단일 분야에 한정된 의미를 넘어 소프트웨어나 제품의 위·변조가 발생하지 않음을 보장할 수 있는 신뢰성을 제공하는 척도라고 할 수 있다.
초창기 안드로이드 생태계는 낮은 보안 성숙도였으나 금융권을 중심으로 루팅 및 코드 위·변조 등의 위험성을 인지하고 대응하기 시작했다. 또한, 근래에는 일련의 모바일 보안사고로 인한 정보유출 및 사회적 이슈가 야기되면서 모바일 보안 위협 강화를 위한 무결성 검증 및 루팅 탐지 외에도 프리다, 디버깅 등의 추가 탐지 방안 대책을 마련하고 있는 추세이다.
[그림 1]은 애플리케이션 공유 사이트에서 개발사의 허가 없이 변조된 애플리케이션을 배포 및 공유하고 있는 환경이다. 주요 변조 타깃은 유료 기능 액세스(Access), 광고 제거 등이 목적이며 게임의 경우 어뷰징(abusing)이 가능하도록 변조되어 있다. 그리고 이와 같이 변조된 애플리케이션을 Mod(Modification) 앱이라고 명칭 한다.
[그림 2]는 코드 변조를 통해 게임 핵(Hack) 기능이 삽입된 Mod 앱 실행 장면이다. 왼쪽 중단 부 빨간 박스를 살펴보면 자동 조준 기능(AIM WHEN FIRE), 자동 스코프 기능(AIM WHEN SCOPE), 적의 위치, 체력 등의 세부정보를 그래픽이나 문자로 볼 수 있는 ESP(Extra Sensory Perception) 기능이 탑재된 것을 알 수 있다. 이러한 비인가된 기능은 크래커(Cracker)에 의해 불법으로 변조되어 게임 생태계를 교란하게 된다. 그리고 [그림 1]과 같은 공유 사이트를 통해 무분별하게 Mod 앱들이 공유되면 정상 게임 생태계 영향도가 낮아지고 게임사의 유료 아이템 판매 및 승률 등에 영향을 미쳐 금전적 손실, 기업 이미지 훼손 등의 영향을 미치게 된다. 따라서 무분별한 생태계 교란이 발생되지 않기 위해서는 무결성 검증이 필요하게 된다.
코드 무결성을 검증하기 위한 대표적인 방법에는 구글에서 제공하는 ‘Play Integrity API’를 사용하는 방법, CRC(Cyclic Redundancy Check) 체크섬 검증, 개발자 사이닝 키(Developer Sinning Key) Hash 값 검증, 리소스 파일 검증 등이 있다.
이제 본격적으로 무결성 검증 방법인 CRC 체크섬, 개발자 사이닝 키 Hash 값 검증과 더불어 애플리케이션 출처 검증, 리소스 검증 방안에 대해서 살펴보고자 한다.
2) Bypass App Name
크래커들은 앱 이름, 이미지, 레이아웃 등 애플리케이션의 리소스 파일을 수정하는 경우에는 변조된 앱이라는 것을 식별하기 위해 앱 이름에 ‘Mod’라는 키워드를 붙이거나, 제작자의 이름 추가 및 앱 아이콘을 제작자의 시그니처 아이콘 이미지 등으로 교체한다. [그림 3]을 살펴보면 좌측 이미지는 변조된 넷플릭스(Netflix)앱으로 변조자의 이름이 추가되었으며, 우측 이미지는 정상 넷플릭스 앱을 의미한다.
[그림 4]의 Bypass App Name 탐지 항목은 설치된 애플리케이션 이름을 검사해 지정된 명칭이 아닌 경우 코드가 변조된 것으로 판단해 탐지하게 된다.
Bypass App Name 항목 탐지를 위해 ANDITER 앱의 이름을 직접 변조하고자 'ANDITER를 활용한 안드로이드 위협 탐지 및 우회 방안 PART 1(루팅)' 에서 사용한 APK Easy Tool을 이용하여 [그림 5]와 같이 대상 앱을 디컴파일(Decompile) 한다.
디컴파일 과정이 완료되었다면 산출물 중 하나인 AndroidManifest.xml 파일에서 [그림 6]과 같이 <application>태그 → android-label 속성을 통해 앱 이름을 확인할 수 있다. 다만, [그림 6]을 보면 앱 이름이 'ANDITER'가 아닌 '@string/app_name'으로 명시되어 있는데 이는 안드로이드에서 사용되는 리소스 참조 구문으로 리소스 파일 중 strings.xml 파일에 정의되어 있는 app_name 요소 값을 호출하겠다는 의미이다.
strings.xml이 위치한 경로는 디컴파일의 산출물 결과 기준으로 [그림 7]과 같이 smali → values → string.xml에서 확인이 가능하다.
[그림 8]과 같이 strings.xml 파일에서 “app_name” 요소에 앱 이름인 “ANDITER”가 명시된 것을 확인 후, 변경하고자 하는 값인 “FAKEANDITER”로 수정하면 된다.
앱 이름을 변경했다면 다시 APK Easy Tool로 돌아와 [그림 9]과 같이 컴파일(Compile) 과정을 진행해 주면 된다.
컴파일 과정 완료 후 디바이스에 앱을 설치하게 되면 [그림 10]과 같이 기존 이름인 “ANDITER”가 아닌 “FAKEANDITER”로 바뀐 것을 확인할 수 있으며, Bypass App Name 탐지 항목에서도 정상 탐지가 될 것이다.
[그림 11]와 같이 isCheckAppName() 함수는 Bypass App Name 탐지 결과를 반환하는 역할을 한다. 코드를 보면 IntegrityDetector 클래스의 생성자를 통해 생성된 인스턴스 this.appName 변숫값을 areEqual() 함수를 통해 “ANDITER”문자열과 동일한지 검사하게 된다.
Bypass App Name 탐지 우회를 위한 후킹 포인트는 아래 3가지로 분류할 수 있다.
① 함수 결과와 상관없이 호출 시 무조건 False 값을 반환하도록 isCheckAppName() 함수를 재 작성하는 방법
② Intrinsics.areEqual() 함수를 후킹 해 함수 호출 시 전달되는 인자 값 “ANDITER”를 변조한 앱 이름과 동일하게 맞춰주는 방법
③ IntegrityDetector 생성자를 통해 생성되는 인스턴스 this.appName 변숫값을 변조한 앱 이름으로 변경하는 방법
여기서는 3번째 방법을 사용해 IntegrityDetctor 클래스에서 인스턴스를 생성할 때 생성 과정을 후킹 하여 appName 값을 변조해 우회해 보도록 하겠다.
[그림 12]는 필자가 작성한 Frida 스크립트로 IntegrityDetector 클래스의 생성자인 “appName” 값을 후킹 하는 기능을 담고 있다. 코드를 살펴보면 ①에서 기존의 우회 스크립트에서 사용되던 Java.use 함수가 아닌 Java.choose 함수가 사용된 것을 볼 수 있다. 해당 함수의 경우 힙(Heap) 메모리에서 인스턴스를 찾을 때 사용되며, ② onMatch 구문을 통해 찾고자 하는 인스턴스를 호출하고 인자로 전달된 변숫값을 통해 인스턴스에 접근하게 된다. ③에서는 인자로 전달된 인스턴스화된 객체(appName) 값을 변조한 앱 이름과 동일하게 수정해 주고 모든 작업이 완료되면 ④의 onComplete이 구문이 동작하게 된다.
작성한 Frida 스크립트를 ADITER 애플리케이션에 어태치한 후, Bypass App Name의 탐지 항목을 체크하면 "Success!"가 출력되어 탐지가 우회된 것을 확인할 수 있다.
3) Bypass Hash Key
애플리케이션이 실행 파일인 APK(Android Package)로 컴파일 될 때 개발자가 지정한 서명키로 앱이 서명된다. 안드로이드의 OS 보안 정책상 서명키가 존재하지 않는 애플리케이션은 디바이스에 설치가 제한되기 때문에 모든 앱은 각자 고유한 서명키를 가지게 된다. 서명키는 일반적으로 보안 해시 알고리즘인 SHA(Secure Hash Algorithm)를 사용하기 때문에 개발자가 생성한 키값을 모르면 해시 다이제스트(Hash Digest)를 크래킹 할 수 없다.
앱 코드 변조에서도 사용한 APK Easy Tool도 컴파일 과정이 끝난 후 마지막 단계에서 자동으로 앱을 서명하는데, [그림 14]와 같이 sign –key 구문의 apkeasytool.pk8과 apkeasytool.pem이 바로 서명과 인증 정보를 등록하기 위한 KeyStore 파일들로 APK Easy Tool에서 임시로 생성한 서명 키값이 들어가게 된다.
[그림 15]의 Bypass Hash Key 탐지 항목은 앱 컴파일 시 사용한 서명 키 정보를 가져와 개발자가 지정한 서명 정보와 다를 경우 앱의 무결성이 손상된 것으로 판단해 탐지하게 된다.
[그림 16]의 isChckHashKey() 함수는 Bypass Hash Key 탐지 결과를 반환 해주는 역할을 한다. 코드를 보면 ①에서 안드로이드 OS 버전에 따른 코드 분기를 나누고 있으며, 코드에 명시된 SDK 28 버전은 안드로이드 9버전을 뜻한다. ②에서는 getPackageInfoCompat(), getApkContentsSigners() 함수를 통해 ANDITER(com.playground.anditer)의 패키지 정보에서 서명 정보를 가져온다. ③에서는 가져온 서명 정보를 Base64로 인 코드(encode) 후 ④ areEqual() 함수를 통해 “7nd5QagBehUoHzVC+c43zic+/ro=” 값과 동일한지 검사하게 된다.
Bypass Hash Key 탐지 우회를 위한 후킹 포인트는 아래 3가지로 분류할 수 있다.
① 함수 결과와 상관없이 호출 시 무조건 False 값을 반환하도록 isCheckHashKey() 함수를 재 작성하는 방법
②서명 정보를 Base 64로 인코딩 하기 위해 사용한 encodeToString() 함수를 후킹해 [그림 16]의 ④에서 서명 정보 비교 시 사용된 “7nd5QagBehUoHzVC+c43zic+/ro=” 값을 반환하도록 변조하는 방법
③areEqual() 함수를 후킹해 서명 정보 비교를 위해 두 번째 인자로 전달되는 값이 “7nd5QagBehUoHzVC+c43zic+/ro=”일 경우 true를 반환하도록 변조하는 방법
여기서는 2번째 방법을 사용해 서명 정보를 Base64로 인코딩 할 때 원본 서명 정보 “7nd5QagBehUoHzVC+c43zic+/ro=” 값을 반환해 탐지를 우회해 보고, 추가로 MT Manager를 이용하여 서명 값을 손상시키지 않고 앱 코드를 변조하는 방법에 대해서도 다뤄보겠다.
[그림 17]은 필자가 작성한 Frida 스크립트로, Base64 클래스의 encodeToString() 함수를 후킹하기 위한 기능을 담고 있다. 코드를 보면 ①에서 encodeToString() 함수 사용을 위한 Base64 클래스 참조를 반환받고 isCheckHashKey() 함수에서 encodeToString() 함수를 호출할 때 전달되는 인자의 데이터 타입에 맞춰 encodeToString() 함수를 오버 로딩으로 구현했다. ②에서는 서명 정보를 Base64로 인코딩 하기 위한 함수 호출 시 원본 서명 정보의 키값 (“7nd5QagBehUoHzVC+c43zic+/ro=”)이 반환되도록 작성했다.
작성한 Frida 스크립트를 ADITER 애플리케이션에 어태치한 후, Bypass Bypass Hash key의 탐지 항목을 체크하면 "Success!"가 출력되어 탐지가 우회된 것을 확인할 수 있다.
MT Manager는 안드로이드 OS의 파일 관리자 및 APK 편집 기능을 가진 애플리케이션으로 디바이스에 설치된 앱을 관리하고, 코드 수정, 텍스트 편집 등의 작업을 수행하는 데 사용되는 강력한 도구이다. 관리자 권한(root)을 부여받지 못할 경우에는 일반적인 파일 관리자 앱과 차이가 없지만, 관리자 권한이 부여될 경우에는 시스템 디렉터리 액세스, 파일 시스템 마운트, 권한 변경 등의 작업과 dex, arsc, xml 파일 편집, APK 서명 및 서명 확인 제거 등의 매우 강력한 기능을 사용할 수 있게 된다.
MT Manager의 경우 앱이 내포하고 있는 기능 특성상 구글 Play Store에서 배포되지 않아 별도의 APK 공유 사이트를 통해서 다운로드해야 한다. 신뢰할 수 있는 대표 APK 공유 사이트로 아래의 두 곳이 존재하며, APK Combo(https://apkcombo.com/ko/mt-manager/bin.mt.plus/)에서 MT Manager 다운로드가 가능하다.
① APK Combo(https://apkcombo.com/ko/)
② APK Pure(https://apkpure.com/kr/)
MT Manager 설치가 되었다면 해당 앱을 사용해 Bypass App Name 탐지 항목 코드를 변조하고 Bypass Hash Key 탐지 결과를 통해 서명 키의 변조 유무를 확인해 보겠다. 먼저 기존에 리패키징(Re-Packaging)된 ANDITER 앱을 지우고 변조되지 않은 ANDITER를 다시 설치한다. ① 그 후 [그림 21]과 같이 ANDITER 앱의 실행 파일인 base.apk 파일이 위치한 곳으로 이동 후 해당 파일을 클릭한다. ② 그 후 출력되는 알림 창에서 VIEW 버튼을 클릭한다.
[그림 22]의 ①과같이 출력되는 ANDITER의 내부 파일 중에서 실행 코드가 담긴 classes.dex 파일을 클릭하고 ② Dex 편집기 기능에서 "Dex Editor plus"를 선택해 준다. ③ ANDITER의 경우 MultiDex로 구성되어 있기 때문에 Dex 파일이 총 두 개의 파일로 분할되는데 모두 선택해 OK 버튼을 클릭해 준다.
[그림 23]과 같이 ① Bypass App Name 탐지 항목의 검사 코드를 가지고 있는 IntegrityDetector 클래스의 smali 코드가 위치한 곳으로 이동한다. [ com → playground → anditer → IntegrityDetector ] ② 그리고 해당 smali 파일의 80번 라인에 위치한 앱 이름인 ANDITER를 변경하고자 하는 이름으로 변경해 준다. 여기서는 "FAKEANDITER"로 변경해 줬다.
코드를 변경했다면 디바이스에서 뒤로 가기 버튼을 클릭해 편집기에서 빠져나온 후 ① [그림 24]와 같이 상단의 망치 버튼을 클릭해 변경한 Dex 파일을 컴파일 해준다. ② 그리고 다시 디바이스에서 뒤로 가기 버튼을 클릭하면 Dex 편집기로 진입하기 전의 화면으로 되돌아가며 [그림 24]의 오른쪽 화면과 같이 알림 창이 출력된다. 여기서 "AUTO SIGN"을 체크해 주고 "OK" 버튼을 클릭하면 최종적으로 MT Manager를 이용한 변조 과정이 끝난 것이다.
ANDITER 앱으로 돌아와 Bypass App Name 항목을 체크해 보면 코드가 정상 변조되어 탐지된 것을 볼 수 있다. 변조 전 탐지 코드에서는 앱 이름이 "ANDITER"인지 검사했다면 변조 후 코드에서는 [그림 23]에서 변경한 것과 같이 "FAKEANDITER"인지를 검사하게 된다. 따라서, 코드가 정상 변조되었다면 [그림 25]와 같이 Bypass App Name 항목에 탐지될 것이고 변조되지 않았다면 탐지되지 않을 것이다.
우리가 여기서 눈여겨봐야 할 것은 "Bypass Hash Key"항목이다. 변조 전과 변조 후의 탐지 결과가 동일한데 이는 서명 키가 변조되지 않았다는 것을 의미한다. 분명 [그림 25]의 Bypass App Name 항목 탐지 결과를 통해 코드가 정상 변조된 것을 확인했고 [그림 24]에서 "AUTO SIGN" 기능을 사용해 서명 과정도 수행되었지만 앱에서는 서명 키 변조를 탐지하지 못하고 있다.
이는 기존의 변조한 코드를 컴파일 하고 APK 파일을 재 설치해 앱을 동작시키는 방식이 아닌 라이브(Live) 상태에서 코드를 변조하기 때문으로 변조된 APK 파일을 재 설치하지 않는 이상 [그림 26]과 같이 서명 키를 제거해도 Java 코드 단에서는 서명 키가 변조된 것을 탐지하지 못한다. 이와 같은 라이브 상태에서 수행하는 코드 변조 방법을 무서명 변조 기법이라고 부르며, Dex와 같은 실행 코드가 담긴 파일은 변경 이력이 바로 적용되지만 서명 키는 적용되지 않는 점을 이용한 변조 방법이다. 따라서, 앱에서 서명 키를 검사해 무결성 변조를 탐지하고 있다면 APK Easy Tool과 같은 디컴파일 도구를 사용하는 것보다 MT Manager와 같은 애플리케이션을 사용하는 것이 훨씬 더 효율적이다.
이를 대응하기 위한 방안에는 뒤에 주제에서 살펴볼 Dex 파일 체크섬 검증과 구글 Play 서명 검증 그리고 디바이스에 MT Manager와 같은 앱이 설치되었는지 확인하는 방법이 있다.
4) Bypass Installer
일반적으로 디바이스에 앱을 설치하는 경우에는 Play 스토어(Google Play Store), 원 스토어(One Store), 갤럭시 스토어(SamSung Galaxy Store) 등과 같이 디바이스의 운영체제별 벤더사에서 제공하는 공식 스토어를 이용한다. 이러한 공식 스토어들은 엄격한 검열 및 보안정책을 적용하여 제작사나 출처가 불분명한 앱은 스토어 업로드를 제한하며, 앱이 등록된 이후에도 스토어에 바로 공개되는 것이 아니라 각 스토어에서 운영 중인 테스트 에뮬레이터를 통해 악의적인 코드 삽입 여부 등의 검증 과정을 거쳐 최종 등록되게 된다. 따라서 MT Manager와 같이 관리자 권한이 필요한 앱이나 Mod앱은 공식 스토어로 공유가 제한되기 때문에 SNS나 공유 사이트 등 별도의 경로를 통해서 다운로드해야 한다.
설치된 앱의 패키지 관리(Package Management) 정보에는 앱을 다운로드했던 출처 정보(Installer Source Information)가 기록되어 있어 [그림 28]과 같이 pm(Package Manager) 명령어로 확인할 수 있다. installer 속성을 보면 다양한 출처 정보를 확인할 수 있는데, 대표적으로 구글의 Play Store는 “com.android.vending”, “com.google.android.packageinstaller”를 사용하고 삼성의 갤럭시 스토어는 패키지 정보에 “samsung”이라는 문자가 포함되어 있다.
반면 비공식 스토어로 다운로드한 앱의 경우에는 [그림 29]와 같이 "null(출처 불명)"로 표시된다. 다만, 공식 스토어를 통해 다운로드한 앱의 경우에도 변조하는 경우에는 리패키징 과정을 통해 출처 정보가 초기화되기 때문에 출처 정보가 “null”로 표기된다.
[그림 30]의 Bypass Installer 탐지 항목은 설치된 앱의 출처 정보를 확인해 지정된 공식 스토어가 아닌 곳에서 다운로드해 설치한 경우 앱이 변조된 것으로 판단해 탐지하게 된다.
[그림 31]의 isCheckInstaller() 함수는 Bypass Installer 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 ① IntegrityDetector 클래스의 생성자를 통해 통해 출처 정보가 담긴 인스턴스 installStore 변수가 생성되고 ② ANDITER의 패키지 정보(com.playground.anditer)에서 출처 정보 비교 시 사용되는 것을 볼 수 있다. 이때, 가져온 출처 정보가 installStore에 저장된 값과 다를 경우 앱이 변조된 것으로 판단해 탐지하게 된다.
Bypass Installer 탐지 우회를 위한 후킹 포인트는 아래 3가지로 분류할 수 있다.
① 함수 결과와 상관없이 호출 시 무조건 False 값을 반환하도록 isCheckInstaller() 함수를 재 작성하는 방법
② IntegrityDetector 생성자를 통해 생성되는 인스턴스 변수인 installStore 값을 변조하는 방법
③ 문자열 비교 시 사용되는 areEqual() 함수를 후킹 해 함수로 들어오는 매개변수 값이 installStore 변수에 정의된 패키지 이름과 같을 경우 True를 반환하도록 하는 방법
여기서는 3번째 방법을 사용해 탐지를 우회해 보고 추가적으로 Package Manager 명령어를 이용한 출처 정보 변조 방법도 다뤄보겠다.
[그림 32]는 필자가 작성한 Frida 스크립트로 areEqual() 함수를 후킹 하기 위한 기능을 담고 있다. 코드를 보면 ①에서 areEqual() 함수 사용을 위해 Intrinsics 클래스 객체를 생성하고 isCheckInstaller() 함수에서 areEqual() 함수를 호출할 때 전달되는 인자의 데이터 타입에 맞춰 areEqual() 함수를 오버로딩으로 구현했다. ②에서는 출처 비교 시 사용되는 패키지 정보가 매개변수로 들어왔을 때만 true 값을 반환하기 위해 사전에 패키지 정보를 정의하고 ③에서 areEqual() 함수 호출 시 들어오는 매개변수 값이 사전에 정의한 패키지 정보와 동일할 경우 true를 반환하도록 작성했다.
작성한 Frida 스크립트를 ADITER 애플리케이션에 어태치한 후, Bypass Installer의 탐지 항목을 체크하면 "Success!"가 출력되어 탐지가 우회된 것을 확인할 수 있다.
추가로 Package Manager 명령어를 이용해 APK 파일 설치 시 출처 정보를 삽입하여 탐지를 우회하는 방법을 살펴보겠다. Package Manager는 adb 셸 내에서 제공하는 패키지 관리자 도구로 명령어를 통해 디바이스에 설치된 앱 패키지에 관한 작업과 쿼리 실행 기능을 지원하는 유용한 도구이다.
실행 방법은 원격 셸 또는 adb 콘솔에서 "pm" 명령어와 함께 다양한 옵션을 조합해 사용할 수 있으며, 이를 통해 약식 분석도 가능하다. 해당 문서에서는 모든 옵션을 다루지는 않고 출처 정보 변경 시 사용하는 옵션에 관해서만 다룰 것이다. Package Manager에 관한 더 다양한 사용 방법과 활용법은 아래의 링크에서 참고할 수 있다.
※ 링크 : https://developer.android.com/studio/command-line/adb?hl=ko#pm
먼저 Package Manager 명령어로 설치된 앱 패키지에서 출처 정보를 가져와보겠다. [그림 35]와 같이 “list packages” 명령어를 통해 패키지 관련 정보 참조가 가능하며 "-i" 옵션과 조합해 출처 정보를 확인할 수 있다. [그림 35]의 명령어 결과 행을 보면 앞서 살펴본 바와 같이 스토어와 같은 공식 루트를 통해 다운로드해 설치한 것이 아니므로, 출처 정보가 “null”로 표기된 것을 볼 수 있다.
이제 ADB를 통해 앱을 설치할 때 출처 정보를 삽입하는 방법을 알아보겠다. 사용 방법은 의외로 간단한데 기존의 설치 명령어에서 "–i" 옵션만 추가해 주면 된다. 여기서는 [그림 36]과 같이 Bypass Installer 항목에서 검사하고 있는 출처 정보(“com.android.vending”)를 기입했으나, 이 외에 다른 출처 정보를 지정해도 무방하다.
설치가 끝났다면 [그림 37]과 같이 출처 정보 확인 시 앞서 살펴본 [그림 35]의 결과와 다르게 “null” 값이 아닌 [그림 36]에서 지정한 “com.android.vending” 출처 정보가 삽입된 것을 볼 수 있으며, ANDITER 앱에서 Bypass Installer 항목을 체크해 보면 탐지되지 않는 것도 알 수 있다.
데이터의 무결성 훼손 여부를 검사하는 방법은 CRC(Cyclic Redundancy Check 순환 중복 검사), 체크섬(Checksum), 해시(MD5, SHA) 등이 있으며 해당 기법들은 데이터의 위·변조 시에 결괏값이 상이해지기 때문에 변경 및 손상 여부를 인지할 수 있게 된다. 모바일 애플리케이션 환경에서도 명시된 무결성 검증 방법을 이용하여 앱 패키지 내에 실행코드가 저장되어 있는 “classes.dex”의 위·변조 여부를 검사할 수 있다. 공격자가 앱을 변조하기 위해서는 “classes.dex”파일을 디컴파일한 산출물인 smali 파일이 수정돼야 한다는 점을 착안한 무결성 검증 방법인 것이다.
[그림 39]는 “crc_file.txt” 파일 내용 차이점에 따른 CRC 산출 값으로 ①에서는 “crc_file.txt” 파일 내용이 “test12345” 였을 때의 결괏값이고 ②는 “test123456” 였을 때 결괏값을 보여준다. 지정한 데이터가 1Byte라도 내용이 변경된다면 산출 값이 달라지기 때문에 이러한 차이를 이용하면 무결성 검증이 가능해 진다.
[그림 40]의 Bypass CRC 탐지 항목은 앱 패키지에서 “classes.dex” 파일의 CRC 값을 산출해 지정된 값과 일치하지 않을 경우 앱이 변조되어 무결성이 훼손된 것으로 판단해 탐지하게 된다. 실습 진행을 위해 해당 챕터에서는 [1.2 Bypass App Name] 챕터에서 변조한 APK 파일을 설치해 진행한다.
[그림 41]의 isCheckCRC() 함수는 Bypass CRC 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 ① ZipFile 클래스를 통해 현재 애플리케이션의 압축된 APK 파일을 열람하고 ②에서 “classes.dex” 파일의 엔트리 정보를 가져와 getCrc() 함수를 통해 CRC 값을 추출한다. ③ 추출한 CRC 값을 미리 정의된 CRC 체크 코드와 비교해 값이 다를 경우 앱이 변조된 것으로 판단해 탐지하게 된다.
Bypass CRC 탐지 우회를 위한 후킹 포인트는 아래 3가지로 분류할 수 있다.
① 함수 결과와 상관없이 호출 시 무조건 False 값을 반환하도록 isCheckCRC() 함수를 재 작성하는 방법
② areEqual() 함수를 이용해 CRC 값 비교 시 전달되는 인자 값을 변조하는 방법
③ ZipFile 클래스를 통해 압축된 APK 파일 열람을 시도할 때 전달되는 APK 파일이 위치한 경로 인자 값을 변조해 사용자가 임의 지정한 파일을 열람할 수 있도록 변조하는 방법
여기서는 3 번째 방법을 사용해 코드 단에서 지정된 APK 파일을 열람하는 것이 아닌 사용자가 지정한 파일(변조되지 않은 원본 APK 파일)을 열람하게 만들어 탐지를 우회해 보도록 하겠다.
Frida 스크립트를 이용해 탐지를 우회하기에 앞서 사전 준비가 필요하다. 먼저 변조되지 않은 ANDITER APK 파일을 [그림 42]와 같이 ADB 명령어를 사용해 디바이스 내 “/data/local/tmp” 경로로 이동시켜준다.
[그림 43]과 같이 ① ADB 원격 셸로 접근 후 ② su 명령어를 사용해 관리자 권한을 획득하고 ③ “/data/local/tmp” 디렉터리에 일반 사용자 읽기 및 쓰기 권한을 부여해 준다. 이는 앱 코드 단에서 “/data/local/tmp” 디렉터리에 위치한 원본 APK 파일에 접근할 수 있도록 해주기 위함으로 해당 디렉터리에 권한이 부여되어 있지 않을 경우 권한 부족으로 우리가 지정하려는 APK 파일에 접근하지 못하게 된다.
사전 준비가 끝났다면 탐지 우회를 위한 Frida 스크립트를 살펴보겠다. ZipFile 클래스는 파일의 경로를 인자로 받아 생성자를 호출해 지정된 압축 파일을 열거나 생성하는 데 사용된다. [그림 44]의 코드를 보면 ①에서는 ZipFile 클래스 사용을 위한 객체를 생성하고 “$init” 키워드를 통해 생성자를 오버로딩으로 구현했다. ②에서는 ZipFile 클래스 생성자 호출 시 전달되는 인자 값인 파일이 위치한 경로에 “com.playground.anditer” 문자열이 포함되어 있을 경우 [그림 42]에서 이동시킨 변조되지 않은 원본 APK 파일을 열람할 수 있도록 했다. 이는 결국 변조된 현재 애플리케이션의 “classes.dex” 파일의 CRC 값을 산출하는 것이 아닌 임의로 지정한 변조되지 않은 원본 APK 파일의 “classes.dex” 파일의 CRC 값을 산출해 비교하게 된다.
작성한 Frida 스크립트를 ADITER 애플리케이션에 어태치한 후, Bypass CRC의 탐지 항목을 체크하면 "Success!"가 출력되어 탐지가 우회된 것을 확인할 수 있다.
02. 동적 로딩(Dynamic Dex Loading) 개요와 탐지 및 우회 방안
1) 동적 로딩 개요
보안은 창과 방패의 대결이라 할 수 있다. 조직의 인프라에 존재하는 취약점을 악용하기 위한 “창”의 공격을 대응하기 위해 공격자의 공격패턴을 분석하고 대응 기법들을 적용하는 “방패“가 작용하게 된다. 모바일 생태계에서도 이러한 공격과 대응의 관계는 유효하다. 공격자는 악의적인 공격코드를 삽입하기 위해 보안 솔루션을 포함한 다양한 우회기법들을 연구하고, 방어자 입장에서는 공격 트리거로 작용할 수 있는 보안 기능이나 탐지코드를 숨길 수 있는 다양한 기법들을 적용하게 된다. 중요한 점은 공격자나 방어자 모두 본인의 패가 공개되지 않도록 코드 노출을 최소화한다는 점이다. 코드 노출 최소화의 기본은 코드의 식별성을 저해하는 난독화를 목적으로 하며, 정적인 형태가 아닌 동적인 형태로 코드를 호출해서 코드의 식별성을 저하시키는 기법을 동적 난독화라고 한다.
동적 로딩(Dynamic Dex Loading) 기법은 안드로이드 애플리케이션에서 Dex 파일을 동적으로 로드해서 실행하는 기법이다. 개발자 입장에서는 새로운 기능 추가 및 업데이트 시에 모듈화를 통해 동적으로 로드하게 되면 앱 유지보수와 확장성, 보안성을 향상시킬 수 있다. 기존에는 동적 로딩이 유지보수와 편의성을 목적으로 사용되었다면, 최근에는 Dex 파일을 동적 로딩하여 공격자가 공격코드를 숨기거나 보안 솔루션이 탐지코드를 숨기는 용도로 사용한다.
동적 로딩은 코드 일부를 숨겨서 난독화 효과를 얻을 수 있는 점이 동적 난독화와 유사하여 안드로이드 동적 난독화라고 부르기도 한다. 하지만 동적 로딩과 동적 난독화는 목적과 의도가 상이하기 때문에 코드 식별을 저해하는 목적으로 사용된다는 점만 인지하면 된다.
동적 로딩 구현을 위해 사용되는 대표적인 자바 클래스로는 ClassLoader, Reflection이 있으며, 안드로이드에서 외부 Dex 파일을 로드하는 데 사용되는 클래스의 종류로는 DexClassLoader, PathClassLoader, BaseDexClassLoader가 있다. 이 중 Dex 파일을 로드하는데 일반적으로 사용
되는 대표적인 클래스는 DexClassLoader이다.
DexClassLoader는 Dex 파일뿐만 아니라 이미지, 텍스트, 레이아웃 등과 같은 리스소 파일도 로드가 가능하다. 그리고 이렇게 로드되는 파일은 일반적으로 앱 패키지 내 “assets” 디렉터리에 저장되어 있으며, 앱 실행 중 필요시 DexClasssLoader를 호출해 “assets” 디렉터리에 존재하는 리소스 및 Dex 파일을 메모리에 로드하게 된다.
[그림 47]은 ANDITER 앱 패키지를 압축 해제했을 때의 모습으로 “assets” 디렉터리에 “dynamic” 이라는 Dex 파일이 존재하는 것을 볼 수 있다. 참고로, ANDITER는 교육에 초점이 맞춰져 있기에 파일이 존재하는데 보안 솔루션과 악성 앱에서 DexClassLoader를 이용한 동적 로딩 기법을 구현할 때에는 [그림 47]과 같이 파일을 미리 생성해 “assets” 디렉터리에 넣어두는 방식이 아닌 별도의 모듈을 사용해서 암호화된 코드를 복호화 해 앱 실행 중에 생성될 수 있도록 한다. 그렇다고 동적 로딩 기법을 사용하는 모든 앱이 그런 것은 아니므로, 분석 시 “assets” 디렉터리가 존재할 경우 확인해 봐야 한다.
다만, “assets” 디렉터리는 앱 패키지 내부에만 존재하고 있어 실제로 앱을 설치해서 확인해 보면 디바이스의 파일 시스템에 해당 디렉터리가 생성되어 있지 않는 것을 알 수 있다. 이는 Android OS 보안 정책에 의한 것으로 앱이 설치될 때 APK에서 파일을 추출하여 앱 내부에서만 접근할 수 있게 되어 있다. [그림 48]은 “assets” 디렉터리에 위치한 파일의 추출 과정을 보여주는 것으로 앱 실행 시 “assets”에서 파일(Dex, 리소스 등)을 읽어 들여 내용을 복사하고 새로운 파일을 생성해서 복사한 내용을 붙여 넣는다. 그 후 앱의 캐시 파일 및 임시 파일을 저장하는 개인 데이터 영역인 “cache” 디렉터리에 파일을 저장하고 필요시 메모리에 로드하여 사용하게 된다.
[그림 49]를 보면 앱의 개인 데이터 영역인 “cose_cache” 디렉터리에 읽어 들인 “assets” 파일이 저장되어 있는 것을 알 수 있다. 이렇게 해서 DexClassLoader를 이용한 동적 로딩 기법의 특징을 알아봤다. 이제 ANDITER 앱에서 사용되는 동적 로딩 기법을 분석해 보겠다.
2) Bypass Dynamic Code
[그림 50]의 Bypass Dynamic Code 탐지 항목은 동적 로딩 기법을 이용해 외부 Dex 파일의 클래스와 함수에 의해 해당 항목에 무조건 탐지되도록 설정되어 있다. 따라서, 외부 Dex 파일을 분석해 탐지가 되지 않도록 우회해야 한다.
[그림 51]의 isCheckDynamic() 함수는 Bypass Dynamic Code 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 ①에서 외부 Dex 파일을 읽어 들인 후 파일의 내용을 새로운 파일에 저장하기 위해 File 클래스 생성자를 호출하여 “dynamic” 이름을 가진 파일 객체를 생성한다. ②에서는 getAssets().open() 함수를 통해 “assets” 디렉터리에 위치한 “dynamic” Dex 파일의 코드를 읽어와 ③ ①에서 생성한 “dynamic” 파일 객체에 해당 코드를 저장한다. ④ 그 후 ③에서 저장한 Dex 파일의 경로를 인자 값으로 사용하여 DexClassLoader 생성자를 호출하고 객체를 생성한 다음, ⑤에서 생성된 인스턴스를 통해 getResult() 함수를 호출하여 탐지 결괏값을 반환하게 된다.
대략적인 분석이 끝났다면 분석한 내용을 기반으로 DexClassLoader에서 호출되는 함수를 우회하기 위한 원본 Dex 파일을 찾아야 한다. 이를 위해 다음 2가지 접근 방식을 살펴볼 것이다.
① DexClassLoader 클래스 생성자를 후킹하여 생성자 호출 시 전달되는 인자 값(파일이 위치한 경로)을 확인하는 방법
② 메모리 매핑 정보를 제공하는 가상 파일에서 흔적을 찾는 방법
먼저 첫 번째 방법부터 살펴보겠다. [그림 52]는 필자가 작성한 Frida 스크립트로, DexClassLoader 생성자를 후킹하기 위한 기능을 담고 있다. ①에서는 DexClassLoader 클래스를 사용하기 위한 참조를 얻고 ②에서는 isCheckDynamic 생성자 호출 시 전달되는 인자 값 중에 파일의 절대 경로를 가지고 있는 첫 번째 인자 값을 로그로 출력한다.
[그림 52]에서 작성한 Frida 스크립트를 실행하면 앱에서 Dex 파일 로드 시 [그림 53]과 같이 Dex 파일이 저장된 절대 경로를 알 수 있다.
[그림 53]에서 확인한 경로로 이동하면 [그림 54]와 같이 “dynamic”이라는 이름을 가진 Dex 파일을 발견할 수 있다.
이제 두 번째로 구동 중인 앱의 메모리 매핑 정보를 활용하여 동적 로딩의 흔적을 찾아보는 방법을 알아보겠다. [그림 55]의 ①을 보면 구동 중인 ANDITER 앱의 PID를 확인하기 위해 “ps” 명령어를 사용했으며, ② “cat” 명령어를 통해 메모리 매핑 정보를 가지고 있는 “/proc/[PID]/maps” 가상 파일의 정보를 출력했다. ③ 출력된 메모리 매핑 정보를 살펴보면 “/data/data/com.playground.anditer/code_
cache/dynamic” 파일이 참조되어 있는 것을 확인할 수 있는데 이는 DexClassLoader 클래스가 외부 Dex 파일을 참조할 때 메모리에 로드하는 특성 때문으로 삭제된 파일의 흔적 확인도 가능하다. 이에 대해서는 뒤의 챕터인 Bypass Hide Code에서 자세히 살펴보겠다.
ADB 도구를 통해 Dex 파일을 추출하기 앞서 권한 부여 작업이 필요한데 권한이 부족할 경우 [그림 56]과 같이 “Permission denied” 오류가 발생한다. ① 먼저 대상 파일을 “/data/local/tmp” 디렉터리로 이동시킨 다음 ② 모든 권한을 부여해 주고 ③ ADB를 통해 Dex 파일을 로컬 PC로 추출한다.
[그림 57]은 [그림 56]에서 추출한 Dex 파일을 JEB 디컴파일러를 사용하여 나타낸 복호화 한 코드로 하나의 클래스(Dynamic)와 멤버 함수(getResult)로 구성되어 있다. 그리고 getResult() 함수는 [그림 51]에서 ClassDexLoader의 인스턴스 객체를 통해 호출된 함수로 호출 시 true를 반환하여 Bypass Dynamic Code 항목에 탐지되도록 만든다.
이제 확인된 클래스와 함수 정보를 이용해 Frida 우회 스크립트를 작성하면 되는데 여기서 문제 한 가지가 있다. 기존에 우회 스크립트를 작성했던 방법처럼 [그림 57]의 패키지 경로 “com.playground.anditer” 로 “Dynamic” 클래스를 참조할 경우 [그림 58]과 같이 경로 참조 실패에 관한 오류가 발생한다.
Frida는 앱이 실행되고 타깃 앱이 ART 런타임에서 동작할 때 런타임에 로드되는 메모리 정보에서 클래스와 함수 정보를 가져와 후킹 작업을 수행하는 데 동적 로딩을 통해 로드한 Dex 파일은 ART에서 가져온 정보가 아니기에 Frida가 해당 영역에 직접 접근할 수 없다. 따라서, Frida를 사용하여 동적 로드한 Dex 파일을 읽어와 클래스에 참조하고 싶다면, 별도의 Dex 파일 로더를 구현하여 Dex 파일을 메모리에 로드 한 후, 해당 영역에 포함된 클래스 정보를 참조해야 한다.
[그림 59]는 [그림 52]의 코드를 기반으로 외부 Dex 파일의 클래스와 함수를 참조하기 위해 Dex 파일 로더를 구현한 Frida 스크립트이다. 코드를 살펴보면 ①에서 생성자 호출 시 전달되는 인자 값을 사용하여 새로운 DexClassLoader 객체를 생성한다. 생성된 DexClassLoader 객체는 hookloadedfunctions() 함수의 인자로 전달되고 ② Java.classFactory.loader에 해당 객체를 할당함으로써 외부 Dex 파일의 클래스 및 함수를 참조할 수 있게 된다. ③에서는 [그림 57]에서 확인한 Dex 파일의 정보를 토대로 Dynamic 클래스를 참조하기 위한 변수를 지정하고 ④ getResult 함수를 재정의하여 false 값을 반환하도록 작성했다.
작성한 Frida 스크립트를 ADITER 애플리케이션에 어태치한 후, Bypass Dynamic Code의 탐지 항목을 체크하면 "Success!"가 출력되어 탐지가 우회된 것을 확인할 수 있다.
3) Bypass Hide Code
Bypass Hide Code 항목도 Bypass Dynamic Code 항목과 동일하게 DexClassLoader를 사용하여 외부 Dex 파일을 가져온다. 다만, Bypass Hide Code는 Bypass Dynamic Code 항목과 달리 Dex 파일을 불러오고 파일을 삭제한다는 점에서 차이가 있다. DexClassLoader를 활용한 동적 로딩 기법에 관한 내용은 이전 챕터인 Bypass Dynamic Code에서 자세히 다루었으므로, 해당 챕터에서는 이에 대한 설명은 생략하고 파일이 삭제되는 방식과 이를 복구하는 방법에 대해 중점으로 살펴보겠다.
[그림 62]의 Bypass Hide Code 항목의 탐지 코드는 Bypass Dynamic Code 항목에서 사용된 탐지 코드와 동일하며, 인자를 통해 파일 삭제 여부를 수행하는 차이만 있다. 코드를 살펴보면 ① 함수 호출 시 Boolean 타입의 인자를 받아와 ② 해당 값이 True인 경우 deleteFile() 함수를 호출하여 읽어 들인 외부 Dex 파일을 삭제하고, False인 경우 삭제하지 않는다.
[그림 63]은 [그림 62]에서 Dex 파일을 삭제할 때 사용된 deleteFile() 함수로, 인자로 전달된 파일 객체를 참조해 파일이 존재할 경우 해당 파일을 삭제하는 간단한 코드이다.
실제로 디바이스 콘솔에서 확인해 보면 [그림 64]와 같이 Bypass Dynamic Code 항목에서는 읽어 들인 Dex 파일이 삭제되지 않았지만, [그림 65]의 Bypass Hide Code 항목에서는 파일이 삭제된 것을 볼 수 있다.
또한, 이전 챕터에서 설명한 대로, DexClassLoader를 통해 읽어 들인 Dex 파일은 구동 중인 앱의 메모리 매핑 정보에 흔적이 남게 된다. 이는 삭제된 파일도 마찬가지로 [그림 66]과 같이 파일 이름 옆에 “(deleted)”라고 표시되어 파일이 삭제된 것을 파악할 수 있다.
동적 로딩 기법을 활용하여 호출되는 클래스와 함수를 Frida를 통해 후킹하기 위해서는 원본 Dex 파일을 확보하는 것이 매우 중요하다. 삭제된 파일을 복구하기 위해서는 일반적으로 2가지 방법이 사용된다. 첫 번째는 파일 삭제 작업을 수행하는 함수를 후킹하여 파일이 삭제되는 것을 막는 방법이며, 두 번째는 메모리에 로드된 Dex 파일을 덤프(dump)하여 원본 파일을 복구하는 방법이다.
먼저 첫 번째 방법부터 살펴보겠다. [그림 67]의 ① Frida 스크립트는 File 클래스의 delete() 함수를 후킹하여 파일이 삭제되는 것을 방지한다. ② 스크립트를 실행한 후 Bypass Hide Code 항목을 체크해보면 파일이 삭제되지 않고 존재하는 것을 볼 수 있다. 다만, 필자의 경험상 실제 애플리케이션에서 해당 방법을 통해 얻은 Dex 파일들은 대부분 암호화되어 있어 별도의 복호화 함수와 키 값을 찾아야 했다.
그러나, Dex 파일이 암호화되어 있더라도 메모리에 로드될 때에는 복호화 된 코드가 적재되므로, 다음으로 소개할 메모리 덤프 방법은 파일 삭제를 방지하는 방법보다 더욱 용이하게 Dex 파일을 복구할 수 있다. 참고로 강화된 보호 메커니즘을 사용하는 앱의 경우에는 메모리에도 암호화된 상태로 저장될 수 있으므로, 복호화를 위한 함수와 키값의 정보를 찾아야 한다.
메모리에 적재된 Dex 파일을 추출하기 위해서는 앱의 메모리를 덤프하고, 덤프 된 파일 중에서 Dex 시그니처를 가진 파일을 선별하여 확인해야 한다. 하지만, Frida-DexDump 도구를 사용하면 이러한 선별 과정들을 생략하고 Dex 파일만을 쉽게 추출할 수 있다. 설치 방법은 간단하다. [그림 68]과 같이 파이썬(Python) 패키지 관리 도구를 통해 설치가 가능하며, 이 외 별도의 파일을 통해 설치를 하고자 하는 경우에는 개발자의 Github(https://github.com/hluwa/FRIDA-DEXDump)에서 확인하면 된다.
Frida-DexDump 도구의 사용법은 Frida 사용법과 유사하며, -f 옵션은 스폰(Spawn), –F 옵션은 어태치 (attache) 방식으로 동작한다. 여기서는 [그림 69]와 같이 스폰 방식을 사용했으며, Bypass Hide Code 항목을 체크하기 전까지 메모리 덤프를 하지 않도록 –-sleep 옵션을 추가했다.
명령어를 실행한 다음에 Bypass Hide Code 항목을 체크하고 잠시 기다리면, [그림 70]과 같이 메모리 덤프가 자동으로 시작된다.
덤프 된 파일은 앱 패키지 이름(com.playground.anditer)을 가진 디렉터리에 저장되며, 이 디렉터리에는 외부 Dex 파일뿐만 아니라 메모리에 적재된 모든 Dex 파일이 함께 저장된다. 저장된 파일 중에서 동적으로 로드되는 Dex 파일을 찾기 위해서는 모든 파일을 확인해 보는 것이 일반적인 절차이지만, 대부분의 동적으로 로드되는 Dex 파일은 코드가 짧기 때문에 용량이 가장 작은 파일부터 확인하는 것이 용이하다.
추출된 파일 중에서 용량이 가장 작은 Dex 파일을 JEB 디컴파일 도구로 확인해 보면 DexClassLoader에서 로드한 Dex 파일인 것을 확인할 수 있다.
우회 과정은 Bypass Dynamic Code 챕터에서 설명한 우회 과정과 동일하기 때문에 중복되므로 해당 부분에 대한 설명은 생략하겠다. [그림 59]에서 작성한 Frida 스크립트를 ADITER 애플리케이션에 어태치한 후, Bypass Hide Code의 탐지 항목을 체크하면 "Success!"가 출력되어 탐지가 우회된 것을 확인할 수 있다.
다음 호에 "ANDITER를 활용한 안드로이드 위협 탐지 및 우회 방안 PART 4-2"가 이어집니다.