보안정보

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

ANDITER를 활용한 안드로이드 위협 탐지 및 우회 방안 : PART 3 (프리다, 피닝)

2023.07.05

7,271

01. 프리다(Frida) 개요 및 우회방안

1) 프리다의 개요

‘ANDITER를 활용한 안드로이드 위협 탐지 및 우회 방안 : PART 2’에서 동적 분석에 사용되는 디버그와 애플리케이션 분석 환경인 에뮬레이터의 탐지 및 우회방법에 대해 살펴보았다. PART3에서는 데이터 위·변조 및 분석도구인 프리다(Frida)와 중간자 공격을 예방하는 기술인 피닝(Pinning)을 분석하고 공격자 관점에서 우회방안을 살펴보도록 하자.

먼저 프리다(Frida)란, Ole에서 개발한 DB(Dynamic Binary Instrumentation)프레임워크로 Windows, MacOS, Linux, Android, iOS 등의 다양한 플랫폼 기반의 네이티브(Native) 애플리케이션을 대상으로 메모리 인젝션(Memory Injection) 기능을 제공하는 파이썬(Python) 라이브러리다. 모바일 플랫폼에서 주로 사용되며, 스크립트 작성 시 JS, C, Swift 언어에 대한 API를 지원한다. 프리다 이외에 오픈소스로 모바일 분석기능을 제공하는 도구는 MobSF, Drozer가 있다.

Frida를 통해서는 후킹 기능 이외에도 디버그, 트래픽 스니핑, 암호 해독 등이 제공되며, 루팅되지 않은 디바이스에서도 동작될 수 있도록 공유 라이브러리인 Gadget을 지원한다. 모바일 분석을 위해서 데이터 위·변조를 제공하는 Frida는 필수적인 분석 도구라고 할 수 있으나, 분석가의 기술 성숙도에 따라서 보안탐지 기능 무력화 및 개발자의 정상적인 기능을 우회하여 비정상 행위를 유발할 수 있게 된다. 일례로 유료 구독서비스 이용이나 게임 핵 제작, 로그정보 수집을 통한 데이터 탈취 등이 대표적인 공격 사례다. 따라서 모바일 보안 입장에서는 Frida는 루팅과 함께 반드시 탐지하고 차단해야 하는 도구 중에 하나다.

이번 호에서는 Frida 도구 탐지 시 사용하는 대표적인 방법 3가지에 대해서 분석해보고, 공격자 관점에서 우회할 수 있는 방안에 대해서 알아보고자 한다.

2) Bypass File & Path

[그림 1] Bypass File & Path 탐지

Frida는 명령어 입·출력을 처리하는 CLI 클라이언트 도구(Frida-Tools)와 해당 도구와 소켓 통신을 하며 사용자가 요청한 명령어를 수행하는 에이전트(Frida-Server)로 구성된다. CLI 클라이언트는 사용자의 로컬 PC에서 동작하고 에이전트는 모바일 디바이스 내에 설치되어 실행되며, 에이전트의 경우 실행 시 /data/local/tmp/re.frida.server 디렉터리 내에 Frida에서 사용하는 모듈 및 소켓 파일을 생성한다.

[그림 2] Bypass File & Path 탐지

Bypass File & Path 탐지 항목은 이러한 특징을 이용해 Frida 실행 시 자동으로 생성되는 관련 파일들이 디바이스에 존재하는지 검사하고 파일 존재 시 Frida 동작을 인지하여 탐지하는 방식이다.

[그림 3] isCheckFridaBinary() 함수 소스코드

[그림 3]의 isCheckFridaBinary() 함수는 ‘Bypass File & Path’ 탐지 결과를 반환해 준다. 코드 상에서 ①은 File 클래스를 사용해 /data/local/tmp 디렉터리 객체를 생성하고 maxDepath() 함수를 통해 깊이 2만큼의 하위 디렉터리 및 파일 정보를 가지고 온다. ②, ③에서는 ①에서 가지고 온 디렉터리 및 파일 이름에 Frida와 linjector라는 문자열이 포함되어 있는지 검사하고 파일 존재 시 Frida 사용자로 탐지한다.

Bypass File & Path 탐지를 우회하기 위한 후킹 포인트는 3가지다. 첫 번째는 함수 결과 상관없이 무조건 false를 반환하도록 isCheckFridaBinary() 함수를 재 작성하는 방법이고, 두 번째는 File 클래스 객체 생성 시 전달되는 인자를 더미 값으로 변조하는 방법이다. 세 번째는 contains() 함수 사용 시 들어오는 비교 문자열의 매개 변숫값을 변조하는 방법이다. 여기서는 두 번째 방법과 권한 설정을 통해 탐지를 우회해 보는 방안을 다뤄보도록 하겠다.

[그림 4] File 클래스 생성자를 후킹하기 위한 Frida 스크립트

