こんにちは。
今回は、Androidプログラミングにおける、セキュリティ上の注意点(特にSQL Injectionについて)を考えていこうと思います。
内容は以下のように進めていきます。
- はじめに〜Androidにおけるセキュアプログラミングの意義〜
- Androidのセキュリティアーキテクチャ
- SQLiteを用いたAndroidアプリのデータ管理方法
- ContentProvider利用時におけるSQL Injection対策
- まとめ
はじめに〜Androidにおけるセキュアプログラミングの意義〜
Androidの情勢
昨今スマートフォン市場の成長が急拡大し、特にAndroidのユーザ数が急速に増えています。
2011年12月には、公式のAndroidMarket(現在はGoogle Play)からダウンロードされたアプリの数が100億に達し、2012年9月には250億に達しました。
実際、2012年の年初は1000個だったAndroid不正アプリは、年末までに35万個へと急増しており、PCの脅威の変遷と比べると約4倍のスピードで成長しています。
こうした中で、サイバー犯罪者たちにとってAndroidスマートフォンは「格好のターゲット」となることは間違いありません。
AndroidユーザはAndroidのセキュリティ構造を正しく理解し、サイバー犯罪者からの攻撃を未然に防ぐ必要があります。
また、Androidアプリ作成者も悪意のある不正Androidアプリからの攻撃を防ぐようセキュアコーディングの知識が求められます。
Android内で保護が必要な情報
Android端末を利用した様々なサービスの中には、個人情報を扱うものが少なくありません。
例えば、連絡先アプリには氏名、電話番号、住所、emailアドレスが登録されています。
また、オフィス系アプリをインストールして仕事の資料を入れている場合もあり得ます。
これらがAndroid不正アプリによって流出し、不正に利用された場合、個人への被害はもちろんのこと、会社に対しても損害を与える可能性があります。
特に上記で挙げた「プライバシー情報」は厳重に管理される必要があります。
不正アプリの種類と感染経路
2012年に世界で確認されたAndroid向け不正アプリの種類には以下の傾向が見られます。
1.高額料金が発生するサービスを悪用するもの(40%)
2.ユーザの意図しないアドウェアを表示させるもの(38%)
3.情報を窃取するもの(25%)
4.不正プログラムをダウンロードするもの(23%)※不正アプリは重複した種類をもつ場合があります。
Android不正アプリの特徴として自動的に拡散するウィルスはまずあり得ません。
後の章で説明しますが、Androidではユーザが明示的に「パーミッション」を設定し、インストールしなければ、Androidアプリを利用できない構造になっているためです。
そのため、Android不正アプリの殆どはユーザ自らインストールすることによって感染します。
従って、一般的にAndroid不正アプリの感染経路には、以下のものが考えられます。
1.Google Playなどのさまざまなアプリケーションストアから不正アプリをインストールする
2.メールに添付された不正アプリをインストールする
3.USB接続したPCから不正アプリをインストールする(adbシェルを経由)
Androidセキュリティインシデント
ここでは、Androidで発生した代表的なセキュリティインシデントを紹介します。
DroidDreamLight(2011.6)
Android端末を狙う際によく使われるのが「DroidDreamLight」です。
DroidDreamLightはAndroid端末向けの各種アプリをトロイの木馬化し、公式マーケット(Google Play等)に公開します。
ユーザは公式マーケットからトロイの木馬化されたアプリを自分の端末にインストールすることで感染します。
感染後、ユーザ端末から個人情報等が流出します。
2011年6月に確認された以後も亜種が次々と登場し、情報収集機能等がより分かり難くなっており、検出が困難になってきています。
Skype(2011.4)
悪質なサードパーティーアプリケーションをAndroid端末にインストールすると、端末に保存されたSkypeのファイルにアクセス可能となる脆弱性が発見されました。
流出の恐れがある情報には、Skypeに登録した個人情報、Skypeコンタクト情報、チャット履歴などのファイルが含まれます。
これはSkypeがAndoidOSのセキュリティ機能で保護されない場所に情報を保存していることで発生しました。
※現在は修正されています。
Geinimi(2010.12)
「Geinimi」は、Android OSを搭載したスマートフォンや情報端末を標的としたボット型のウィルスです。
このウィルスは、複数の感染したコンピュータ同士で「ボットネット」を構成する「ボット」型のウィルスの一種です。
感染すると、悪意ある犯罪者の指令に応じて意図しない電話発信やメールの送受信、個人情報の漏えいなどの被害を引き起こす可能性があります。
独立行政法人 情報処理推進機構(IPA)によると、Geinimiは2010年末頃より確認されましたが、Androidを標的としたボット型ウィルスが確認されたのは初と言われています。
現在でも中国を中心に出回っています。
Droid09(2010.1)
「Droid09」という匿名の開発者が、オンラインバンキングアプリに見せかけて作成したものです。
実在の金融機関名をかたり、Android Market Place(現在のGoogle Play)で販売していました。
名前を使われた金融機関は米国の銀行や信用組合など多数に上り、いずれもアプリは無許可で開発されたもので、Android端末上でオンラインバンキングのWeb画面を開くだけで、実際の取引には利用できず、ユーザ名やパスワードなどの情報が盗み出された可能性があります。
Androidのセキュリティアーキテクチャ
Androidのセキュリティを構成する各要素
署名
すべてのAndroidアプリケーションは証明書での署名が必要で、そのプライベート鍵は開発者が保管します。(自己証明書)
署名がないアプリケーションは、インストール、起動ができず、またGoogle Playでの公開もできません。
(Eclipseで起動できるのは、ADT[Android Development Tools]が自動で署名を行なっている為)
署名の目的
・アプリケーションの作成者を識別 (同じ署名ではないとアップデート出来ないなど)
・sharedUserIdを使ったアプリケーション間連携 (詳しくは、アプリケーション間のデータ連携)
サンドボックス
Android はサンドボックスという概念により、アプリケーション間を強制的に分離しています。
サンドボックス(sandbox:砂箱,砂場 ): 外部から受け取ったプログラムを保護された領域で動作させることによってシステムが不正に操作されるのを防ぐセキュリティモデルのことです。
実行されるプログラムは保護された領域に入り、ほかのプログラムやデータなどを操作できない状態にされて動作するため、プログラムが暴走したりウイルスを動作させようとしてもシステムに影響が及ばないようになっています。
ユーザID
インストールされたAndroidアプリケーションには、それぞれ一意の Linux ユーザID が割り当てられ、対応する固有のプロセスが実行されます。
これにより、他のアプリケーションと互いに影響を及ぼさないサンドボックス化がなされます。
このユーザID は、アプリケーションがデバイスにインストールされると割り当てられ、アプリケーションがデバイス上に生存している間、固定値として保持されます。
サンドボックス外のリソースに対するアクセス方法
デバイスの保護された機能やリソースへのアクセス
パーミッション(users-permission)
マニフェスト・ファイル(AndroidManifest.xml)を通じて、デバイスの保護された機能やリソースに対してアプリケーションからアクセスすることを要求します。(デフォルトでは一切アクセス許可が付与されていません)
この要求は、アプリケーションのインストール時に表示され、アクセス許可または拒否をユーザーによって決められます。
アプリケーション間のデータ連携
ContentProvider
Androidには、ContentProviderを経由することにより、アプリケーション間で情報共有できる仕組みが用意されています。
ContentProviderの詳細については、後の章で詳述します。
sharedUserId (ユーザーIDの共有)
異なるアプリケーションに同じユーザIDを割り当てることにより、すべてのファイルがお互いに読み書き可能になります。
ユーザーIDを共有するには以下の条件が必要になります。
・同じ署名を受けたアプリケーションであること
・AndroidManaifest.xml内で、同じandroid:sharedUserIdを指定していること
ファイル生成時のパーミッション指定
openFileOutput()、getSharedPreferences()、openOrCreateDatabase()などでファイルを生成する際、以下のパーミッションフラグを設定することにより、他のアプリからのアクセス権を制御できます。
・MODE_PRIVATE : アクセス不可モード(デフォルト)
・MODE_WORLD_READABLE : 読み込み可能モード
・MODE_WORLD_WRITEABLE : 書き込み可能モード
また、”| “を使用して、複数のパーミッションを組み合わせて指定することが可能です(*注意 : パーミッションが衝突した場合、緩いパーミッションが優先されます)。
※ただし、不特定多数への設定となります。
SQLiteを用いたAndroidアプリのデータ管理方法
SQLiteとAndroidアプリからのアクセス方法
SQLite
SQLiteとは、サーバとしてではなく、アプリケーションに組み込んで使用する著作権フリーで軽量なデータベースエンジンです。
SQLiteは非常に軽量で高速であるため、Androidでもアプリケーションの内部データを管理するためのデータベースエンジンとして、標準で組み込まれています。
AndroidアプリからのSQLiteの利用方法
AndroidでSQLiteを利用する場合、SQLiteOpenHelperを継承したHelperクラスを作成して使用します。
SQLiteとContentProvider
Androidでは、通常はSQLiteのデータもアプリケーション固有の内部データとして管理されているため、外部アプリからアクセスすることはできなくなっています。
従って、複数のアプリで共通のデータを使用する場合、ContentProviderを利用することになります。
ContentProvider利用時におけるSQL Injection対策
ContentProvider利用における問題点
以下は、会議室予約データを管理するアプリと、その管理データを照会する機能を持つアプリのサンプルです。
SampleAppで作成した会議室予約DBの内容をContentProviderを通じてMalwareAppで参照しますが、その際、意図的にSQLインジェクションを用いて、SampleApp側で想定していなかったデータも取得できるようになっています。
SampleApp
【helperクラス】
[java] public class SampleDBHelper extends SQLiteOpenHelper { /** * コンストラクタ * @param context */ public SampleDBHelper(Context context) { // 任意のデータベースファイル名と、バージョンを指定する super(context, "sample.db", null, 1); } /** * このデータベースを初めて使用する時に実行される処理 */ @Override public void onCreate(SQLiteDatabase db) { db.execSQL( "create table schedule_tbl (" + "_id INTEGER primary key autoincrement not null, " + "title TEXT, " + "content TEXT, " + "date TEXT, " + "place TEXT, " + "timestamp TEXT );"); db.execSQL( "create table user_tbl (" + "_id INTEGER primary key autoincrement not null, " + "name TEXT, " + "phone TEXT, " + "email TEXT, " + "timestamp TEXT );"); } } [/java]
【脆弱性のあるContentProvider】
[java] public class SampleWeakProvider extends ContentProvider { SampleDBHelper databaseHelper; @Override public boolean onCreate() { databaseHelper = new SampleDBHelper(getContext()); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables("schedule_tbl"); projection = new String[] { "title", "content", "date" }; selection = "content LIKE '%" + selectionArgs[0] + "%'"; selectionArgs = null; Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, null); return c; } } [/java]
MalwareApp
【呼び出し元】
[java] public class MainActivity extends Activity { private static final int REQ = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btn2 = (Button) this.findViewById(R.id.button2); btn2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { // ここで第4引数(selectionArgs)に不正なSQLをインジェクション String keyword = "a%' or 1 = 1) UNION ALL select name, phone, email from user_tbl where ('%' = '"; Uri contentUri = Uri.parse("content://jp.co.opentone.fsol.sampleweakprovider"); try { Cursor cur = getContentResolver().query(contentUri, null, null, new String[] { keyword }, null); StringBuilder sb = new StringBuilder(); sb.append("----------"); while (cur.moveToNext()) { sb.append(cur.getString(0)); sb.append(" / "); sb.append(cur.getString(1)); sb.append(" / "); sb.append(cur.getString(2)); sb.append("\n"); } sb.append("----------"); TextView txt = (TextView) findViewById(R.id.textView1); txt.setText(sb.toString()); } catch (Exception e) { TextView txt = (TextView) findViewById(R.id.textView1); txt.setText(e.getMessage()); } } }); } [/java]
ContentProviderを使ってアプリケーション間のデータ連係を実現する場合、データの取得先である呼び出し側のアプリでquery()メソッドを呼び出すため、ContentProviderとなるデータ提供側のアプリでSQLインジェクション対策を実施する必要があります。
上記のコードでは、MainActivityクラスの15行目で、queryメソッドの第4引数(selectionArgs)に不正なSQLを埋め込んでいます。
呼ばれる側のアプリでこのような呼び出し方を想定して対策をしていないと、想定外のSQLが発行されることによって、不正にデータを抜き取られてしまうことになります。
ContentProviderでのSQLインジェクション対策
上記のように、呼び出し側で意図的にSQLインジェクションを行うことができてしまうため、ContentProviderはそのような不正な呼び出し方をされることを想定して設計する必要があります。
以下のサンプルは、対策を施したContentProviderの実装です。
ポイントは、ContentProviderを実装したクラスでquery()メソッドをオーバーライドし、そのquery()メソッド内でパラメータに渡される文字列をエスケープしています(8行目、9行目)。
SampleApp
【脆弱性対策を施したContentProvider】(query()メソッドの実装箇所を抜粋)
[java] public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { try { SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables("schedule_tbl"); projection = new String[] { "title", "content", "date" }; // 第4引数(selectionArgs)のシングルクォーテーションをエスケープして、SQLiteQueryBuilder.query()の第3引数(selection)に設定 String condition = selectionArgs[0].replaceAll("'", "''"); selection = "content LIKE '%" + condition + "%'"; selectionArgs = null; Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, null); return c; } catch (Exception e) { Log.e(e.getClass().getName(), e.getMessage()); return null; } } [/java]
まとめ
Android自体にはセキュリティの仕組みが備わっていますが、その仕組みを理解せずにアプリを開発すると、思わぬ脆弱性を組み込んでしまうことになります。
ユーザが悪意を持ったアプリをそれと知らずにインストールし、アプリが求める権限を疑いもなく与えてしまうことにより、電話帳のデータを抜き取られるというような事件も実際に起きています。
今後、我々エンジニアはこのような一見無害なアプリから個人情報などの機密データを保護するために、保護対象とするデータは端末側に保存しない、またはやむを得ず端末側に保存するとしても、暗号化して保存するような対策を施すことが求められてくるでしょう。
しかし、今回の記事で紹介したように、ContentProviderの仕組みを不適切に使用してしまうと、たとえ上記のような対策を施したとしても、自分が開発したアプリ経由で機密データを抜き取られるような脆弱性を含んでしまうことになります。
Andoridのアプリケーション開発者は、今回紹介したSQL Injection対策はもちろん、Androidのセキュリティの仕組みと危険性を十分に理解した上でアプリ開発をすることが重要になってきます。