Security

Finding security bugs in Android applications the hard way

Ostorlab is a community effort to build a mobile application vulnerability scanner to help developers build secure mobile applications. One of the new key components of the scanner detection capabilities is a new shiny static taint engine for Android Dalvik Bytecode that was heavily optimized for performance and low false positives.

Fri 16 June 2017

Ostorlab is a community effort to build a mobile application vulnerability scanner to help developers build secure mobile applications. One of the new key components of the scanner detection capabilities is a new shiny static taint engine for Android Dalvik Bytecode that was heavily optimized for performance and low false positives.

A simple version of a Static Taint Engine computes how user controlled input propagates inside an application. Tracking the flow uses tainting of variables and attributes, hence the name 'Static Taint Engine'. This taint information serves to detect vulnerabilities in the application.

Few months after shipping the initial version of the engine and scanning over 10.000 mobile applications uploaded by users, these are some of the key results we have collected so far.

The static taint engine has detected over 600 high risk vulnerabilities, ranging from content provider SQL injection, insecure SSL/TLS server certificate validation (detected statically), command injection, insecure shared preferences, weak cryptography, hard coded keys and many other classes of vulnerabilities.

These are some examples of the vulnerabilities found using the static taint engine, I was cautious to only share examples from voluntarily insecure applications:

Content provider SQL injection:

The second parameter (1 if you count from 0) of the method android.database.sqlite.SQLiteDatabase.delete() will cause a SQL injection if user-controlled.
The parameter is exposed by the exported content provider method jakhar.aseem.diva.NotesProvider.delete(), hence making the application vulnerable to a SQL injection:

[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
===========

User controlled parameter is used to construct an SQL parameter vulnerable to SQL injection Method 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;
    }

Command injection:

This is an example of the use of a dangerous commands that sets insecure permissive permissions using the mode '777', read write execute to user, group and other, it can't get more permissive that that :/ :

[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
===========

The application executes a dangerous command
Method 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;
    }

Hard-coded encryption keys:

The use of hard-coded encryption keys are another example of common vulnerabilities we see in mobile applications, in the example, the string 'superSecurePassword' is used to called an encryption method:

[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
===========

The application uses a hardcoded key to encrypt the data
Method 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;
    }

Insecure SSL/TLS service certificate validation:

This is an example of a method using the insecure ALLOW_ALL_HOSTNAME_VERIFIER to construct an SSL/TLS certificate validation scheme:

[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
===========

Use of the insecure attribute ALLOW_ALL_HOSTNAME_VERIFIER to validate TLS certificate Method 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;
    }

Key concepts:

The engine is almost a full year effort that started as a PoC in Python. Python allowed for quick prototyping, focusing on the algorithms and data structures. The current implementation uses a graph representation of the taint propagation inside a single function (see graph)

alt text
Selection_071

The graph is used to evaluate the taint of other functions offering a very fast and real world usable static taint engine, while at same time taking into account object oriented aspect of Dalvik Bytecode to ensure accurate taint propagation.

To generate a taint graph, a list of execution paths are compiled and evaluated singularly, then fused with a global function taint.

This approach is however limited if the function has an exponential execution path structure (see graph example), this problem is commonly known as path explosion and is a strong limitation of static analysis methods, like symbolic execution.

alt text
Selection_072

To remediate this limitation, we transform the problem into a 'search problem' rather then a 'brute force problem'. A path selection algorithm selects paths with the highest probability of the presence of a vulnerability, for instance if a particular path do not cross any sink function - sink functions might cause a vulnerability if called using user controlled parameters - then there is no vulnerability to look for and the execution paths are excluded.

alt text
Pasted_Image0

The current implementation was rewritten in C++14 after investigating several other programming languages (Rust, Go and C) which offered over 200x gain in execution speed.

There are still room to increase performance and code coverage, but also fix several false positives due to the use of a default over-tainted graph for low level native methods.

These capabilities are already part of Ostorlab Scanner and are continuously, and silently :), being enhanced every day.

We urge you to test it and share your feedback. If there is a vulnerability that you think we are missing or a false positive that the scanner is reporting, we would love to hear from you and try to work on a ways to fix it or detect it.