[그림 4]는 File() 클래스 생성자를 후킹 하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 File 클래스 객체를 생성하고 isCheckFridaBinary() 함수에서 File 객체를 생성할 때 전달되는 인자의 데이터 타입에 맞춰 생성자를 오버로딩으로 구현했다. ②에서는 File 객체 생성 시 들어오는 매개 변숫값이 /data/local/tmp와 같다면 임의의 경로로 변조하고 변조한 값을 인자로 원본 File 클래스 객체를 생성해 결괏값을 반환하게 된다.

[그림 5] Bypass File & Path 탐지 우회 성공

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

[그림 6] /data/local/tmp 디렉터리 권한 확인

다음으로 권한 설정을 통한 탐지 우회 방법을 살펴보자. 디바이스에 설치된 모든 애플리케이션들은 자신만의 고유한 UID를 할당받아 실행하고 실행 시에 일반 사용자 권한으로 동작한다. 애플리케이션 내에서 수행되는 코드 단의 권한도 동일하기 때문에 디렉터리 및 파일 열람 시 일반 사용자 권한이 존재하지 않는다면 데이터를 읽어 올 수 없게 된다. Android OS 9.0 이하의 버전에서는 Java API를 이용한 방법을 통해 일부 디렉터리에 권한 부여가 가능했지만, Android OS 10.0 이후 버전부터 도입된 ‘Scoped Mode’도입으로 이 또한 불가능하다. 권한 설정을 통한 탐지 우회 방법은 이러한 방안에 착안해 우회하는 방법이다.

/data/local/tmp 디렉터리의 경우 모바일 분석 도구 및 기타 도구 이용 시 사용되는 디렉터리로 일반적으로 일반 사용자에게 읽기 권한이 제한되어 있다. 다만, 특정 도구 사용을 위해 해당 디렉터리에 읽기 및 쓰기 권한을 부여해야 할 경우가 생기는데 이 경우 애플리케이션에서 해당 디렉터리에 접근할 수 있게 된다. 따라서, 애플리케이션에서 탐지를 위해 디렉터리 정보를 읽어 올 때 일반 사용자 읽기 권한을 제거하면 우회가 가능하다.

[그림 7] /data/local/tmp 디렉터리 권한 변경

chmod는 Linux/Unix 환경에서 파일 및 디렉터리 권한 변경에 사용되는 명령어로 ①에서 chmod를 통해 /data/local/tmp 디렉터리 권한을 771로 변경한다. ②를 보면 일반 사용자 권한의 읽기 및 쓰기 권한이 제거된 것을 볼 수 있다.

[그림 8] 권한 변경을 통한 Bypass File & Path 탐지 우회 성공

Bypass File & Path 탐지 항목을 체크하면 [그림 8]과 같이 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.

3) Bypass Port

[그림 9] Bypass Port 탐지

Frida는 클라이언트 & 서버(에이전트) 통신 구조로, 클라이언트에서 요청한 명령은 서버에서 실행되게 된다. 서버와 클라이언트는 연결을 위해 특정 포트를 사용하는데, 별도의 포트설정이 없으면 디폴트 포트인 27042 포트를 사용해 클라이언트와 통신한다. 디바이스에서 Frida 서버를 실행시킨 후 netstat 명령어를 통해 디바이스의 네트워크 정보를 확인해 보면 [그림 10]과 같이 Frida 디폴트 포트인 27042 포트가 클라이언트와의 통신을 위해 열려 있는 것을 알 수 있다.

[그림 10] Frida 서버에서 클라이언트와 연결을 위해 사용하는 포트

Bypass Port 탐지 항목은 디바이스에서 Frida 서버 포트가 열려있는지 검사하고 해당 포트가 열려 있을 경우 Frida 서버가 동작 중인 것으로 판단해 탐지하게 된다.

[그림 11] isCheckFridaPort() 함수 소스코드

[그림 11]의 isCheckFridaBinary() 함수는 Bypass Port 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 ①에서 Thread를 생성하고 디바이스 포트 검사를 위해 FridaDetector.isCheckFridaPort..
inlined.Runnable.1 클래스 객체 정보를 받아오고 있다.

[그림 12] FridaDetector.isCheckFridaPort..inlined.Runnable.1 클래스 소스코드

[그림 12] FridaDetector.isCheckFridaPort..inlined.Runnable.1 클래스 코드를 보면 ① Socket 클래스를 사용해 디바이스의 27000 ~ 27500 포트 중 연결되어 통신 중인 포트를 검사하고 있으며, isCheckFridaPort() 함수에서 ② Thread.start() 구문 실행 시 run() 함수가 자동으로 호출되어 해당 코드를 동작시킨다.

Bypass Port 탐지를 우회하기 위한 후킹 포인트는 두 가지가 있다. 첫 번째는 함수 결과와 상관없이 무조건 false를 반환하도록 isCheckFridaPort() 함수를 재 작성하는 방법이고 두 번째는 통신 중인 포트를 검사할 때 사용하는 Socket 클래스를 후킹해 생성자 인수 값으로 전달되는 포트 번호를 변조하는 방법이다. 여기서는 두 번째 방법과 Frida 서버 설정 방법을 통해 탐지를 우회 보도록 하겠다.

