Neutron, notre moteur d’IA, a obtenu un score de 96,75 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Sécurité

Sécurité

Trouver des failles de sécurité dans les applications Android à la dure

Ostorlab est un effort communautaire visant à construire un scanner de vulnérabilités d'applications mobiles pour aider les développeurs à créer des applications mobiles sécurisées. L'un des nouveaux composants clés des capacités de détection du scanner est un tout nouveau moteur de taint statique pour le bytecode Dalvik d'Android, fortement optimisé pour la performance et un faible taux de faux positifs.

Ostorlab est un effort communautaire visant à construire un scanner de vulnérabilités d'applications mobiles pour aider les développeurs à créer des applications mobiles sécurisées. L'un des nouveaux composants clés des capacités de détection du scanner est un tout nouveau moteur de taint statique pour le bytecode Dalvik d'Android, fortement optimisé pour la performance et un faible taux de faux positifs.

Une version simple d'un moteur de taint statique calcule la manière dont une entrée contrôlée par l'utilisateur se propage à l'intérieur d'une application. Le suivi du flux repose sur le marquage (taint) des variables et des attributs, d'où le nom de « moteur de taint statique ». Ces informations de taint servent à détecter les vulnérabilités de l'application.

Quelques mois après la livraison de la version initiale du moteur et le scan de plus de 10 000 applications mobiles importées par les utilisateurs, voici quelques-uns des principaux résultats que nous avons recueillis jusqu'ici.

Le moteur de taint statique a détecté plus de 600 vulnérabilités à haut risque, allant de l'injection SQL dans les content providers, à la validation non sécurisée des certificats serveur SSL/TLS (détectée statiquement), à l'injection de commandes, aux préférences partagées non sécurisées, à la cryptographie faible, aux clés codées en dur et à bien d'autres classes de vulnérabilités.

Voici quelques exemples de vulnérabilités trouvées grâce au moteur de taint statique ; j'ai pris soin de ne partager que des exemples tirés d'applications volontairement vulnérables :

Injection SQL dans un content provider :

