보안정보

전문화된 보안 관련 자료, 보안 트렌드를 엿볼 수 있는
차세대 통합보안관리 기업 이글루코퍼레이션 보안정보입니다.

ANDITER를 활용한 안드로이드 위협 탐지 및 우회 방안 : PART 1

2023.05.03

8,050

01. 모바일 생태계 환경 및 운영체제 점유율 현황

1990년경 Tim Berners-Lee에 의해 세계 최초의 브라우저인 WWW(World Wide Web)가 공개되고 21세기부터 인터넷 보급이 본격화되면서 PC나 휴대용 디바이스 등의 전자기기 발전이 급속도로 증가하였다. 인터넷의 발전과 디바이스의 보편화는 모바일 생태계를 촉발하여 2007년 스마트폰의 시초라 부를 수 있는 애플의 아이폰 1세대가 출시되고 1년 후인 2008년 T-Mobile에서 세계 최초의 안드로이드 스마트폰을 출시하였다. 출시 당시에는 스마트폰의 하드웨어나 성능 저하로 인해 사용자들의 관심이 저조하였으나 하드웨어와 소프트웨어 기술의 발전으로 지금의 모바일 생태계가 조성되었다.

[그림 1] 글로벌 모바일 OS 시장 점유율(출처 : 위키백과)

지금의 모바일 생태계는 구글의 Android와 애플의 iOS간의 양호상투 양상이라 할 수 있다. 노키아와 인텔의 미고, 퀄컴의 BREW, MS사의 윈도우 폰 등 다양한 모바일 운영체제가 존재하지만 모바일 생태계의 상당수 점유율을 차지하고 있는 곳은 Android와 iOS뿐이다. [그림 1]을 보더라도 Android와 iOS가 모바일 생태계에서 차지하는 비율이 절반 이상을 차지하고 있는 것을 알 수 있으며, 2021년 기준 Android 시장 점유율은 72.20%에 육박한다.

02. Android OS 버전별 특징 및 아키텍처

1) Android OS 버전별

Android OS는 리눅스 커널을 기반으로 제작한 모바일 운영 체제와 미들웨어 및 애플리케이션이 포함된 오픈소스 플랫폼이다. 초창기 Android OS는 디지털카메라의 운영체제로 개발되었으나 iPhone 등 경쟁사들의 스마트폰 출시로 인해 스마트폰 전용 운영체제로 변경되었다. 오픈 소스 플랫폼 특성상 개방성이 높아 iOS에 비해 사용자 접근 편의성이 높고 다양한 개발도구 및 API가 제공되기 때문에 애플리케이션 개발에 참여하기 용이하다.

[표 3-1] Android OS 버전별 코드네임과 API 버전

Android OS 버전은 필자가 글을 집필하는 시점으로 14.0 버전까지 릴리즈 되었다. 운영체제 버전은 디바이스 하드웨어의 성능에 따라 지원버전이 상이하며, 보안 및 Java API 이슈로 인해 상당수는 7.0미만 버전은 지원하지 않아 7.0이상만 사용이 가능하다고 볼 수 있다.

Android OS버전이 높아질수록 기능 추가 이외에도 보안 요소가 강화되고 있다. 초창기 Android OS는 보안 성숙도가 낮아 iOS에 비해 구조적인 운영체제 취약점이 자주 발견되었다. 특히 7.0미만 버전에서 취약점이 자주 발견되어 악성코드가 내장된 애플리케이션 상당수는 7.0미만을 타기팅하고 있다. 이후 보안 이슈를 해소하고자 구글은 M 개발자 버전 발표를 통해 애플리케이션 권한 분리 등을 적용해 무분별한 디바이스 접근 권한 사용을 제한하고 사용자 동의하에서 사용할 수 있도록 변경하였다.

이후 신규 Android OS가 공개될 때 마다 새로운 보안 모델 적용 및 고도화를 통해 9.0이하 버전에서 애플리케이션 프로세스 정보 접근, 네트워크 상태 정보 열람 등이 가능하던 문제로부터 해소되기도 하였다.

2) Android OS Architecture

[그림 2] Android OS 구조
[그림 3] Android OS 구조

Android OS 구조는 크게 6개의 영역으로 분류할 수 있으며, 리눅스 커널 계층, 하드웨어 추상화 계층, 네이티브 라이브러리 계층, 런타임 계층, 자바 API 프레임워크 계층, 애플리케이션 계층으로 나누어진다. 물론 모든 계층에 대해서 알고 있을 필요는 없다. 개발자들도 개발 시 OS 구조보다는 각 액티비티의 생명 주기를 더 비중 있게 다루기 때문이다. 하지만, 보안을 진단하는 우리의 입장에서는 관심 있게 봐야 할 계층이 있는데 바로 런타임 계층이다.

런타임 계층은 실행 중인 애플리케이션 코드들을 OS가 이해할 수 있도록 컴파일 해주는 기능을 가지고 있다. [그림 3]과 같이 Java 코드로 만들어진 애플리케이션을 가방 머신 위에서 동작 시키기 위해 Dex 파일로 변환하고 그 후 런타임 계층의 ART가 라이브러리를 통해 기계어 코드로 번역해 애플리케이션을 동작하게 된다.

여기서 중요한 것은 런타임 계층에서 바로 컴파일 과정을 수행한다는 것이다. Java 코드에서 실행 바이너리 코드가 포함된 Dex 파일까지 변환 과정이 이루어지는 점을 이용해 코드를 변조하거나 애플리케이션의 리소스를 빼내는 등의 작업이 가능해져 애플리케이션 복제, 무단 수정 등이 해당 컴파일 과정에서 이루어지는 것이다. 추가로 보안 진단 시에도 루팅, 디버깅 등의 탐지를 우회할 때도 해당 과정을 이용해 우회하게 된다.

3) Android OS 보안 모델

Android OS 보안 모델의 핵심은 권한 분리(Privilege Separation)와 최소 권한 원칙(Principle of Privilege)이다. 각 애플리케이션은 전용 보안 샌드박스에 설치되어 권한을 가진 리소스에만 접근할 수 있고 다른 애플리케이션에 영향을 주거나 데이터를 임의로 가져와 쓰기 등의 작업을 할 수 없다.

조금 더 자세히 말하자면 Android OS는 애플리케이션이 설치될 때 앱 ID라고 불리는 고유한 UID를 애플리케이션에 할당하게 되고 해당 UID로만 실행되는 전용 프로세스 내에서 앱을 실행시킨다. 즉, 각각의 애플리케이션들은 자신만이 읽고 쓸 수 있는 전용 디렉터리를 부여받는 것이다. 이를 통해 설치된 모든 애플리케이션들은 일반 사용자 권한으로 실행되며 프로세스 수준과 파일 수준에서 샌드박스 처리되어 보호된다.

03. 애플리케이션 보안 위협과 탐지 방안

1) 애플리케이션 무결성 및 데이터 위·변조