[그림 13] Socket 클래스 생성자를 후킹하기 위한 Frida 스크립트

[그림 13]은 Socket 클래스 생성자를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 Socket 클래스 객체를 생성하고 FridaDetector.isCheckFridaPort..inlined.Runnable.1 클래스의 run() 함수에서 Socket 객체를 생성할 때 전달한 인자의 데이터 타입에 맞춰 생성자를 오버로딩으로 구현했다. ②에서는 Socket 객체 생성 시 들어오는 매개 변수 포트 값이 27042일 경우 임의의 포트 번호로 변조하고 변조한 값을 인자로 원본 Socket 클래스 객체를 생성해 결괏값을 반환하게 된다.

[그림 14] Bypass Port 탐지 우회 성공

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

[그림 15] 포트 지정 방식을 통한 Bypass Port 탐지 우회 성공
[그림 16] Frida 서버 옵션

추가로 Frida 서버의 포트 지정을 이용한 탐지 우회 방법도 다뤄보도록 하겠다. [그림 16]은 Frida 서버 실행 시 설정할 수 있는 옵션에 관한 설명으로 -h 옵션을 통해 확인할 수 있으며 이 중 –l 옵션의 경우 Frida 서버의 동작 IP 주소 및 포트 지정을 가능케 해준다.

[그림 17] Frida 서버 포트 지정

-l 옵션을 사용해 임의의 포트 번호를 지정하고 Frida 서버를 실행시킨다. 그 후 netstat 명령을 통해 디바이스의 네트워크 정보를 확인해 보면 [그림 17]과 같이 지정한 포트 번호로 동작되고 있는 것을 볼 수 있다. 애플리케이션에서 Frida 탐지 시 지정 포트 또는 포트의 범위를 검사하고 있을 경우 위와 같이 검사하지 않는 포트 번호를 지정해 사용하면 쉽게 우회가 가능하다. Bypass Port 탐지 항목을 체크하면 [그림 15]와 같이 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.

4) Bypass Module

[그림 18] 프로세스 정보 확인 시 출력되는 Frida 관련 라이브러리 모듈

Frida의 후킹 기능은 [그림 18]과 같이 애플리케이션의 메모리 주소를 참조해 호출되는 특정 함수 코드를 가로채서 변조한 코드로 돌려주게 된다. Frida는 후킹과 더불어 기능 사용을 위해 애플리케이션 프로세스에 특정 라이브러리를 삽입하며 클라이언트 측에서 요청한 명령어를 삽입한 라이브러리 모듈을 통해 동작시킨다.

[그림 19] Bypass Module 탐지

Bypass Module 탐지 항목은 실행중인 애플리케이션 프로세스 정보를 확인하여 매핑되는 Frida 관련 라이브러리가 존재하는지 검사하고 존재할 경우 Frida 서버가 동작 중인 것으로 판단해 탐지하게 된다.

[그림 20] isCheckFridaModule() 함수 소스코드

[그림 20]의 isCheckFridaModule() 함수는 Bypass Module 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 ①에서 프로세스 정보 확인을 위해 /proc/self/maps 파일 객체를 생성하고 있다. /proc/self 디렉터리에는 현재 실행 중인 프로세스에 관한 정보가 저장되어 있으며 maps 파일에서 해당 프로세스의 매핑된 메모리 주소 공간을 확인할 수 있다. ②에서는 maps 데이터를 읽어와 frida 라는 문자열이 포함되어 있는지 검사하며 문자열이 존재할 경우 Frida 사용자로 탐지하게 된다.

Bypass Module 탐지를 우회하기 위한 후킹 포인트는 세 가지가 있다. 첫 번째는 함수 결과 상관없이 무조건 false를 반환하도록 isCheckFridaModule() 함수를 재 작성하는 방법이고 두 번째는 File 객체를 읽어올 때 사용된 BufferReader.readLines() 함수를 후킹해 특정 문자열이 포함되어 있을 경우 더미 값으로 변조하는 방법이다. 그리고 세 번째는 contains() 함수 호출 시 전달되는 인자 값의 비교 문자열을 변조하는 방법이다. 여기서는 두 번째 방법을 사용해 탐지를 우회해 보도록 하겠다.

[그림 21] BufferedReader 클래스의 readLines() 함수를 후킹하기 위한 Frida 스크립트

[그림 21]은 BufferdReader 클래스의 readLines() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 TextStreamsKT 클래스 객체를 생성하고 isCheckFridaModule() 함수에서 readLines() 함수를 호출할 때 전달되는 인자의 데이터 타입에 맞춰 readLines() 함수를 오버로딩으로 구현했다. ②에서는 반환 값 검사를 위해 원본 readLines() 함수를 호출해 결괏값을 받아온다. 이때, 받아오는 결괏값이 List 타입이기 때문에 interator() 함수를 통해 List에 저장되어 있는 모든 요소 값을 가져온다. ③에서는 가져온 데이터의 문자열 중 frida 가 포함되어 있는지 검사하고 포함되어 있다면 더미 List를 생성해 반환한다.

