22 Kasım 2015

Android SSL Pinning (Certificate Pinning) Hakkında Herşey - Bölüm 2

Android SSL pinning makalemizin birinci bölümünde SSL altyapımızı oluşturmuştuk. Bu bölümde Android uygulamamızda SSL pinning işlemini uygulayacağız.



1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package com.btrisk.httpsclient;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyStore;
import java.security.SecureRandom;

import android.content.Context;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

public class MainActivity extends AppCompatActivity {

    String strJson = "{\"prm1\":\"deneme1\",\"prm2\":\"deneme2\"}";
    TextView output = null;
    StringBuilder sb = new StringBuilder();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        output = (TextView)findViewById(R.id.textView1);

        Button btn = (Button) findViewById(R.id.button);
        btn.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                makePostRequest();
            }
        });
    }

    private void makePostRequest() {

        (new Thread(new Runnable() {

            @Override
            public void run() {
                BufferedReader reader=null;
                String yanit=null;
                try {

                    char[] passphrase = "btrisk".toCharArray();
                    KeyStore ksTrust = KeyStore.getInstance("BKS");
                    Context context = getApplicationContext();

                    ksTrust.load(context.getResources().openRawResource(R.raw.btrmobiletruststore), passphrase);
                    TrustManagerFactory tmf = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
                    tmf.init(ksTrust);

                    SSLContext sslContext = SSLContext.getInstance("TLS");
                    sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());

                    HttpsURLConnection urlConn = null;
                    URL url = new URL("https://btrmobile/servis.php");
                    urlConn = (HttpsURLConnection)url.openConnection();
                    urlConn.setSSLSocketFactory(sslContext.getSocketFactory()); //BURASI SERTİFİKA KONTROLÜNÜN YAPILDIĞI SATIR

                    urlConn.setDoOutput(true);
                    OutputStreamWriter wr = new OutputStreamWriter(urlConn.getOutputStream());
                    wr.write( strJson );
                    wr.flush();

                    reader = new BufferedReader(new InputStreamReader(urlConn.getInputStream()));
                    StringBuilder sb = new StringBuilder();
                    String line = null;
                    while((line = reader.readLine()) != null)
                    {
                        sb.append(line + "\n");
                    }
                    yanit = sb.toString();
                }
                catch(Exception ex)
                {
                    Log.d(">>>>>>>>>>>>>>>","HTTP isteginde hata olustu");
                    Log.d(">>>>>>>>>>>>>>>", "exception", ex);
                }
                finally
                {
                    try
                    {
                        reader.close();
                    }
                    catch(Exception ex) {}
                }
                try {
                    JSONArray jsonYanit = new JSONArray(yanit);

                    for(int i=0;i<jsonYanit.length();i++){
                        JSONObject json_data = jsonYanit.getJSONObject(i);
                        sb.append("istek parametresi 1: " + json_data.getString("istek_prm1") + "\n");
                        sb.append("istek parametresi 2: " + json_data.getString("istek_prm2") + "\n");
                    }
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            output.setText(sb.toString());
                        }
                    });

                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        })).start();

    }

}




Burada basit bir Android uygulaması oluşturduk, yukarıda da oluşturduğumuz tek aktivitenin kodlarını görüyorsunuz. Tamlığı sağlamak amacıyla aşağıda kullanacağımız view’ın XML kodunu ve manifest dosyasını da paylaşıyorum:


1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Yanit Alani"
        android:id="@+id/textView1"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="64dp" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Istek Gonder"
        android:id="@+id/button"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="163dp" />
</RelativeLayout>


Manifest dosyası


1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.btrisk.httpsclient" >
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


Manifest dosyasında bahsedilmeye değer tek konu uygulamaya ağ erişim izni alabilmek için eklediğimiz android.permission.INTERNET erişim satırı.

Ekran Layout dosyamızda ise sadece bir Text View ve onun hemen altında da bir düğme bulunuyor.


Artık MainActivity java kodumuzu açıklamaya başlayabiliriz.


