보안정보

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

Flutter Application의 보안진단 방안 : SSL Pinning

2024.03.06

1,021

01. Flutter Framework와 SSL Pinning의 개념

1) Flutter Framework 개요

Android와 iOS 진영으로 양분된 모바일 플랫폼 생태계로 인해 모바일 앱 개발자 입장에서는 동일 서비스라 할지라도 운영체제환경에 따라 각기 다른 언어로 모바일 앱을 개발해야 하기 때문에 개발에 소요되는 중복적인 리소스 투입이 불가피하였다. 이에 따라 Android와 iOS 환경을 모두 지원할 수 있는 크로스플랫폼 앱 개발 프레임워크의 필요성이 부각되면서 2010년경부터 현재까지 Flutter, React Native, Kotlin Multiplatform Mobile, Ionic, Xamarin, NativeScript 등 크로스 모바일 앱 개발 플랫폼의 관심이 꾸준히 증가하는 추세다.

Java, Kotlin, Swift 등 플랫폼 별 Native 언어와 무관하게 사용할 수 있다는 개발 편이성 및 효율성 등의 강점을 보이며 국내 모바일 앱 서비스 스타트업 상당수는 크로스 모바일 앱 개발 플랫폼 중 하나인 Flutter 사용이 두드러지게 되었다. [그림 1]과 같이 크로스 모바일 앱 개발 프레임워크의 시장 점유율 중 2022년의 시장점유율을 통해 이러한 사실을 입증하고 있다. 따라서 모바일 앱의 보안강화 및 대응을 위해서는 Flutter에 대한 공격벡터 및 대응 방안을 수립할 필요성이 높아짐에 따라 이번 호에서는 Flutter Architecture와 공격벡터를 분석해 보고 실무적으로 반영할 수 있는 대응 방안을 제시하고자 한다.

[그림 1] Cross-Platform Framework 사용률 (출처 : Cross-platform mobile frameworks used by software developers worldwide from 2019 to 2022)

Flutter는 Dart 언어를 사용하여 개발되어 Native 코드로 컴파일되기 때문에 Native와 비슷한 수준의 퍼포먼스를 보이며 소스코드를 실시간으로 수정하고 테스트할 수 있는 기능인 Hot Reload 기능을 제공하는 강점을 통해 고 비교군으로 꼽히는 크로스 모바일 앱 개발 플랫폼인 React-Native보다 높은 점유율을 보이고 있다. 특히 웹 브라우저 기술에 의존하지 않고 자체 렌더링 엔진을 제공하고 있으며, Google의 Material Design을 지원하여 디바이스에 최적화된 반응형 UI를 제공할 수 있게 하는 특징을 가지고 있다.

[그림 2] Flutter와 Native간의 통신 매커니즘(출처 : Platform channels– Flutter)

[그림 2]와 같이 Flutter에서 사용하는 Dart 코드가 C/C++ 작성된 Flutter 엔진을 호출하는 형태로 앱이 동작하며 이 매커니즘을 Platform Channel이라고 한다. Flutter는 Native와 양방향 통신을 지원하는데, Flutter에서 Native 호출 시 엔진에서 MethodChannel을 통해 Native와 상호작용하게 된다. 이에 따라 Dart 코드로 구현하면 Android, iOS를 구분할 필요 없이 플러그인 구현이 가능하다. Native와 상호작용할 때 MethodChannel을 이용하는 특성 때문에, Native 라이브러리 및 플러그인에 대해 보안진단 시 다른 방식의 접근이 필요하다.

[그림 3] Platform 별 SSL/TLS 라이브러리 및 Flutter 상관 관계

SSL Pinning의 경우 Flutter를 사용하지 않는 앱들은 주로 Okhttp, NSURLSession 라이브러리를 사용하여 Certificate 검증을 하나, Flutter는 http.Client 클래스의 생성자에 SecurityContext 객체를 전달하여 인증서 검증 여부를 설정하고 사용자가 설치한 인증서를 신뢰하지 않는 부분들이 Platform Channel을 이용하는 Flutter의 특성이라고 할 수 있다.