Le deuxième paramètre (1 si l'on compte à partir de 0) de la méthode android.database.sqlite.SQLiteDatabase.delete() provoquera une injection SQL s'il est contrôlé par l'utilisateur.
Le paramètre est exposé par la méthode jakhar.aseem.diva.NotesProvider.delete() du content provider exporté, ce qui rend l'application vulnérable à une injection SQL :

[TAINT] Parameter '1' ==*==*==*==*==>>> Sink '[u'Landroid/database/sqlite/SQLiteDatabase;', u'delete', u'(Ljava/lang/String; Ljava/lang/String; [Ljava/lang/String;)I', u'1', u'SQL_SINK']'
===========
|__Ljakhar/aseem/diva/NotesProvider;->delete(Landroid/net/Uri; Ljava/lang/String; [Ljava/lang/String;)I / 0
 |__Landroid/content/ContentResolver;->notifyChange(Landroid/net/Uri; Landroid/database/ContentObserver;)V (no childs) / 1
 |__Landroid/content/Context;->getContentResolver()Landroid/content/ContentResolver; (no childs) / 1
 |__Landroid/content/UriMatcher;->match(Landroid/net/Uri;)I (no childs) / 1
 |__Landroid/database/sqlite/SQLiteDatabase;->delete(Ljava/lang/String; Ljava/lang/String; [Ljava/lang/String;)I (no childs) / 1
 |__Landroid/net/Uri;->getLastPathSegment()Ljava/lang/String; (no childs) / 1
 |__Landroid/text/TextUtils;->isEmpty(Ljava/lang/CharSequence;)Z (no childs) / 1
 |__Ljakhar/aseem/diva/NotesProvider;->getContext()Landroid/content/Context; (no childs) / 1
 |__Ljava/lang/IllegalArgumentException;->(Ljava/lang/String;)V (no childs) / 1
 |__Ljava/lang/StringBuilder;->()V (no childs) / 1
 |__Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder; (no childs) / 1
 |__Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder; (no childs) / 1
 |__Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; (no childs) / 1
 |__Ljava/lang/StringBuilder;->toString()Ljava/lang/String; (no childs) / 1
===========

Un paramètre contrôlé par l'utilisateur est utilisé pour construire un paramètre SQL vulnérable à l'injection SQL Méthode jakhar.aseem.diva.NotesProvider.delete() :

public int delete(android.net.Uri p8, String p9, String[] p10)
    {
        int v0;
        switch (jakhar.aseem.diva.NotesProvider.urimatcher.match(p8)) {
            case 1:
                v0 = this.mDB.delete("notes", p9, p10);
                break;
            case 2:
                String v2_6;
                int v3_0 = this.mDB;
                StringBuilder v5_1 = new StringBuilder().append("_id = ").append(p8.getLastPathSegment());
                if (android.text.TextUtils.isEmpty(p9)) {
                    v2_6 = "";
                } else {
                    v2_6 = new StringBuilder().append(" AND (").append(p9).append(41).toString();
                }
                v0 = v3_0.delete("notes", v5_1.append(v2_6).toString(), p10);
                break;
            default:
                throw new IllegalArgumentException(new StringBuilder().append("Divanotes(delete): Unsupported URI ").append(p8).toString());
        }
        this.getContext().getContentResolver().notifyChange(p8, 0);
        return v0;
    }

Injection de commandes :

Voici un exemple d'utilisation d'une commande dangereuse qui définit des permissions permissives non sécurisées avec le mode '777', lecture, écriture et exécution pour l'utilisateur, le groupe et les autres : on ne peut pas faire plus permissif que ça :/ :

[TAINT] String '/system/bin/chmod -R 0777 F1.txt file12.txt' ==*==*==*==*==>>> Sink '[u'Ljava/lang/Runtime;', u'exec', u'([Ljava/lang/String; [Ljava/lang/String; Ljava/io/File;)Ljava/lang/Process;', u'Object', u'COMMAND_SINK']'
===========
|__Lcom/ibm/android/analyzer/test/cmdinjection/CommandInjection6;->onCreate(Landroid/os/Bundle;)V / 0
 |__Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V (no childs) / 1
 |__Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String; (no childs) / 1
 |__Lcom/ibm/android/analyzer/test/cmdinjection/CommandInjection6;->cmdRuntime(Ljava/lang/String; I)V / 1
  |__Landroid/content/Context;->getFilesDir()Ljava/io/File; (no childs) / 2
  |__Landroid/util/Log;->i(Ljava/lang/String; Ljava/lang/String;)I (no childs) / 2
  |__Ljava/io/File;->getAbsolutePath()Ljava/lang/String; (no childs) / 2
  |__Ljava/lang/Exception;->printStackTrace()V (no childs) / 2
  |__Ljava/lang/Runtime;->exec(Ljava/lang/String; [Ljava/lang/String; Ljava/io/File;)Ljava/lang/Process; (no childs) / 2
  |__Ljava/lang/Runtime;->exec(Ljava/lang/String; [Ljava/lang/String;)Ljava/lang/Process; (no childs) / 2
  |__Ljava/lang/Runtime;->exec(Ljava/lang/String;)Ljava/lang/Process; (no childs) / 2
  |__Ljava/lang/Runtime;->exec([Ljava/lang/String; [Ljava/lang/String; Ljava/io/File;)Ljava/lang/Process; (no childs) / 2
  |__Ljava/lang/Runtime;->exec([Ljava/lang/String; [Ljava/lang/String;)Ljava/lang/Process; (no childs) / 2
  |__Ljava/lang/Runtime;->exec([Ljava/lang/String;)Ljava/lang/Process; (no childs) / 2
  |__Ljava/lang/Runtime;->getRuntime()Ljava/lang/Runtime; (no childs) / 2
  |__Ljava/lang/StringBuilder;->()V (no childs) / 2
  |__Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; (no childs) / 2
  |__Ljava/lang/StringBuilder;->toString()Ljava/lang/String; (no childs) / 2
 |__Lcom/ibm/android/analyzer/test/cmdinjection/CommandInjection6;->getIntent()Landroid/content/Intent; (no childs) / 1
 |__Ljava/lang/StringBuilder;->()V (no childs) / 1
 |__Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; (no childs) / 1
 |__Ljava/lang/StringBuilder;->toString()Ljava/lang/String; (no childs) / 1
===========

L'application exécute une commande dangereuse
Méthode com.ibm.android.analyzer.test.cmdinjection.CommandInjection6.onCreate() :

protected void onCreate(android.os.Bundle p7)
    {
        super.onCreate(p7);
        android.content.Intent v2 = this.getIntent();
        String v0 = v2.getStringExtra("exec");
        if (v0 == null) {
            String v1 = v2.getStringExtra("execR");
            if (v1 == null) {
                this.cmdRuntime("/system/bin/chmod 0777 /data/data/com.ibm.android.analyzer.test/1.txt", 5);
                this.cmdRuntime("/system/bin/chmod -R 0777 F1.txt file12.txt", 5);
            } else {
                this.cmdRuntime(new StringBuilder().append("/system/bin/sh ").append(v1).toString(), 5);
            }
        } else {
            this.cmdRuntime(v0, 5);
        }
        return;
    }

Clés de chiffrement codées en dur :

L'utilisation de clés de chiffrement codées en dur est un autre exemple de vulnérabilité courante que nous observons dans les applications mobiles ; dans cet exemple, la chaîne 'superSecurePassword' est utilisée pour appeler une méthode de chiffrement :

[TAINT] String 'superSecurePassword' ==*==*==*==*==>>> Sink '[u'Ljavax/crypto/spec/SecretKeySpec;', u'', u'([B Ljava/lang/String;)V', u'0', u'CIPHER_SINK']'
===========
|__Lcom/android/insecurebankv2/MyBroadCastReceiver;->onReceive(Landroid/content/Context; Landroid/content/Intent;)V / 0
 |__Landroid/content/Context;->getSharedPreferences(Ljava/lang/String; I)Landroid/content/SharedPreferences; (no childs) / 1
 |__Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String; (no childs) / 1
 |__Landroid/content/SharedPreferences;->getString(Ljava/lang/String; Ljava/lang/String;)Ljava/lang/String; (no childs) / 1
 |__Landroid/telephony/SmsManager;->getDefault()Landroid/telephony/SmsManager; (no childs) / 1
 |__Landroid/telephony/SmsManager;->sendTextMessage(Ljava/lang/String; Ljava/lang/String; Ljava/lang/String; Landroid/app/PendingIntent; Landroid/app/PendingIntent;)V (no childs) / 1
 |__Landroid/util/Base64;->decode(Ljava/lang/String; I)[B (no childs) / 1
 |__Lcom/android/insecurebankv2/CryptoClass;->()V / 1
  |__Ljava/lang/Object;->()V (no childs) / 2
 |__Lcom/android/insecurebankv2/CryptoClass;->aesDeccryptedString(Ljava/lang/String;)Ljava/lang/String; / 1
  |__Landroid/util/Base64;->decode([B I)[B (no childs) / 2
  |__Lcom/android/insecurebankv2/CryptoClass;->aes256decrypt([B [B [B)[B / 2
   |__Ljavax/crypto/Cipher;->doFinal([B)[B (no childs) / 3
   |__Ljavax/crypto/Cipher;->getInstance(Ljava/lang/String;)Ljavax/crypto/Cipher; (no childs) / 3
   |__Ljavax/crypto/Cipher;->init(I Ljava/security/Key; Ljava/security/spec/AlgorithmParameterSpec;)V (no childs) / 3
   |__Ljavax/crypto/spec/IvParameterSpec;->([B)V (no childs) / 3
   |__Ljavax/crypto/spec/SecretKeySpec;->([B Ljava/lang/String;)V (no childs) / 3
  |__Ljava/lang/String;->([B Ljava/lang/String;)V (no childs) / 2
  |__Ljava/lang/String;->getBytes(Ljava/lang/String;)[B (no childs) / 2
 |__Ljava/io/PrintStream;->println(Ljava/lang/String;)V (no childs) / 1
 |__Ljava/lang/Exception;->printStackTrace()V (no childs) / 1
 |__Ljava/lang/String;->([B Ljava/lang/String;)V (no childs) / 1
 |__Ljava/lang/String;->toString()Ljava/lang/String; (no childs) / 1
 |__Ljava/lang/StringBuilder;->()V (no childs) / 1
 |__Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; (no childs) / 1
 |__Ljava/lang/StringBuilder;->toString()Ljava/lang/String; (no childs) / 1
===========

L'application utilise une clé codée en dur pour chiffrer les données
Méthode com.android.insecurebankv2.MyBroadCastReceiver.onReceive() :

public void onReceive(android.content.Context p17, android.content.Intent p18)
    {
        String v12 = p18.getStringExtra("phonenumber");
        String v10 = p18.getStringExtra("newpass");
        if (v12 == null) {
            System.out.println("Phone number is null");
        } else {
            try {
                android.content.SharedPreferences v13 = p17.getSharedPreferences("mySharedPreferences", 1);
                this.usernameBase64ByteString = new String(android.util.Base64.decode(v13.getString("EncryptedUsername", 0), 0), "UTF-8");
                String v8 = new com.android.insecurebankv2.CryptoClass().aesDeccryptedString(v13.getString("superSecurePassword", 0));
                String v2 = v12.toString();
                String v4 = new StringBuilder().append("Updated Password from: ").append(v8).append(" to: ").append(v10).toString();
                android.telephony.SmsManager v1 = android.telephony.SmsManager.getDefault();
                System.out.println(new StringBuilder().append("For the changepassword - phonenumber: ").append(v2).append(" password is: ").append(v4).toString());
                v1.sendTextMessage(v2, 0, v4, 0, 0);
            } catch (Exception v9) {
                v9.printStackTrace();
            }
        }
        return;
    }

Validation non sécurisée des certificats SSL/TLS :

Voici un exemple de méthode utilisant le ALLOW_ALL_HOSTNAME_VERIFIER, non sécurisé, pour construire un schéma de validation des certificats SSL/TLS :

[TAINT] Class 'Lorg/apache/http/conn/ssl/SSLSocketFactory;' ==*==*==*==*==>>> Sink '[u'Lorg/apache/http/conn/ssl/SSLSocketFactory;', u'setHostnameVerifier', u'(Lorg/apache/http/conn/ssl/X509HostnameVerifier;)V', u'Object', u'SSLTLS_SINK']'
===========
|__Lcom/ibm/android/analyzer/test/domainvalidation/InsecureApacheSSFAllowAllHostnameVerifier$1;->call()Ljava/lang/Void; / 0
 |__Landroid/util/Log;->i(Ljava/lang/String; Ljava/lang/String;)I (no childs) / 1
 |__Ljava/lang/Exception;->printStackTrace()V (no childs) / 1
 |__Ljava/net/URL;->(Ljava/lang/String;)V (no childs) / 1
 |__Ljava/net/URL;->openConnection()Ljava/net/URLConnection; (no childs) / 1
 |__Ljava/security/KeyStore;->getDefaultType()Ljava/lang/String; (no childs) / 1
 |__Ljava/security/KeyStore;->getInstance(Ljava/lang/String;)Ljava/security/KeyStore; (no childs) / 1
 |__Ljava/security/KeyStore;->load(Ljava/io/InputStream; [C)V (no childs) / 1
 |__Ljavax/net/ssl/HttpsURLConnection;->connect()V (no childs) / 1
 |__Ljavax/net/ssl/SSLContext;->getInstance(Ljava/lang/String;)Ljavax/net/ssl/SSLContext; (no childs) / 1
 |__Ljavax/net/ssl/SSLContext;->init([Ljavax/net/ssl/KeyManager; [Ljavax/net/ssl/TrustManager; Ljava/security/SecureRandom;)V (no childs) / 1
 |__Lorg/apache/http/conn/ssl/SSLSocketFactory;->(Ljava/security/KeyStore;)V (no childs) / 1
 |__Lorg/apache/http/conn/ssl/SSLSocketFactory;->setHostnameVerifier(Lorg/apache/http/conn/ssl/X509HostnameVerifier;)V (no childs) / 1
===========

Utilisation de l'attribut non sécurisé ALLOW_ALL_HOSTNAME_VERIFIER pour valider un certificat TLS Méthode com.ibm.android.analyzer.test.domainvalidation.InsecureApacheSSFAllowAllHostnameVerifier$1.call() :

public Void call()
    {
        try {
            android.util.Log.i(this.this$0.TAG, "1");
            javax.net.ssl.SSLContext.getInstance("TLS").init(0, 0, 0);
            java.net.URL v4_1 = new java.net.URL("https://1.www.s81c.com/i/v17/t/ibm_logo_print.png?dv1");
            android.util.Log.i(this.this$0.TAG, "2");
            javax.net.ssl.HttpsURLConnection v5_1 = ((javax.net.ssl.HttpsURLConnection) v4_1.openConnection());
            java.security.KeyStore v3 = java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType());
            v3.load(0, 0);
            android.util.Log.i(this.this$0.TAG, "3");
            new org.apache.http.conn.ssl.SSLSocketFactory(v3).setHostnameVerifier(org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
            android.util.Log.i(this.this$0.TAG, "4");
            v5_1.connect();
            android.util.Log.i(this.this$0.TAG, "5");
        } catch (Exception v0) {
            android.util.Log.i(this.this$0.TAG, "exception 1!");
            v0.printStackTrace();
        }
        return 0;
    }

Concepts clés :

Le moteur représente presque une année entière d'effort ; il a commencé comme une PoC en Python. Python a permis un prototypage rapide, en se concentrant sur les algorithmes et les structures de données. L'implémentation actuelle utilise une représentation sous forme de graphe de la propagation du taint à l'intérieur d'une seule fonction (voir le graphe)

texte alternatif
Selection_071

Le graphe sert à évaluer le taint des autres fonctions, ce qui offre un moteur de taint statique très rapide et utilisable en conditions réelles, tout en tenant compte de l'aspect orienté objet du bytecode Dalvik pour garantir une propagation du taint précise.

Pour générer un graphe de taint, une liste de chemins d'exécution est compilée et évaluée individuellement, puis fusionnée avec un taint global de la fonction.

Cette approche est toutefois limitée si la fonction possède une structure de chemins d'exécution exponentielle (voir l'exemple de graphe) ; ce problème, communément appelé explosion des chemins, est une limitation importante des méthodes d'analyse statique, comme l'exécution symbolique.

texte alternatif
Selection_072

Pour remédier à cette limitation, nous transformons le problème en « problème de recherche » plutôt qu'en « problème de force brute ». Un algorithme de sélection de chemins retient les chemins ayant la plus forte probabilité de présence d'une vulnérabilité ; par exemple, si un chemin donné ne traverse aucune fonction sink (les fonctions sink peuvent causer une vulnérabilité si elles sont appelées avec des paramètres contrôlés par l'utilisateur), il n'y a alors aucune vulnérabilité à rechercher et les chemins d'exécution sont exclus.

texte alternatif
Pasted_Image0

L'implémentation actuelle a été réécrite en C++14 après l'étude de plusieurs autres langages de programmation (Rust, Go et C), ce qui a apporté un gain de vitesse d'exécution de plus de 200x.

Il reste encore de la marge pour augmenter les performances et la couverture du code, mais aussi pour corriger plusieurs faux positifs dus à l'utilisation d'un graphe sur-marqué par défaut pour les méthodes natives de bas niveau.

Ces capacités font déjà partie d'Ostorlab Scanner et sont continuellement, et discrètement :), améliorées chaque jour.

Nous vous encourageons à le tester et à partager vos retours. S'il existe une vulnérabilité que vous pensez que nous manquons ou un faux positif que le scanner signale, nous aimerions beaucoup en entendre parler et essayer de trouver un moyen de le corriger ou de la détecter.