프리다 활용 (3)

옥영진·2020년 9월 19일
0

Frida

목록 보기
10/11

SSL Pinning 우회

SSL Pinning 이란?

SSL Pinning은 클라이언트 측에서 사용하는 기법으로, SSL 통신에서 취약하다고 알려져 있는 중간자 공격을 방지할 수 있다. 클라이언트 측에 신뢰할 수 있는 인증서들을 저장하고, 이는 이후 실제 통신 과정에서 서버가 제공하는 인증서와 비교하는 데 사용된다. 만약 이 과정에서 미리 저장된 인증서와 서버가 제시한 인증서가 일치하지 않는다면, 연결은 중단될 것이고, 유저의 정보가 서버로 전송되지 않을 것이다. 이런 방식으로 클라이언트 측에 설치되는 다른 인증서로는 통신이 되지 않고 서버에서 제공하는 인증서로만 통신할 수 있게 하는 것이다.
Burp Suite와 같은 프록시 도구를 사용해서 서버와 통신하는 패킷을 중간에 가로챌 수 있지만, 이 SSL Pinning이 적용된 클라이언트에서 보내는 패킷은 중간에서 패킷을 가로챌 수 없다. 따라서 이 SSL Pinning을 어떻게 우회하는지 알아보자.

사전 준비

일단 Burp Suite부터 설치하자. 최신 Community Edition을 설치하면 된다. Burp Suite 설치 후, 녹스 앱 플레이어와 같이 안드로이드에서 프록시 연결하는 방법은 구글링을 통해 바로 알아낼 수 있으므로 이를 참고하자. 프록시 설정 후 암호화 통신을 하는 패킷도 확인하기 위해 Burp Suite 인증서를 설치하자. 안드로이드에서 브라우저 앱을 킨 후 http://burp 에 접속하여 우측 상단에 있는 CA Certificate 를 클릭하여 인증서를 다운로드 받는다.

cacert.der 이라는 파일이 다운로드 될텐데, 파일 관리자 앱을 통해 다운로드 받은 디렉토리로 들어가서 해당 파일의 확장자를 .cer 로 변경한다.

이제 다운로드 받은 증명서를 설치하기 위해 설정 - 보안 - 자격증명 저장소 - SD 카드에서 설치 를 통해 증명서를 설치한다.

SD 카드에서 설치 를 누르면 저장소 탐색기가 나타날텐데, cacert.cer 파일을 선택하면 다음과 같은 화면이 나올텐데 증명서 이름은 원하는 것으로 설정하고 확인을 눌러 설치한다.

설치 후에 구글에 접속하고나서 Burp Suite를 확인해보면 https 암호화 통신을 하는 패킷도 확인이 가능해진다.

SSL Pinning이 적용된 앱을 설치해 보자.

https://github.com/smuldr/android-ssl-pinning

위 url 에서 안드로이드 소스를 다운로드 받아 안드로이드 스튜디오를 통해 apk 파일로 생성한 뒤 이를 녹스 앱 플레이어에 설치한다.

해당 앱을 설치했을 때의 화면이다. URL은 총 세 개가 있고, 그 아래 Use Pinned SSL connection 이라고 체크박스가 있는데 여기를 체크 한 후 Submit request 버튼을 누르면 SSL Pinning이 적용된 암호화 통신을 요청하고, 체크를 해제하면 적용되지 않은 암호화 통신을 시도한다. URL 목록은 총 세 개가 있는데 그 중 하나는 https 가 아닌 http 프로토콜이 적용되어 SSL Pinning을 적용하든 하지 않든 성공적으로 통신할 수 있음을 확인할 수 있다.

하지만 나머지 두 개의 URL은 SSL Pinning 적용 후 요청을 보내면 Connection refused 라는 통신 실패 메세지가 표시 된다. SSL Pinning을 적용하지 않고 Burp Suite를 이용하여 패킷을 인터셉트 해보자.

체크박스를 해제하고, Burp Suite 에서는 Intercept is on 상태에서 요청을 보내면 위 캡처화면처럼 https 통신 패킷 역시 인터셉트가 가능하다. 이번에는 SSL Pinning이 적용된 상태에서의 요청 패킷을 인터셉트 시도해 보자.

Intercept is on 상태이지만 패킷을 잡지 못하고 앱에서는 연결 거부 메세지가 나타났다. 이처럼 SSL Pinning이 적용된 암호화 통신 패킷은 프록시 도구로 그 패킷을 분석할 수가 없다. 이제 이 SSL Pinning을 우회해 보자.

프리다 코드 공유

