본문 바로가기
보안/iOS

[iOS] 플러터(Flutter) HTTPS 프록시 연결

by stephen26 2022. 2. 7.

지난 글에서 플러터(Flutter) 앱의 프록시 설정에 대해서 다룬 바 있다. 하지만 해당 설정 만으로는 HTTPS 패킷을 캡처할 때 오류가 발생한다. 

 

[iOS] 플러터(Flutter) 앱 프록시 설정

Flutter 앱은 시스템 프록시를 인식하지 않으며 자체 인증서 저장소를 사용하기 때문에 시스템 내 Wi-Fi 설정에서 프록시를 설정해도 의미가 없고 설정을 통해 인증서를 설치해도 유효성이 검증되

noasand.tistory.com

 

플러터가 사용하는 다트는 시스템 내 CA 인증서 저장소 대신 앱 자체 인증서 저장소를 사용하기 때문에 프레임워크 내 SSL 인증서 검증을 실시하는 함수에 대한 후킹을 통해 검증 절차 우회가 필요하다.

 


프록시 설정 이후 HTTPS 통신 발생 시 발생하는 'SSL handshake' 오류 내용을 확인하고 앱 내 어떠한 라이브러리에서 발생하고 있는지  분석하도록 한다.

 

위 Debugging 로그를 살펴보면 'SSL handshake' 오류는 구글의 SSL 라이브러리인 BoringSSL의 'handshake.cc' 파일에서 발생하고 있음을 확인할 수 있다.

 

오픈소스인 BoringSSL의 handshke.cc 내 오류 지점은 위와 같다. 해당 함수를 더 자세히 살펴보도록 하자.

 

해당 함수는 'ssl_x509.cc' 파일 내 선언되어 있으며 함수 명은 'ssl_crypto_x509_session_verify_cert_chain( )' 이다. 해당 함수가 플러터 앱에서 SSL 인증서의 유효성을 검증한다.

 

그리고 이 함수는 bool 타입으로 인증서의 유효성 검증에 성공하였을 땐 '1'을 검증에 실패했을 때는 '0'을 반환한다. 따라서 이 함수의 Offset을 찾아 함수가 호출될 때 반환 값을 변조하면 인증서 검증을 우회할 수 있다.

 

BoringSSL 라이브러리는 Flutter 프레임워크 파일에 내장되어 있다. 프록시 연결 대상 앱의 .ipa 파일에서 Flutter 파일을 추출하고 해당 파일에서 SSL 검증 함수의 offset을 찾도록 한다.

 

앞서 BoringSSL 오픈소스 분석 시 우리가 찾고자 하는 'ssl_crypto_x509_session_verify_cert_chain( )' 함수 내부에는 'ssl_client' 문자열이 하드코딩 되어 있음을 확인한 바 있다. 이를 바탕으로 해당 값을 참조하고 있는 함수를 찾아낸다.

 

그리고 이렇게 찾아낸 함수의 초기 Byte 패턴을 추출해낸다. 해당 함수의 offset을 가져와 직접 참조해도 되지만 패턴 검색을 통하면 새로운 앱을 점검할 때 마다 세팅할 수고가 줄어든다.

 

  var m = Process.findModuleByName("Flutter");
  var ragnes = "FF 03 05 D1 FC 6F 0F A9 F8 5F 10 A9 F6 57 11 A9 F4 4F 12 A9 FD 7B 13 A9 FD C3 04 91 08 0A 80 52 48 00 00 39"
  Memory.protect (m.base,m.size,"rwx");
  Memory.scan (m.base, m.size, ragnes, {
    onMatch: function (address, size) {
      console.log ("\n[+] 'ssl_crypto...' found match at: " + address.toString () + "(" + size.toString ()+")");
      Interceptor.attach (address, {
        onEnter: function(args) {
          console.log("\n[*] Hooking func is called")
        },
        onLeave: function(retval) {
          console.log ("[-] Original Return Value: " + retval)
          retval.replace (0x1);
          console.log ("[-] New Return Value: " + retval)
        }
      });
    },
    onError: function (err) {
      console.log ("[!] Exception: " + err.message);
    },
    onComplete: function () {
      console.log ("[*] Instance Finished");
    }
  });

그리고 Frida를 이용하여 해당 함수의 반환 값을 변조하도록 한다. 코드 전체 구조를 간단히 설명하자면 먼저 함수의 초기 Byte 패턴을 통해 offset을 찾아내고 인증서 검증에 실패했을 때 반환되는 값 '0'을 '1'로 변조하는 기능을 지닌다.

 

$ frida -Uf [App Name] -l sslBypass.js --no-pause

코드가 정상적으로 동작했다면 위와 같은 결과를 확인할 수 있다. 그리고 다시 HTTPS 통신을 발생시키면 패킷이 정상적으로 Intercept 됨을 확인할 수 있다.

 

댓글