[그림 4] 2016년과 2023년의 비교 (출처 : OWASP Mobile Top 10 - Comparison between 2016 and 2023)

Flutter로 개발한 앱에서도 다양한 취약점이 발현될 수 있어 보안진단이 필요하며 Flutter를 사용하는 앱 진단 시 기능적 요소 변조, SSL 인증서 고정 우회(SSL Pinning), WebView, DeepLink 호출 등 에서의 취약점 등을 점검하게 되며 [그림 4]의 OWASP-2023-Intial Release 내 M1, M2, M5, M6, M7, M8 총 6개로 가장 많은 공격 표면을 가진 Web 영역에 대해 점검하기 위해 이번 문서에서 SSL Pinning Bypass 방법론을 소개한다.

2) SSL Pinning의 개념

앞서 설명한 것과 같이 가장 많은 공격 표면을 가지고 있는 Web 영역에 대한 진단 시 요청(Request), 응답(Response)에 대한 변조를 통해 취약점을 도출하므로 네트워크(패킷) 모니터링은 반드시 필요하다고 할 수 있다. 모니터링 방법은 크게 두 가지로 볼 수 있는데, HTTP 송수신을 처리하는 Native 라이브러리(URLConnection, NSMutableURLRequest, NSURL 클래스)를 통한 네트워크 통신 모니터링, SSL Pinning Bypass를 통한 프록시 설정으로 중간(MITM)에서 네트워크 통신 모니터링이 있다.

[그림 5] SSL Pinning Bypass (출처 : A Framework to Secure the Development and Auditing of SSL Pinning in Mobile Applications: The Case of Android Devices)

SSL Pinning은 클라이언트와 서버의 SSL 통신 과정에서 MITM 공격을 방지하기 위해 클라이언트 측에 신뢰할 수 있는 인증서를 저장하고, 서버의 인증서와 일치 여부를 검증한다. 시스템에 저장되어 있는 ‘신뢰할 수 있는 인증서 저장소’에 포함된 인증서를 통해 서버와 검증을 진행하며, 대부분의 Application은 사전에 정의된 인증서를 제외하고 모든 인증서를 거부하도록 구성된다.

SSL Pinning을 우회하기 위해 Application에서 사전에 정의된 인증서 목록을 변조하여 공격자가 만든 가짜 인증서를 신뢰하도록 설정해야 한다. 일반적으로 진단 시 Burp Suite에서 제공하는 CA(인증서)를 사용하며, Proxy 설정을 통해 Burp Suite가 가상의 라우터 역할을 하게 된다. 해당 과정을 요약하면 신뢰할 수 있는 인증서 저장소에 CA를 설치하고 [그림 5]의 5번 과정에서 가짜 인증서라도 “Match” 판단이 발생하도록 변조 후 해당 CA로 공격자(Burp Suite)와 통신하게 된다. 최종적으로 공격자는 클라이언트와 서버의 통신과정을 중계하여 요청, 응답 패킷을 가로챌 수 있다.

02. Flutter Application 인증서 검증 로직 분석

1) SSL/TLS 라이브러리

Flutter Framework로 개발된 앱에서 SSL/TLS 통신을 구현하기 위해 OkHttp와 같은 Java 기반 라이브러리를 사용할 수 있지만, 이 경우 해당 클래스를 Overloading 하는 방식으로 SSL Pinning Bypass이 가능하다. 하지만, 앞에서 소개한 Java 기반 라이브러리로 SSL/TLS 통신 구현 시 Platform을 구분하여 별도의 구현이 필요하므로 Flutter의 장점인 Cross-Platform 특성이 퇴색된다.

