İçeriğe geç
Sedat Özdemir
Yazılar

android-pentest

Tersine Mühendislikten Runtime Manipülasyonuna: Cebimizdeki Mayın Tarlasında Yürümek

Mobil güvenliğin sadece SSL Pinning olmadığını, Frida ile runtime'da nasıl dans edileceğini ve yapılan o kritik hataları yaşanmış tecrübelerle konuşalım.

Sedat Özdemir
· 5 dk okuma

Sektöre ilk girdiğim, kendimi "her şeyi hackleyebilirim" sandığım o toy dönemlerden birindeyim. Bir bankacılık uygulaması için sızma testi yapmam istendi. APK'yı aldım, jadx-gui ile bir güzel açtım. Kodun içinde gezerken bir baktım ki her şey kabak gibi ortada; ne bir obfuscation var, ne de karmaşık bir yapı. "Tamam" dedim, "Bu akşam erken çıkarım, açıklar havada uçuşuyor."

Kayıt olma fonksiyonuna odaklandım, oradaki bir kontrolü atlatmam gerekiyordu. Kodu kendimce modifiye edip uygulamayı tekrar imzalamaya (re-signing) çalıştım. Ama ne yaptıysam uygulama crash oluyor. Saatler geçti, gece 3 oldu, ben hala "Neden açılmıyor bu?" diye ekrana bakıyorum. Meğer uygulama, native katmanda (C++) kendi imzasını kontrol eden bir check barındırıyormuş ve ben sadece Java katmanına bakarak dünyayı fethedeceğimi sanmışım. O gün anladım ki mobil güvenlik, sadece statik kod analizi yapıp "Aha burada API key var!" demekten çok daha fazlası. O günden beri bir mobil uygulamaya bakarken, onun sadece bir yazılım değil, içinde onlarca farklı savunma mekanizması olan canlı bir organizma olduğunu hatırlarım.

Statik Analizin Ötesi: Sadece Bakmak Yetmez

Genelde yapılan en büyük hata, APK veya IPA dosyasını bir decompiler’a atıp orada "secret_key" diye arama yapmaktan ibaret sanılması. Evet, bu bazen işe yarar (ve hala çok fazla geliştirici Firebase anahtarlarını veya AWS secret’larını strings.xml içinde unutuyor), ama gerçek Red Team operasyonlarında karşımıza çıkan asıl canavarlar runtime’da saklı.

Statik analizde dikkatimi çeken ilk şey genelde AndroidManifest.xml dosyası olur. Burası uygulamanın "anayasası" gibidir. exported="true" olan her activity, service veya broadcast receiver aslında dış dünyaya açılmış birer penceredir. Eğer bu pencerelerden içeri kimin girdiğini kontrol etmiyorsanız, başka bir uygulama sizin uygulamanızın yetkilerini kullanarak veri sızdırabilir.

Mesela şu basit ama tehlikeli Deep Link zafiyetine bakalım:

<activity android:name=".WebViewActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="testApp-app" android:host="webview" />
    </intent-filter>
</activity>

Burada testApp-app://webview?url=https://kotuniyetli.com gibi bir link ile kullanıcının uygulamanız içinde istediğiniz siteyi açmasını sağlayabilirsiniz. Eğer WebView üzerinde setJavaScriptEnabled(true) açıksa ve bir de addJavascriptInterface ile Java objeleri JS’e expose edildiyse, geçmiş olsun. Telefonun rehberinden tut, dosya sistemine kadar her yere sızabiliriz.

Frida: Mobil Dünyanın İsviçre Çakısı

Eğer mobil sızma testleriyle uğraşıyorsanız ve Frida kullanmıyorsanız, muhtemelen bir eliniz arkadan bağlı dövüşüyorsunuzdur. Frida, uygulamanın kalbine (runtime) sızıp, fonksiyonları istediğimiz gibi manipüle etmemizi sağlar.

Hadi klasik bir "Root Detection" atlatma senaryosu üzerinden gidelim. Uygulama açılırken isDeviceRooted() diye bir fonksiyon çağırıyor ve true dönerse kapanıyor olsun. Statik analizde bu fonksiyonu bulduk. Şimdi Frida ile bu fonksiyonun her zaman false dönmesini sağlayalım:

Java.perform(function () {
    let RootCheck = Java.use("com.testApp.security.CheckManager");
    
    RootCheck.isDeviceRooted.implementation = function () {
        console.log("[*] Root check fonksiyonu yakalandı, 'false' dönülüyor...");
        return false;
    };
});

Terminalden şu komutu verdiğin an: frida -U -f com.testApp.app -l bypass_root.js --no-pause

Uygulama artık senin kölen olmuş demektir. O fonksiyon arka planda ne yaparsa yapsın, senin yazdığın mantık çalışır. Bu sadece başlangıç; aynı mantıkla SSL Pinning mekanizmalarını da darmadağın edebiliyoruz.

SSL Pinning: Güvenlik mi, Yoksa Yalancı Bahar mı?

Geliştiricilerin "Kesinlikle hacklenemeyiz" dediği nokta genelde SSL Pinning oluyor. Sertifika pinning, uygulamanın sadece belirli bir sertifikaya güvenmesini sağlayarak aradaki MITM (Man-in-the-Middle) saldırılarını engellemeye çalışır. Ama biz Red Team tarafında ne diyoruz? "Client tarafındaki her kontrol, aşılmaya mahkumdur."