String strJson = "{\"prm1\":\"deneme1\",\"prm2\":\"deneme2\"}";
TextView output = null;
StringBuilder sb = new StringBuilder();

Bu kod bölümünde sunucuya göndereceğimiz JSON mesajını oluşturuyoruz, view’ımızdaki TextView ile eşleştireceğimiz bir TextView değişkeni tanımlıyoruz ve gelen yanıtı işlemek üzere bir StringBuilder nesnesi oluşturuyoruz.


@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    output = (TextView)findViewById(R.id.textView1);

    Button btn = (Button) findViewById(R.id.button);
    btn.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
            makePostRequest();
        }
    });
}

onCreate metodu Activity yüklendiğinde çalışacak olan metod. Bu metod içinde view Layout’umuzu atıyoruz, ekranımızda yer alan Text View’ı output adlı değişkenle ilişkilendiriyoruz Ayrıca ekranımızdaki düğmeyi de btn adlı değişkenle ilişkilendiriyoruz.

Bu bölümdeki en önemli kod düğmemizin onClick olayında çalışacak olan metodun ismini atadığımız bölüm. Burada onClick adında bir metodu tanımlıyoruz ve bu metodun içinde de makePostRequest metodunu çağırıyoruz.


private void makePostRequest() {

    (new Thread(new Runnable() {

        @Override
        public void run() {
            BufferedReader reader=null;
            String yanit=null;

makePostRequest metodumuzun içinde yeni bir thread açıyoruz. Bunu yapma nedenimiz Android’in biraz aşağıda yapacağımız HTTP isteklerini ancak asenkron olarak yapmamıza izin vermesi. Android bu konuda programcıya bir seçenek bırakmıyor ve programcıyı ekranı kilitleyebilecek HTTP isteğini farklı bir thread’de yapmaya zorluyor.


try {

    char[] passphrase = "btrisk".toCharArray();
    KeyStore ksTrust = KeyStore.getInstance("BKS");
    Context context = getApplicationContext();

Bu bölümde ilk olarak keystore’a erişmek için keytool aracına verdiğimiz passphrase’i tanımlıyoruz. ksTrust adında bir KeyStore nesnesi oluştururken bu nesneyi BKS (bouncycastle) formatında olarak tanımlıyoruz. Bunun nedeni keystore dosyamızı oluştururken bu formatı kullanmış olmamız. Context nesnemizi oluştururken bu nesneye uygulama context’ini atıyoruz. Bu nesneye hemen aşağıda keystore dosyamıza erişmek için ihtiyacımız olacak.


ksTrust.load(context.getResources().openRawResource(R.raw.btrmobiletruststore), passphrase);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
tmf.init(ksTrust);

KeyStore dosyamızı ksTrust nesnemize yüklüyoruz. Bunun için yukarıda belirttiğim gibi context nesnesinden faydalanıyoruz. getResources().openRawResource() metodları uygulamanın res/raw dizini altında btrmobiletruststore dosyasını arıyor. Yukarıda keystore dosyamızı oluştururken bir de bks şeklinde bir uzantı tanımlamıştık. Bir dosyayı raw bir kaynak olarak tanımladığımızda ve bu şekilde eriştiğimizde Android dosya uzantısına izin vermiyor. Bu nedenle dosyayı uygun dizine yerleştirirken uzantısını sildik.


Daha sonra tmf isimli bir TrustManagerFactory nesnesi oluşturuyoruz. Tüm bu SSL doğrulama mekanizmasını tersine mühendislik çalışmasına tabi tutmadığımız için API’leri oluşturan kişilerin nasıl bir yol izlediklerini tam ve detaylı olarak açıklamak zor. Ancak API kurallarına göre bu işlemi gerçekleştirmek zorundayız. Burada son olarak TrustManagerFactory nesnemize keystore’umuzu parametre olarak veriyor ve initialize ediyoruz.


SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());

HttpsURLConnection urlConn = null;
URL url = new URL("https://btrmobile/servis.php");
urlConn = (HttpsURLConnection)url.openConnection();
urlConn.setSSLSocketFactory(sslContext.getSocketFactory()); //BURASI SERTİFİKA KONTROLÜNÜN YAPILDIĞI SATIR

Yine API kuralları gereği bir SSLContext nesnesi oluşturuyor ve bu nesne ile trust manager yapısını ilişkilendiriyoruz.

Bundan sonrası klasik bir HTTPS bağlantı işlemi için yapılması gerekenlerden oluşuyor, ancak tek bir farkla. Yukarıda normal kullanımdan hemen sonra bir yorum belirttiğimiz son satır geliyor. Bu satırda kurulan iletişim için sunucunun ilettiği sertifika bizim özel olarak ürettiğimiz CA sertifikası ile doğrulanmaya çalışılıyor. İşte bu satırı eğer iptal edebilirsek SSL pinning işlemini ortadan kaldırabiliriz, tabi kullanacağımız sertifikanın Android tarafından güveniliyor olması şartıyla.

Bu satırı nasıl geçersiz kılacağımıza dair açıklamayı daha sonra yapacağız.


urlConn.setDoOutput(true);
OutputStreamWriter wr = new OutputStreamWriter(urlConn.getOutputStream());
wr.write( strJson );
wr.flush();

reader = new BufferedReader(new InputStreamReader(urlConn.getInputStream()));
StringBuilder sb = new StringBuilder();
String line = null;
while((line = reader.readLine()) != null)
{
    sb.append(line + "\n");
}
yanit = sb.toString();

Yukarıda sertifika kontrolünden hemen sonra sunucuya istek iletiliyor ve sunucudan gelen yanıt da yanit değişkeninde saklanıyor.


catch(Exception ex)
{
    Log.d(">>>>>>>>>>>>>>>","HTTP isteginde hata olustu");
    Log.d(">>>>>>>>>>>>>>>", "exception", ex);
}
finally
{
    try
    {
        reader.close();
    }
    catch(Exception ex) {}
}

İlk try bloğumuzun catch bölümünde Android’in loglama altyapısını kullanarak eğer bir hata oluşursa hatanın içeriğini görmeye çalışıyoruz. Finally bloğunda ise reader nesnemizi ortadan kaldırıyoruz.


try {
            JSONArray jsonYanit = new JSONArray(yanit);

            for(int i=0;i<jsonYanit.length();i++){
                JSONObject json_data = jsonYanit.getJSONObject(i);
                sb.append("istek parametresi 1: " + json_data.getString("istek_prm1") + "\n");
                sb.append("istek parametresi 2: " + json_data.getString("istek_prm2") + "\n");
            }
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    output.setText(sb.toString());
                }
            });

        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
})).start();