지금까지 Android OS에 대한 구조와 보안 모델에 대해 살펴보았다. 그렇다면 보안 모델만 있다면 Android에 설치된 애플리케이션이 안전할 수 있을까? 애석하게도 그렇지 않다. Android에 설치된 각 애플리케이션들은 자신만의 고유한 앱 ID를 할당받고 고정된 UID 값을 통해 애플리케이션을 실행하기 때문에 서로의 영역을 침범하지 않는다.

하지만 일부 조작을 통해 관리자 권한을 획득하고 특정 애플리케이션이 권한을 부여받으면 다른 애플리케이션에서 리소스의 읽고 쓰기 등의 작업이 가능해진다. 동작 중인 애플리케이션의 메모리 값을 수정해 사용자 임의로 데이터를 위·변조 할 수 있게 되고 관리자 권한을 가지고 있다면 특정 애플리케이션을 이용해 다른 애플리케이션의 코드를 무단으로 변조 및 수정할 수 있기 때문에 사전에 루팅(Rooting) 여부 검사가 필수적이다.

2) 루팅(Rooting)

루팅(Rooting)이란 공격자가 관리자(root) 권한을 획득하는 행위를 의미하며, 디바이스에서 루트 권환 획득 시 이론적으로 S/W로 가능한 모든 것을 할 수 있다. 예로 하드웨어 성능 조절, 벤더 사 기본 앱 삭제, 알림음 및 폰트 변경, 다양한 키 매핑 및 기능 추가 등이 있다. 루팅이 불법이라고 생각할 수도 있으나 Android OS는 오픈 소스 플랫폼이고 이것을 변경하는 것은 사용자의 의사에 따라 변경이 가능하기 때문에 불법이 아니다.

커스터마이징된 디바이스를 구현하여 사용자 편의성을 증가시킬 수 있는 루팅을 왜 제작사들이 불법으로 식별하고 탐지하는 걸까? 대표적인 이유는 루팅 시 애플리케이션의 데이터 및 코드를 무단으로 위·변조 할 수 있기 때문이다. 일례로 필자가 진단한 A사의 애플리케이션 경우 유료 구독 회원들만 이용할 수 있는 서비스가 존재했는데 간단한 코드 변조를 통해 쉽게 우회해 서비스 이용이 가능했으며 조건에 따라서 쿠폰 생성, 이벤트 중복 참여 등의 행위도 가능했다. 따라서, 애플리케이션의 데이터 및 코드 위·변조 방지를 위해서 반드시 루팅을 탐지하고 차단해야 한다.

3) 루팅 디바이스 탐지 방안

애플리케이션 실행 시 실행시킨 디바이스가 루팅된 디바이스인지 탐지하기 위한 방법은 다수 존재한다. 대표적으로 △ 디바이스 루팅 시 함께 설치되는 파일과 패키지 검사, △ 특정 명령어 실행 가능 여부 확인, △ 루팅 시 변조되는 boot 이미지 설정 값 확인, △ 실행 중인 앱 프로세스 확인 등이 있다.

다만, 이를 우회하기 위한 방법도 다양하기 때문에 개발자는 한 가지 탐지 방법만 채택해 적용하는 것이 아닌 다수의 기법들을 개발해 탐지해야 한다. 하나의 클래스에서 모든 탐지 로직들을 구현하지 말고 여럿 클래스에 나눠서 구현해야 하며 네이티브 코드를 추가할 수도 있다. 디컴파일(Decompile)이나 리버스 엔지니어링(Reverse Engineering) 등 역분석 기술을 통해 쉽게 코드 분석이 가능해 질 수 있기 때문에 애플리케이션 배포 시 반드시 난독화 모듈을 적용해야 한다. 또한 운영 여건에 따라서 보안 에이전트 및 MDM 등의 보안 솔루션 적용도 고려할 수 있다.

4) 애플리케이션 무결성 탐지 방안

애플리케이션의 데이터 및 코드를 위·변조 하는 방법에는 크게 2가지가 있다. 첫 번째는 실행 중인 애플리케이션 메모리 값을 수정해 임의의 데이터로 덮어 씌우는 방법이고 두 번째는 애플리케이션 실행 파일을 디컴파일 후 소스코드(어셈블리)를 변조해 다시 실행 파일로 컴파일하는 방법이다.

첫 번째 방법은 후킹 도구를 통해서 함수를 후킹(Hook)해 반환 값을 변조하거나 기존 함수를 새롭게 재 작성 하는 방식이 대표적이며, 두 번째 방법은 이전 챕터인 Android OS Architecture에서 설명했던 Dex 파일을 디컴파일해 추출되는 smali 코드를 변조해 다시 실행 파일로 컴파일 하는 것이다. 이와 같이 기존 애플리케이션 파일을 디컴파일 후 다시 컴파일하는 일련의 과정을 리패키징(Re-Packaging) 또는 리빌딩(Re-Building)이라고 한다. 첫 번째 방법은 실행 중인 애플리케이션의 데이터를 위·변조하는 것이기 때문에 무결성 침해라고 보기는 힘들다.

다만, 두 번째 방법의 경우는 직접 애플리케이션 코드를 위·변조하는 것이기 때문에 무결성이 침해된다고 볼 수 있다. 첫 번째 방법의 경우 위에서 언급했던 루팅 디바이스 탐지와 후킹 도구를 탐지해 차단할 경우 어느정도 예방이 가능하지만 두 번째 방법의 경우는 이것과는 관련이 없어 별도의 무결성 탐지 방법을 사용해야 한다.

예로 애플리케이션 Hash key 검증, 리소스 Hash 검증, Google에서 제공하는 무결성 검증, Google Play Store와 One Store와 같은 지정된 마켓 외 사설 마켓에서 설치가 되었는지 검증하는 탐지 방법이 있고 애플리케이션 리소스를 암호화해 컴파일 시 리소스 파일을 읽어오지 못하게 만들어 리패키징 자체를 막는 방법도 있다. 추가적으로 위에서 언급한 것과 같이 운영 여건에 따라서, 보안 에이전트 및 MDM 등의 보안 솔루션 적용도 고려할 수 있다.

5) 그 외 탐지 방법론

지금까지 루팅 디바이스와 무결성 변조 탐지 방안에 대해 살펴봤다. 그렇다면 루팅 디바이스와 무결성 변조만 탐지하면 애플리케이션은 안전한 걸까? 아니다. 루팅 디바이스, 무결성 변조 탐지 외에도 위에서 잠시 언급한 후킹 도구 탐지, 디버깅 모드 탐지, 에뮬레이터 탐지, 피닝(Pinning) 기법 등을 운영 환경에 맞춰 추가 적용해야 하며, 이에 대해서는 뒤에 챕터에서 왜 탐지해야 하고, 어떤 방식으로 탐지할 수 있는지 실습을 통해 자세히 다루도록 하겠다.

04. Android 보안 위협 탐지 및 우회 방안

1) ANDITER 소개

[그림 4] ANDITER 로고