https://codeshare.frida.re/

위 URL은 프리다 코드들을 공유하고 있는 사이트이다. 이 중에서 SSL Pinning 우회 프로젝트를 이용해 볼 것이다.

인기있는 프로젝트 목록 중에 SSL Pinning 우회 프로젝트가 있는데 해당 프로젝트 페이지에 접속하면 코드와 바로 사용해 볼 수 있는 명령어가 나와있다.

일단 설치했던 앱의 패키지 이름을 알아낸 뒤, 위 명령어에서 -U 옵션과 --no-pause 옵션을 더 붙여서 실행해 보자.

명령어를 실행하니 에러가 출력된다. 확인해보면 /data/local/tmp/cert-der.crt 라는 파일이 존재 하지 않는다고 나와 있다. 한 번 코드를 살펴 보자.

코드 중간에 /data/local/tmp/cert-der.crt 라는 인증서 파일을 가져오는 것을 알 수 있다. 따라서 Burp Suite 인증서를 해당 디렉토리 내에 같은 이름으로 복사하자.

이전에 다운로드 받았던 Burp Suite 인증서를 위 디렉토리 내에 복사한다. 복사한 후 다시 실행해 보자.

이번에도 에러가 출력되었는데 끝에 보면 Permission denied 라고 되어 있다. 해당 파일에 대한 권한 설정을 해주면 된다.

그 후에 명령어를 실행하면 성공적으로 SSL Pinning 우회 코드가 삽입되었다.

이제는 SSL Pinning을 적용한 뒤 통신을 요청해도 해당 패킷을 Burp Suite에서 인터셉트가 가능하다.

SSL Pinning 우회 코드 작성

일단 해당 앱을 jadx 로 디컴파일하여 로직을 확인해 보자.

PinnedSSLDemoActivity 클래스 내의 onSubmit() 메소드가 호출되었을 때, SSL Pinning 사용 체크박스에 체크가 되어 있다면 sslContext 라는 변수에 getPinnedSSLContext() 메소드의 반환값이 저장된다. 체크가 되어 있지 않다면 기본값인 NULL이 저장된다. 이 sslContext 의 값에 따라 SSL Pinning이 적용된 암호화 통신을 할 지 안할지 결정하는 것을 알 수 있다. 따라서, sslContext 의 값을 결정하는 getPinnedSSLContext() 메소드의 로직을 확인해 보자.

해당 메소드의 반환값은 SSLContext 객체이고, 이 객체는 PinnedSSLContextFactory 클래스의 getSSLContext() 메소드의 반환값이다. 이 메소드의 인자로 전달되는 input 값은 load-der.crt 라는 파일을 로드한 것인데, 이 파일은 Resources 디렉토리 내에 assets 디렉토리에 존재한다.

물론 이 load-der.crt 를 Burp Suite 인증서로 교체한 후 다시 apk 파일로 리패키징 후 사이닝 과정을 거치면 SSL Pinning 우회 과정 없이 통신 패킷을 Burp Suite로 인터셉트할 수 있지만, 이는 해당 앱이 무결성 검증(앱을 위변조 못하도록)을 하지 않았을 때만 가능하다.

그렇다면 이 파일을 가져온 input 값을 인자로 전달 받는 getSSLContext() 메소드의 로직을 살펴 보자.

리턴문을 보니 인자값으로 전달 받은 input 값이 loadCertificate() 메소드의 인자 값으로 전달되고, 이 메소드의 반환값이 createKeyStore() 메소드의 인자 값으로, 그 반환값이 다시 createTrustManager() 메소드의 인자 값으로, 마지막으로 그 반환값이 createSSLContext() 의 인자 값으로 전달되어 반환되는 값을 최종적으로 반환하고 있다. 정리하자면,

[안드로이드 SSL 통신 시 클라이언트 인증서 사용 순서]
1. 클라이언트 인증서 로드
2. 클라이언트 인증서를 이용하여 KeyStore 생성
3. 클라이언트 인증서를 이용하여 TrustManager 생성
4. KeyStore, TrustManager를 이용하여 SSLContext 생성

가장 먼저, 인증서를 로드하기 위해 호출되는 loadCertificate() 메소드의 로직을 확인해 보자.

CertificateFactory 라는 클래스의 getInstance() 메소드를 통해 반환되는 인스턴스의 generateCertificate() 메소드의 인자 값으로 인증서 파일이 전달되어 반환되는 값을 리턴하고 있다. 이 CertificateFactory 클래스는 자바에서 제공하는 기본 클래스로, java.security.cert.CertificateFactory 이다. 이 로직에서 중요한 것은 인자 값으로 전달 받은 input 변수인데, 이 변수값을 조작하여 assets 내에 있는 인증서가 아닌 이전에 다운로드 받은 Burp Suite 인증서 파일로 바꿔서 SSL Pinning을 우회해 보자.