[그림 22] Bypass Module 탐지 우회 성공

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

02. 피닝(Pinning) 개요 및 우회방안

1) 피닝의 개요

[그림 23] 중간자 공격(Man in the Middle)

SSL/TLS는 암호화 기반 인터넷 보안 프로토콜로 웹에서 전달되는 모든 데이터를 암호화해 스니핑(Sniffing), 스누핑(Snooping)의 위협을 방지한다. 다만, 클라이언트 측에서 전송되는 패킷을 중간에 가로채 조작하는 중간자 공격(Man in the Middle)에 취약하다는 특징을 가지고 있어 전송되는 SSL/TLS 패킷 내용 획득 시, : 애플리케이션이 서버와 통신할 때 전달되는 데이터를 탈취하거나, 패킷 조작을 통한 데이터 위변조 및 인증을 무력화 시킬 수 있게 된다. 이에 대응하고자 나온 방안이 SSL/TLS Pinning(이하 피닝)으로 클라이언트에 신뢰성이 보장된 인증서들을 사전에 등록해서 서버와 통신할 때 등록된 인증서가 아니면 통신을 하지 못하도록 막아버리는 기술이다.

[그림 24] 디바이스에 등록된 Root CA
[그림 25] Root CA에 미등록된 인증서로 통신 시 오류 발생

피닝은 보편적으로 두 가지 방법을 통해 구현된다. 첫 번째는 서버와 통신 시 반환되는 인증서가 디바이스의 Root CA에 의해 신뢰할 수 있는 인증서인지 검증하는 방법으로 정상 사용자의 경우 문제가 없으나 패킷 위·변조 목적으로 프록시(Proxy) 서버를 경유해 데이터를 주고받는 사용자의 경우 인증서 관련 오류(NET_ERR_CERT_AUTHORITY_INVALID)로 인해 서버와 통신을 하지 못하게 된다.

정상적인 경우에는 [그림 24]와 같이 디바이스에 등록된 Root CA를 사용해 통신하지만, [그림 25]와 같이 Root CA에 등록되지 않은 인증서로 통신을 시도하는 경우 오류가 발생한다. 이 외에도 서버 인증서의 유효기간 만료, 폐기된 인증서 사용 시 관련 오류가 발생하기도 한다.

[그림 26] 고정 인증서 비교 과정

두 번째 방법은 클라이언트(디바이스) 단에서 인증서를 고정해 사용하는 방법으로 고정되어 있는 인증서 외의 인증서를 서버가 반환하는 경우 통신을 하지 못하도록 하는 방법이다. 이 경우 허용하는 서버의 인증서 값이 애플리케이션 코드 단에 하드 코딩 되어있거나 별도의 방법을 통해 값을 불러와 인증서 값과 비교하게 된다.

이번 챕터부터는 앞서 설명한 두 가지의 피닝 기법을 기술하고 또 이를 공격자의 관점에서 어떻게 우회할 수 있는지 우회 방안에 대해 다뤄보도록 하겠다.

2) 사전 준비

원활한 실습 진행을 위해 탐지되고자 하는 경우의 서버 인증서 변조를 위해 직접 중간자 공격을 수행해야 한다. 다만, SSL/TLS 중간자 공격을 하기 위해서는 별도의 프록시 서버를 구성하거나 도구를 사용해야 한다. 일반적으로 중간자 공격 시 많이 사용되는 도구로는 PortSwigger에서 제작한 Burp Suite, OWASP 프로젝트 중 하나인 OWASP ZAP이 있으며 해당 챕터에서는 Burp Suite를 사용해 중간자 공격을 수행하도록 하겠다.

[그림 27] Burp Suite 다운로드 과정