이번 챕터부터는 필자가 제작한 ANDITER 애플리케이션을 통해 루팅, 디버깅, 프리다 등 다양한 탐지 방법을 배우고 또 이를 어떤 방안을 통해 우회할 수 있는지 알아보는 시간을 가질 것이다. ANDITER에 대해 간략히 소개하자면 실제 애플리케이션에서 사용되는 8가지 종류의 탐지 기법들을 학습하고 이를 우회하는 방안도 익힐 수 있는 모바일 모의 침투 테스팅 도구이다.

ANDITER 애플리케이션은 아래의 GitHub 주소에서 다운로드할수 있으며, 난독화 적용 버전(Encrypt)과 난독화 미 적용 버전(NoEncrypt)을 제공하고 있어 소스코드에 난독화 알고리즘을 적용했을 때와 적용하지 않았을 때의 차이점을 학습할 수 있다. 해당 문서에서는 난독화 미 적용 버전으로 실습이 진행된다 본 시리즈는 총 4편으로 진행되며 본편인 Part1에서 루팅 탐지 및 우회 방법 등에 대해서 살펴보고자 한다.

* GitHub Anditer : https://github.com/naroSEC/Anditer

2) 사전 준비

실습을 진행하기에 앞서 해당 문서에서 사용된 실습 환경과 도구는 아래와 같다.

[표 2] 실습에 사용된 디바이스 사양
[표 3] 실습에 사용된 도구

실습에서 사용된 휴대용 디바이스는 총 2대로 Android OS 11.0 버전과 9.0 버전이다. Nox는 Android용 에뮬레이터로 뒤에 실습하게 될 에뮬레이터 탐지 항목에서 사용될 예정이다. 실습을 진행하기 위해서는 반드시 루팅 디바이스가 필요하며, 디바이스가 존재하지 않을 경우 Nox와 같은 가상 에뮬레이터를 사용해도 무방하다. 사용된 도구로는 소스코드 분석을 위한 디컴파일러 도구인 JEB 디컴파일러, 애플리케이션 리패키징 시 사용할 APK Easy Tool과 후킹 도구인 Frida이다. 각 도구들은 다운로드 주소 링크를 통해 다운로드할 수 있으며 추가적으로 필요한 도구는 실습 챕터에서 소개하도록 하겠다.

3) 루팅

3.1) Bypass Packages

[그림 5] ANDITER 실행 화면

ANDITER 애플리케이션 실행 시 [그림 5]와 같은 화면이 출력된다. 상단 탭에는 루팅, 디버깅, 에뮬레이터, 프리다, 피닝, 무결성, 지문 항목 선택이 가능하고 각 종류에 해당하는 탐지 항목들을 선택해 탐지 여부를 확인할 수 있으며, 항목 클릭 시 해당 항목에 대한 간략한 설명도 볼 수 있다.

먼저 살펴볼 항목은 Bypass Packages이다. 디바이스 루팅 시 사용된 응용 프로그램 종류에 따라 다르겠지만, 디바이스 내부 저장소에는 특정 패키지들이 설치된다. 설치된 패키지들은 특정 동작 수행을 위해 관리자 권한을 획득하거나 또는 디바이스 정보를 변경하는 데 사용되며, 설치되는 대표적인 패키지들로는 com.topjohnwu.magisk, com.kingo.root 등이 있다.

Bypass Packages 항목은 디바이스에 설치된 애플리케이션 패키지 목록을 검사해 루팅 시 설치되는 알려진 패키지 목록들이 존재하는지 검사하고 패키지 존재 시 루팅 디바이스로 판단해 탐지하게 된다. 추가로 루팅 시 사용된 응용 프로그램 종류에 따라 설치되는 패키지 항목들이 다 상이하기 때문에 탐지가 안될 수도 있다. 원활한 실습을 위해 탐지를 원하는 경우 ANDITER를 다운로드했던 GitHub에서 루팅 탐지용 애플리케이션을 설치하면 된다.

[그림 6] Bypass Packages 항목 탐지

루팅 탐지용 애플리케이션 설치 후 Bypass Packages 항목의 탐지 여부를 체크해 보면 Fail이란 문자열이 출력되며, 탐지가 된 것을 볼 수 있다. JEB 도구를 통해 직접 애플리케이션을 디컴파일해 코드를 확인하고 어떤 방식으로 탐지하는지 확인해 보도록 하겠다.

[그림 7] 루팅 검사 시 사용되는 패키지 목록
[그림 8] isCheckRootingInstalled() 함수 소스코드

[그림 8]의 isCheckRootingInstalled() 함수는 Bypass Packages 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 ①에서 [그림 7]의 rootingPackages에서 패키지 목록을 가져오고 있으며 ② getPackagesInfoCompat() 함수를 통해 가져온 패키지 정보를 반환받게 된다. 패키지 정보가 있을 경우 디바이스에 해당 패키지가 설치되어 있는 것으로 판단해 탐지하게 된다.

Bypass Packages 탐지를 우회하기 위한 포인트는 두 가지가 있다. 첫 번째는 [그림 7]의 검사하는 패키지 목록을 가지고 있는 rootingPackages 리스트 요소를 더미 값으로 변조하는 방법이고 두 번째는 isCheckRootingInstalled() 함수에서 사용되는 While문의 조건식을 변조해 While 구문이 동작되지 않도록 만드는 방법이다. 여기서는 리패키징 방식으로 두 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.

[그림 9] APK Easy Tool 디컴파일

코드 수정을 위해서는 classes.dex 파일이 필요하다. Dex 파일에는 Android 런타임에서 애플리케이션 실행에 필요한 코드가 포함되어 있으며, 애플리케이션 실행 파일인 apk 파일을 압축 해제하면 확인 할 수 있다. 하지만, Dex 파일의 경우 기계어로 작성되어 있어 사람이 읽고 또 그것을 수정하기에는 무리가 있다. 따라서, Dex 파일을 사람이 읽을 수 있도록 표현한 언어인 smali 코드로 변환해줘야 한다.

[그림 9]의 APK Easy Tool은 Dex 파일을 smali 코드로 변환해 주는 도구이다. 원래는 apktool이라는 도구의 콘솔 명령어를 이용해 직접 디컴파일 해줘야 하지만 APK Easy Tool을 사용한다면 클릭 한 번으로 쉽게 디컴파일된 smali 코드를 추출할 수 있다. 또한, APK Easy Tool은 디컴파일 기능 외에도 컴파일 기능과 자동 사이닝 기능이 있어 리패키징 과정을 빠르고 간편하게 만들어준다. 사용 방법은 Browse 버튼을 클릭해 추출을 원하는 apk 파일을 선택해 주고 Decompile 버튼을 클릭하면 된다.

[그림 10] Apk Easy Tool 디컴파일 산출물

디컴파일이 완료 되었다면 [그림 10]과 같은 산출물이 도출되고 이 중 smali 디렉터리를 확인해보면 추출된 smali 코드를 패키지 별로 확인할 수 있다.

smali 코드 추출이 완료되었다면 텍스트 편집기를 통해 코드를 수정하면 된다. 그런데 여기서 문제가 하나 있는데 smali 코드는 패키지, 클래스, 함수 별로 존재하고 있어 수정하려는 함수가 포함되어 있는 코드가 어떤 코드인지 찾기가 쉽지 않다는 것이다. 이를 위해 JEB 도구에서는 강력한 기능을 제공하고 있는데 바로 smali 코드와 Java 코드의 매핑 기능이다.

