29 Kasım 2015

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

Android SSL pinning makalemizin üçüncü ve son bölümünde daha önceki iki bölümde uyguladığımız SSL pinning kontrolünü aşmanın yollarını araştıracağız, ve tabi ki aşacağız.

Artık masanın öbür tarafında geçme zamanımız geldi. Şimdiye kadar hep programcı gözüyle bir takım çalışmalar yaptık. Eğer bu uygulamayı test etmemiz gerekiyor ve araya girmek istiyorsak SSL pinning kontrolünden (daha doğrusu özelliğinden) kurtulmamız gerekiyor.

Burada ilk akla gelen çözüm hazır bir araç aramak. Bunu yaptığımızda “Android SSL TrustKiller” uygulamasını kolayca bulabiliriz.


Bu aracı incelediğimizde ise SSL TrustKiller’ın Android Cydia Substrate aracını kullandığını görürüz.



Buraya kadar herşey güzel. Ancak Cydia Substrate aracının yazarının web sitesini incelediğinizde bu aracın Android üzerindeki desteğinin ancak 4.3 verisiyonuna kadar olduğunu görürsünüz.


Bu da minimum Android API serviyesi 19 ve üzeri olan uygulamaları test ederken bu araçlar işimize yaramaz anlamına geliyor. Buna ek olarak Cydia Substrate API seviyesi 18 ve altı olsa bile emülatörle de uyumlu değil, yani mutlaka bir cihaz üzerinde kullanmalısınız.

Cydia Substrate aracı açık kaynak kodlu bir araç değil, olmak zorunda da değil. APK paketini incelediğimizde ise bolca native makine koduna derlenmiş kütüphane içerdiğini görüyoruz.


Bu bize bu aracın Java API’leri üzerinden işletim sistemi imkanlarını kullanan bir araç olmadığı ve Linux katmanında işletim sistemi özelliklerini kendi amaçları doğrultusunda kullanarak sıra dışı işlemler yapan bir araç olduğu fikrini veriyor. Kütüphaneler makine dilinde olduğundan tersine mühendislik işlemini de Java uygulamalarında olduğu gibi kısa bir zamanda yapma imkanı yok.


Yani kolayca bir araç kullanarak SSL pinning işlemini her durumda atlatamayabiliriz. Buna ek olarak bazen karşılaştığımız sıra dışı durumlar da mevcut. Örneğin Android Java kütüphaneleri dışında “curl” gibi linux araçları ile HTTPS istekleri yapan uygulamalarla da karşılaşabiliyoruz. Bu araçlar sistemin güvendiği sertifikaları dahi kullanamadığında programcı kendi bilgisayarının üzerindeki bir tarayıcının tüm güvenilen sertifikalarını indirerek bir sertifika deposu dosya hazırlıyor. Bu durumda uygulamanın davranışını analiz ederek bu sertifika deposuna kendi proxy aracınızın CA sertifikasını da eklemek dışında bir çareniz kalmıyor. Bu nedenle mutlaka uygulama üzerinde hem statik hem de dinamik yöntemlerle tersine mühendislik yapmak zorunda kalabilirsiniz. Bir aracın tüm problemleri ortadan kaldırması pek mümkün değil.

Çözüm yoluna geçmeden önce bir de debugger ile uygulama akışına müdahale ederek bu kontrolü atlatma yolunu izlemek istiyorum. Bu yöntem uygulama kontrollerinin pek çoğunu ortadan kaldırmak veya manipüle etmek için çok faydalı bir yöntem. Değinmek istememin sebebi de bu. Ancak bizim problemimiz için maalesef yeterli olamayacak.