Burp Suite 도구는 무료 버전인 Community와 유료 버전인 Professional으로 구분되며 무료 버전을 다운로드(https://portswigger.net) 받아 설치하면 된다.

[그림 28] Proxy 설정 과정 (1/6)

Burp Suite를 실행 후 [그림 28]과 같이 ① 설정(Settings) 탭을 클릭해 ② Tools – Proxy 카테고리를 선택하고 ③ Add 버튼을 클릭한다.

[그림 29] Proxy 설정 과정 (2/6)

[그림 29]의 ① Proxy 서버의 통신 포트를 지정하고 ② 리스닝 할 IP 대역을 All interfaces로 설정한다.

[그림 30] Proxy 설정 과정 (3/6)

사용 중인 컴퓨터와 모바일 디바이스를 공유기 또는 핫스팟(Hot Spot) 기능을 이용해 동일 네트워크 대역을 사용할 수 있도록 설정한다. 그 후 연결된 네트워크 설정에서 [그림 30]과 같이 ① 프록시 모드를 수동으로 변경하고 ② 모바일 디바이스와 같은 대역에 연결된 PC의 IP 주소와 [그림 29]에서 설정한 포트 번호를 입력해 준다.

[그림 31] Proxy 설정 과정 (4/6)

설정이 잘 되었는지 확인을 위해 통신 패킷을 직접 인터셉트해 보겠다. [그림 31]과 같이 ① Burp Suite 상단의 Proxy 설정 탭을 클릭 후 ② Intercept를 클릭하고 ③ Intercept is off 버튼을 클릭해 Intercept is on으로 변경해 주면 패킷을 인터셉트할 준비가 된 것이다.

[그림 32] Proxy 설정 과정 (5/6)

모바일 디바이스에서 브라우저를 통해 구글(www.google.co.kr)에 접근 시 [그림 32]와 같이 연결 관련 오류가 발생한다. 이때, 브라우저 하단의 고급 탭을 클릭 후 사이트 이동 버튼을 클릭해 준다.

[그림 33] Proxy 설정 과정 (6/6)

[그림 33]과 같이 Burp Suite에서 구글 사이트와의 통신 패킷이 인터셉트 되었다면 정상적으로 설정이 된 것으로 인증서 변조 과정을 위한 중간자 공격 준비가 완료되었다.

[그림 34] 프록시 구성도

3) Bypass Pinning(Root CA)

앞서 피닝은 보편적으로 두 가지 방법을 통해 구현된다고 설명했었다. 그중 첫 번째인 SSL/TLS 인증서가 모바일 OS에 내장되어 있는 Root CA에 의해 신뢰할 수 있는 인증서인지 확인하는 방법을 살펴보겠다. SSL/TLS 인증서는 서버가 클라이언트에게 반환하는 것으로 중간자 공격 시 [그림 34]와 같이 프록시 서버를 거쳐 패킷이 전송되기 때문에 요청한 서버의 인증서가 아닌 프록시 서버의 인증서가 클라이언트에게 반환된다. 때문에 디바이스에서는 프록시 서버의 인증서와 Root CA를 비교해 인증서 유효성을 검증하게 되고 등록된 CA의 공개키가 존재하지 않을 경우 신뢰할 수 없는 인증서로 판별하게 된다.

[그림 35] Bypass Pinning(Root CA)

Bypass Pinning(Root CA) 탐지 항목은 위와 같은 특징을 이용해 통신을 시도하는 서버에서 반환하는 인증서가 디바이스 Root CA에 의해 신뢰할 수 있는 인증서인지 확인하고 신뢰할 수 없는 인증서인 경우 중간자 공격을 시도 중인 것으로 판단해 탐지하게 된다.

[그림 36] isCheckRootCA() 함수 소스코드

[그림 36]의 isCheckRootCA() 함수는 Bypass Pinning(Root CA) 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 PinningDetector.isCheckRootCA.sendRequestJob.1 클래스가 사용된 것을 볼 수 있으며 코루틴(Coroutine) 관련 로직을 관리하는 클래스이다. 코루틴으로 작성된 코드는 이와 같이 컴파일 과정에서 스레드 관련 작업을 별도의 클래스 파일로 생성해 관리하고 필요시 호출해 사용하게 된다.

[그림 37] PinningDetector.isCheckRootCA.sendRequestJob.1 클래스의 invokeSuspend() 함수

[그림 37]은 PinningDetector.isCheckRootCA.sendRequestJob.1 클래스 코드 중 서버와 통신 작업을 수행하는 invokeSuspend() 함수의 코드이다. 코루틴에서 suspend 함수의 경우 네트워크 관련 비동기 작업 시 사용되는 함수로 호출 시 수행 중이던 작업을 일시 중지하고 suspend 함수 작업이 끝날때 까지 대기하게 된다. 코드를 보면 ① 서버와의 통신을 위해 OkHttp 라이브러리가 사용되었으며 이때, 서버의 주소를 ④에서 받아와 요청 패킷을 보낸다. 요청에 성공할 경우 ③ onResponse() 함수가 동작하게 되고 요청 실패 시 ② onFailure() 함수가 실행된다.

그런데 해당 함수의 코드에서는 인증서가 신뢰할 수 있는 인증서인지 검증하는 구문이 존재하지 않는다. 그 이유는 인증서 신뢰 유무를 OkHttp 라이브러리에서 확인하기 때문이다. OkHttp의 경우 SSL/TLS 통신 시 내부적으로 HandShake 과정을 거쳐 서버의 인증서가 디바이스에 내장되어 있는 Root CA에 의해 신뢰할 수 있는 인증서인지 검증하게 된다. 따라서, 인증서 검증을 우회하기 위해서는 Root CA를 통해 인증서 신뢰 유무 검사 시 사용되는 함수를 후킹해 신뢰할 수 있는 인증서로 변조해야 한다.