[그림 11] JEB 매핑 기능

사용 방법은 간단하다. 사용자가 원하는 Java 코드 영역에 마우스를 클릭하고 Tab 버튼을 누르거나 마우스 오른쪽 버튼 클릭 후 출력되는 팝업 창에서 디컴파일 버튼을 클릭하면 된다. 반대로 smali 코드에서 Java 코드 매핑도 가능하다.

[그림 11]은 isCheckRootingInstalled() 함수명을 smali 코드랑 매핑한 결과이다. 추가로 코드에 마우스를 가져다 대고 잠시 기다리면 매개변수, 반환 값 등 코드에 대한 간략한 설명이 나오는데 그 중 Decscriptor에는 smali 코드 경로가 표시되어 있다. 해당 기능을 사용하지 않고도 패키지 명과 함수 명을 통해서 smali 코드를 찾을 수 있으나, 난독화 모듈이 적용된 코드에서는 함수명이 a,b,c 알파벳 또는 숫자 등의 의미 없는 문자열로 지정되어 있어 찾는데 시간이 오래 소요되기 때문에 위 기능을 활용할 경우 분석 시간을 단축 시킬 수 있다.

[그림 12] isCheckRootingInstalled() 함수의 smali 코드와 Java 코드 매핑

smali 코드 위치가 확인이 되었다면 smali 코드에서 변조 포인트를 찾아야 한다. JEB 도구로 돌아와 [그림 12]의 매핑된 코드를 살펴보면 ① smali 코드 구문이 Java 소스코드의 while문 조건 구문이며 v4, v2 레지스터리에 저장된 값을 비교해 v3 값이 v1 보다 클 경우 38로 분기된다. 반대로 v3 값이 v1 보다 작을 경우 0x00000012 ~ 0x00000028 주소까지 구문들이 동작해 패키지 검사를 하게 된다. ② 분기문 38의 smali 코드를 살펴보면 함수를 호출한 곳으로 false 값이 반환된다. 따라서, 첫 번째 smali 코드 구문의 조건문을 참으로 만들어 분기문 38로 분기시킨 다음 false 값을 반환할 수 있도록 변조하면 된다.

[그림 13] isCheckRootingInstalled() 함수의 smali 코드
[표 4] smali 코드의 if 문법

[그림 13]의 표시된 구문이 [그림 12]에서 확인한 while문 조건 구문이다. 탐지 우회를 위해서는 해당 조건 구문을 참으로 만들어야 하기 때문에 [표 4]를 참고해 조건식을 수정해주면 된다. 여기서는 if-ge를 if-le로 수정했다.

[그림 14] APK Easy Tool 컴파일

samli 코드 수정이 완료되었다면 실행 파일인 apk 파일로 다시 만들어줘야 한다. smali 코드를 apk 파일로 만들기 위해서는 smali 코드를 컴파일해 Dex 파일로 변환하고 사이닝 및 리빌딩 과정을 거쳐야 한다. 이러한 일련의 과정들은 손도 많이 가고 각각의 과정에서 그에 맞는 도구들을 사용해야 하지만, APK Easy Tool를 사용하면 원 클릭으로 smali 코드를 다시 apk 파일로 리패키징 할 수 있다. 디컴파일 했던 apk 디렉터리를 다른 곳으로 이동시키지 않았다면 [그림 14]와 같이 Compile 버튼을 클릭하면 자동으로 컴파일 되어 apk 파일로 만들어진다.

[그림 15] Bypass Packages 탐지 우회 성공

컴파일 과정이 끝났다면 산출물인 apk 파일을 설치 후 ANDITER에서 Bypass Packages 항목을 체크하면 Success!가 출력되며 탐지가 우회된 것을 볼 수 있다.

3.2) Bypass Binaries

[그림 16] Bypass Binaries 탐지

디바이스 루팅 시 사용된 루팅 패치 프로그램에 따라서 상이할 수 있지만 관리자 권한을 획득하기 위한 명령어 파일인 su와 UNIX 명령행 도구들을 제공하는 busybox, 루팅 관리 매니저 등이 함께 설치된다. 대표적인 설치 파일들로는 su, busybox, magisk, supersu, daemonsu 등이 있다.

Bypass Binaries 항목은 이와 같이 루팅 시 설치되는 바이너리 파일들을 검사해 파일이 존재할 경우 루팅 디바이스로 판단해 탐지하게 된다.

이전 [Bypass Packages] 탐지 파트에서는 실제 런타임 코드를 수정하고 리패키징 과정을 거쳐 탐지를 우회했었다. 다만, 코드 변조 방법은 직접 코드를 수정하기 때문에 애플리케이션의 무결성을 해치게 된다. 실습하고 있는 ANDITER의 경우 애플리케이션 실행 시 별도의 무결성 변조 탐지가 동작하지 않아 문제없지만, 상용 애플리케이션의 경우 무결성 변조 탐지가 이루어지기 때문에 추가적인 탐지 우회가 필요하다. 하지만, 후킹 도구인 Frida를 사용한다면 무결성을 해치지 않고도 코드 수정이 가능하다. 이번 Bypass Packages 탐지 항목에서는 Frida 도구를 사용해 탐지를 우회해 보도록 하겠다.