일반적으로 Flutter로 개발된 Application은 모든 Platform을 구분하지 않고 동작할 수 있도록 Google 社에서 개발한 오픈소스 SSL/TLS 라이브러리인 BoringSSL을 사용한다. 해당 라이브러리는 Github에 Google 공식 계정을 통해 공개되어 있으며 Flutter 엔진에 포함 되어있다. BoringSSL은 Native 라이브러리 C/C++로 구현되어 있어, Java 라이브러리를 우회하는 방식인 Overloading을 통한 SSL Pinning Bypass가 제한되어 Function Offset 추적을 통해 레지스터, 반환 값 조작을 통해 구현해야 한다.

2) 인증서 검증 로직 개요

[그림 6] 인증서 검증 로직 개념도

BoringSSL의 인증서 검증 로직 구현 개념도는 [그림 6]과 같다. 이번 분석의 핵심인 session_verify_cert_chain 함수는 [표 1]과 같은 동작을 수행한다.

[표 1] session_verify_cert_chain 함수의 동작 목록

Flutter 앱에서 Proxy를 설정 및 CA를 설치한 상태로 HTTPS 요청 시 발생하는 에러로그부터 상향식으로 분석을 진행하여 ssl_x509.cc 파일 내에 session_verify_cert_chain의 반환 값을 True로 변조하여 [표 1]의 검증 결과 반환 동작이 항상 성공하여 사용자 CA를 신뢰하도록 하는 것이 목표이다.

3) handshake.cc 파일 분석

앞서 설명한 것 같이 Flutter는 시스템 인증서 저장소를 신뢰하지 않고 앱 자체 인증서 저장소만을 사용하므로 인증서 검증 메소드에 대한 우회가 필요하다. Proxy 설정 후 Flutter로 https 요청을 전송하면 Logcat을 통해 [그림 7]과 같은 로그를 확인할 수 있다.

[그림 7] Flutter Certificate Fail Log

출력된 로그를 확인하면 handshake.cc 파일의 354번 라인에서 CERTIFICATE_VERIFY_FAILED가 발생한 것을 알 수 있다. handshake.cc는 BoringSSL 라이브러리에 존재하는 파일로 Flutter가 SSL/TLS 프로토콜을 구현하고 보안 연결을 하기 위해 Flutter 엔진에 포함되어 있다. 로그 내 라인 정보(354)의 위치를 바탕으로 로그가 발생하는 로직을 확인하면 [그림 8]과 같다.

[그림 8] handsake.cc 파일 내 session_verify_cert_chain 함수 호출

[그림 8]의 코드를 해석하면 386번 라인의 session_verify_cert_chain의 결과에 따라 ssl_verify_ok, ssl_verify_invalid 값을 ret 변수에 설정하고, 392번 라인에서 ret 변수가 ssl_verify_invalid 이면 CERTIFICATE_VERIFY_FAILED 로그를 출력한다. 해당 로그는 [그림 7]에서 확인한 로그와 일치하는 것을 알 수 있다. handshake.cc의 동작 과정을 확인하였으면 다음으로 session_verify_cert_chain 함수의 동작을 확인해야 한다.

Github에서 BoringSSL 라이브러리를 pull 하여 session_verify_cert_chain 함수를 찾은 결과, internal.h 헤더 파일에서 구조체를 선언하고, ssl_x509.cc 파일에서 ssl_crypto_x509_session_verify_cert_chain 함수를 session_verify_cert_chain 필드에 할당하여 초기화하는 것을 알 수 있다.

[그림 9] session_verify_cert_chain 선언 및 할당
[그림 10] session_verify_cert_chain 함수의 선언 타입

session_verify_cert_chain 함수는 bool 타입으로 선언되어 변조 Case가 True or False로 좁혀진다. 만약, 별도의 Object 등이 반환된다면 동적 디버깅을 통해 Register를 분석하여 반환 값을 결정해야 한다. 하지만, bool 타입은 Frida의 onLeave Callback 에서 Return Value를 반전시키면 다른 처리 흐름을 유도할 수 있다.