Burp Suite’i ayarladın, sertifikayı cihaza kurdun ama trafik akmıyor mu? Muhtemelen pinning var. Frida ile popüler kütüphanelerin (OkHttp3, TrustManager vb.) kontrol noktalarını hooklayarak bu trafiği saniyeler içinde çözebiliriz. Ancak bazen karşımıza "Obfuscated" (karmaşıklaştırılmış) kodlar çıkar. Fonksiyon isimleri a(), b(), c() halindedir. İşte orada iş biraz "geek" seviyesine çıkıyor. Kodun akışını takip edip, hangi fonksiyonun network trafiğinden sorumlu olduğunu anlamak için stack trace basmamız gerekir.

Java.perform(function() {
    var Thread = Java.use("java.lang.Thread");
    var instance = Thread.currentThread();
    var stack = instance.getStackTrace();
    for (var i =0; i < stack.length; i++) {
        console.log(stack[i].toString());
    }
});

Bu çıktı bize uygulamanın hangi yollardan geçerek o hatayı verdiğini söyler. İz sürmek mobil güvenliğin en zevkli kısmıdır.

Native Katmana İniş: Korkutucu Ama Gerekli

Java/Kotlin katmanında işler bir şekilde halloluyor ama iş JNI (Java Native Interface) üzerinden C/C++ kodlarına gelince birçok arkadaş "Buraya kadarmış" diyor. demeyin. libnative-lib.so gibi dosyalar aslında hazine sandığıdır.

Ghidra veya IDA Pro ile bu kütüphaneleri açtığınızda, uygulamanın en kritik algoritmalarının, şifreleme anahtarlarının veya custom protokollerinin orada saklandığını göreceksiniz. Bazen geliştirici, Java katmanında çok sıkı önlemler alır ama native tarafta strcmp ile şifre kontrolü yapar. Hafıza (memory) üzerinde Frida ile Interceptor.attach kullanarak native fonksiyonların argümanlarını okuyabiliriz:

var nativeFuncPtr = Module.findExportByName("libnative-lib.so", "Java_com_testApp_app_NativeLib_checkLicense");

Interceptor.attach(nativeFuncPtr, {
    onEnter: function(args) {
        console.log("[*] Native fonksiyona girildi.");
        console.log("Argüman 1 (License Key): " + Memory.readUtf8String(args[2]));
    },
    onLeave: function(retval) {
        console.log("[*] Fonksiyon bitti, dönen değer: " + retval);
    }
});

Bu script ile native katmanda dönen o "gizli" verileri çatır çatır okuyabilirsin.

Güvenli Sanılan Veri Depolama: SharedPreferences ve Keystore

Bir diğer büyük yanılgı: "Veriyi SharedPreferences içinde tutuyorum, kimse erişemez." Cihaz rootluysa (ki bir saldırganın ilk yapacağı iş budur veya emülatör kullanır), o XML dosyaları metin belgesi gibi okunur.

Android Keystore kullanmak iyi bir pratik, evet. Ama Keystore'un nasıl yapılandırıldığı daha önemli. setUserAuthenticationRequired(true) denilmediği sürece, biyometrik kontrol yapılmadan da o anahtarlara (yine runtime manipülasyonu ile) erişmek mümkün olabiliyor.

Operasyonlarda sıkça karşılaştığım bir durum: Uygulama veriyi şifreliyor ama şifreleme anahtarını (Key) uygulamanın içinde sabit (hardcoded) tutuyor.

// Yapmayın şunu abi, lütfen!
public static final String AES_KEY = "S3cur1ty_2024!";

Bunu gördüğüm an yüzümde oluşan o sinsi gülümsemeyi tahmin edersiniz. Statik analizde bunu kaçırsam bile, Cipher.init metodunu hookladığımda o anahtar kucağıma düşüyor.

Uygulamayı Zırhlandırmak: Gerçekten Mümkün mü?

Peki, bu kadar saldırı yöntemi varken ne yapacağız?

  1. Obfuscation (R8/ProGuard): Sadece kodun okunabilirliğini azaltmaz, aynı zamanda unused code'ları temizler. Ama yetmez, kritik yerlerde daha agresif çözümler (DexGuard gibi) gerekebilir.
  2. Native Checkler: Önemli mantıkları C++ katmanına taşıyın ama orada da "security by obscurity" (gizleyerek güvenlik) tuzağına düşmeyin.
  3. Root/Emulator Detection: Bunlar %100 çözüm değil ama "script kiddie" seviyesindeki saldırganları eler.
  4. Certificate Transparency: Sadece pinning yapmayın, CT loglarını da kontrol edin.
  5. Biometric Prompt: Hassas işlemlerden önce mutlaka sistem seviyesinde bir biyometrik doğrulama isteyin.

Mobil güvenlik, kedi-fare oyununun en yoğun yaşandığı alanlardan biri. Biz Red Team tarafında her zaman bir açık kapı arıyoruz, geliştiriciler ise o kapıyı bizden önce kapatmaya çalışıyor. Ama günün sonunda, en güvenli uygulama "hiç yazılmamış olandır" klişesine sığınmadan, her adımda "Saldırgan olsam buradan nasıl geçerdim?" diye düşünmek bizi kurtaracak olan tek şey.

Şimdi elindeki o APK'yı tekrar bir incele; bakalım o gece 3'te benim göremediğim o küçük native check'i sen görebilecek misin?

İlgili yazılar