Frida는 Windows, macOS, Linux, Android, iOS QNX 기반의 네이티브 애플리케이션을 후킹 할 수 있도록 기능을 제공하는 파이썬(Python) 라이브러리이다. 대표적으로 Android, iOS 모바일 플랫폼에서 많이 사용되며, Frida 스크립트 작성 시 Js, C, Swift 등 여러 언어에 대한 API를 지원한다. Frida에 대한 상세 기능과 사용법은 Frida 공식 문서(https://frida.re/)에 잘 설명되어 있어 해당 문서에서는 이에 대해 별도로 다루지 않는다.

[그림 17] 루팅 검사 시 사용되는 시스템 디렉터리 및 파일 목록
[그림 18] isCheckRootingBinary() 함수 소스코드

[그림 18]의 isCheckRootingBinary() 함수는 Bypass Binaries 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 ① rootingPath에서 시스템 디렉터리 목록을 가져오고 있으며 ②에서는 루팅 시 설치되는 파일 목록을 가져오고 있다. ③에서는 ①에서 가져온 디렉터리에 ② 루팅 시 설치되는 파일이 존재하는지 검사한다. 이때, 파일이 디바이스에 존재하는지 확인하는 함수가 File 클래스의 exists() 함수이며 파일이 존재할 경우 true를 반환하고 없을 경우 false를 반환한다.

대략적인 코드 분석이 끝이 났다면, Frida 도구를 통해 후킹 해야 할 공격포인트를 찾아야 한다. 해당 코드에서 탐지를 우회하기 위한 후킹 포인트는 세 가지가 있다.

아래와 같은 세 가지 후킹 포인트가 존재하며, 여기서는 세 번째 File 클래스의 exists() 함수 후킹 방법을 사용하겠다. 첫 번째는 함수 결과와 상관없이 무조건 false를 반환하도록 isCheckRootingBinary() 함수를 재 작성하는 방법이고 두 번째는 File 클래스 객체 생성 시 전달되는 인수 값인 rootingPath, rootingBinaries를 더미 값으로 변조하는 방법이다. 그리고 세 번째는 File 클래스의 exists() 함수를 후킹해 반환 값을 변조하는 방법이다. 여기서는 세 번째 방법을 사용해 탐지를 우회해 하도록 하겠다.

[그림 19] exists() 함수를 후킹하기 위한 Frida 스크립트

[그림 19]는 exists() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ① isCheckRootingBinary() 함수에서 루팅 시 설치되는 파일 목록인 rootBinaries 값을 그대로 가지고 왔으며 ②에서는 exists() 사용을 위해 File 클래스 객체를 반환받고 Frida 자바 스크립트 API인 implementatio을 사용해 함수를 재 작성했다. ③에서는 exists() 함수 호출 시 호출하는 객체의 파일명을 가져와 ①의 rootBinaries 목록에 포함되어 있을 경우 false 반환하고 포함되어 있지 않다면 원본 exists() 함수를 호출해 그 결괏값을 반환한다. 즉, ①에서 선언된 파일을 검사하려는 경우 무조건 false를 반환하고 그 외에는 정상 결과를 반환하게 된다. 추가로 더 다양한 Frida 코드를 보고 싶다면 Frida Code Share(https://codeshare.frida.re/)를 참고하기 바란다.

[그림 20] Bypass Binaries 우회 성공

Frida를 통해 작성한 스크립트를 ADITER 애플케이션에 어태치 한다. 그 후 Bypass Binaries탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.

3.3) Bypass Command Execution

[그림 21] Bypass Command Execution 탐지

모든 OS에는 운영체제가 어떤 프로세스를 실행시킬 때 경로를 참조할 수 있도록 PATH 환경 변수가 제공된다. 이는 리눅스 커널 기반인 Android OS도 동일하며 콘솔 단에서 명령어 사용 시 시스템 환경 변수에 등록된 PATH를 참조해 명령어를 찾고 실행시키게 된다. 그리고 디바이스 루팅 시 설치되는 su 명령어도 시스템 환경 변수에 등록되어 호출 위치와 상관없이 명령어 사용이 가능해진다.

Bypass Command Execution 탐지 항목에서는 이러한 특징을 이용해 which 명령어를 통해 PATH 변수에 등록된 디렉터리 경로에 su 명령어가 존재하는지 검사하고 명령어 존재 시 루팅 디바이스로 판단해 탐지하게 된다. 추가로 which 명령어는 특정 명령어를 찾아주는 기능을 한다.

[그림 22] isCheckRootingExec() 함수 소스코드

[그림 22]의 isCheckRootingExec() 함수는 Bypass Command Execution 탐지 결과를 반환해 주는 역할을 한다. 코드를 살펴보면 Runtime.getRuntime().exec() 함수가 사용된 것을 볼 수 있는데 Runtime.exec()는 Windows, Linux, Unix 등의 시스템 명령어를 Java를 통해 실행한 후 그 결과를 받아오는 기능을 가지고 있다. 위 코드에서는 which와 su 를 인수 값으로 실행 결과를 받아와 su 명령어가 해당 디바이스에 존재하는지 검사한다.

이번 파트부터는 별도의 언급이 없는 이상 Frida를 통해 탐지 우회를 진행한다. Bypass Module 탐지를 우회하기 위한 후킹 포인트는 두 가지가 있다. 첫 번째는 함수 결과 상관없이 무조건 false를 반환하도록 isCheckRootingExec() 함수를 재 작성하는 방법이고 두 번째는 Runtime 클래스의 exec() 함수 호출 시 전달되는 인수 값을 더미 값으로 변조하는 방법이다. 여기서는 두 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.

[그림 23] Runtime.exec() 함수를 후킹하기 위한 Frida 스크립트

[그림 23]은 Runtime클래스의 exec() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ① exec() 함수 사용을 위해 Runtime 클래스 객체를 반환받고 isCheckRootingExec()에서 exec() 함수 호출 시 전달되는 인수의 데이터 타입에 맞춰 exec() 함수를 오버로딩으로 구현했다. ②에서는 함수로 들어오는 매개 변수 중 su 문자열이 포함되어 있을 경우 더미 값으로 변조하고 ③ 변조한 더미 값을 인수로 원본 exec() 함수를 호출해 결과 값을 반환하게 된다.

[그림 24] Bypass Command Execution 탐지 우회 성공

Frida를 통해 작성한 스크립트를 ADITER 애플케이션에 어태치 한다. 그 후 Bypass Command Execution 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.

3.4) Bypass Build-Tags

[그림 25] Bypass Build-Tags 탐지

루팅 패치 프로그램을 통해 디바이스 루팅을 진행할 때 해당 프로그램은 디바이스의 boot 이미지 설정값들을 수정하게 된다. 그리고 변경된 수정 사항들은 Android 시스템 파일인 build.prop에 반영되어 디바이스 부팅 시 참조하게 된다. Bypass Build-Tags 탐지 항목은 Android 시스템 파일인 build.prop 파일의 시스템 속성 값을 검사하고 비정상 값 존재 시 루팅 디바이스로 판단해 탐지하게 된다.

[그림 26] build.prop 시스템 속성 확인

일반적으로 바뀌는 시스템 속성 값은 ro.build.type, ro.build.tags, ro.build.display.id, ro.build.description, ro.build.fingerprint 등이 있으며, 디바이스 콘솔에서 getprop 명령어를 통해 확인이 가능하다.

[그림 27] isCheckRootingKeys() 함수 소스코드

[그림 27]의 isCheckRootingKeys() 함수는 Bypass Module 탐지 결과를 반환해 주는 역할을 한다. 코드를 살펴보면 위에서 언급한 시스템 속성 값인 ro.build.type, ro.build.keys 등이 getSystemProperty 클래스의 prop() 함수 호출 시 인수 값으로 사용되고 있으며, 결과 반환 값은 arrayList0 리스트 변수에 저장된다. 그 후 contains() 함수를 통해 반환 값 중 test-keys 와 일치하는 문자열이 있는지 검사하게 된다.

[그림 28] getSystemProperty 클래스 소스코드

isCheckRootingKeys() 함수에서 사용된 getSystemProperty 클래스를 살펴보면 prop() 함수 하나만 존재하고 있고, 매개 변수로 들어온 문자열은 Class 클래스를 통해 로드된 클래스의 함수 인수 값으로 사용되며 해당하는 시스템 속성 값을 반환한다

Bypass Module 탐지를 우회하기 위한 후킹 포인트는 두 가지가 있다. 첫 번째는 함수 결과 상관없이 무조건 false를 반환하도록 isCheckRootingKeys() 함수를 재 작성하는 방법이고 두 번째는 prop 함수 호출 시 인수 값으로 전달되는 문자열을 변조하는 방법이다. 여기서는 두 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.

