Kefilm — Android NDK ile API anahtarını gizlemek
--
Giriş
Projeye başlamadan önce MovieDB’nin bize sağladığı API anahtarını, proje içinde daha temiz bir kullanım için anahtarı /res
klasörü içinde String
olarak tutmayı planlıyordum. Sonuçta bu projeyi GooglePlay Store’da yayınlamayıp sadece GitHub’ta yayınlayacaktım ve API anahtarının bulunduğu dosyayı upload etmeyerek bu güvenlik problemini çözebilirdim. Fakat projeden daha fazla tecrübe elde etmek için API anahtarını Android NDK ile tersine mühendislik saldırılarına karşı biraz daha güvenilir hale getirmek istiyorum.
Android NDK, API anahtarı gizliliğine %100 bir koruma sağlamayacak fakat tersine mühendislik yapılarak kırılmasını daha zor hale getirecek. Bu yöntem dışında bir sunucu aracılığıyla public/private key exchange
yöntemiyle de API anahtarını projede gizlemek mümkün ama bu proje için herhangi bir sunucuya ihtiyaç olmadığından dolayı bu yöntemi tercih etmeyeceğim.
[Bu konu hakkında izlediğim tutorial’a bu linkten ulaşabilirsiniz.]
Android NDK’i kullanabilmek için Android Studio’nun SDK Manager > SDK Tools
bölümünden aşağıdaki araçları indirmemiz gerekiyor.
- LLDB: Android Studio’nun native kodu debug etmesi için gereken tool.
- NDK(Native Development Kit): Android projenizde C/C++ kodunu kullanmanızı sağlayan araçlar bütünü.
- CMake: Gradle ile birlikte çalışan ve Native kütüphanenizi build etmeye yarayan bir tool. Bazı projelerde
ndk-build
tercih edilebiliyor fakat bu yazının yazıldığı gün itibariyle Android Studio’nun default(varsayılan) build tool’uCMake
.
Tahminen ndk-build
legacy projelerde diğer bir deyişle eski projelerde tercih edilen bir tooldu. Bu yüzden Android Studio ndk-build
tooluna desteğini sürdürüyor ama bu projede varsayılan olan CMake
toolunu kullanacağım.
Projeyi yaratma kısmına geçelim.
Android Studio kolaylık sağlaması açısından yeni proje yaratırken ‘Project Template’ seçtiğimiz sırada en altta ‘Native C++’ template’i ile bize bu kolaylığı sağlıyor. Fakat ben bu yolu izlemeyip ‘Empty Activity’ template’i ile açmış olduğum projeye manuel olarak dosyaları ekleyeceğim.
Projelerde genelde tercih edilen directory mimarisi app/src/main
klasörünün içine /cpp
veya /jni
adında bir klasör açılması ve C/C++ kodlarının bu klasör içine konulması. Bu prensibe uymak diğer geliştiricilerin de projenize kolay alışması açısından tercih edilmesini doğru buluyorum.
/cpp
klasörünün içine api-keys.cpp
adında bir dosya açtım. Bu .cpp
dosyası içinde MovieDB API anahtarını döndüren bir fonksiyon yer alacak.
Örnek bir api-keys.cpp
dosyası:
Bu yazı itibariyle bir güvenlik katmanı oluşturmaya çalıştığımız için bu kod parçasının ne işe yaradığına dair adım adım giderek neyin ne işe yaradığına bakalım. Eğer okumak istemiyorsanız CMake bölümüne atlayabilirsiniz. Tamamen opsiyonel.
jni.h
JNI (Java Native Interface)
Java’da yazdığımız kodlarda memory yönetimi ve performans limitlerini aşmak için native kod kullanmamız gerekebilir. JNI bu iki ayrı programlama dili arasında köprü görevi görür. Bu tür mekanizmalara [Foreign function interface] denir.
<string>
C++’da char dizisi yerine direkt olarak string kullanmamız için gereken header.
extern “C” JNIEXPORT jstring JNICALL
[What is the effect of extern “C” in C++? — Stack Overflow]extern “C”
: Bunun kullanılmasının sebebi yukarıdaki linkte belirtildiği üzere C++’ın fonksiyon adlarının overload özelliği varken C’nin bulunmamaktadır. Bu yüzden C++ compiler’ı fonksiyon isimlerini unique(eşsiz) bir id olarak kullanamaz. Bu yüzden bu isim yönetimini(name mangling) fonksiyonun argümanlarıyla ilgili bilgileri ekleyerek bu sorunu çözer. C dilinde ise overload özelliği olmadığı için isim yönetimine(name mangling) ihtiyacı yok. Sonuç olarak C++’ta bir fonksiyona extern “C”
atadığımız zaman C++ compiler’ı fonksiyonla ilgili argüman bilgilerini bağlama(linkage) yapmak için kullanmaz.
JNIEXPORT
ve JNICALL
taglari ile ilgili çok bilgi bulamadım fakat JNIEXPORT
kullanılan fonksiyonun bir sembol ya da dinamik tabloda gözükmesini sağladığı söyleniyor. JNICALL
’ın da ardından gelen Java_me_turkergoksu…
kısmının Android projesinde nasıl çağrılacağı ile ilgili bilgi sağlanması için mutlaka kullanılması gereken bir tag olduğunu tahmin ediyorum. jstring
ise fonksiyonun hangi tipte return yapacağını atıyoruz. İlerde bu konuyla ilgili daha kapsamlı bilgiye sahip olduğum an burayı düzenleyeceğim.
Java_me_turkergoksu_kefilm_ApiKeyLibrary_getMovieDbApiKeyFromJNI
[JNI Example Details]
C++’da yazdığımız fonksiyonun isminin başına Android projesinin package ismini, projede hangi classta yer aldığını eklememiz gerek. Ayrıca ayrımlar (.) ile değil (_) ile yapılıyor.
Genel formül
Java\_{PACKAGE_NAME}\_(CLASS_NAME)\_(FUNCTION_NAME)
(JNIEnv *env, jobject object)
Dikkat etmiş olabileceğiniz diğer bir durum ise fonksiyonun parametreleri. Android projesinde kullanacağımız fonksiyonun aslında bir parametresi olmayacak. Sadece String
döndürmesini istiyoruz. Fakat native kodda yazdığımız bu iki parametre default olarak eklememiz gereken parametreler.
Bu parametrelerden ilki olan env parametresi JNI Runtime Environment’ına point eden bir değişken. Fonksiyonların JVM’e callback atmasını sağlar. İkinci parametre jobject
ise bu fonksiyonu çağıran objeyi gösteren bir pointer.
Fonksiyonun geri kalanı programlamayla uğraşan kişiler için anlaşılması kolay olduğunu düşündüğüm için devamına değinmeyeceğim.
CMake
Sıra CMake scriptine geldi. Direkt olarak tutorialda yazan scripti kopyalayıp yapıştırabilirsiniz. add_library
ve target_link_libraries
kısmındaki bazı yerlerin C++ dosyasınızın bulunduğu dizin ve ismine göre değişiklik gösterebilir.
Sırada build.gradle(app)
içine şu kısmı ekleyerek işlemimizi tamamlıyoruz:
android {defaultConfig {
…
}buildTypes {
…
}externalNativeBuild {
cmake {
path ‘CMakeLists.txt’
}
}}
En son olarak Android projesinde kullanımını göstereyim:
Sonuç
Ben de yaparken öğrendiğim için açıkçası biraz hayal kırıklığına uğradım. Bu yöntemin tercih edilmesinin önemli sebeplerinden biri Native C/C++ kodunu decompile edilmesinin Java koduna göre daha zor olması. Fakat ekstra herhangi bir encryption(şifreleme) katmanı olmadan güvenlik açısından maalesef büyük bir fark göremedim. Native C/C++ kodunun Java’ya göre daha zor decompile edilmesinin sebeplerinden biri Java kodu byte koda compile edilirken meta datalar içerir ve bu yüzden decompile edilmesi daha kolay. Fakat C/C++ meta data olmadan direkt olarak assembly’e çevrildiği için daha zor decompile ediliyor.
Konuyla ilgili biraz daha araştırma yaptıktan ve birkaç geliştiricinin fikrini de aldıktan sonra izlenen belirli tek bir yöntemin olmadığını gördüm. Her geliştirici birtakım güvenlik katmanları ekleyerek saldırganın işini zorlaştırmaya çalışıyor. Bunların arasında native kod kullanmak, API anahtarının limitlerini iyi belirlemek, bazı şifreleme yöntemleri, EncrypyedSharedPreferences
(API>23) vs…
EncryptedSharedPreferences
’ın runtime’da nasıl bir güvenlik sağlayacağından emin değilim. Örnek olarak StackOverflowda bir [kullanıcı] API call’ın yapıldığı yere hook ettiği sırada hem decrypted değeri hem de şifreleme anahtarını elde edebildiğini söylemiş. Bu yüzden uygulamada herhangi bir runtime protection olmadan EncryptedSharedPreferences
’ın da bu durum için çok bir anlam ifade etmediğini söylemek mümkün.
Bu yüzden araştırmaları runtime protection üzerine yoğunlaştırmak daha mantıklı olacağı kanaatindeyim. Bu yönde karşıma DexGuard ve RASP kavramı çıktı. Bu yazının içeriğinin dışına çıkmamak için bu konuları başka bir yazıda araştırmak ve tartışmak daha mantıklı olacaktır.
En son olarak hiçbir zaman %100 güvenli bir sistemin/uygulamanın yapılamayacağının farkındayım fakat yukarıda bahsettiğim yöntemlerle maalesef saldırganın işini yeterince zorlaştıramadığımızı düşünüyorum. 1 günde exploit edebileceği bir sistemi “basit yöntemlerle” 10 günde etmesiyle arasında bir fark yok. Bu gün sayılarını kriptolojide de kullanılan bazı matematiksel problemler gibi binlere, onbinlere çıkarmadan yeterli caydırıcılığı sağlamak mümkün değil.
[Projeye buradan ulaşabilirsiniz.]
Okuduğunuz için teşekkürler.