Sunucudan aldığımız yanıtı tekrar formatlayarak View’ımızdaki TextView’a yazıyoruz ve basit uygulamamız böylece işlevini tamamlamış oluyor. Ancak burada yine bir Android kuralına uygun davranmamız gerekiyor. Asenkron olarak açılmış bir thread’den View alanlarından herhangi birine müdahale etme şansımız olmadığı için runOnUiThread metodunu kullanmak zorundayız. Tabi bu işlemi işletim sistemi arkada nasıl gerçekleştiriyor bu başka bir derin konu ve bizim şu an için ilgi alanımızda değil.

Artık uygulamamızı test etmeye başlayabiliriz.

Uygulamamızı bir sanal makinede başlatalım. Öncelikle sanal makinemizin Trusted sertifikaları arasında yabancı bir sertifika olmadığından emin olalım.


Uygulamamızı çalıştırmadan önce, hatırlarsanız sunucumuza isimle erişmemiz gerektiğinden ve bu neden sunucumuza “btrmobile” adıyla HTTPS isteğini yaptığımızdan bahsetmiştim. Ancak uygulamanın sanal makine üzerinde çalışırken bu ismi IP adresine çözümleyebilmesi lazım. Bunun için en pratik yöntem Android cihaz üzerindeki hosts dosyasına bu bilgiyi eklemek. Öncelikle sunucumuzun çalıştığı IP adresini bulalım.


