Upgrade to Pro — share decks privately, control downloads, hide ads and more …

bzhcmp2019

 bzhcmp2019

Outils et bonnes pratiques de développement d'applications Android sécurisées

Ne faites pas confiance à votre application : une fois déployée, tout peut arriver...
Les sujets abordés, dans les grandes lignes :
* Le code source : obfuscation
* Où cacher ses clés d'API? Le stockage de données sécurisées
* Communication serveur : Certificate pinning et attaques man-in-the-middle
* Détecter les devices hackés : SafetyNet API / Anti root
* Anti-tampering / debugging / instrumentation
* Live hacking (éventuellement)

Alain Muller

March 21, 2019
Tweet

More Decks by Alain Muller

Other Decks in Technology

Transcript

  1. SÉCURITÉ = PROTECTION ▸ Propriété Intellectuelle Code Source ▸ Données

    utilisateur Stockage interne ▸ Backend Communication serveur
  2. “ The Attack Surface describes all of the different points

    where an attacker could get into a system, and where they could get data out. [owasp.org]
  3. “ Obfuscation is the obscuring of the intended meaning of

    communication by making the message difficult to understand, usually with confusing and ambiguous language. [wikipedia]
  4. DÉMO STEP 0 : values/strings.xml STEP 1 : hardcoded constant

    STEP 2 : enable Proguard Projet : Jancsoro/SimpleWeatherApp strings/aapt apktool jadx-gui
  5. NetworkService.kt package com.designhumanist.faragojanos.weaterforecast.nework companion object Factory { fun create(context: Context)

    = NetworkService(context) // Load native library init { System.loadLibrary("native-lib") } } // Link to native function private external fun invokeNativeFunction(): String native-lib.cpp #include <jni.h> extern "C" { JNIEXPORT jstring JNICALL Java_com_designhumanist_faragojanos_weaterforecast_nework_NetworkService _invokeNativeFunction(JNIEnv *env, jobject instance) { return env ->NewStringUTF("d2af3044bfa40b5bd5ff40cf8ee8034c"); } }
  6. CMakeLists.txt cmake_minimum_required(VERSION 3.4.1) add_library( # Specifies the name of the

    library. native-lib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). main/cpp/native-lib.cpp )
  7. SharedPreferences = XML Lisible Émulateur : possibilité d’accéder en root

    à la mémoire interne! The common location where SharedPreferences are stored in Android apps is: /data/data/<package name>/shared_prefs/<filename.xml>
  8. $ adb root restarting adbd as root $ adb shell

    generic_x86_64:/ # cat /data/data/com.designhumanist.faragojanos.weaterforecast/ shared_prefs/WeatherForecast.xml <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <string name="LOGIN">alain </string> <string name="PASS">plop </string> </map> private fun saveCredentials(login: String, pass: String) { val sharedPreferences = getSharedPreferences("WeatherForecast", Context.MODE_PRIVATE) val editor = sharedPreferences.edit() editor.putString("LOGIN", login) editor.putString("PASS", pass) editor.apply() }
  9. generic_x86_64:/ # cat /data/data/com.designhumanist.faragojanos.weaterforecast/shared_prefs/Hawk2.xml <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map>

    <string name="PASS"> java.lang.String ##0V@AQJRCdjCn9HHTNtXdFoW49XSTO9Kk4a0+qnGCLz+xwMXzzu2 </string> <string name="LOGIN"> java.lang.String ##0V@AQL7/rang0Ww4RSIrBZZ8TK9md9bAgd5bKiK0S0KHKyTQGS/NA == </string> </map> private fun saveCredentials(login: String, pass: String){ Hawk.init(this).build() Hawk.put("LOGIN", login) Hawk.put("PASS", pass) }
  10. Stockage interne = Exploitable! Même sur device non rooté! Carte

    SD -> n’en parlons même pas! DÉMO STEP 5 > 8 : Add realm encryption $ adb backup com.designhumanist.faragojanos.weaterforecast $ abe unpack backup.ab backup.tar $ tar -xvf backup.tar Android backup extractor ❤ https://github.com/nelenkov/android-backup-extractor
  11. “ The man-in-the middle attack intercepts a communication between two

    systems. Once the TCP connection is intercepted, the attacker acts as a proxy, being able to read, insert and modify the data in the intercepted communication. [owasp.org]
  12. “ Certificate Pinning is the process of associating a host

    with their expected X509 certificate. Once a certificate is known or seen for a host, the certificate is associated or 'pinned' to the host. [owasp.org] Warning: Certificate Pinning is Dangerous! Do not use certificate pinning without the blessing of your server's TLS administrator! [square]
  13. companion object Factory { private val HOSTNAME = "api.openweathermap.org" private

    val BASE_URL = "https: //$HOSTNAME/data/2.5/" fun create(): WeatherApiService { val okHttpClient = OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) .certificatePinner(CertificatePinner.Builder() .add(HOSTNAME, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") .build()) .build() return Retrofit.Builder() .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .baseUrl(BASE_URL) .client(okHttpClient) .build() .create(WeatherApiService ::class.java) } }
  14. --> GET https://api.openweathermap.org/data/2.5/weather?q=paris&units=metric&APPID=ade0c9f95 63238c309e0bbf92c5075ec --> END GET <-- HTTP FAILED:

    javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure! Peer certificate chain: sha256/0yOBPqOzhyp7U/418MLiLzURUneGendsdgs9G2+I2CA=: CN=*.openweathermap.org,OU=EssentialSSL Wildcard,OU=Domain Control Validated sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=: CN=COMODO RSA Domain Validation Secure Server CA,O=COMODO CA Limited,L=Salford,ST=Greater Manchester,C=GB sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=: CN=COMODO RSA Certification Authority,O=COMODO CA Limited,L=Salford,ST=Greater Manchester,C=GB Pinned certificates for api.openweathermap.org: sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
  15. “ Anti-tamper software (or tamper- resistant software) is software which

    makes it harder for an attacker to modify it. [wikipedia]
  16. fun isHacked(context: Context): Boolean { val GOOGLE_INSTALLER = "com.android.vending" val

    AMAZON_INSTALLER = "com.amazon.venezia" val myPackageName = "YOUR app package name" // App has been renamed ? if (context.packageName.compareTo(myPackageName) != 0) { return true // BOOM! } // App properly installed ? val installer = context.packageManager .getInstallerPackageName(myPackageName) ?: return true // BOOM! // App is relocated ? return installer.compareTo(GOOGLE_INSTALLER) != 0 && installer.compareTo(AMAZON_INSTALLER) != 0 }
  17. fun isEmulator(): Boolean { return (Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("unknown") || Build.MODEL.contains("google_sdk")

    || Build.MODEL.contains("Emulator") || Build.MODEL.contains("Android SDK built for x86") || Build.MANUFACTURER.contains("Genymotion") || Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic") || "google_sdk" == Build.PRODUCT) } fun isInDebugMode(): Boolean { return Debug.isDebuggerConnected() } @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1) fun isDeveloperModeEnabled(context: Context): Boolean { val devOptions = Settings.Secure.getInt(context.contentResolver, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) return devOptions > 0 }
  18. • RootTools -> https://github.com/Stericson/RootTools beaucoup d’outils, overkill pour juste détecter

    le root • RootBeer -> https://github.com/scottyab/rootbeer Solution simple et efficace • Code natif -> https://stackoverflow.com/a/37237473/5664885 fail le RootCloak • SafetyNet API -> https://stackoverflow.com/a/45363495/5664885 Made by Google, validation côté serveur… • Crashlytics -> https://stackoverflow.com/a/35628977/5664885 Solution simple mais lib Fabric Détecter le root
  19. BONUS Parcourir les diff d’un code source pour trouver les

    éventuelles clé d’API qui auraient été ensuite supprimées : git log -S<expression> -p
  20. commit ccaaf0aafa4975b0cbd9fc93e9bf63c04e9c2658 (tag: STEP1) Author: Alain Muller <[email protected]> Date: Tue

    May 29 11:42:13 2018 +0200 Remove API_KEY from XML res diff app/src/main/java/com/designhumanist/faragojanos/weaterforecast/nework/Netwo rkService.kt ... class NetworkService(private val context: Context) { + val API_KEY = "d2af3044bfa40b5bd5ff40cf8ee8034c" val DAYS: Int = 4 fun getWeather(city: String): Single<CurrentWeather> { return WeatherApiService.create().getCurrentWeather(city, context.getString(R.string.metric), - context.getString(R.string.APIKEY)) + API_KEY) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .toSingle() ... diff app/src/main/res/values/strings.xml <resources> <string name="app_name">WeaterForecast</string> - <string name="APIKEY">d2af3044bfa40b5bd5ff40cf8ee8034c</string> <string name="metric">metric</string>