[그림 38] TrustmanagerImpl 클래스의 checkTrustedRecursive() 함수

[ trustmanagerImpl 클래스 소스코드 자료 ] https://github.com/google/conscrypt/blob/master/common/src/main/java/org/conscrypt/TrustManagerImpl.java#L521

trustmanagerImpl 클래스는 Android OS에서 통신을 시도하는 서버가 반환하는 인증서를 Root CA를 통해 인증서 유효성 검증 시 사용되며 이 중 checkTrustedRecursive() 함수에서 인증서 체인을 검증하게 된다. 해당 함수의 경우 호출 시 제공된 인증서 체인을 기반으로 Root CA를 통해 신뢰할 수 있는 인증서를 찾아 반환하게 되는데 이때, 빈 리스트를 반환하게 되면 인증서 검증을 수행하지 않고 모든 인증서를 신뢰할 수 있는 것으로 처리한다. 따라서, 해당 함수를 후킹해 항상 빈 리스트를 반환하게 만들면 인증서 검증 과정 우회가 가능해진다.

이 외에도 동일 클래스의 getTrustedChainForServer() 함수, verifyChain() 함수 조작을 통한 우회 방법과 문서에서는 언급하지 않았지만 인증서 자격 증명 시 사용되는 TrustManageer 인터페이스를 이용한 Burp Suite 인증서를 추가하는 방법을 이용한 우회 방안도 존재한다.

Bypass Pinning(Root CA) 탐지 우회를 위해 여기서는 checkTrustedRecursive() 함수를 후킹해 항상 빈 리스트 값을 반환하도록 만들어 우회해 보도록 하겠으며 추가로 디바이스 설정을 통한 간단한 우회 방안도 살펴보도록 하겠다.

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

[그림 39]는 TrustManagerImpl 클래스의 checkTrustedRecursive() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 TrustManagerImpl 클래스 객체를 생성하고 인증서 체인 검사를 위해 checkTrustedRecursive() 함수 호출 시 전달되는 인자의 데이터 타입에 맞춰 checkTrustedRecursive() 함수를 오버로딩으로 구현했다. ②에서는 checkTrustedRecursive() 함수 호출 시 사용할 리스트를 생성하기 위해 ArrayList 클래스의 객체를 생성했으며 ③ 함수가 호출되면 ②에서 생성한 ArrayList 객체를 이용해 빈 리스트를 만들어 반환한다.

[그림 40] Bypass Pinning(Root CA) 탐지 우회 성공

[그림 40]과 같이 Frida를 통해 작성한 스크립트를 ADITER 애플리케이션에 어태치 한다. 그 후 프록시가 연결된 상태에서 Bypass Module 탐지 항목을 체크하면 Success! 가 출력되며 탐지가 우회된 것을 볼 수 있다.

추가로 Burp Suite 인증서를 디바이스 Root CA에 등록해 탐지를 우회하는 방법도 다뤄보도록 하겠다. 참고로 해당 방법의 경우 고정 인증서 검증 방식의 피닝 기법이 아닌 Root CA에 의해 신뢰할 수 있는 인증서인지 검증하는 방법에서만 사용해 우회가 가능하다.

[그림 41] Burp Suite 인증서 추출 과정 (1/2)

[그림 41]과 같이 Burp Suite 설정의 ① Tools – Proxy 카테고리에 접근 후 ② import / export CA certificate 버튼을 클릭한다. ③ 인증서 유형은 Certificate in DER format을 선택하고 ④ Next 버튼을 클릭한다.

[그림 42] Burp Suite 인증서 추출 과정 (2/2)

[그림 42]와 같이 ① 저장할 인증서의 이름을 지정하고 ② Save 버튼을 클릭한다.

[그림 43] 인증서 인코딩 및 변환

openssl을 이용해 추출한 인증서 파일(cacert.der)을 .pem 확장자로 인코딩 및 변환한다.