Daha sonra Android cihazımıza shell bağlantısı kuralım. Eğer bir iOS cihaz ile çalışıyor olsaydık cihazı Jailbrake etme işleminin yanı sıra ayrıca bir de OpenSSH kurmamız ve shell erişimini SSH üzerinden gerçekleştirmemiz gerekirdi. Android ortamında ister emülatör isterse cihaz kullanımı sırasında ADB aracı ile bu işlemi rahatlıkla gerçekleştirebiliriz. Ancak hosts dosyasında az sonra yapacağımız değişikliği yapabilmek için root erişim hakkına ihtiyacımız var. Emülatör ortamında zaten bu hakka sahibiz.


Akıllı telefonlar ve diğer cihazlar normalde shell erişimi ile kullanılmak üzere üretilmediğinden maalesef Android üzerinde öntanımlı olarak vi veya nano gibi uygulamalar bulunmuyor. Bu nedenle hosts dosyasının içeriğini görebilsek de başka bir üçüncü parti yazılımı Android üzerine yüklemeden dosyada değişiklik yapamayız. Bu problemi dosyayı bilgisayarımıza alıp orada işleyerek aşacağız. Bu işlem için Android Studio ile birlikte gelen Android Device Monitor’ü kullanacağız.


“hosts” dosyamızı aşağıdaki gibi düzenliyoruz.


Daha sonra bu dosyayı tekrar cihaza atmaya kalktığımızda aşağıdaki gibi file system’in read only olduğu uyarısını alıyoruz.


Bunun nedeni “system” partition’ının read only olarak mount edilmiş olması. “/etc” dizinimiz de bu partition’ın altında olduğu için “system” partition’ını read write olarak tekrar mount etmemiz lazım. Bu arada “/etc” dizini nasıl “system” partition’ının altında olur derseniz “/etc” dizininin sembolik link olarak “/system/etc” dizinine bağlandığını görebilirsiniz.


Bu işlemden sonra hosts dosyamızı ezebiliriz. Dosyamızın son hali aşağıdaki gibi.


Bu aşamadaki durumumuzu özetlersek:
  • Sertifikamızı pin’ledik, daha doğrusu btriskCA sertifika otoritemizi pin’ledik. Uygulamamız bundan sonra sadece ve sadece btriskCA tarafından imzalanmış sertifikalar ile el sıkışma işlemini tamamlayacak.
  • btriskWWW sunucu sertifikamızı ve ilişkili özel anahtarı web sunucumuza uygun dizinlere yerleştirdik ve gerekli sunucu ayarlarını yaptık.
  • Mobil cihaz emülatörümüzün hosts dosyasını düzenledik.
Artık uygulamamız çalışmaya hazır.



Uygulamamız beklediğimiz gibi çalıştı. Şimdi diyelim ki bu uygulamayı test etmek istiyoruz ve Burp ile araya girmemiz gerekiyor. Cihaz üzerinde proxy ayarlarını yapalım. Bu arada ben bu çalışmayı web sunucusunun da üzerinde çalıştığı bilgisayarda yapacağımdan ve Burp’e gelecek olan istek “btrmobile” sunucusuna yönelik olarak geleceğinden “hosts” dosyasına bilgisayarımda da ekleme yapmam gerekli.


Android üzerinde Proxy ayarımızı yapalım, Burp proxy’miz de web sunucumuz ile aynı PC üzerinde çalıştığından cihaz üzerinde hosts dosyasında tanımladığımız IP adresini kullanacağız.


Şimdi uygulamamızı tekrar başlatalım.



LogCat çıktımıza baktığımızda oluşan hatanın SSL sertifikası doğrulama ile ilgili olduğunu görüyoruz.


Android SSL pinning makalemizin bu bölümünde Android uygulamamızı geliştirerek araya girme çabasını boşa çıkardık. Bir sonraki bölümde SSL pinning kontrolünü nasıl etkisiz hale getirebileceğimizi inceleyeceğiz.

<<Önceki Bölüm                                                                                                      Sonraki Bölüm>>