Kefilm — Android NDK ile API anahtarını gizlemek

Goksu Turker
5 min readMar 30, 2020

--

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’u CMake.

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.

--

--