4) Session_Verify_Cert_Chain 함수 분석

지금까지의 분석을 통해 session_verify_cert_chain 함수의 Return을 변조하면 흐름이 변경된다는 것을 확인했으나, Frida를 통해 스크립트를 구성하기 위해선 해당 함수의 Offset을 알아야 한다.
먼저, Flutter Framework의 엔진 라이브러리인 libflutter.so 파일을 디컴파일러로 열어서 분석한다. 오픈소스 도구인 Ghidra를 사용하여 분석하는 예시를 제시할 것이며, IDA, JEB 등을 사용해도 무방하다.

[그림 11] libflutter.so 로드 주소(0x100000)

[STEP 1] : 해당 라이브러리의 포맷 헤더에서 메모리가 로드되는 주소를 확인해야 한다. 현재 Ghidra로 라이브러리를 열었을 때 코드 섹션이 메모리에 로드되는 지점은 0x100000 인 것을 알 수 있다.
[STEP 2] : 디컴파일러로 분석을 할 때, 함수명이 온전히 복원되지 않으므로 함수의 위치를 특정할 단서가 필요하다. session_verify_cert_chain 함수의 위치는 ssl_509.cc에서 단서를 얻을 수 있다.

[그림 12] ssl_509.cc 파일 내 문자열

[그림 12]와 같이 386번 라인에 “ssl_client”, “ssl_server” 두 개의 문자열이 소스코드상에 노출되어 있으며 이 문자열을 통해 디컴파일러에서 검색을 진행한다.

[그림 13] ssl_client 문자열 주소(0x1b15ea)

[그림 13]에서 ssl_client 문자열이 0x1b15ea 주소에 저장되어 있는 것을 알 수 있다. 일반적으로 문자열이 주소 그대로 참조되지 않으므로 해당 주소 상단에 참조를 위한 식별자가 존재하는지 확인해야 한다.

[그림 14] 문자열 식별자 주소(0x1b1000)

[STEP 3] : 식별자가 0x1b1000에 존재하고, ssl_client 문자열은 식별자로부터 0x5ea byte 만큼 떨어져 있다. [STEP 2]와 [STEP 3]에서 얻은 단서로 0x5ea를 Program Text 기능으로 검색한다.

[그림 15] 0x5ea 검색 시 참조하는 함수의 주소(0x65a58c)

0x65a584에서 x9 레지스터에 식별자의 주소인 0x1b1000을 adrp(상대 주소)로 지정한다. 0x65a58c에서 지정된 상대 주소에 0x5ea를 add하여 최종적으로 x9 레지스터가 ssl_client 문자열이 저장된 주소인 0x1b15ea를 가리키게 된다. 또한, 해당 값들이 FUN_0065a4ec 함수에서 동작하는 것을 알 수 있다. 해당 함수는 이름에서 알 수 있듯이 0x65a4ec 주소에서 시작한다.

04. Application 변조의 개념 및 방안

1) Application 변조 개념 및 사용 기술

SSL Pinning Bypass를 적용하기 위해선 Application을 변조해야 한다. Reverse Engineering을 통해 Symbol을 분석하여 동작 흐름을 변경할 수 있으나 보통, Decompile 도구를 통해 고급 언어로 변환하여 분석한다.

하지만, Flutter의 경우 엔진이 구현된 라이브러리를 로드 후 FlutterView를 통해 Flutter 코드가 표시되므로 Dart로 작성된 코드를 분석해야 하고, 대부분의 Decompile 도구에서 Dart 언어를 지원하지 않아 정적 분석을 통해 리패키징하는 방식은 비효율적이다.

따라서, 공개된 라이브러리 참고 및 동적, 정적 분석을 통해 Memory Patch를 진행하여 앱을 변조하는 방식이 더 효율적이다. 3단원에서 Ghidra를 통해 libflutter.so(Flutter 엔진) 파일을 정적 분석하여 메모리 주소와 함수의 Offset을 바탕으로 Frida를 사용하여 동적 분석 및 Memory Patch를 하는 방안에 대해 소개하겠다.