[그림 29] systemProperty 클래스의 prop() 함수를 후킹하기 위한 Frida 스크립트

[그림 29]는 systemPropertyClass의 prop() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ① isCheckRootingKeys() 함수에서 prop() 함수 호출 시 전달 했던 시스템 속성을 그대로 가져와 리스트로 선언했고 ②에서는 prop() 함수 사용을 위해 getSystemProperty 클래스 객체를 반환 받았다. ③ 에서는 prop() 함수로 들어오는 매개 변숫값이 ①에서 선언한 리스트에 포함되어 있을 경우 더미 값으로 변조하고 ④ 변조한 더미 값을 인수로 원본 prop 함수를 호출해 결괏값을 반환한다.

[그림 30] Bypass Build-Tags 우회 성공

Frida를 통해 작성한 스크립트를 ADITER 애플케이션에 어태치 한다. 그 후 Bypass Build-Tags 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.

3.5) Bypass Writeable

[그림 31] Bypass Writeable 탐지
[표 5] 시스템 설정 디렉터리

디바이스 루팅 시 설치된 루팅 관련 애플리케이션 또는 응용 프로그램을 통해서 디바이스의 주요 설정을 변경할 수 있게 되며, 설정 변경 시 디바이스 내부 저장소에 위치한 시스템 디렉터리에 일반 사용자 쓰기 권한을 부여하는 경우가 있다. Bypass Writeable 탐지 항목은 디바이스의 시스템 디렉터리의 권한을 검사하고 일반 사용자 권한에 쓰기 권한이 부여되어 있을 경우 루팅 디바이스로 판단해 탐지하게 된다.

디바이스 벤더사와 Android OS의 Linux 커널 버전에 따라 설정 파일이 위치한 디렉터리가 상이할 수 있지만 일반적으로 디바이스 시스템 설정 파일들이 위치한 디렉터리는 [표 5]와 같다. 참고로 Bypass Writeable 탐지 항목의 경우 사용 디바이스에 따라 탐지되는 사람이 있고 탐지되지 않는 사람이 있을 것이다. 필자의 경우도 항목에 탐지되기 위해 별도의 애플리케이션을 통해서 설정을 변경했다. 원활한 실습 진행을 위해 탐지를 원하는 경우 Android 디버그 브리지를 통해 설정을 변경할 수 있다.

[그림 32] /vendor/bin 디렉터리 권한 부여

Android 디버그 브리지는 Android 디바이스와 통신하며, 디버깅 등의 작업과 애플리케이션 설치, 디바이스 설정 확인, 시스템 log 출력 등의 작업을 할 수 있도록 지원하는 다목적 명령줄 도구이다.
또한, 로컬 PC에서 디바이스 접속 기능을 지원해 USB만 연결되어 있다면 바로 사용이 가능하다. 여기서는 기능을 이용해 Bypass Writeable 항목에서 탐지하고 있는 디렉터리 권한을 위 [그림 32]와 같이 변경해 주겠다. Android 디버그 브리지 파일은 https://developer.android.com/studio/releases/platform-tools?hl=ko 링크에서 다운로드 가능하다.

설치가 되었다면, Android 디버그 브리지 파일이 위치한 곳으로 이동 후 아래와 같이 진행하면 된다.


① 콘솔 창에서 디바이스 진입을 위해 adb shell 명령어를 입력해 준다.
② 시스템 설정 파일들이 위치한 중요 디렉터리들은 일반 사용자에게 변경 권한이 존재하지 않아 관리자 권한이 필요하다. 따라서, su 명령어를 사용해 관리자 권한을 획득해 준다.
③ chmod 명령어를 사용해 /vendor/bin 디렉터리의 권한을 변경한다.
④ 권한 변경이 정상적으로 되었는지 ls 명령어를 통해 확인한다.
⑤ 권한 변경이 되었다면 위 [그림 32]와 같이 출력되며 Bypass Writeable 탐지 항목을 체크해보면 루팅 디바이스로 탐지될 것이다.

[그림 33] 디렉터리 권한 검사 시 사용되는 writeAbleFiles

이제 Bypass Writeables 탐지 항목의 소스코드를 살펴보겠다. 우리가 먼저 눈여겨봐야 할 것은 writeAbleFiles 리스트 변수다. 해당 변수는 시스템 설정 파일이 위치한 디렉터리 목록들이 저장되어 있다. 그중에는 우리가 Android 디버그 브리지를 통해 권한을 변경한 /vendor/bin 디렉터리도 포함되어 있다.

[그림 34] isCheckRWFile() 함수 소스코드

Bypass Writeable 항목의 탐지 결과를 반환하는 함수는 isCheckRWFile() 함수로 위에서 확인한 writeAbleFiles 변수가 ①에서 사용되고 있는 것을 볼 수 있다. 그리고 반복문을 통해서 writeAbleFiles 변수에 저장된 디렉터리를 하나씩 확인한다. 이때, ②에서 Files 클래스를 사용해 권한을 확인하는데 파일의 권한을 가져오는 함수가 바로 getPosixFilepermissions() 함수이다. 반환되는 권한 값에서 OTHERS_WRITE 문자열이 존재할 경우 루팅 디바이스로 판단해 탐지하게 된다.

Bypass Writeable 탐지를 우회하기 위한 후킹 포인트는 세 가지가 있다. 첫 번째는 함수 결과 상관없이 무조건 false를 반환하도록 isCheckRWFile() 함수를 재 작성하는 방법이고 두 번째는 getPosixFilePermissions() 함수 호출 시 전달되는 인수 값을 더미 값으로 변조하는 방법이다. 세 번째는 디렉터리 객체를 반환 받기 위해 사용되는 Paths 클래스의 get() 함수를 후킹해 함수로 들어오는 매개 변수 값을 더미 값으로 변조하는 방법이다. 여기서는 세 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.

[그림 35] Paths 클래스의 get() 함수를 후킹하기 위한 Frida 스크립트

[그림 35]는 Paths 클래스의 get() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 Paths 클래스 사용을 위해 클래스 객체를 반환 받고 isCheckRWFile() 함수에서 get() 함수 호출 시 전달되는 인수의 데이터 타입에 맞춰 get() 함수를 오버로딩으로 구현한다. ②에서는 함수로 들어오는 매개 변숫값을 더미 값으로 변조하고 변조한 더미 값을 인수로 원본 get() 함수를 호출해 결괏값을 반환한다.

[그림 36] Bypass Writeable 우회 성공

Frida를 통해 작성한 스크립트를 ADITER 애플케이션에 어태치 한다. 그 후 Bypass Writeable탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.

3.6) Bypass System Property

[그림 37] Bypass System Property 탐지