JDB ile çalışmaya başlamadan önce aslında Java ortamında Run Time’da bir prosesin hafıza alanına başka bir paketten nesnelerin yüklenebileceğini açıklamak istiyorum. Bu imkan JDI arayüzü ile sağlanıyor (com.sun.jdi) . Ancak JDB debugger’ı JDI arayüzünü kullanan bir istemci uygulama ve JDB’nin amacı bizim gibi manipülasyon yapmak isteyenlere yardımcı olmak değil, sadece programcılara yardımcı olmak. Bu nedenle debug edilen prosesin akışına bozucu yönde yapılabilecek müdahaleleri destekleyen bir fonksiyonalitesi yok, hatta daha önceden var olan “load” komutu bile araçtan kaldırılmış. JDB aracı bu nedenle bizim pek işimize yaramayacak. JDI arayüzünün sağladığı imkanları class yükleme fonksiyonalitesini de içerecek biçimde içeren bir debugger client bildiğimiz kadarıyla yok. Bununla birlikte 2012 yılından bu imkanı kullanarak geliştirilmiş bir araştırma projesi var (https://github.com/iSECPartners/android-ssl-bypass). Fakat proje aktif olarak desteklenmemiş, bu yüzden çok kullanışlı olduğunu söyleyemeyiz. Justine Osborne isimli araştırmacı kabaca Java diliyle kendi Java Debugger client uygulamasını geliştirmiş ve cihaza yüklediği class’ları bu client ile hedef uygulama hafıza alanına yükleyerek uygun yerde normalde çalışacak olan TrustManager nesnesi ile kendi yüklediği nesnesinin yerini değiştiriyor. Bu sınıfta bulunan ve esasında hiçbir kontrol uygulamayan checkServerTrusted metodu çalıştığında da herhangi bir engel oluşmadan SSL pinning kontrolü atlatılabiliyor.

Biz hata aldığımız son senaryomuzu, yani SSL pinning nedeniyle Burp ile araya girdiğimiz durumda alınan hatayı, tekrar yaşayacağız, ancak bu defa JDB ile uygulamamıza attach olarak acaba bu hatanın oluşmasına engel olabilir miyiz ve SSL pinning’i atlatabilir miyiz bunu inceleyeceğiz. Bunu yapmaya çalışırken aslında asıl yardımcımız statik analiz olacak. Burada açık kaynak kodlu bir sisteme yönelik çalışmanın avantajını da yaşayacağız.

Öncelikle Android ortamında nasıl debug yapabileceğimizi konuşalım. Birazdan izleyeceğimiz prosedürü Android Studio veya Eclipse’in ve Dalvik Debug Monitor Service (DDMS) aracı bizim için otomatik olarak yaptığını düşünün.


Android debugging işlemi aslında temel olarak uzaktan debug için diğer sistemlerde de kullanılan bir yöntemi kullanıyor. Burada tabi kabaca Android sistem mimarisinin de farkında olmak lazım. Bizim debug edeceğimiz katman Java Virtual Machine (daha doğrusu Android için Dalvik VM) katmanında çalışan byte code seviyesinde. Bunun altında Linux katmanı ve native uygulamalar (yani makine diline derlenmiş kütüphaneler) var.

Biz bir Android cihaza veya emülatöre uzaktan bağlanarak debug işlemini gerçekleştiriyoruz. Bu işlemi TCP katmanında bir ağ erişimi ile yapıyoruz. Debug işlemi için sunucu tarafında bir agent ve debug eden sistem üzerinde de bu agent’ın anladığı dili konuşacak bir arayüze ve bu arayüzü kullanan debug aracına ihtiyacımız var. Kavramsal olarak anlatmak da anlamak da zor biliyorum, bu durumun debug etme işlemini gösterdiğimde biraz daha iyileşeceğini umuyorum.

Windows ortamında JDB ile debug işlemi için aşağıdaki süreci izleyebiliriz. Öncelikle cihaz veya emülatör üzerinde debug edeceğimiz uygulamanın kurulu olması ve debug işlemi için beklemeye başlaması gerekiyor. Bunun için önce cihaz üzerinde Developer Options bölümüne giriyoruz.



Bu ekrandan Select Debug App seçeneğini seçiyoruz (yukarıdaki görüntüde uygulamamız zaten seçilmiş görünüyor).

Gelen listeden hangi uygulamayı debug etmek istiyorsak seçiyoruz.


Biz uygulamamız olan Httpsclient uygulamasını seçtik. Son olarak Wait for Debugger seçeneğinin seçili olduğundan emin olmamız lazım.


Uygulamamızı başlattığımızda uygulama bir debugger’ın kendisine bağlanmasını bekliyor olacak.


Artık JDB tarafına geçebiliriz. JDB’nin debug edilen proses’e TCP üzerinden attach olduğunu söylemiştik. Ancak bundan önce ADB aracının yardımına ihtiyacımız var. ADB aracı ile kullandığımız PC ve debug edilecek uygulamanın cihaz üzerindeki process id’si arasında köprü kuracağız. Bunun için 2 yol var; birincisi “adb jdwp” komutuyla cihaz üzerinde çalışan process’lerin id’lerini listeleyebiliriz. Ancak bu çıktı ne yazıkki hangi id’nin hangi app’a ait olduğu bilgisini içermiyor.


Son uygulamayı biz çalıştırdığımız için son id’nin bize ait olduğunu varsayabiliriz. Ancak bundan kesin olarak emin olmak için yapılması gereken process id listesini uygulamayı çalıştırmadan ve çalıştırdıktan sonra almak. Aradaki fark bizim process id’miz olacak.

Daha sonra aşağıdaki komutu çalıştırarak localhost üzerindeki boş bir TCP portunu debug edilecek uygulama ile ilişkilendirebiliriz:

# adb forward tcp:9999 jdwp:2389

Diğer yöntem ise DDMS aracını kullanmak. Bu araç Android Studio kurulumu ile birlikte geliyor.

DDMS’in sol üst köşesinde çalışan uygulama listesini görebilirsiniz. Yanında kırmızı böcek olan process adından da emin olabileceğimiz gibi bizim uygulamamız.


Bu satırı seçtiğimizde sağ taraftaki bölümde 8700 rakamını görüyoruz. Bu bizim adb forward komutu ile yaptığımız işlemin DDMS tarafından yapıldığı ve bilgisayarımız üzerindeki TCP 8700 portundan uygulamaya debug etme amaçlı erişebileceğimiz anlamına geliyor.

Artık JDB ile debug etmeye başlabiliriz.

C:\> jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=8700



Cihaz üzerinde proxy olarak Burp’ü tanımladığımızı da tekrar hatırlatalım.

Uygulama üzerinde herhangi bir breakpoint koymadığımız için ISTEK GONDER düğmesine bastığımızda uygulama hata alarak sonlanacak.


Aldığımız hatanın 116. satırda olduğunu söylüyor JDB. Hata üreten metodun org.json.JSONTokener.nextCleanInternal() olduğunu görüyoruz. Halbuki SSL ile ilgili bir hata yaşamayı bekliyorduk. Aslında SSL hatasını aldık, ama hatayı ele aldığımız için debugger’a düşmedi.


Hata alacağımız bölgeye biraz daha yakınlaşmak istiyoruz inceleme yapmak için. Hata alınan context’e yaklaşarak sertifika kontrolü yapılan bölümü atlatabilir miyiz onu incelemeye çalışacağız. Bunun için öncelikle doğru alana yakın bir bölüme breakpoint koymamız lazım. Daha doğrusu hatanın gerçekleşeceği bölüme yakın bir noktada çağrılan bir metoda breakpoint koyacağız. Çünkü test ettiğimiz uygulamaların genellikle elimizde kaynak kodu olmayacağından bir satır belirtme imkanımız olmayacak. Biz de bu duruma uygun davranalım.

Elimizde bir Android uygulama var ancak kaynak kodu yok ise statik analiz için sırasıyla aşağıdaki adımları izlemeliyiz.

Bu noktada öncelikle Android üzerinde çalışan sanal makinenin Dalvik Virtual Machine olduğunu ve bunun JVM’den farklı olduğunu belirtmeliyim. Bu problem Google ile Oracle arasındaki lisans anlaşmazlığından ve Java VM’in mobil cihazlar için yeterince verimli bulunmamasından kaynaklanıyor. Bu nedenle her ne kadar Android uygulamalarını Java dilinde yazsak da derlenen dil Java Byte Kod seviyesine değil önce byte kod’a ve daha sonra DEX formatına derleniyor. Bu format byte kod’dan oldukça farklı bir format ancak byte kod’a da geri çevrilebiliyor. Elbette bu dönüşüm her zaman mükemmel olmayabiliyor, yani kullandığımız araçlar bu dönüşümü bazen tam olarak başarılı biçimde gerçekleştiremeyebiliyor. Yine de çoğu durumda işimize yarayan sonuçlar elde edbiliyoruz. Biz de önce bu DEX kodunu JAR arşivine dönüştüreceğiz. Bunun için dex2jar aracından faydalanacağız.

Projemizin derlenmiş kodu şu dizinde yer alıyor:


Dex2jar aracı ile paketimizi Jar paketine çeviriyoruz.


Şimdi Jar paketimizi JDGUI aracı ile inceleyeceğiz. JDGUI aracı byte level kodu Java kaynak kodu seviyesine decompile ediyor. Ancak herhangi bir obfuscation uygulamamıza rağmen kaynak kodumuz önce byte level koda, daha sonra da dex koduna çevrildiği için aşağıda gördüğünüz gibi her zaman mükemmel bir sonuç elde edemiyoruz.



Böyle durumlarda maalesef SMALI seviyesine inmek zorundasınız. SMALI nedir derseniz DEX kodun decompile edilmiş hali diyebiliriz. DEX kodunu SMALI koduna çevirmek için ise APKTOOL aracını kullanabiliriz.


SMALI kodlarımız şu dizinin altında yer alıyor.


MainActivity sınıfımızın smali kodunu incelediğimizde makePostRequest metodumuzun kodunun oldukça kısa olduğunu görüyoruz. Çünkü derleyici Java dilinin alt sınıf özelliği dolayısı ile inline olarak tanımlanan sınıfları ayırarak farklı sınıflar halinde derliyor. Burada asıl işin yapıldığı, yani HTTP isteğinin yapıldığı bölümün com/btrisk/httpsclient/MainActivity$2 sınıfında olduğunu anlıyoruz.


Daha önceki deneme yanılmalarımdan ve incelemelerimden hatanın oluştuğu metodun javax.net.ssl.SSLContext.getSocketFactory() metodu olduğunu biliyorum. Bu metodu Smali kodu içinde şu şekilde görüyoruz. Invoke-virtual instruction’ı ile bu metod çağrılıyor. SMALI reversing ile ilgilenmek isteseniz, aslında göründüğü kadar korkutucu değil. Pek çok dilde olduğu gibi bazı instruction’lar çok sık kullanılıyor. Onlardan birisi de bu instruction ve temelde yaptığı işlem statik olmayan bir metodu çağırmak.


Diyelimki uygulama kodunu bu şekilde statik analize tabi tutmaya çalıştık, uygulama çalışma anında nasıl davranıyor ve biz SSL pinning işlemini JDB ile atlatmak için bir yol bulabilir miyiz bunu anlamak için JDB’de yukarıdaki metoda bir breakpoint koyalım.

Bunun için uygulamamızı sıfırdan başlatalım ve tekrar attach olalım.

Debugger bize henüz bu sınıfı yüklemediğini, ancak yüklendiğinde istediğimiz şekilde ilgili metoda breakpoint’i uygulayacağını belirtiyor.


Şimdi breakpoint noktamıza gelebilmek için uygulamanın düğmesine tıklayarak çalışmasına izin verelim.

Şu anda breakpoint noktamıza ulaştık.


Locals JDB komutuyla lokal değişkenleri kontrol edebiliriz. Bu noktada herhangi bir lokal değişkenimiz tanımlanmış değil.


Bu noktadan sonra step (step in), next ve step up komutları ile satırları ve metodları işletebiliriz. Açıkçası çağrılan API’ler iç içe bir çok API çağırıyor ve hatanın uygulamamızın tam olarak hangi satırında gerçekleştiğini biliyor olmamıza rağmen bu şekilde manuel olarak pek bir sonuca ulaşamadık. Aslında hata ele alma kodumuz da bu süreçte işimizi zorlaştırdı, çünkü hata ele alındığı için kontrol debugger’a devredilmedi. Ben de bunca kargaşa içinde hatanın oluştuğu yeri yakalayamadım.


Ama hata ele almamızın bir avantajı da var, logcat’den faydalanarak stack trace bilgisini görebiliriz.

Burada hatanın tam olarak "com.android.org.conscrypt.TrustManagerImpl.checkTrusted" metodunda gerçekleştiğini görüyoruz. Bu metodu biz doğrudan çağırmadık, ancak çağırdığımız bir API bu metodun çağrılmasına neden oldu.


Bu yüzden şimdi de “com.android.org.conscrypt.TrustManagerImpl.checkTrusted” metoduna breakpoint koyalım.


Uygulamanın düğmesine basarak breakpoint noktasına gelelim ve bu noktada lokal değişkenlere bir göz atalım.


Bu noktaya kadar gelmemizin sebebi uygulamanın hata aldığı bir nokta varsa bundan kaçınmak ve SSL pinning kontrolünden de kurtulmak idi. JDB’nin sağladığı fonksiyonalite ile ancak değişkenlerin değerlerini değiştirebilir ve hatanın alındığı noktadan önce bir "if" statement'ı var ise bunu manipüle ederek ilgili kontrolden kaçınmaya çalışabiliriz. Ancak bu konuda ilerleme sağlayabilmek için uygulamanın kaynak koduna da erişmemiz lazım. Cihaz üzerinde bu bilgiye ulaşamasak da Android işletim sisteminin açık kaynak kodlu olması sayesinde ilgili metodun koduna internetten erişebiliriz.


Fonksiyonumuzun başlangıcı aşağıdaki gibi.


Uygulamanın ürettiği exception’ı tekrar hatırlayalım.

"Trust anchor for certification path not found."


Bu exception açıklamasının kodun içinde yer aldığı alan aşağıdaki gibi. Teorik olarak bu satıra kadar uygulamayı işletip trustAnchor her ne ise buna bir takım veri yapılarını atayarak veya bu nesnenin isEmpty metodunun sonucunu false olarak döndürerek bu noktada hatayı atlatabilirim. Ama korkarım uygulamanın akışı bu noktadan sonra sağlıklı olmaz ve yine bir noktada hata alırım. Belki denenebilir, ama ben daha etkili bir yöntem olduğunu bildiğimden bu işle uğraşmak istemiyorum.


Bu etkili yöntem nedir derseniz, aslında çok basit. Uygulamanın elimizdeki versiyonunun bir APK paketi olduğunu düşünelim. Daha önce yaptığımız gibi bu paketi APKTOOL ile smali koduna çevirdikten sonra hataya yol açan metod çağrılarını uygulamadan çıkararak kodu tekrar derleyeceğim. Bu yeniden derleme işlemini APKTOOL ile yapabiliyorum. Tabi buradaki şansım bu metod çağrılarını kodun içinden çıkarsam da uygulama akışı açısından herhangi bir problem yaşamıyor olmam. Bunun için tekrar smali kodumuza dönelim.

Burada açıkça söylemek gerekirse yüzeysel bir SMALI bilgisi başınızı belaya sokabilir. Özellikle koddan çıkaracağımız bölümlerin sonuçları sonraki satırlarda kullanılıyorsa bu durumlara çok dikkat etmemiz lazım.

Aşağıdaki kodda öncelikle aşağıdaki satırı devreden çıkarıyoruz.

invoke-virtual {v15}, Ljavax/net/ssl/SSLContext;->getSocketFactory()Ljavax/net/ssl/SSLSocketFactory;

Bu satırın çıktısı v21 register’ına atanıyor. Bu değer ise hemen aşağıdaki sileceğimiz bir sonraki metod çağrısında parametre olarak kullanılıyor. Yani güvendeyiz.

invoke-virtual {v0, v1}, Ljavax/net/ssl/HttpsURLConnection;->setSSLSocketFactory(Ljavax/net/ssl/SSLSocketFactory;)V

Bu satırın döndürdüğü herhangi bir değer yok, yani sondaki V harfinden de anlaşılabileceği gibi Void döndürüyor.

Düzenlenmiş kodumuzun son hali aşağıdaki gibi.


Şimdi kodumuzu tekrar derleyelim.


Tabi APKTOOL ile yeniden derlenmiş bir kodu hemen emülatör veya cihaza atamayız. Öncesinde imzalamamız lazım.


Artık uygulamamızı emülatöre yükleyerek SSL pinning’i atlatıp atlatamayacağımızı görelim.


Maalesef yine hata aldık. Ama bu defa sebebi normalde her zaman yaptığımız bir işlemi yapmamış olmamızdan kaynaklandı. Araya girmek için kullandığımız Burp’ün CA sertifikasını cihaza yüklemediğimiz için. SSL pinning’i devreden çıkardık, ama bu defa da cihazın SSL kontrolüne yakalandık.



Cihazımıza Burp’ün CA sertifikasını yükleyelim.



Ve sonunda araya girmeyi başardık ve uygulamamız çalıştı




JDB SSL Pinning’i atlatmak için pek işimize yaramamış ve hacker ihtiyaçlarına yönelik geliştirilmemiş olabilir, ancak Java platformu reflection’ı desteklediği için runtime’da herhangi bir metodu basit veri tiplerinde parametre alıyor veya hiç parametre almıyorsa kolayca çağırabiliriz. Bu uygulamanın işlevine bağlı olarak istemci tarafında uygulanan kontrolleri aşabilmek için bize önemli bir yardımcı olabilir.

BİR BAŞKA SSL PINNING AŞMA YÖNTEMİ

SSL pinning’i aşmak için bu örneğe özel bir diğer yol da uygulamanın kullandığı keystore dosyasının içine araya girmek için kullandığımız Burp’ün kullandığı CA sertifikasını koymak olabilirdi. Bu koda bakarak çok kolay anlaşılabilir bir yöntem olmamakla birlikte SSL pinning uygulama örneklerinin Google’dan araştırılması ve kullanılan yöntemin iyice anlaşılması neticesinde rahatlıkla uygulanabilecek bir yöntem.

Android SSL Pinning makalemizi tamamlarken son not olarak şunu belirtmeliyim; bu makalede belli bir yöntemle SSL pinning’i uyguladık ve bu yöntemi bertaraf etmenin yollarını araştırdık. Ancak bu süreçte kullandığımız yöntem ve araçlara gerekli detayda değinmeye çalıştık. Bu bilgi farklı şekillerde ve kütüphanelerle SSL pinning’i uygulayan uygulamalara karşı çözüm geliştirmek için size statik ve dinamik analiz yapabilme konusunda yardımcı olacaktır.