집 앞에 큰 GS 마트에서 결제를 하려고 하니 우리동네GS 애플리케이션을 통해 결제를 하면
특정 제품을 할인해 주는 이벤트를 하고 있었다.
그래서 처음으로 우리동네GS를 설치하였다.
그 후, 직업병인지는 모르겠지만, 해당 애플리케이션은 어떻게 구성되어 있나? 싶어서
패킷을 한번 캡쳐해서 보려고 Burp로 패킷을 보려고 했지만 아무 패킷이 잡히지 않았다.
그래서 APK를 아래와 같이 다운로드 후 추출하여 살펴보았다.

그리고 습관적으로 lib 폴더를 보았다.

여기서 libflutter.so 파일을 보고,
왜 Burp에서 패킷이 안 잡혔는지 바로 이해가 되었다.
그럼 자, Flutter가 뭘까?
Flutter는 구글이 개발한 오픈소스 멀티플랫폼 앱 개발 프레임워크이다.
하나의 코드(Dart 언어 사용)로 안드로이드, iOS, 웹, 데스크톱 애플리케이션을
동시에 만들 수 있는 것이 가장 큰 특징이다.
따라서 하나의 코드를 제대로 잘 만들어 놓으면 위의 플랫폼 모두 하나의 코드로
동시에 만들 수 있다는 것이다.
그럼 다시 돌아와서,
모바일 보안 점검에서 가장 기본이 되는 작업은 애플리케이션과 서버 사이의
HTTPS 트래픽을 평문으로 살펴보는 것이다.
하지만, Flutter로 작성된 앱은 이 두 가지 방법 모두 통하지 않는다.
그 이유는 크게 2가지가 있다.
Flutter 자체가 시스템에서 설정한 프록시를 무시하기 때문이고
Flutter는 OS의 신뢰 저장소를 쓰지 않고 자체 CA 목록을 정적으로 내장하기 때문에
일반적으로는 Burp로 바로 패킷을 캡처할 수 없다.
일단 애플리케이션은 사용자가 WIFI 설정에서 프록시로 IP:PORT로 지정하면
안드로이드 OS는 그 값을 자바 프레임워크 계층에 아래와 같이 조회가 가능하도록 설정한다.
시스템 프로퍼티 http.proxyHost / http.proxyPort
java.net.ProxySelector.getDefault() 가 이 값으로 채워짐
ConnectivityManager.getDefaultProxy() 로도 조회 가능
자바단이기 때문에 이 설정은 커널이 패킷을 강제적으로 프록시로 보내는 것이 아니다.
자바단은 클라이언트단 위쪽 레벨이기 때문이다.
따라서 단지 "프록시 설정이 이렇게 되어 있다"라고 알려줄 뿐이고,
실제로는 그 프록시로 요청을 보낼지는 각 HTTP/HTTPS 라이브러리의 코드가 결정한다.
[일반 앱]
Retrofit / OkHttp / HttpURLConnection / ....
│ (내부적으로)
▼
ProxySelector.getDefault() ← 안드로이드가 시스템 프록시 값으로 채워둠
│ "프록시 = IP:PROT 이네"
▼
그 프록시로 연결하고 CONNECT api.example.com:443 을 전송
│
▼
Burp 도착
즉 일반 애플리케이션이 프록시 설정으로 쉽게 패킷이 잡히는 이유는
OS가 강제해서가 아니라, 그 앱이 쓰는 HTTP 라이브러리가 "시스템 프록시를 조회하는 코드"를
내장하고 있기 때문이다.
그럼 왜 Flutter는 왜 그 코드를 실행하지 않는가????
Flutter는 안드로이드 자바 계층을 아예 거치지 않는다.
Dart 코드에서 package:http를 쓰든 package:dio 를 쓰든,
무조건 dart:io의 HttpClient로 모이고, 이것은 Flutter 엔진(C++) 안에서 동작한다.
여기서 dart:io의 HttpClient에 일반적인 애플리케이션에서 조회한 시스템 프록시를 읽는 코드가 없으므로,
위에서 어떤 패키지를 써도 프록시를 안 탄다.
즉 시스템 프록시가 "무시" 되는 것이 아니라 사실 애초에 조회 대상도 아니고 조회할 기능이 없는 것이다.
[Flutter 앱]
Dart 코드 (dio / http)
│
▼
dart:io HttpClient.findProxy ← 기본값 "DIRECT"
│ (프록시 조회 코드가 아예 없음)
▼
DNS로 얻은 진짜 서버 IP 로 곧장 connect()
│
▼
Burp 안 거침 (진짜 서버로 바로 감)
그럼 Flutter는 왜 이렇게 설계되었을까??
바로 원소스로 크로스 플랫폼에 설치하기 위해서이다.
시스템 프록시는 안드로이드 자바 프레임워크(java.net ,ProxySelector, ConnectivityManager, ....)이다.
Flutter는 크로스 플랫폼과 성능을 위해 이 자바 세계를 일부러 우회하고 자체 I/O 스택(Dart VM + 엔진)으로
동작한다.
그래서 안드로이드에만 있는 프록시 배관과 연결되는 다리(bridge)가 처음부터 없었던 것이다.
이제 또 2번째 문제가 있다.
Flutter는 시스템 CA 인증서를 신뢰하지 않는다.
안드로이드에서는 신뢰 저장소가 2개가 있다.
시스템 저장소와 사용자 저장소다.
안드로이드 7(API 24)+ 부터는 앱이 network_security_config로 명시하지 않는 한
사용자 CA를 기본 신뢰하지 않기 때문에 대부분
시스템 저장소로 우리가 흔히 아는 /system/etc/security/cacerts 경로에 인증서를 넣는 이유다.
이렇듯 자바 계열 애플리케이션의 TLS는 ART(자바 런타임)의 X509TrustManager 가 담당하며,
서버 인증서 체인을 검증한다.
[일반 앱]
서버 인증서 → ART X509TrustManager
→ 안드로이드 시스템+사용자 CA 저장소와 대조 Burp CA를 사용자 저장소에 설치
→ Burp가 서명한 가짜 인증서가 "신뢰됨" → 통과
그래서 SSL 피닝을 우회할 때 한 번씩 보이는 것이 X509TrustManager 이다.
어쨌든 안드로이드 저장소와 인증서를 검증하는 메소드를 조작/설정하는 선에서 해결된다.
Flutter/Dart의 TLS는 libflutter.so 에 정적 링크된 BoringSSL 이 수행한다
그리고 이 BoringSSL이 신뢰하는 루트 CA 목록은
안드로이드 시스템/사용자 저장소가 아니라,
Dart SDK/Flutter 엔진에 컴파일 타임에 박아 넣은 Mozilla NSS 루트 CA 번들이다.
[Flutter 앱]
서버 인증서 → libflutter.so 내부 BoringSSL
→ libflutter.so 에 *내장된* Mozilla 루트 목록과 대조 Burp CA를 안드로이드 저장소에 설치
→ Flutter는 그 저장소를 *읽지 않음* → 무의미
Burp가 서명한 인증서 = 내장 루트에 없는 CA → 체인 검증 실패 → 반환값 0 → 핸드셰이크 중단
즉 Burp CA를 안드로이드에 아무리 잘 설치해도 소용없다.
Flutter의 신뢰 목록은 애플리케이션 바이너리 안에 있고,
그 목록은 OS 저장소와 완전히 분리되어 있기 때문이다.
따라서 일반 애플리케이션에서 패킷을 잡던 방법이 먹히지 않았던 것이다.

그래서 Flutter로 만들어진 애플리케이션의 패킷을 분석하기 위해서는 libflutter.so 파일을 분석하여
내부 인증서 검증 함수를 직접 우회하는 수밖에 없다.
그렇게 분석을 마치면 아래와 같이 Flutter로 만들어진 애플리케이션도
Burp로 패킷을 캡처할 수 있다.

'개인 프로젝트' 카테고리의 다른 글
| 42. 코레일(Korail) + 고속 버스 매크로 - 표의 민족 (0) | 2026.09.15 |
|---|---|
| 41. 고속버스 티머니 매크로 (16) | 2026.09.10 |
| 40. 모바일 게임 분석: 우르르 용병단(Unity IL2CPP-LIAPP) (0) | 2026.06.15 |
| 39. 모바일 게임 분석: 드래곤 슬레이어 키우기(Unity IL2CPP - DoveRunner) (0) | 2026.06.10 |
| 38. 모바일 게임 분석: 운빨존많겜 (Unity IL2CPP-AppGuard) (5) | 2026.06.07 |









































































































