루팅 패치 프로그램을 통한 디바이스 루팅 방식은 크게 두 가지로 나눠진다. 첫 번째는 리커버리(복구 모드)를 통한 커스텀 바이너리 패치 방식이고 두 번째는 Android OS 초기 버전에서 사용되던 방법으로 default.prop 파일의 ro.secure 값을 변경해 루팅 커널 이미지를 제작하는 방식이다. 이 경우 루팅을 진행하지 않은 순정 디바이스 경우 시스템 속성인 ro.secure와 ro.adb.secure 값이 1인 반면에 루팅 디바이스는 1외의 값으로 설정된다. Bypass System Property 탐지 항목은 디바이스 시스템 속성을 검사해 ro.secure, ro.adb.secure 값이 1이 아닐 경우 루팅 디바이스로 판단해 탐지하게 된다.

[그림 38] ro.secure, ro.adb.secure 값 확인

ro.secure, ro.adb.secure 값은 디바이스 콘솔에서 getprop 명령어를 통해 확인이 가능하며 사용하는 디바이스 또는 루팅 패치 방식에 따라서, ro.secure, ro.adb.secure 값이 1인 사람이 있고, 0인 사람이 있을 것이다. 예전 방식의 루팅 패치 또는 낮은 버전의 Android OS를 사용하는 경우 0일 가능성이 있다. 원활한 실습을 위해 탐지를 원하는 경우 두 가지 방법을 통해서 시스템 속성 값을 변경할 수 있다.

첫 번째 방법은 Android 9.0 이하에서 사용 가능한 방법이다. Android OS에는 build.prop 설정을 변경하기 위한 setprop이라는 명령어가 존재하는데 현재는 Android OS 보안 정책에 의해 해당 명령어를 사용하더라도 값이 변경되지 않는다. 하지만, 별도의 바이너리 패치를 적용하면 setprop 명령어를 통해서 속성 값 변경이 가능해진다. 패치 파일은 아래의 https://github.com/jedy/mprop 링크를 통해 다운로드 및 사용법을 참고하면 된다. 추가로 시스템 속성 값을 변경하기 앞서 주의할 점이 있는데 ro.secure 값이 1이 아닌 다른 값으로 변경될 경우 USB 인식, 부팅 관련 문제가 발생할 수 있다. 따라서, 값 변경 시 ro.secure 값은 변경하지 않고 ro.adb.secure 값만 0으로 변경한다.

두 번째 방법은 Android 5.0 이상에서 사용 가능하며, Magisk를 이용한 방법이다. Magisk는 https://github.com/topjohnwu/Magisk링크에서 Github에서 다운로드해 설치하면 된다.

[그림 39] Magisk props 실행 초기 화면

Magisk 설치가 완료되었다면, 디바이스에 접속해 props 명령을 실행한다. 그러면 [그림 39]와 같은 화면이 출력되며 5번을 선택해 시스템 설정 페이지로 이동한다.

[그림 40] Magisk 시스템 설정 페이지

새로운 시스템 설정을 위해 n을 선택해 [그림 40]과 같이 New custom prop 설정 페이지로 이동해 준다.

[그림 41] 시스템 속성 입력 화면

변경을 원하는 시스템 속성 이름은 [그림 41]과 같이 입력한다. 여기서는 ro.adb.secure를 입력해 주면 된다.

[그림 42] 시스템 속성 값 변경 화면

위 [그림 42]와 같이 확인 페이지가 출력되면 y를 선택하고, 변경을 원하는 값을 입력해 주면 된다. ro.adb.secure 값을 0으로 변경해 줘야 하기 때문에 여기서는 0을 입력하면 된다. 그 후 디바이스 재 부팅이 진행되고 변경된 값이 시스템 설정에 적용된다.

[그림 43] isCheckForProps() 함수 소스코드

[그림 43]의 isCheckForProps() 함수는 Bypass System Property 탐지 결과를 반환해 주는 역할을 한다. 먼저 ①을 보면 이전 Bypass Build-Tags 항목에서 사용됐던 getSystemProperty 클래스의 prop() 함수가 사용되는 것을 볼 수 있다. 해당 함수는 매개 변수로 들어오는 문자열에 해당하는 시스템 설정값을 반환해 주는 기능을 가지고 있으며, 여기서는 ro.secure, ro.adb.secure 속성 값을 반환해 s, s1 변수에 저장하고 ②에서 저장된 값이 0인지 equals() 함수를 통해 검증해 0이라면 루팅 디바이스로 탐지하게 된다.

Bypass System Property 탐지를 우회하기 위한 후킹 포인트는 세 가지가 있다. 첫 번째는 함수 결과 상관없이 무조건 false를 반환하도록 isCheckForProps() 함수를 재 작성하는 방법이고 두 번째는 Bypass Build-Tags 항목을 우회하기 위해 사용했던 방법과 동일하게 getSystemProperty 클래스의 prop() 함수로 들어오는 매개 변수를 더미 값으로 변조하는 방법이다. 그리고 세 번째는 equals() 함수를 후킹해 결괏값을 false로 반환하게 만드는 방법이다. 여기서는 첫 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.

[그림 44] isCheckForProps() 함수를 후킹하기 위한 Frida 스크립트

[그림 44]는 isCheckForProps() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. ①에서는 isCheckForProps 함수 사용을 위해 RootingDectector 클래스 객체를 반환받고 ②에서는 ①에서 반환받은 RootingDectector 클래스의 isCheckForProps() 함수를 implementation을 사용해 함수 호출 시 무조건 false를 반환하도록 재 작성했다. Bypass System Property 탐지 항목을 우회하기 위한 코드는 위 세 줄의 코드가 전부이며 이전 탐지 항목들도 위와 같이 코드 작성 시 우회가 가능하다.

다만, 이 방법은 사용 시 주의가 필요한데 함수 재 작성 시 원본 함수의 데이터 반환 타입을 맞춰줘야 하고 또한, 해당 함수에서 다른 액티비로 데이터를 전송하거나 Context 정보를 수정하는 경우에도 동일한 기능을 수행할 수 있도록 코드를 작성해야 한다. 그렇지 않고 단순히 false 또는 true 값만 반환하게 될 경우 애플리케이션은 높은 확률로 오류가 발생해 종료된다. 따라서, 위 방법 사용 시 사전에 재 작성하려는 함수에 대한 정밀 분석이 이뤄져야 한다.

[그림 45] Bypass System Property 우회 성공

Frida를 통해 작성한 스크립트를 ADITER 애플케이션에 어태치 한다. 그 후 Bypass System Property 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.

3.7) Bypass Check Process

[그림 46] Bypass Check Process 탐지

애플리케이션은 크게 두 가지로 구분되어 나눠진다. 쇼핑몰, 은행, 학교 행정 업무 등과 같은 사용자 편의성을 위해 만들어진 애플리케이션과 관리자 권한을 획득해 시스템 설정 변경, 데이터 위 변조 등 분석 및 악의적인 목적을 가지고 만들어진 애플리케이션이다. 그리고 후자의 경우 대부분이 관리자 권한을 필요로 하는 루팅 전용 애플리케이션들로 Magisk Manager, MT Manager, Game Guardian 등이 있으며, 이들은 백그라운드 프로세스에서 동작하다가 필요시 사용자에 의해 호출된다.