2) 변조방안 : Frida를 통한 Memory Patch

Frida는 실행 중인 앱의 동작을 분석하는 동적 분석 오픈 소스 도구로, Memory Patch, Function Overload, Hooking, Debug 기능 등을 제공한다. Android의 경우 Flutter와 같은 모듈을 변조하기 위해서 아래 과정과 같은 절차를 진행해야 한다.

[STEP 1] : 앱에서 사용 중인 모듈의 목록을 출력한다. 이때, Process.enumerateModules 함수를 사용하게 된다.
[STEP 2] : 사용 중인 모듈 중 Flutter가 존재하는지 확인한다.
[STEP 3] : Flutter 모듈이 메모리에서 로드된 시작 주소(BaseAddress)를 확인한다. 이때, 사용하는 함수는 Module.findBaseAddress, Process.findModuleByName 등이 있다.
[STEP 4] : Flutter 모듈의 BaseAddress에서 변조할 함수의 위치(Offset) 만큼 포인터를 이동한다.
[STEP 5] : STEP4의 메모리 주소로 함수를 후킹 한다. 함수 후킹을 위해 Interceptor.attach 함수를 사용한다.
[STEP 6] : onLeave Callback 함수에서 Return 되는 값을 변조한다.

[STEP 4]에서 변조할 함수의 Offset을 구하기 위해 Flutter의 소스코드를 분석할 필요가 있다. 우선 SSL Pinning Bypass를 위해 SSL Verification을 수행하는 로직을 확인해야 한다.

05. Frida 스크립트 작성

1) Flutter 라이브러리 확인

[그림 16] 라이브러리 사용 확인 함수

먼저, 앱에서 Flutter 라이브러리를 사용하는지 확인해야 한다. Android의 경우 “libflutter.so”, iOS의 경우 “Flutter”를 매개변수로 전달하면 사용 여부에 따라 True/False 를 반환한다. 만약, Flutter 라이브러리가 아닌 모든 모듈의 정보를 출력하고 싶다면, 8번 라인에 if 구문을 제거하면 된다.

[그림 17] 스크립트 실행 결과

2) BaseAddress 확인

[그림 18] 라이브러리 사용 확인 함수

[그림 16]에서 작성한 함수(moduleExist)의 결과에 따라 함수가 동작하며, findModuleByName 으로 모듈의 BaseAddress(mInfo.base)를 확인할 수 있다. 물론, moduleExist에서 사용한 함수인 enumerateModules도 onMatch에서 Callback으로 전달되는 객체에서도 확인이 가능하다.

[그림 19] 스크립트 실행 결과

모듈이 메모리에 로드되는 주소는 앱이 실행될 때마다 매번 달라진다. 추가적으로 findModuleByName 함수가 반환하는 객체(모듈 정보)에서 사용가능한 속성은 [표 2]와 같이 존재한다.

[표 2] 사용가능한 속성

3) 함수 Return Value 변조

[그림 20] Return Value 변조 함수

일련의 함수들을 한 번에 진행하기 위해 moduleHook 함수의 매개변수(1번 라인)와 retPatch 함수를 호출하는 7번 라인을 추가했다. loadOffset 매개변수에 디컴파일러를 통해 확인했던 코드 섹션이 메모리에 로드되는 지점인 0x100000 전달해야 한다. 세 번째 매개변수인 offset에 마찬가지로 디컴파일러로 확인한 session_verify_cert_chain의 Offset(0x65a4ec)이 들어가야 한다. 마지막 매개변수는 변조할 Return Value이다. 0은 실패, 1은 성공이므로 session_verify_cert_chain 함수의 결과가 항상 성공하도록 1을 전달한다.