* openssl 다운로드 (https://slproweb.com/products/Win32OpenSSL.html)

[그림 44] 인증서 해시 값 추출 및 파일명 변경

[그림 44]와 같이 openssl을 이용해 변환한 cacert.pem 파일의 해시(Hash)값을 추출하고 cacert.pem 파일의 이름을 추출한 해시값으로 변경하면 인증서 추출 과정이 모두 완료된 것으로 해당 인증서를 디바이스의 Root CA에 추가해 주면 된다.

[그림 45] 디바이스 인증서 추가 과정

[그림 45]와 같이 ① adb(Android Debug Bridge) 도구의 push 명령을 사용해 추출이 완료된 인증서를 디바이스 /data/local/tmp 디렉터리로 업로드한다. ② 디바이스에서 인증서를 인식하기 위해서는 인증서 관리 디렉터리(cacerts)로 이동시켜줘야 하는데 해당 디렉터리의 경우 읽기 전용 파티션으로 설정되어 있어 mount 명령어를 통해 쓰기 권한을 추가해 줘야 한다. ③ 업로드한 인증서를 인증서 관리 디렉터리(/system/etc/security/cacerts/)로 복사 후 ④ 변경했던 파티션 권한을 다시 원상복구해 준다.

[그림 46] 디바이스 인증서 추가 과정

[그림 46]과 같이 갤럭시 Z플립 기종의 디바이스 기준으로 설정 – 생체 인식 및 보안 – 기타 보안 설정 – 인증서 확인 항목에서 추가한 인증서를 확인할 수 있으며, Burp Suite 인증서인 PortSwigger CA가 정상 등록된 것을 확인할 수 있다.

[그림 47] 디바이스 인증서 추가 과정

인증서 등록이 완료되었다면 [그림 47]과 같이 인증서 오류 없이 인터넷에 정상 접근되는 것을 확인할 수 있으며, Bypass Pinning(Root CA) 탐지 항목 또한, 별도의 Frida 스크립트 없이 정상 우회되는 것을 볼 수 있다.

4) Bypass Pinning(Allow CA)

[그림 48] Bypass Pinning(Allow CA)

이번 챕터에서 살펴볼 피닝 기법은 고정 인증서 검증 방식으로 애플리케이션에서 허용하는 인증서 외의 인증서를 서버가 반환하는 경우 통신을 하지 못하도록 하는 방법이다. 앞서 살펴본 Root CA에 의한 인증서 신뢰 기반의 피닝 기법이 중간자 공격을 막기 위한 가장 기초적인 방어 방법이었다면 고정 인증서 방식은 조금 더 높은 보안을 요구하는 플랫폼에서 사용되는 방법이다.

[그림 49] Bypass Pinning(Root CA)

고정 인증서란 디바이스에 임의의 인증서를 설치해 서버와 통신하는 방식이 아닌 애플리케이션의 자바 코드 내에서 특정 호스트와 통신할 때 지정한 인증서의 해시 값과 서버 인증서의 해시 값을 비교해 동일한 해시 값일 경우 통신을 허용하는 방식이다. 인증서 지정 방법으로는 가장 기본적인 하드 코딩 방식부터 공격자에 의한 변조 방지를 위한 리소스 다운로드, 동적 로딩 기법 등을 통해 지정하는 방법이 있다.

Bypass Pinning(Allow CA) 탐지 항목은 통신을 시도하는 서버의 인증서 해시 값이 애플리케이션의 자바 코드에서 지정한 인증서 해시 값과 다를 경우 중간자 공격을 시도 중인 것으로 판단해 탐지하게 된다.

[그림 50] isCheckAllowCA() 함수의 소스코드

[그림 50]의 isCheckAllowCA() 함수는 Bypass Pinning(Allow CA) 탐지 결과를 반환해 주는 역할을 한다. 코드를 보면 ①에서 OkHttp.certificatePinner 클래스 객체를 생성할 때 호스트 이름과 서버의 인증서 해시 값을 인자로 넘겨주고 있으며 해당 해시 값이 서버와 통신 시 인증서 비교에 사용되는 값이다. 따라서, 서버가 반환하는 인증서의 해시 값을 "sha256/+hIxKEB6NLsH9zGi9fx81iY3nbFfuipoNjz1liP
ufEI=“와 비교해 다를 경우 통신을 하지 못하게 한다. ② ①에서 생성한 객체를 코루틴 인자로 넘겨줘서 서버와 실질적인 통신 작업을 수행한다.

[그림 51] invokeSuspend() 함수

[그림 51]은 2.2 Bypass Pinning(Root CA) 탐지 항목의 [그림 37]에서 살펴본 함수와 동일한 기능을 수행하는 코루틴의 suspend() 함수이다. ① [그림 50]의 isCheckAllowCA() 함수에서 생성한 OkHttp.certificatePinner 클래스 객체를 통해 통신을 수행하게 되며 ② 통신 성공 시 onResponse() 함수가 동작되며 ③ 통신 실패 시 onFailure() 함수가 실행된다.

[그림 52] okhttp3.CertificatePinner 클래스의 check$okhttp() 함수 소스코드

[그림 52]의 check$okhttp() 함수는 클라이언트와 서버가 SSL/TLS HandShake를 진행할 때 인증서의 유효성 검사 시 사용되는 함수이다. ① 함수 내에서 사용되는 findMatchingPins() 함수의 경우 호스트 이름과 매핑되는 인증서의 핀 리스트를 반환하는 역할을 수행하는 데 호스트 이름과 일치하는 고정된 인증서가 존재하지 않을 경우 빈 리스트를 반환하게 된다. ② ①에서 반환받은 리스트가 빈 리스트라면 check$okhttp() 함수는 인증서에 대한 유효성 검사를 수행하지 않고 null 값을 반환하게 되며 이는 고정 인증서 핀을 우회하고 어떤 인증서든 사용할 수 있게 허용하게 된다.