setTimeout(function() {
  Java.perform(function() {
    var CertificateFactory = Java.use("java.security.cert.CertificateFactory");
    var cf = CertificateFactory.getInstance("X.509");
    
    var FileInputStream = Java.use("java.io.FileInputStream");
    var fi = FileInputStream.$new("/data/local/tmp/cert-der.crt");
    var ca = cf.generateCertificate(fi);
  })
})

FileInputStream 클래스를 이용하여 Burp Suite 인증서를 로드한 후에 generateCertificate() 메소드의 인자 값으로 전달하였다.

이제 두 번째로 KeyStore를 생성하기 위해 호출하는 createKeyStore() 메소드의 로직을 확인해 보자.

KeyStore는 인증서 증명을 위한 키 값들을 저장한 것으로, 이 로직은 그대로 사용하면 된다. 마지막에 setCertificateEntry() 메소드를 호출할 때 전달하는 인자 값으로 위에서 생성한 증명서인 ca 를 전달한다.

setTimeout(function() {
  Java.perform(function() {
    var CertificateFactory = Java.use("java.security.cert.CertificateFactory");
    var cf = CertificateFactory.getInstance("X.509");
    
    var FileInputStream = Java.use("java.io.FileInputStream");
    var fi = FileInputStream.$new("/data/local/tmp/cert-der.crt");
    var ca = cf.generateCertificate(fi);
    
    var KeyStore = Java.use("java.security.KeyStore");
    var keyStoreType = KeyStore.getDefaultType();
    var keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    keyStore.setCertificateEntry("ca", ca);
  })
})

세 번째로 TrustManager를 생성하는 createTrustMangager() 메소드의 로직을 확인해 보자. 해당 메소드의 로직 역시 그대로 작성하면 된다.

setTimeout(function() {
  Java.perform(function() {
    var CertificateFactory = Java.use("java.security.cert.CertificateFactory");
    var cf = CertificateFactory.getInstance("X.509");
    
    var FileInputStream = Java.use("java.io.FileInputStream");
    var fi = FileInputStream.$new("/data/local/tmp/cert-der.crt");
    var ca = cf.generateCertificate(fi);
    
    var KeyStore = Java.use("java.security.KeyStore");
    var keyStoreType = KeyStore.getDefaultType();
    var keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    keyStore.setCertificateEntry("ca", ca);
    
    var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");
    var tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
    var tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
    tmf.init(keyStore);
    var tmf_get = tmf.getTrustManagers();
  })
})

마지막으로 createSSLContext() 메소드를 확인해 보자.

이 메소드를 호출할 때 위에서 생성한 TrustManager 배열 객체를 전달하고, 해당 객체를 SSLContext 클래스의 init() 메소드의 두 번째 인자 값으로 전달한다. 이 init() 메소드를 재작성하여 위에서 생성한 tmf_get 변수를 인자 값으로 전달하도록 하자. 물론 위에서 TrustManager를 생성하는 코드를 작성했을 때 처럼 init() 메소드를 직접 호출해도 되지만, 재작성을 통해 다른 로직은 원래 작성되어 있던 코드대로 진행하도록 할 것이다.

setTimeout(function() {
  Java.perform(function() {
    var CertificateFactory = Java.use("java.security.cert.CertificateFactory");
    var cf = CertificateFactory.getInstance("X.509");
    
    var FileInputStream = Java.use("java.io.FileInputStream");
    var fi = FileInputStream.$new("/data/local/tmp/cert-der.crt");
    var ca = cf.generateCertificate(fi);
    
    var KeyStore = Java.use("java.security.KeyStore");
    var keyStoreType = KeyStore.getDefaultType();
    var keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    keyStore.setCertificateEntry("ca", ca);
    
    var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");
    var tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
    var tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
    tmf.init(keyStore);
    var tmf_get = tmf.getTrustManagers();
    
    var SSLContext = Java.use("javax.net.ssl.SSLContext");
    SSLContext.init.implementation = function(a, b, c) {
      SSLContext.init.call(this, a, tmf_get, c);
    }
  })
})

성공적으로 SSL Pinning 우회를 하여 Burp Suite로도 암호화 통신 패킷을 인터셉트할 수 있었다.

profile
안녕하세요 함께 공부합시다

0개의 댓글