retPatch 함수의 13번 라인에서 BaseAddress에 Offset을 더한 값에서 loadOffset을 뺄셈하게 된다.
결과적으로 funcAddress 변수에 현재 실행 중인 앱의 session_verify_cert_chain 함수 위치가 저장된다. onLeave Callback(20번 라인)에서 Return Value를 1로 고정하게 된다.

[그림 21] Return Value 변조 결과
[그림 22] 변조 후 요청 성공

session_verify_cert_chain 함수의 결과를 변조하기 전엔 Burp Suite에서 요청에 대한 패킷이 Intercept 되지 않고, 앱에서 “HTTPS: ERROR” 메시지를 반환 및 Logcat 에서도 에러로그가 발생했지만, 변조 이후 패킷이 정상적으로 Intercept 된다. SSL Pinning Bypass가 완료된 것이며, 이후 Web Application 영역에 대한 진단을 진행할 수 있다.

06. 보안조치 및 강화방안

동적 분석을 위해선 LLDB, GDB, Frida를 사용하게 된다. 하지만, 해당 디버거들은 ROOT 권한을 사용 가능해야 정상적으로 동작하므로 Application 내 디바이스 임의 개조(루팅, 탈옥) 탐지 로직을 구현하여 방지할 수 있다. 추가적인 방안으로 USB Debugging 옵션을 점검하고 Frida, Xposed 등의 프로세스가 존재하면 강제 종료시키는 로직을 구현하여 안전한 Application 사용 환경을 제공해야 한다.

또한, Application의 무결성 검증(Hash, Sign 검증)을 통해 위변조된 앱에서 정상적인 서비스가 제공되지 않도록 제한해야 한다. Native 코드와 상호 작용할 수 있는 Flutter 플러그인을 통해 해당 로직들을 구현하는 방법도 있으므로 이러한 환경의 앱을 마주했을 때 우회 및 대응 방안을 제시하는 방법을 숙지해 둘 필요가 있다.

SSL Pinning Bypass 이후 webkit.WebView, URLConnection, NSMutableURLRequest 등의 클래스를 후킹 하여 특정 URL 접근 시 다른 페이지로 Redirect 하는 스크립트를 구성하여 Rooting Device 에서의 취약점 시나리오를 구성할 수 있다. 물론, Web Application 영역에서 발현되는 취약점과 연계하여 다양한 시나리오로 연계할 수 있을 것이다.

지금까지 설명한 공격벡터에 대한 대응 방안 및 우회 방법에 대한 보다 상세한 내용은 [표 3]과 같이 자사 홈페이지에 공개되어 있으니 해당 자료들을 참고하여 Application의 보안성을 강화할 수 있는 계기가 되기를 기대해 본다.

[표 3] 참고 컨텐츠

07. 참고자료

1) Cross-platform mobile frameworks used by software developers worldwide from 2019 to 2022
https://www.statista.com/statistics/869224/worldwide-software-developer-working-hours/
2) Flutter architectural overview
https://docs.flutter.dev/resources/architectural-overview
3) OWASP Mobile Top 10 - Comparison between 2016 and 2023
https://owasp.org/www-project-mobile-top-10/
4) A Framework to Secure the Development and Auditing of SSL Pinning in Mobile Applications: The Case of Android Devices
https://www.mdpi.com/1099-4300/21/12/1136
5) NVISOsecurity - flutter-testapps
https://github.com/NVISOsecurity/blogposts/tree/master/flutter-testapps
6) Intercept Flutter traffic on iOS and Android (HTTP/HTTPS/Dio Pinning)
https://blog.nviso.eu/2022/08/18/intercept-flutter-traffic-on-ios-and-android-http-https-dio-pinning/
7) Intercepting Flutter traffic on Android (ARMv8)
https://blog.nviso.eu/2020/05/20/intercepting-flutter-traffic-on-android-x64/
8) Google / BoringSSL
https://github.com/google/boringssl