Bypass Check Process 항목은 디바이스에서 실행 중인 프로세스 목록을 확인해 Magisk와 같은 특정 애플리케이션들이 동작 중인지 확인하고 동작 중이면 루팅 디바이스로 판단해 탐지하게 된다.

[그림 47] isCheckRootingProcesses() 함수 소스코드

[그림 47]의 isCheckRootingProcesses() 함수는 Bypass Check Process 탐지 결과를 반환해 주는 역할을 한다. 코드를 살펴보면 ①에서 ps –ef 문자열을 isRootCommand() 함수의 인수 값으로 전달해 그 결괏값을 반환받고 있다. ②에서는 contains() 함수를 통해 반환받은 결괏값이 magisk라는 문자열이 포함되어 있는지 검사하고 있으며, 해당 문자열을 포함하고 있다면, true를 반환해 루팅 디바이스로 탐지하게 된다. 참고로 코드에서 사용된 ps 명령어는 현재 실행 중인 프로세스 목록 및 상태를 출력해 준다.

[그림 48] isRootCommand() 함수 소스코드

[그림 48]은 isCheckRootingProcesses() 함수에서 ps 명령어 동작을 위해 사용됐던 isRootCommand() 함수의 코드이다. 코드를 살펴보면 조금 특이한 점이 있는데 바로 ①에서 사용된 su 명령어이다. su 명령어는 이전에도 언급했듯이 디바이스에서 일반 사용자가 관리자 권한을 획득하기 위해 사용하는 명령어로 주로 루팅 전용 애플리케이션들에서 사용된다. 해당 명령어가 여기서 사용된 이유는 프로세스 목록에 접근하기 위해서는 관리자 권한이 필요하기 때문이다. ②에서는 ①을 통해 획득한 su 권한을 가진 프로세스를 이용해 인수로 전달되는 명령어를 수행하게 된다.

[그림 49] 애플리케이션 권한으로 ps 명령어 수행 시 결과
[그림 50] 관리자 권한으로 ps 명령어 수행 시 결과

[그림 49]는 ANDITER 애플리케이션 권한으로 ps 명령어를 수행한 결과이고 [그림 50]은 관리자 권한으로 ps 명령어를 수행한 결과이다. 두 그림을 보면 알 수 있듯이 똑같은 명령어라도 어떤 권한을 가지고 동작되냐에 따라 결과가 달라진다. 이러한 결과가 나타나는 이유는 바로 Android 보안 모델 때문이다. 예전 Android 버전에서는 일반 사용자 권한이라도 프로세스 목록에 접근할 수 있었으며, isRootCommand() 함수와 같이 Runtime 클래스의 exec() 함수를 사용하지 않더라도 Java API를 통해 실행 중인 프로세스 상태 값 확인이 가능했다. 하지만, Android OS 버전이 업그레이드되면서 보안 모델도 함께 강화되어 이제는 일반 사용자 권한으로 확인이 불가해졌다. 때문에, 위 [그림 48] 코드에서 프로세스 목록 확인을 위해 su 명령어가 사용됐지만 상용 애플리케이션에서는 이와 같은 방법보다는 su 명령어 사용 가능 유무를 통해 루팅 디바이스를 탐지하는 경우가 더 많다.

Bypass Check Process 탐지를 우회하기 위한 후킹 포인트는 세 가지가 있다. 첫 번째는 함수 결과 상관없이 무조건 false를 반환하도록 isCheckRootingProcesses() 함수를 재 작성하는 방법이고 두 번째는 equals() 함수 호출 시 전달되는 인수 값을 변조하는 방법이다. 그리고 세 번째는 isRootComm and() 함수의 매개 변수로 들어오는 문자열을 검사해 ps –ef일 경우 더미 값으로 변조하는 방법이다. 여기서는 세 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.

[그림 51] isRootCommand() 함수를 후킹하기 위한 Frida 스크립트

[그림3-51]은 isRootCommand() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 RootTools 클래스 사용을 위해 클래스 객체를 반환 받고 isRootCommand() 함수를 오버로딩으로 구현한다. ②에서는 isRootCommand() 함수로 들어오는 매개 변수 값을 검사해 ps –ef인 경우 더미 값으로 변조하고 변조한 더미 값을 인수로 원본 isRootCommand() 함수를 호출해 그 결과를 반환하게 된다.

[그림 52] Bypass Check Process 우회 성공

Frida를 통해 작성한 스크립트를 ADITER 애플케이션에 어태치 한다. 그 후 Bypass Check Process 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.

05. 마무리

지금까지 실제 애플리케이션에서 루팅된 디바이스를 어떠한 방법으로 탐지하고 공격자는 또 이것을 어떠한 방식으로 우회하는지에 대해 살펴보았다. 문서에서 다룬 탐지 방법은 일반적으로 개발자 포럼에서 공유되고 사용되는 기법이다. 아직 한국에는 소프트웨어 개발 보안 가이드처럼 모바일 시큐어 코딩이라고 부를 수 있는 정량화된 가이드가 존재하지 않는다. 때문에 개발자들은 애플리케이션 개발 시 포럼에서 공유되는 탐지 코드를 그대로 사용하는 경우가 부지기수이며 이는 문서에서 기술한 것과 같이 간단히 작성된 코드를 통해 쉽게 우회할 수 있다.

이를 방지하기 위한 기법들로 네이티브 코드를 이용한 탐지 방법, 동적 로딩 기법을 이용한 탐지 방법, 코드 분리를 통해 분석을 어렵게 만드는 방법 등이 있으며 이에 대해 문서 PART 4에서 다룰 예정이다. 추가로 해당 문서는 Android 애플리케이션을 대상으로 최소한의 보안 위협을 대응하고자 제시하는 하나의 가이드이며 공격자들이 어떠한 방식을 사용해 탐지를 우회하고 데이터를 위·변조 할 수 있는지에 대한 설명이라고 볼 수 있다.

PART 2에서는 공격자들이 애플리케이션 동적 분석 시 사용하는 디버깅과 에뮬레이터를 실제 애플리케이션에서 어떠한 방법으로 탐지하고 공격자는 또 이것을 어떠한 방식을 통해 우회할 수 있는지 살펴보도록 하겠다.

06. 참고자료

[1] Android OS 구조
https://onlyfor-me-blog.tistory.com/386
https://m.blog.naver.com/siryua/221000280654
[2] Dex 파일 구조
https://bugday.tistory.com/45
[3] Android 가상 머신
https://www.charlezz.com/?p=42686
[4] Android 보안 모델
https://aroundck.tistory.com/5122
https://aroundck.tistory.com/5123
https://greencloud.tistory.com/108
[5] 모바일 보안위협과 대응방안
https://www.igloo.co.kr/security-information/%EB%AA%A8%EB%B0%94%EC%9D%BC-%EB%B3%B4%EC%95%88%EC%9C%84%ED%98%91%EA%B3%BC-%EB%8C%80%EC%9D%91%EB%B0%A9%EC%95%88/
[6] Android Scope
https://medium.com/hongbeomi-dev/android-11-scoped-storage-%EB%8C%80%EC%9D%91%ED%95%98%EA%B8%B0-6b4319cfac19