当社のAIエンジンNeutronが、UCバークレーのCyberGymベンチマークで96.75%のスコアを記録しました。 詳細を見る

セキュリティ

セキュリティ

Androidアプリケーションのセキュリティバグを地道な方法で見つける

Ostorlabは、開発者が安全なモバイルアプリケーションを構築できるよう、モバイルアプリケーションの脆弱性スキャナーを構築するコミュニティの取り組みです。スキャナーの検出機能を支える新たな主要コンポーネントの一つが、パフォーマンスと誤検知の少なさを重視して徹底的に最適化された、Android Dalvikバイトコード向けの新しい静的テイントエンジンです。

Ostorlabは、開発者が安全なモバイルアプリケーションを構築できるよう、モバイルアプリケーションの脆弱性スキャナーを構築するコミュニティの取り組みです。スキャナーの検出機能を支える新たな主要コンポーネントの一つが、パフォーマンスと誤検知(フォールスポジティブ)の少なさを重視して徹底的に最適化された、Android Dalvikバイトコード向けの真新しい静的テイントエンジンです。

静的テイントエンジンの単純なバージョンは、ユーザーが制御する入力がアプリケーション内部でどのように伝播するかを計算します。フローの追跡には変数と属性へのテイント(汚染)付けを用いるため、「静的テイントエンジン」と呼ばれます。このテイント情報は、アプリケーションの脆弱性を検出するために使われます。

エンジンの初期バージョンをリリースし、ユーザーがアップロードした10.000件以上のモバイルアプリケーションをスキャンしてから数か月が経ちました。これまでに得られた主な結果を紹介します。

静的テイントエンジンは600件を超える高リスクの脆弱性を検出しました。その内容は、コンテンツプロバイダーのSQLインジェクション、安全でないSSL/TLSサーバー証明書の検証(静的に検出)、コマンドインジェクション、安全でないSharedPreferences、脆弱な暗号、ハードコードされた鍵など、多岐にわたる脆弱性クラスに及びます。

以下は、静的テイントエンジンを使って発見された脆弱性の例です。共有する例は、意図的に脆弱に作られたアプリケーションのものに限るよう注意しました。

コンテンツプロバイダーのSQLインジェクション

メソッドandroid.database.sqlite.SQLiteDatabase.delete()の2番目のパラメーター(0から数えると1)は、ユーザーが制御できる場合にSQLインジェクションを引き起こします。
このパラメーターは、エクスポートされたコンテンツプロバイダーのメソッドjakhar.aseem.diva.NotesProvider.delete()によって公開されているため、アプリケーションは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
===========

ユーザーが制御するパラメーターが、SQLインジェクションに対して脆弱なSQLパラメーターの構築に使われています メソッド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;
    }

コマンドインジェクション

これは、モード「777」、つまりユーザー、グループ、その他のすべてに読み取り・書き込み・実行を許可する、安全でない緩いパーミッションを設定する危険なコマンドの使用例です。これ以上緩くはできません :/

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

アプリケーションが危険なコマンドを実行しています
メソッド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;
    }

ハードコードされた暗号鍵

ハードコードされた暗号鍵の使用も、モバイルアプリケーションでよく見られる脆弱性の一例です。この例では、文字列「superSecurePassword」が暗号化メソッドの呼び出しに使われています。

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

アプリケーションが、ハードコードされた鍵を使ってデータを暗号化しています
メソッド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;
    }

安全でないSSL/TLSサービス証明書の検証

これは、安全でないALLOW_ALL_HOSTNAME_VERIFIERを使って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
===========

TLS証明書の検証に、安全でない属性ALLOW_ALL_HOSTNAME_VERIFIERが使われています メソッド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;
    }

主要な概念

このエンジンは、PythonによるPoCとして始まり、ほぼ丸1年をかけて開発されたものです。Pythonのおかげで素早くプロトタイプを作成でき、アルゴリズムとデータ構造に集中できました。現在の実装では、単一の関数内部のテイント伝播をグラフで表現しています(グラフを参照)。

代替テキスト
Selection_071

このグラフを使ってほかの関数のテイントを評価することで、非常に高速で実環境でも使える静的テイントエンジンを実現しています。同時に、Dalvikバイトコードのオブジェクト指向的な側面も考慮し、正確なテイント伝播を確保しています。

テイントグラフを生成するには、実行パスのリストを作成してそれぞれを個別に評価し、その後、関数全体のテイントと統合します。

ただし、関数の実行パスが指数関数的な構造を持つ場合(グラフの例を参照)、このアプローチには限界があります。この問題は一般にパス爆発と呼ばれ、シンボリック実行などの静的解析手法にとって大きな制約となっています。

代替テキスト
Selection_072

この制約を解消するために、当社は問題を「総当たりの問題」ではなく「探索の問題」に変換しています。パス選択アルゴリズムは、脆弱性が存在する可能性が最も高いパスを選びます。たとえば、あるパスがシンク関数(ユーザーが制御するパラメーターで呼び出されると脆弱性を引き起こし得る関数)を一つも通らない場合、探すべき脆弱性は存在しないため、その実行パスは除外されます。

代替テキスト
Pasted_Image0

現在の実装は、ほかのいくつかのプログラミング言語(Rust、Go、C)を検討したうえでC++14で書き直されており、実行速度は200x以上向上しました。

パフォーマンスとコードカバレッジにはまだ改善の余地があります。また、低レベルのネイティブメソッドに対してデフォルトで過剰にテイントされたグラフを使っていることに起因する、いくつかの誤検知も修正する必要があります。

これらの機能はすでにOstorlab Scannerに組み込まれており、日々継続的に、そしてひっそりと :) 強化されています。

ぜひ試してフィードバックをお寄せください。当社が見逃していると思われる脆弱性や、スキャナーが報告している誤検知があれば、ぜひお知らせください。修正や検出の方法を検討いたします。

タグ:

android, sql injection