Bypass Pinning(Allow CA) 탐지를 우회하기 위한 후킹 포인트는 두 가지가 있다. 첫 번째는 인증서의 유효성 검사를 수행하지 않게 항상 null 값을 반환하도록 check$okhttp() 함수를 재 작성하는 방법이고 두 번째는 호스트 이름에 매핑되는 인증서를 찾을 때 사용하는 findMatchingPins() 함수의 결괏값을 매핑되는 인증서가 없을 때와 동일하게 항상 빈 리스트를 반환하도록 변조하는 방법이다. 여기서는 첫 번째 방법을 사용해 우회해 보도록 하겠다.

[그림 53] check$okhttp() 함수를 후킹하기 위한 Frida 스크립트

[그림 53]은 check$okhttp() 함수를 후킹하기 위해 필자가 작성한 Frida 스크립트이다. 코드를 보면 ①에서 check$okhttp() 함수 사용을 위해 okhttp3.CertificatePinner 클래스 객체를 생성하고 인증서 유효성 검사를 위해 check$okhttp() 함수 호출 시 전달되는 인자의 데이터 타입에 맞춰 check$okhttp() 함수를 오버로딩으로 구현했다. ② 해당 함수가 호출되면 인증서 유효성 검사를 수행하지 않도록 null 값을 반환한다. 자바 스크립트에서 null 값을 반환할 때는 return 키워드만 명시하면 된다.

[그림 54] Bypass Pinning(Allow CA) 탐지 우회 성공

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

03. 마무리

지금까지 공격자들이 애플리케이션의 데이터를 위·변조하기 위한 목적으로 자주 사용하는 프리다(Frida) 도구의 탐지 방안과 우회 기법, 중간자 공격(Man in the Middle)을 예방하고자 사용하는 피닝(Pinning) 기법과 이를 우회하는 방법에 대해 살펴보았다.

프리다는 모바일 애플리케이션의 보안을 위협하는 공격자에게 있어서는 필수적인 도구로 이론적으로 애플리케이션 단에서 동작하는 모든 네이티브 코드에 한해서 변조 및 우회가 가능하다. 실제로 잘 만들어진 프리다 스크립트는 대 다수의 보안 탐지 코드를 무력화 시킬 수 있기에 반드시 사전에 프리다 도구를 탐지해 차단 하는 조치가 이뤄져야 하고 문서에서 기술한 탐지 방안 외에도 최근 보안 솔루션에서 많이 적용되고 있는 역 후킹 기법 등을 도입해 애플리케이션에서 발생할 수 있는 보안 위협을 방어해야 한다.

애플리케이션 보안 위협은 네이티브 코드 단에서만 발생하는게 아니다. 루팅, 디버그, 프리다 등이 애플리케이션 내에 동작하는 코드 위·변조를 막기 위한 조치 단계였다면 이후는 피닝을 통해 애플리케이션이 API 서버와 통신하며 받아오는 데이터 노출을 최소화하고 이에 따른 데이터 위·변조를 막기 위한 단계이다. 일례로 2017년 여기어때에서 SQL Injection 공격을 통해 개인정보 99만건이 유출된 사건도 있다. 물론, SQL Injection, XSS, File Upload, File Download 등의 웹 기반 공격은 백엔드(Back-End) 단의 탐지 로직이 중요하다. 하지만, 그 전에 이러한 웹 기반 공격을 가능케 하는 것이 바로 중간자 공격이다. 따라서, 중간자 공격을 막는다는 것은 애플리케이션과 통신하는 서버 단에서 발생할 수 있는 보안 위협을 방어하는 것과 같은 이치라고 할 수 있다. 문서에서 기술한 피닝 기법 외에도 별도로 인증서가 신뢰할 수 있는 인증서인지 검증하는 코드 추가가 필요하며 서버 단에서는 종단간 암호화 적용을 고려할 수 있다.

PART 3는 이로써 마치며 PART 4에서는 악성 애플리케이션과 보안 솔루션에서 탐지 및 주요 기능 코드들을 감추고자 적용하는 동적 로딩 기법과 애플리케이션의 코드 무결성 변조를 방지를 위한 탐지 기법을 다뤄보고 공격자의 관점에서 이를 어떠한 방식으로 우회할 수 있는지 살펴보겠다.

04. 참고자료

[1] HTTPS와 SSL 인증서, SSL 동작방법 : https://wayhome25.github.io/cs/2018/03/11/ssl-https/
[2] HTTPS 통신 원리 쉽게 이해하기 : https://nuritech.tistory.com/25
[3] FRIDA : https://frida.re/
[4] 중간자 공격 : https://www.appsealing.com/kr/%EC%A4%91%EA%B0%84%EC%9E%90-%EA%B3%B5%EA%B2%A9/
[5] OkHttp : https://square.github.io/okhttp/
[6] OKHTTP SSL Pinning : https://linears.tistory.com/entry/Android-OKHTTP-SSL-Pinning-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0