Android コンポーネントのロジックの脆弱性についての話

序文

社会がセキュリティにますます注目するようになるにつれて、コード署名、ポインタ署名、アドレスのランダム化、分離ヒープなど、さまざまな防御プログラミングや脆弱性緩和策がオペレーティング システムに徐々に追加されます。多くの一般的なメモリ破損の脆弱性は、これらの緩和策の下では安定して悪用することが困難であることがよくあります。したがって、攻撃者は徐々に論理的な抜け穴に注目するようになってきています。論理的な抜け穴は通常、安定性が高く、風水の影響を受けませんが、同時に深く隠されており、多くのビジネス規範の中で見つけるのが困難です。また、形態が多様であるため汎用性が低く、産業連関の観点からも優先度の高い研究方向ではない可能性がある。いずれにせよ、これは常に監視すべき攻撃対象領域です。したがって、この記事では、Android プラットフォームを対象とした一般的なロジックの脆弱性をいくつか紹介します。

4つの主要なコンポーネント

Android に触れたことがある人なら「4 つの主要コンポーネント」について聞いたことがあるはずですが、アプリケーションを開発するときに最初に学ぶのは、各コンポーネントのライフ サイクルです。いわゆる 4 つの主要コンポーネントは、それぞれ アクティビティサービスブロードキャスト レシーバー 、および コンテンツ プロバイダーを指します。これらのコンポーネントの実装の詳細については、公式ドキュメント: Application Fundamentals[1] を参照してください。

セキュリティ研究では、4 つの主要コンポーネントは特別な注目に値します。これらはアプリケーションが外部と通信するための重要なブリッジであり、アプリケーション内でも、これらのコンポーネントは相互に疎結合の関係を構築するために使用されます。たとえば、アプリケーション自体はカメラの許可を申請する必要はありませんが、(システム) カメラ アプリケーションは、あたかも単独で写真を撮っているかのように、コンポーネント間の相互通信を通じてカメラを開いてキャプチャされた写真を取得できます。

コンポーネントの相互作用のプロセスにおいて、中心となるデータ構造は Intent[2] であり、これがほとんどのコンポーネント間の通信のキャリアとなります。

インテント 101

公式発表によれば、インテントとは「実行される操作の抽象的な記述」であり、直訳すると「インテント」とも言えます。例えば、カメラをオンにして写真を撮る、ブラウザを開いてURLにアクセスする、設定インターフェースを開くなど、すべてをインテントで記述することができます

インテントには、明示的なインテントと暗黙的なインテントという 2 つの主な形式があります。この 2 つの違いは、前者は明示的に指定されるのに対し、後者はコンポーネントを指定しないことですが、システムがインテントを理解するのに役立つ 十分な情報 (など )Componentを使用することです。 ACTIONCATAGORY

インテントの主な機能はアクティビティを開始することなので、このシナリオを例として、ソース コードからインテントの具体的な実装を分析します。アクティビティを開始するための一般的なコード スニペットは次のとおりです。

Intent intent = new Intent(context, SomeActivity.class);
startActivity(intent);

ここでは明示的なインテントが使用されていますが、それが重要ではありません。通常、Activity で呼び出されるため、コードは Activity.startActivityFrameworks/base/core/java/android/app/Activity.java で呼び出され、ここではコピー アンド ペーストする必要はありません。つまり、呼び出しリンクは次のとおりです。

  • • Activity.startActivity()

  • • Activity.startActivityForResult()

  • • Instrumentation.execStartActivity()

  • • ActivityTaskManager.getService().startActivity()

  • • IActivityTaskManager.startActivity()

最後の呼び出しはインターフェイスであり、これは非常に一般的なパターンです。次のステップはその実装を見つけることです。事故がなければ、この実装は別のプロセスにあるはずです。実際、次の場所にもあります system_server 。

  • • ActivityTaskManagerService.startActivity()

  • • ActivityTaskManagerService.startActivityAsUser()

  • • ActivityStarter.execute()

最後のメソッドは、次のように、呼び出し元、userId、フラグ、callingPackage、最も重要なインテント情報など、前に渡された情報を使用してアクティビティを開始する準備をします。

private int startActivityAsUser(...) {
    // ...
    return getActivityStartController()
            .obtainStarter(
                intent, "startActivityAsUser")
            .setCaller(caller)
            .setCallingPackage(callingPackage)
            .setCallingFeatureId(callingFeatureId)
            .setResolvedType(resolvedType)
            .setResultTo(resultTo)
            .setResultWho(resultWho)
            .setRequestCode(requestCode)
            .setStartFlags(startFlags)
            .setProfilerInfo(profilerInfo)
            .setActivityOptions(bOptions)
            .setUserId(userId)
            .execute();
}

ActivityStarter.execute() の主なロジックは次のとおりです。

int execute() {
    // ...
    if (mRequest.activityInfo == null) {
        mRequest.resolveActivity(mSupervisor);
    }
    res = resolveToHeavyWeightSwitcherIfNeeded();
    res = executeRequest(mRequest);

}

このうち、resolveActivity 起動するアクティビティの情報を取得するために使用され、例えば暗黙的起動の場合、要件を満たすターゲットが複数存在する場合があり、どのアプリケーションを開くかをユーザーに尋ねるポップアップメニューが表示されます。executeRequest 途中で、主に関連するアクセス許可をチェックし、すべてのアクセス許可が条件を満たした後で startActivityUnchecked 実際の呼び出しを実行するために呼び出します。

Android12 アプリの起動プロセス分析 [3] でほとんどのプロセスを紹介しましたが、ここでは Intent 自体の役割に焦点を当てます。上記の分析から、Intent はマルチプロセス通信におけるメッセージ キャリアであることがわかり、そのソース コード定義からも、Intent 自体がシリアル化してプロセス間で受け渡せる構造であることがわかります。

public class Intent implements Parcelable, Cloneable { ... }

Intent これには多くのメソッドと属性がありますが、当面はここでは展開しません。後で特定の脆弱性が導入されたときに分析されます。次の記事では、主に 4 つの主要なコンポーネントから始まり、いくつかの一般的な脆弱性モードと設計トラップをそれぞれ紹介します。

アクティビティ

Activity[4] はアクティブ ウィンドウとも呼ばれ、ユーザーと直接対話するグラフィカル インターフェイスです。APP の主な開発タスクの 1 つは、各アクティビティを設計し、それらの間のジャンプとリンクを計画することです。通常、アクティビティは全画面アクティブ ウィンドウを表しますが、フローティング ウィンドウ、マルチウィンドウなどの他の形式で存在することもできます。UI ウィンドウとして、通常はレイアウトに XML ファイルを使用し、Activity クラスを継承してそのライフ サイクル関数 onCreate や onPause その他のライフ サイクル メソッドを実装します。

如果开发者定义的 Activity 想通过 Context.startActivity 启动的话,就必须将其声明到 APP 的 manifest 文件中,即 AndroidManifest.xml[5]。应用被安装时,PackageManager 会解析其 manifest 文件中的相关信息并将其注册到系统中,以便在 resolve 时进行搜索。

在 adb shell 中可以通过 am start-activity 去打开指定的 Activity,通过指定 Intent 去进行启动:

am start-activity [-D] [-N] [-W] [-P <FILE>] [--start-profiler <FILE>]
        [--sampling INTERVAL] [--streaming] [-R COUNT] [-S]
        [--track-allocation] [--user <USER_ID> | current] <INTENT>

作为用户界面的载体,Activity 承载了许多用户输入/处理、以及外部数据接收/展示等工作,因此是应用对外的一个主要攻击面。下面就介绍几种较为常见的攻击场景。

生命周期

Activity 经典的生命周期图示如下:

Activity Lifecycle

通常开发者只需要实现 onCreate 方法,但是对于一些复杂的业务场景,正确理解其生命周期也是很必要的。以笔者在内测中遇到的某应用为例,其中某个 Activity 中执行了一些敏感的操作,比如开启摄像头推流,或者开启了录音,但只在 onDestroy 中进行了推流/录音的关闭。这样会导致在 APP 进入后台时候,这些操作依然在后台运行,攻击者可以构造任务栈使得受害者在面对恶意应用的钓鱼界面时候仍然执行目标应用的后台功能,从而形成特殊的钓鱼场景。正确的做法应该是在 onPaused 回调中对敏感操作进行关闭。

攻击者实际可以通过连续发送不同的 Intent 去精确控制目标 Activity 生命周期回调函数的触发时机,如果开发时没有注意也会造成应用功能的状态机异常甚至是安全问题。

Implicit Exported

前述したように、開発者が定義した Activity を使用して startActivity 起動する場合は、 AndroidManifest.xml で宣言する必要があります <activity> 。宣言の例は次のとおりです。

<activity xmlns:android="http://schemas.android.com/apk/res/android" android:theme="@android:01030055" android:name="com.evilpan.RouterActivity">
  <intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="demo" android:host="router"/>
  </intent-filter>
</activity>

activity[6] では多くのプロパティがサポートされています。重要な属性の 1 つは、 現在のアクティビティが他のアプリケーションandroid:exportedコンポーネントによって開始できるかどうかを示すことですこの属性にはいくつかの特徴があります。

  1. 1. 属性はデフォルトにすることができ、デフォルト値は false;

  2. 2. アクティビティがこのプロパティを明示的に設定せず、アクティビティで定義されている場合 <intent-filter>、デフォルト値は です true

つまり、開発者はアクティビティのエクスポートを明示的に指定できませんが、指定されているため、 intent-filter実際にエクスポートされます。つまり、対応するアクティビティを他のアプリケーションから呼び出すことができます。この状況は初期には非常に一般的でした。たとえば、APP はパスワードを変更するための一連のインターフェイスを設計しました。最初に古いパスワードを入力し、次に新しいパスワードを入力するインターフェイスにジャンプする必要があります。後者がエクスポートされている場合、攻撃者は新しいパスワードを入力するインターフェイスを直接呼び出すことができ、それによって古いパスワードの検証ロジックがバイパスされます。

Google はこの問題を深く認識しており、Android 12 以降では、アプリケーションのアクティビティにインテントフィルターが含まれる場合、明示的に true または false を指定する必要があり、デフォルトは許可されないと規定しています android:exported 。Android 12 では、exported 属性を明示的に指定せず、インテント フィルター アクティビティを持つアプリケーションは、インストール中に PackageManager によって直接拒否されます。

フラグメント注入

アクティビティは、UI のコア コンポーネントとして、同じインターフェイス内に複数の再利用可能なサブインターフェイスを表示するなど、モジュール開発もサポートしています。Fragments[7] コンポーネント、または「フラグメント」は、この設計アイデアから生まれました。FragmentActivity アクティビティ内の 1 つ以上のフラグメントを結合してコードの再利用を容易にすることができ、フラグメントのライフ サイクルはホスト アクティビティの影響を受けます。 

Fragment Injection 脆弱性は 2013 年に初めて発生しましたが、ここではその原理のみを紹介し、元の記事と論文はこのセクションの最後に添付されています。脆弱性の中心となるのはシステムが提供するクラスであり PreferenceActivity 、開発者はこれを継承して便利な設定機能を実装することができ、このクラスのonCreate関数には以下の機能があります。

protected void onCreate() {
    // ...
    String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT);
    Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
    // ...
    if (initialFragment != null) {
        switchToHeader(initialFragment, initialArguments);
    }
}

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);
}

switchToHeaderInner 文字列とバンドル パラメータがインテントから取得され、最終的にインスタンス化のために 渡されることがわかります Fragmentインスタンス化のプロセスは次のとおりです。

public static Fragment instantiate(Context context, String fname, Bundle args) {
    // ...
    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;
}

古典的なリフレクション呼び出しは、受信文字列を Java クラスとしてインスタンス化し、そのパラメーターを設定します。なんだこれ、デシリアライズですよ!そして、実際の脆弱性はここからであり、受信パラメータは攻撃者によって制御可能であるため、攻撃者はそれを内部クラスとして設定し、開発者が予期しない機能に触れる可能性があります。元のレポートでは、設定アプリケーションで PIN パスワードを設定するフラグメントをターゲット入力として使用していましたが、これはプライベート フラグメントであり、PIN コードを許可なく変更する機能につながります。当時、他の多くのユーザー アプリケーションも PreferenceActivity を使用していたため、この脆弱性は広範囲に影響を及ぼし、結果として悪用される内容はアプリケーション自体の機能 (つまり、有用なガジェットの有無) に応じて異なりました。

switchToHeaderInner 上記のコードは最新の Android 13 からの抜粋であることに注意してください。このメソッド では、  isValidFragment Android の独自の修正の 1 つである判断が追加されています。つまり、PreferenceActivity のサブクラスにこのメソッドの実装を強制します。そうしないと、実行時に例外がスローされます。しかしそれでも、利便性のために直接継承して返す開発者はまだたくさんいます true 。

フラグメント インジェクションは PreferenceActivity の問題であるように見えますが、その核心は依然として信頼できない入力の検証が不完全であることに変わりはなく、次の例では同様の脆弱性パターンが何度も見られます。

参考記事:

  • • Android フレームワークの新しい脆弱性: フラグメント インジェクション[8]

  • • アンドロイドは断片に崩壊する.pdf (wp)[9]

  • • フラグメントインジェクションを理解する[10]

  • • フラグメントインジェクションの脆弱性を修正する方法[11]

クリックジャッキング

アクティビティは UI の主要なキャリアであるため、ユーザーとの対話も重要な機能です。従来の Web セキュリティでは、ターゲット Web サイトが被害者にクリックさせたいケースを指定された場所 (iframe など) に配置し、ホスト内の関連コンポーネントを使用してターゲットをカバーおよび誘導するクリックジャッキングの手法がすでに存在していました。これにより、被害者はコインにいいねをしたり、ワンクリック休暇を収集したりするなど、知らないうちに機密操作を実行します。

Android でも同様の攻撃手法が登場しており、システムの機密性の高いポップアップ ウィンドウの前にある攻撃者のカスタム TextView を覆い、被害者に特定の有害な操作を確認させるよう誘導します。もちろん、これには攻撃者のアプリケーションがフローティング ウィンドウ アクセス許可 ( SYSTEM_ALERT_WINDOW) を持っている必要があります。新しい Android システムでは、このアクセス許可を取得するアプリケーションはユーザーからの複数の確認を必要とします。

過去 2 年間で、AOSP にも次のようなクリックジャッキングの脆弱性がいくつか出現しました。

  • • CVE-2020-0306: Bluetooth 検出要求確認ボックスのオーバーライド

  • • CVE-2020-0394: Bluetooth ペアリング ダイアログ オーバーライド

  • • CVE-2020-0015: 証明書のインストールダイアログの上書き

  • • CVE-2021-0314: アンインストール確認ダイアログの上書き

  • • CVE-2021-0487: カレンダーのデバッグダイアログの上書き

  • • ...

システム アプリケーションの場合、クリックジャッキングを防ぐ方法は一般に、  SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS 権限を使用し、レイアウト パラメーターで指定することで UI がカバーされないようにすることです。

通常のアプリケーションでは HIDE_NON_SYSTEM_OVERLAY_WINDOWS 権限を申請することはできませんので、レイアウトを に設定してフォームをカバーした後の入力イベントを禁止する方法と、View.onFilterTouchEventForSecurity メソッドをオーバーロードしてその中で他のアプリケーションのカバーを検出する方法の 2 つの防御策が一般的 filterTouchesWhenObscured です true 。Android 12 では、システムはデフォルトで filterTouchesWhenObscured 属性を有効にしています。これは、デフォルトでセキュリティの古典的な実装でもあります。

クリックジャッキングの操作の詳細と軽減ソリューションについては、OPPO Security Lab の記事 「無視できない脅威: Android におけるクリックジャッキング攻撃」を参照してください。

クリックジャッキングと同様の脆弱性として StrandHogg というものがありますが、詳細は以下のオリジナル記事を参照してください。重要な点は、  Activity のallowTaskReparenting および taskAffinity プロパティを使用して、そのタスク スタックをターゲット アプリケーションとして偽装することです。これにより、ターゲット アプリケーションを開いたときに、TaskStack の後入れ先出し機能により、ユーザーには攻撃者のアプリケーションが表示され、アプリケーションがフィッシング シナリオにさらされることになります。

その後、同じセキュリティ チームが  、主に の  AUTOMERGE 機能を使用するStrandHogg 2.0バージョンを提案しました。ActivityStarter2 つのアプリケーション A と B があると仮定すると、 A1 で呼び出された後 startActivites(B1, A2, B2) 、タスク スタックは (A1, B1) および (A2, B2) から (A1, B1, A2, B2) にマージされます。つまり、他のアプリケーションのアクティビティが同じタスク スタックに含まれるため、フィッシング シナリオが発生します。ただし、この脆弱性は比較的特殊なため、Google ではかなり前に修正済みとなっておりますので、詳しくは以下の参考記事をご覧ください。

  • • StrandHogg の脆弱性[12]

  • • StrandHogg 2.0 – Android の新たな深刻な脆弱性[13]

  • • StrandHogg 2.0 (CVE-2020-0096) 修正[14]

インテントのリダイレクト

インテント リダイレクトは、名前が示すように、ユーザーによって渡された信頼できない入力を転送します。これはサーバー側の SSRF 脆弱性に似ています。典型的な脆弱性の例は次のとおりです。

protected void onCreate (Bundle savedInstanceState) {
   Intent target = (Intent) getIntent().getParcelableExtra("target");
   startActivity(target);
}

ユーザーによって渡されたターゲットは Parcelable 直接 Intent オブジェクトに変換され、このオブジェクトは startActivity 呼び出すパラメーターとして使用されます。この例に関する限り、考えられる害は、攻撃者が任意の構造化インテント データを使用して、エクスポートされていないプライベート アプリケーションであっても、ターゲット APP 内の任意のアプリケーションを起動できることです。ただし、ターゲットがエクスポートされていないアプリケーションは、攻撃者が提供するインテント内のパラメータをさらに解析し、組み込み Webview で任意の Javascript コードを実行したり、ファイルをダウンロードして保存したりするなど、さらなる被害を引き起こす可能性があります。

実際、インテント リダイレクトは、プライベート アクティビティ コンポーネントを開始する以外にも、次のような他のインターフェイスにも使用できます。

  • • startActivity[15]

  • • startService[16]

  • • sendBroadcast[17]

  • • setResult[18]

注: 各メソッドには、startActivityForResult などの複数の派生メソッドがある場合があります。

最初の 3 つは理解しやすいかもしれません。つまり、インターフェイスの開始、サービスの開始、ブロードキャストの送信です。最後のものは setResult 、トラブルシューティング中に無視される可能性があります。これは主に、現在のアクティビティの呼び出し元に追加データを返すために使用されます。主にシナリオで使用されます startActivityForResult 。また、呼び出し元に対するユーザーの信頼できないデータが汚染される可能性があります。

防御の観点から、受信した Intent をパラメータとして上記 4 つのインターフェイスに直接送信しないことをお勧めします。これを行う必要がある場合は、事前に次のような十分なフィルタリングとセキュリティ検証を実行する必要があります。

  1. 1. コンポーネント自体を android:exported に 設定しますfalseが、これはユーザーがアクティブに送信したデータを阻止するだけであり、 setResult 返されたデータを傍受することはできません。

  2. Intent 2.コンポーネント コンテキストでの呼び出しなど、 取得したデータが信頼できるアプリケーションからのものであることを確認します が、データを構築して 返す getCallingActivity().getPackageName().equals("trust.app")可能性のある悪意のあるアプリケーションに注意して くださいgetCallingActivitynull

  3. Intent 3.コンポーネントがエクスポートされていない独自のコンポーネントを指していない、 が含まれていないなど、 転送される有害な動作がないことを確認します FLAG_GRANT_READ_URI_PERMISSION (詳細については、以下の ContentProvider の脆弱性を参照してください)。

  4. 4. ...

しかし、Google自体でも完全な検証を保証できない可能性があることが判明した。Wuheng Lab によって最近提出された高リスクの脆弱性は、 CVE-2022-20223 その典型的な例です。

private void assertSafeToStartCustomActivity(Intent intent) {
    // Activity can be started if it belongs to the same app
    if (intent.getPackage() != null && intent.getPackage().equals(packageName)) {
        return;
    }
    // Activity can be started if intent resolves to multiple activities
    List<ResolveInfo> resolveInfos = AppRestrictionsFragment.this.mPackageManager
            .queryIntentActivities(intent, 0 /* no flags */);
    if (resolveInfos.size() != 1) {
        return;
    }
    // Prevent potential privilege escalation
    ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
    if (!packageName.equals(activityInfo.packageName)) {
        throw new SecurityException("Application " + packageName
                + " is not allowed to start activity " + intent);
    }
}

これは、起動対象のパッケージ名が現在の呼び出し元のパッケージ名と一致するかどうかを判断するために使用されます ActivityInfo.packageName が、実際には、明示的な意図は、componentName によって起動対象を指定することであり、優先度が高く、後者は偽造できるため、チェックがバイパスされます Intent.packageName 。実は上記数行のコードには別の脆弱性が存在するので、興味のある方は以下の参考リンクを参照してください。

そのため、インテントのリダイレクトに関する潜在的な問題が発生した場合は、もう少し時間をかけて詳しく調査すると、悪用可能なシナリオが見つかる可能性があります。

サービス

Service[20] には 2 つの主な機能があります。1 つはバックグラウンドで長時間実行される環境を APP に提供することであり、もう 1 つは独自のサービスを外部に提供することです。アクティビティの定義と同様に、サービスは使用する前にマニフェストで宣言する必要があります。サービス内のコードもアクティビティと同様にメインスレッド上で実行され、デフォルトではアプリケーションのプロセス内にあることに注意してください。

サービスの 2 つの主な機能の区別に従って、サービスを開始するには対応する 2 つの形式があります。

  1. 1.  Context.startService(): バックグラウンド サービスを開始し、システムにスケジュールを設定させます。

  2. 2.  Context.bindService(): (外部) アプリケーションにサービスをバインドさせ、サービスが提供するインターフェイスを使用させます。これは RPC サーバーとして理解できます。

サービスを開始する 2 つの方法のライフサイクル図は次のとおりです。

サービスのライフサイクル

青い部分はクライアント側で呼び出され、システムはリクエストを受信した後、対応するサービスを開始し、対応するプロセスが開始されていない場合は、zygote に開始するように通知します。サービスの作成に使用されるメソッドに関係なく、システムは  そのサービスの呼び出しonCreate と メソッドを呼び出します。onDestroy全体的なプロセスは、Activity の起動プロセスと似ているため、ここでは詳しく説明しません。

シェルには、 start-activity サービスの開始を容易にするコマンドも用意されています。

am start-service [--user <USER_ID> | current] <INTENT>

Service コンポーネントに関連するいくつかの脆弱性を紹介しましょう。

ライフサイクル

サービスの起動のライフ サイクルは以前に紹介しました。これは一般にアクティビティ プロセスと似ていますが、注意する必要がある相違点がいくつかあります。

  1. 1. Activity ライフサイクル コールバック メソッドとは異なり、onCreate、onDestory などの Serivce コールバック メソッドのスーパークラス実装を呼び出す必要はありません。

  2. 2. Service クラスの直接のサブクラスはメイン スレッドで実行され、通常、ブロックされた複数のリクエストを同時に処理する場合は新しいスレッドで実行する必要があります。

  3. 3. IntentService これは Service のサブクラスであり、Worker スレッドで実行するように設計されており、ブロックされた複数の Intent リクエストをシリアルに処理できます。API-30 は、API-30 以降は廃止されたインターフェイスとしてマークされ、実装には WorkManager または JobIntentService を使用することが推奨されます。

  4. 4. クライアントは バインディング サービスを停止するためにstopSelf または を使用しますstopService が、サーバーには対応するコールバックがなく onStop 、破棄される前にのみコールバックを受け取ります onDestory

  5. 5. フォアグラウンド サービスはステータス バーに通知を提供し、ユーザーがサービスが実行中であることを認識できるようにする必要があります。

バインドされたサービス [21] の場合、Android システムはバインドされたクライアント参照カウントに従ってサービスを自動的に破棄しますが、サービスが callback を実装している場合、システムはサービスを開始済みの状態と見なすため、サービスを明示的に停止する必要があります onStartCommand() 。さらに、サービスがクライアントの再バインドを許可する場合は、次の図の例に示すように、クライアントが次回バインドするときに同じ IBinder を受け取るように、onUnbind メソッドを実装して true を返す必要があります。

リバインド

サービスの宣言サイクルは、プロセス間のバインディング関係が関係するため、アクティビティの宣言サイクルよりも複雑です。そのため、理解せずに堅牢でないコード、さらには問題のあるコードを作成する可能性が高くなります。

暗黙的なエクスポート

アクティビティと同様に、サービスもサービス[22]を使用してマニフェスト内で宣言する必要があり、 android:exported 属性もあります。この属性のデフォルト値の定義も同じです。つまり、デフォルトは ですが false、インテントフィルターが含まれている場合、デフォルトは です true同様に、Android 12 以降では、サービスのエクスポート プロパティを明示的に指定することも必須です。

サービスハイジャック

アクティビティとは異なり、Android では暗黙的なインテントを使用してサービスを開始することはお勧めしません。サービスはバックグラウンドで実行されるため、目に見えるグラフィカル インターフェイスがないため、ユーザーはどのサービスが暗黙的なインテントによって開始されたかを確認できず、送信者はインテントが誰によって受信されるかがわかりません。

代表的な脆弱性としてサービスハイジャックが挙げられ、攻撃者はサービスのターゲットと同じインテントフィルターを宣言し、より高い優先度を設定することで、ターゲットサービスに送信されるべきインテントを傍受することができ、機密情報が含まれている場合はデータ漏洩の原因にもなります。

ただし、 bindService この状況での害はさらに深刻で、攻撃者がターゲットの IPC サービスになりすまして、間違ったデータや有害なデータを返す可能性があります。したがって、Android 5.0 (API-21) 以降では、暗黙的なインテントを使用して bindingService を呼び出すと、直接例外がスローされます。

監査対象のアプリケーションがサービス内で提供されている場合は intent-filter、トラブルシューティングに注力する必要があります。

AIDL

バインディング サービスは IPC サーバーとして使用でき、バインド時にサーバーが AIDL インターフェイスのインスタンスを返す場合、クライアントはインターフェイスの任意のメソッドを呼び出すことができることを意味します。実際のケースは Tiktok で IndependentProcessDownloadService、  DownloadService onBind で上記の AIDL インターフェイスのインスタンスを返します。

com/ss/android/socialbase/downloader/downloader/DownloadService.java:

if (this.downloadServiceHandler != null) {
    return this.downloadServiceHandler.onBind(intent);
}

そして、  tryDownload URLとファイルパスを指定してファイルをローカルにダウンロードして保存する方法もあります。攻撃者は AIDL ファイルを持っていませんが、呼び出しに対するリフレクションを通じて法的リクエストを構築することができます。PoC のキー コードは次のとおりです。

private ServiceConnection mServiceConnection = new ServiceConnection() {
    public void onServiceConnected(ComponentName cName, IBinder service) {
        processBinder(service);
    }

    public void onServiceDisconnected(ComponentName cName) { }
};

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Intent intent = new Intent("com.ss.android.socialbase.downloader.remote");
    intent.setClassName(
        "com.zhiliaoapp.musically",
        "com.ss.android.socialbase.downloader.downloader.IndependentProcessDownloadService");
    bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
}

private void processBinder(IBinder binder) {
    ClassLoader cl = getForeignClassLoader(this, "com.zhiliaoapp.musically");
    Object handler = cl.loadClass("com.ss.android.socialbase.downloader.downloader.i$a")
            .getMethod("asInterface", IBinder.class)
            .invoke(null, binder);

    Object payload = getBinder(cl);

    cl.loadClass("com.ss.android.socialbase.downloader.downloader.i")
            .getMethod("tryDownload", cl.loadClass("com.ss.android.socialbase.downloader.model.a"))
            .invoke(handler, payload);
}

private Object getBinder(ClassLoader cl) throws Throwable {
    Class utilsClass = cl.loadClass("com.ss.android.socialbase.downloader.utils.g");
    Class taskClass = cl.loadClass("com.ss.android.socialbase.downloader.model.DownloadTask");
    return utilsClass.getDeclaredMethod("convertDownloadTaskToAidl", taskClass)
            .invoke(null, getDownloadTask(taskClass, cl));
}

キーは、 Context.getForeignClassLoader 他のアプリケーションの ClassLoader を取得するために使用します。

脆弱性の詳細参照: TikTok Android アプリの脆弱性[23]

インテントリダイレクト

これは実際には、アクティビティの対応する脆弱性と似ています。クライアントがサービスを開始/バインドするとき、暗黙的または明示的なインテントも指定されます。信頼できないデータが他のコンポーネントを開始するためのパラメータとしてサーバーによって使用される場合、同じインテント リダイレクトの問題が発生する可能性があります。 サービスに実装されたパラメータgetIntent() など、他のデータ ソースがあること に注意してください 。onHandleIntent

実際、インテント リダイレクトの害を最初に示唆した「LaunchAnywhere」脆弱性は、システム サービスに由来するもので、正確に言えば AccountManagerService 脆弱性です。AccountManager の通常の実行フローは次のとおりです。

  1. 1. 共通アプリケーション (A と表記) は、AccountManager.addAccount を呼び出して、特定の種類のアカウントの追加を要求します。

  2. 2. AccountManager は、アカウントを提供するアプリケーション (B として示される) の Authenticator クラスを検索します。

  3. 3. AccountManager は B の Authenticator.addAccount メソッドを呼び出します。

  4. 4. AccountManager は、B から返されたインテントに従って、B のアカウント ログイン インターフェイス (AccountManagerResponse.getParcelable) を呼び出します。

ステップ 4 では、システムは B から返されたデータが B のログイン インターフェイスを指していると考えますが、実際には B が他のコンポーネント、さらにはシステム コンポーネントを指すようにすることができ、これによりインテント リダイレクトの脆弱性が生じます。ここでの意図のソースはかなり曲がりくねっていますが、本質は依然として攻撃者によって制御可能です。

この脆弱性と悪用プロセスの詳細については、launchAnyWhere: アクティビティ コンポーネントの権限バイパス脆弱性の分析 (Google バグ 7699048)[24] を参照してください。

受信機

Broadcast Receiver[25]、略してレシーバーは、ブロードキャスト受信機です。上で説明したアクティビティとサービスの連携は 1 対 1 ですが、多くの場合、1 対多または多対多の通信ソリューションが必要になる可能性があり、ブロードキャストはこの機能を引き受けます。たとえば、機内モードのオン、ネットワーク ステータスの変化、バッテリー残量低下など、さまざまなイベントが発生すると、Android システム自体が関係するすべてのアプリケーションにブロードキャスト通知を送信します。これは、ブロードキャスト データのキャリアと同様、典型的なパブリッシュ/サブスクライブ設計パターンです Intent

前のアクティビティやサービスとは異なり、Receiver はマニフェストで宣言して登録できます (静的登録と呼ばれます)。また、アプリケーションの実行中に動的に登録することもできます。ただし、いずれの場合でも、定義されたブロードキャスト レシーバーは BroadcastReceiver[26] を継承し、そのライフ サイクル メソッドを実装する必要があります onReceive(context, intent)

Context である Activity や Service とは異なり、BroadcastReceiver の親クラスは Object であるため、onReceive は追加のコンテキスト オブジェクトも渡すことに注意してください。

シェルでブロードキャストを送信するコマンドは次のとおりです。

am broadcast [--user <USER_ID> | all | current] <INTENT>

よくあるご質問を順番に記載させていただきます。

暗黙的なエクスポート

静的に登録されたレシーバーの使用に関して特別なことは何もありません。例は次のとおりです。

<receiver android:name=".MyBroadcastReceiver"  android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
    </intent-filter>
</receiver>

以前と同じデフォルトのエクスポートの問題もありますが、皆さんも見飽きていると思いますので、これ以上詳しくは述べません。次に、次のような動的登録状況を確認します。

BroadcastReceiver br = new MyBroadcastReceiver();
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
this.registerReceiver(br, filter);

マニフェスト内の定義よりも動的登録を使用した場合のエクスポート許可の問題を無視する方が簡単な場合があります。上記のコード スニペットはブロードキャストを動的に登録しますが、エクスポートされた属性を明示的に宣言していないため、デフォルトでエクスポートされます。実際のところ、 registerReceiver を使用して簡単に設定する方法はないようで exported=false、Google の公式提案では、エクスポートする必要のない放送受信機の使用を登録するか LocalBroadcastManager.registerReceiver 、登録時にアクセス許可を指定することになっています。

アクセス許可を指定する場合、それがカスタム アクセス許可である場合は、アプリケーション マニフェストで宣言する必要があります。次に例を示します。

<permission android:name="com.evilpan.MY_PERMISSION"
    android:protectionLevel="signature"/>
<uses-permission android:name="com.evilpan.MY_PERMISSION" />

署名 承認を要求しているアプリが、パーミッションを宣言しているアプリと同じ証明書で署名されている場合にのみシステムが付与するパーミッションを示します。証明書が一致する場合、ユーザーに通知したり明示的に許可を求めたりすることなく、許可が自動的に付与されます。詳細については、protectionLevel[27]を参照してください。

最後に、動的登録時にこの権限を指定します。

this.registerReceiver(br, filter, "com.evilpan.MY_PERMISSION", null);

許可制限を設けずにエクスポートブロードキャスト受信機を登録すると、攻撃者によって偽造された悪意のあるデータを受信することになり、onReceive 時の検証が適切に行われない場合、不正アクセスやインテントリダイレクトなどの脆弱性が存在し、さらなるセキュリティ上の危険が発生する可能性があります。

この種のセキュリティ問題は数多くありますが、代表的なものは、Pwn2Own の Samsung Galaxy S8 を突破するために使用された PpmtReceiver の脆弱性です [28]。

情報漏洩

上記は主にブロードキャストの送信者を制限する観点からパーミッションを設定していますが、実はこのパーミッションはブロードキャストの受信者も制限することができますが、メッセージを送信する際には追加の指定が必要で、例えば上記のパーミッションを持つ受信者のみにブロードキャストの受信を許可したい場合の送信コードは以下のようになります。

Intent it = new Intent(this, ...);
it.putExtra("secret", "chicken2beautiful")
sendBroadcast(it, "com.evilpan.MY_PERMISSION");

2 番目のパラメータがない場合、デフォルトでは、条件を満たすすべての受信者がブロードキャスト情報を受信できます。このとき、送信したIntentに機密データが含まれていると情報漏洩の原因となる可能性があります。

実際のケースは CVE-2018-9581[29] です。システムは android.net.wifi.RSSI_CHANGED をブロードキャストするときに機密データ RSSI を運びます。このブロードキャストはすべてのアプリケーションで受信でき、間接的に物理的な位置情報の漏洩につながります。(面白い?)

Broadcast Receiver の場合、許可タグの役割が特に明らかであることがわかります。たとえば、システム ブロードキャストの場合 BOOT_COMPLETED、通常はシステム アプリのみがブロードキャストを送信する権限を持っています。これはフレームワークの AndroidManifest.xml[30] で定義されます。

アプリケーションのカスタム ブロードキャストでは、通常、上記のカスタム パーミッションが使用されるため、複数のアプリケーションが同じパーミッションを定義した場合はどうなるのかという疑問が自然に浮かびます。実はこれは歴史的な脆弱性で、Androidの初期には最初に定義されたパーミッションを優先する戦略が取られていましたが、Andorid 5以降では、2つのアプリケーションが同じパーミッションの定義が異なることが明確に定義され(署名が同じでない限り)、そうしないと後からインストールしたアプリケーションでエラー警告が発生します INSTALL_FAILED_DUPLICATE_PERMISSION 。興味のある考古学者は、次の関連記事を参照してください。

  • • カスタム権限による脆弱性[31]

  • • カスタム権限の脆弱性と「L」開発者プレビュー[32]

インテントのリダイレクト

原理については多くを語る必要はありません。ケースを見てください。この脆弱性は Tiktok にあります NotificationBroadcastReceiver 。インテント フィルターの定義により、コンポーネントはデフォルトでエクスポートとして設定されるため、次のように、外部アプリケーションのブロードキャストを受信し、ブロードキャスト内の信頼できないデータを直接使用してアクティビティを開始できます。

通知ブロードキャストレシーバー

脆弱性の詳細については、次を参照してください: Overcured は TikTok Android アプリの危険な脆弱性を検出します[33]

コンテンツプロバイダー

Content Provider[34]、つまりコンテンツプロバイダーを略して Provider と呼びます。Android アプリケーションは通常、MVC 構造 (モデル-ビュー-コントローラー) として実装され、モデル部分は表示する独自のビュー (グラフィカル インターフェイス) のデータ ソースです。ただし、アプリケーションが他のデータを使用するために独自のデータを提供したり、他のアプリケーションからデータを取得したりする必要がある場合があります。

ContentProvider を定義するには、ContentProvider[35] クラスから継承し、6 つのメソッド、、、、、 および を  実装 する queryだけで済みますこのうち、onCreate がメインスレッドでシステムによって呼び出される以外のメソッドは、クライアント プログラムによってアクティブに呼び出されます。カスタム プロバイダーはプログラム リストで宣言する必要があります。これについては後で詳しく説明します。insertupdatedeletegetTypeonCreate

プロバイダーは主にデータベースのような追加、削除、変更、およびクエリ インターフェイスを実装していることがわかります。クライアントの観点から見ると、クエリ プロセスは従来のデータベースのクエリと似ています。たとえば、システム SMS をクエリするためのコード スニペットは次のとおりです。

Cursor cursor = getContentResolver().query(
    Telephony.Sms.Inbox.CONTENT_URI,           // 指定要查询的表名
    new String[] { Telephony.Sms.Inbox.BODY }, // projection 指定索要查询的列名
    selectionClause,                           // 查询的过滤条件
    selectionArgs,                             // 查询过滤的参数 
    Telephony.Sms.Inbox.DEFAULT_SORT_ORDER);   // 返回结果的排序
while (cursor.moveToNext()) {
    Log.i(TAG, "msg: " + cursor.getString(0));
}

その中には  、ContentProvider のクライアント リモート インターフェイスであるサブクラスがあり、透過的なリモート プロキシ呼び出しを実現できます ContentResolver 。  クエリのテーブル名と見なすことも、 列名と見なすこともでき、返されるカーソルはクエリ結果行のイテレータです。ContentInterfacecontent_uriprojection

前の 3 つのコンポーネントとは異なり、シェルでプロバイダー コンポーネントにアクセスするためのツールは です content

プロバイダーでよくある問題を紹介します。

権限

データキャリアとしてのプロバイダーの観点から、セキュリティアクセスと許可制御は当然のことながら最優先事項です。例えば上記のコード例で言えば、SMSにアクセスするためのインターフェースですが、誰もが自由にアクセスできてしまうと、明らかに情報漏洩の原因となります。前に簡単に説明したように、アプリケーションで定義されたプロバイダーは、provider[36] タグを使用してプログラム マニフェスト ファイルで宣言する必要があります。exported その中には、外部からアクセスできるかどうかを示す共通 属性permission と、アクセスに必要な権限を示す属性があり、もちろん readPermission/writePermission 属性など、読み取りと書き込みで異なる権限を使用することもできます。

たとえば、上記の SMS データベースは次のように宣言されます。

<provider android:name="SmsProvider"
    android:authorities="sms"
    android:multiprocess="false"
    android:exported="true"
    android:singleUser="true"
    android:readPermission="android.permission.READ_SMS" />

他のアプリケーションがアクセスしたい場合は、マニフェスト ファイルで対応するアクセス許可を宣言する必要があります。

<uses-permission android:name="android.permission.READ_SMS" />

これはすべてよく理解されており、他のコンポーネントも同様の特性を持っています。さらに、プロバイダー自体は、より詳細な権限制御、つまり、grantUriPermissions[37] を提供します。これは、クライアントにこのプロバイダーへのアクセスを一時的に許可するかどうかを示すブール値です。一時的に権限を付与する操作プロセスは、一般的に次のとおりです。

  1. startActivityForResult 1. クライアントは、 Sendなどを使用して、アクセスするコンテンツ URI を指定して、プロバイダーが配置されているアプリケーションにインテントを送信します 。

  2. 2. アプリケーションは、Intent を受信すると、それが許可されているかどうかを判断し、許可されている場合は、Intent を作成し、対応する Content URI (要求された URI と一致しない可能性があります) の読み取り/書き込みが許可されていることを示す flags フラグを設定し、最終的にそれを使用してクライアントに返し FLAG_GRANT_[READ|WRITE]_URL_PERMISSIONます setResult(code, intent) 。

  3. 3. クライアントの onActivityResult は返された Intent を受け取り、その中の URI を使用してターゲットのプロバイダーに一時的にアクセスします。

読み取りを例にとると、Intent.flags FLAG_GRANT_READ_URI_PERMISSION[38] が含まれている場合、インテントの受信者Intent.data(つまりクライアント) には、受信者の有効期間が終了するまで、URI の一部に対する一時的な読み取り許可が付与されます 。さらに、プロバイダー アプリケーションは、 Context.grantUriPermission メソッドをアクティブに呼び出して、対応するアクセス許可をターゲット アプリケーションに付与することもできます。

public abstract void grantUriPermission (String toPackage, 
                Uri uri, 
                int modeFlags)
public abstract void revokeUriPermission (String toPackage, 
                Uri uri, 
                int modeFlags)

GrantUriPermissions 属性は、URI 粒度でアクセス許可の読み取り/書き込み制御を実行できますが、注意すべき点が 1 つあります。grantUriPermissions によって一時的に付与されたアクセス許可は、 readPermission、writePermission、permission、exported 属性によって課される制限を無視しますつまり、 exported=falseクライアントが対応するコンテンツ プロバイダーを申請しなく てもuses-permission、一度許可が与えられれば、対応するコンテンツ プロバイダーにアクセスできるようになります。

さらに、<provider> サブタグ Grant-uri-permission[39] があります。grantUriPermissions が に設定されている場合でも、 falseこのタグの下で定義された URI サブセットには、アクセス許可を一時的に取得することで引き続きアクセスできます。このサブセットは、プレフィックスまたはワイルドカードを使用して、URI の承認されたパス範囲を指定できます。

プロバイダー権限の設定を誤ると、予期せぬ悪意のあるプログラムによってアプリケーションデータがアクセスされて情報漏洩につながったり、サンドボックスのデータが上書きされてRCEが発生したりする可能性がありますが、このような事例は後ほど多く見られます。

ファイルプロバイダー

前述したように、カスタム プロバイダーは 6 つのメソッドを実装する必要がありますが、Android はいくつかの一般的なシナリオでプロバイダーに対応するサブクラスを作成しており、ユーザーはこれらのサブクラスを継承し、必要に応じていくつかのサブクラス メソッドを実装できます。一般的なシナリオの 1 つは、ContentProvider を使用してアプリケーション ファイルを共有することです。システムは、 FileProvider アプリケーション定義ファイルの共有とアクセスを容易にするために ContentProvider を提供します。ただし、適切に使用しないと、任意のファイルの読み取りと書き込みの問題が発生する可能性があります。

FileProvider[40] は、XML を使用してファイル アクセス制御を指定する機能を提供します。通常、プロバイダ アプリケーションは FileProvider クラスを継承するだけで済みます。

public class MyFileProvider extends FileProvider {
   public MyFileProvider() {
       super(R.xml.file_paths)
   }
}

file_paths はユーザー定義の XML であり、 meta-data マニフェスト ファイルで指定することもできます。

<provider xmlns:android="http://schemas.android.com/apk/res/android" android:name="com.evilpan.MyFileProvider" android:exported="false" android:authorities="com.evilpan.fileprovider" android:grantUriPermissions="true">
  <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@7F15000E"/>
</provider>

resource を指します res/xml/file_paths.xmlアクセスできるファイル パスはこのファイルで定義されており、FileProvider は事前に指定されたファイルのコンテンツ URI のみを生成します。ファイルパス構成の例は次のとおりです。

<paths>
  <root-path name="root" path=""/>
  <files-path name="internal_files" path="."/>
  <cache-path name="cache" path=""/>
  <external-path name="external_files" path="images"/>
</paths>

paths タグは、さまざまなディレクトリのサブパスに対応する、複数のタイプのサブタグをサポートします。

  • •  files-path: Context.getFilesDir()

  • •  cache-path: Context.getCacheDir()

  • •  external-path:Environment.getExternalStorageDirectory()

  • •  external-files-path: Context.getExternalFilesDir()

  • •  external-cache-path: Context.getExternalCacheDir()

  • •  external-media-path: Context.getExternalMediaDirs()[0]

より具体的には 、システムのルート ディレクトリ root-pathを表しますFileProvider によって生成される URI 形式は通常 、上記の Provider のように、 ファイル にアクセスするために 使用できます 。/content://authority/{name}/{path}content://com.evilpan.fileprovider/root/proc/self/maps/proc/self/maps

root-path FileProvider の仕様は危険な兆候であり、攻撃者が一時的なアクセス許可を取得すると、アプリケーションのすべてのプライベート データを読み取ることができます。 

たとえば、TikTokの歴史には次のような本当の抜け穴がありました。

<provider android:name="android.support.v4.content.FileProvider" android:exported="false" android:authorities="com.zhiliaoapp.musically.fileprovider" android:grantUriPermissions="true">
        <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/k86"/>
    </provider>

ここでは継承を必要とせずに直接使用されます FileProviderxml/k86.xml ファイルの内容は次のとおりです。

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:amazon="http://schemas.amazon.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
    <root-path name="name" path=""/>
    <external-path name="share_path0" path="share/"/>
    <external-path name="download_path2" path="Download/"/>
    <cache-path name="gif" path="gif/"/>
    ...
</paths>

一時的な権限を取得した後、アプリケーションは任意のファイルを読み書きできるようになります。

隠された ...

ContentProvider クラスには、実装する必要がある 6 つのメソッドに加えて、他の隠しメソッドがいくつかあります。これらは通常、デフォルトで実装されるか、サブクラスによってオーバーライドできます。

  • • ファイルを開く

  • • openFileHelper

  • • 電話

  • • ...

これらの隠されたメソッドは、意図せずにセキュリティ上の問題を引き起こす可能性があるため、このセクションではいくつかのケースを通してその理由を分析します。

ファイルを開く

ContentProvider で共有ファイルの読み書き機能を実装したい場合は、 openFile メソッドをオーバーライドすることで実装することもできますが、このメソッドのデフォルト実装では FileNotFoundException 例外がスローされます。

開発者は実装時に開いたローカル ファイルを直接返すことはありませんが、いくつかのサブディレクトリ ファイルを選択的に返します。ただし、コードが厳密に記述されていない場合、パス トラバーサルなどの問題が発生する可能性があり、典型的な脆弱性の実装は次のとおりです。

 @Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    File file = new File(getContext().getFilesDir(), uri.getPath());
    if(file.exists()){
        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }
    throw new FileNotFoundException(uri.getPath());
}

同じファミリーの別の同様のメソッド openAssetFile。そのデフォルト実装は openFile を呼び出します。

public @Nullable AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode)
        throws FileNotFoundException {
    ParcelFileDescriptor fd = openFile(uri, mode);
    return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
}

場合によっては、開発者はパス トラバーサルを防御する必要があることを知っていても、その防御姿勢が間違っており、次のような場合にバイパスされる可能性もあります。

public ParcelFileDescriptor openFile(Uri uri, String mode) {
    File f = new File(DIR, uri.getLastPathSegment());
    return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
}

ここでは、最後のレベルのファイル名のみを取得する getLastPathSegment ためにこれを使用したい が、実際には、URL エンコードされたパスによってバイパスできます。たとえば、%2F..%2F..path%2Fto%2Fsecret.txt が返されます /../../path/to/secret.txt

さらにもう 1 つの誤った防御策は、 UriMatcher.match メソッド lookups を使用することです../が、これも URL エンコードによってバイパスされます。防御とフィルタリングの正しい方法は次のとおりです。

public ParcelFileDescriptor openFile (Uri uri, String mode) throws FileNotFoundException {
  File f = new File(DIR, uri.getLastPathSegment());
  if (!f.getCanonicalPath().startsWith(DIR)) {
    throw new IllegalArgumentException();
  }
  return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
}

詳細については、パストラバーサルの脆弱性[41]を参照してください。

openFileHelper

ContentProvider にはあまり知られていないメソッドもあります openFileHelper 。そのデフォルトの実装では、現在のプロバイダーの列データを使用して _data ファイルを開きます。ソース コードは次のとおりです。

protected final @NonNull ParcelFileDescriptor openFileHelper(@NonNull Uri uri,
        @NonNull String mode) throws FileNotFoundException {
    Cursor c = query(uri, new String[]{"_data"}, null, null, null);
    int count = (c != null) ? c.getCount() : 0;
    if (count != 1) {
        // If there is not exactly one result, throw an appropriate
        // exception.
        if (c != null) {
            c.close();
        }
        if (count == 0) {
            throw new FileNotFoundException("No entry for " + uri);
        }
        throw new FileNotFoundException("Multiple items at " + uri);
    }

    c.moveToFirst();
    int i = c.getColumnIndex("_data");
    String path = (i >= 0 ? c.getString(i) : null);
    c.close();
    if (path == null) {
        throw new FileNotFoundException("Column _data not found.");
    }

    int modeBits = ParcelFileDescriptor.parseMode(mode);
    return ParcelFileDescriptor.open(new File(path), modeBits);
}

このメソッドの主な機能は、サブクラスがメソッドを迅速に実装できるようにすることであり openFile 、通常はサブクラス内でメソッドを直接オーバーライドしません。ただし、列に基づいてファイルを開く機能により _data 、攻撃者は悪意のあるデータを挿入し、間接的に任意のファイルの読み書きを達成する可能性があります。

SemClipboardProvider典型的なケースとしては、接続時にユーザー データを検証しないSamsung 製携帯電話があります 。

public Uri insert(Uri uri, ContentValues values) {
    long row = this.database.insert(TABLE_NAME, "", values);
    if (row > 0) {
        Uri newUri = ContentUris.withAppendedId(CONTENT_URI, row);
        getContext().getContentResolver().notifyChange(newUri, null);
        return newUri;
    }
    throw new SQLException("Fail to add a new record into " + uri);
}

プロバイダーは system_server プロセス中であり、非常に高い操作権限を持っており、この脆弱性を悪用すると、攻撃者はシステム レベルで任意のファイルを読み書きすることができます。

ContentValues vals = new ContentValues();
vals.put("_data", "/data/system/users/0/newFile.bin");
URI semclipboard_uri = URI.parse("content://com.sec.android.semclipboardprovider")
ContentResolver resolver = getContentResolver();
URI newFile_uri = resolver.insert(semclipboard_uri, vals);
return resolver.openFileDescriptor(newFile_uri, "w").getFd(); 

この脆弱性は他の脆弱性とともに野生の攻撃で使用されており、Google TAG チームによって捕捉されました。このフルチェーンの分析については、Project Zero の最近の記事「A Very Powerful Clipboard: Analysis of a Samsung in-the-wild Exploit Chain[42]」を参照してください。

電話

ContentProvider は、 call サーバー側で定義されたメソッドを呼び出すためのメソッドを提供し、その関数シグネチャは次のとおりです。

public Bundle call (String authority, 
                String method, 
                String arg, 
                Bundle extras)
public Bundle call (String method, 
                String arg, 
                Bundle extras)

デフォルトの実装は、直接返す空の関数です null。開発者はこの関数をオーバーライドしていくつかの動的メソッドを実装でき、戻り値も呼び出し元に渡されます。

call これは通常の RPC 呼び出しに似ていますが、ここには小さな落とし穴があり、これも開発者ドキュメントで特別にマークされています。Android システムは関数の権限をチェックしません。これは、システムが呼び出しでデータが読み取られるか書き込まれるかがわからないためであり、そのためマニフェストで定義された権限制約に従って判断することができません。 したがって、開発者は呼び出し内のロジックに対して権限の検証を実行する必要があります。

開発者がこのメソッドを実装しても検証を行わなかったり検証が不十分な場合、不正な呼び出しが発生する可能性があります。1 つのケースは、   OPPO のシステム アプリケーションでの call メソッドの実装で、受信する dex ファイルを直接ロードするために DexClassLoader が使用され、これにより攻撃者のコードが特権プロセスで実行される直接的な原因となり、基本クラスを継承するすべてのプロバイダーが影響を受けます () CVE-2021-23243。 HostContentProviderBase

SliceProvider また、一部のシステムプロバイダでは、call メソッドを通じてリモートオブジェクトのインスタンスを取得できるものもあり、例えば、「Android における特殊な攻撃対象領域 (3) - 隠しコール機能 [43]」では、著者はシステムアプリケーション内で PendingIntent オブジェクトを取得し、さらにそれを利用して任意のブロードキャスト を偽造する機能を実現しています KeyguardSliceProvider 。

他の

Android システムには、上記の 4 つの主要コンポーネントに直接関係する脆弱性以外にも、分類が難しい脆弱性が多数存在しますが、このセクションでは主に、最も一般的な脆弱性のいくつかを選択して簡単に紹介します。

保留中の意図

PendingIntent[44] は Intent の表現であり、Intent オブジェクト自体ではなく、Parcelable オブジェクトです。オブジェクトが他のアプリケーションに渡された後、他のアプリケーションは、送信者として指定されたインテントによって指定された操作を実行できます。PendingIntent は、次の静的メソッドのいずれかを使用して作成されます。

  • • getActivity(Context, int, Intent, int);

  • • getActivities(Context, int, Intent[], int);

  • • getBroadcast(Context, int, Intent, int);

  • • getService(Context, int, Intent, int);

PendingIntent 自体は、システムによる生データ記述子への単なる参照であり、大まかに次のように理解できます Intent 的指针このため、PendingIntent を作成したアプリケーションが閉じられた後でも、他のアプリケーションは引き続きデータを使用できます。元のアプリが後で再起動され、同じパラメーターで PendingIntent を作成した場合、実際に返される PendingIntent は、以前に作成されたものと同じトークンを指します。なお、filterEquals[45] メソッドは、インテントが同じかどうかを判断するために使用され、アクション、データ、タイプ、アイデンティティ、クラス、カテゴリが同じかどうかを判断します。ここではリストされていないため、追加部分のみが異なるインテントも同等とみなされます extra 。

PendingIntent は他のアプリケーションの特性を表すことができるため、シナリオによっては悪用に使用される可能性があります。たとえば、開発者が次のようなデフォルトの PendingIntent を作成し、それを他のアプリケーションに渡すとします。

pi = PendingIntent.getActivity(this, 0, new Intent(), 0);
bundle.putParcelable("pi", pi)
// send bundle

この PendingIntent を受信した悪意のあるアプリケーションは、元のインテントを取得し、それを使用して Intent.fillin 空のフィールドを埋めることができ、元のインテントが上記の空のインテントである場合、攻撃者はそれを特定のインテントに変更して、エクスポートされていないプライベート アプリケーションを含むアプリケーションをターゲットとして起動することができます。典型的なケースは初期のbroadAnywhere[46]脆弱性で、Android設定アプリケーションのaddAccountメソッドはPendingIntentブロードキャストを作成しますが、インテントの内容は空であるため、悪意のあるアプリケーションがブロードキャストのアクションを埋めるインテントを受け取り、システムブロードキャストの不正送信、テキストメッセージの偽造、工場出荷時の設定の復元を実現します。

このような問題を軽減するために、Andorid では Intent.fillin の書き換えに多くの制限を設けています。たとえば、既存のフィールドは変更できない、コンポーネントとセレクターのフィールドは変更できない(FILL_IN_COMPONENT/SELECTOR を追加で設定しない限り)、暗黙的なインテントのアクションは変更できないなどです。

しかし、一部の研究者は、暗黙的インテントを悪用する方法、つまり、フラグを変更して FLAG_GRANT_WRITE_URI_PERMISSION を追加し、データ URI を変更して被害者のプライベート プロバイダーを指すように変更し、パッケージを攻撃者に変更する方法を提案していますが、同時に、攻撃者は自分のアクティビティ内で同じインテント フィルターを宣言し、インテントが転送されるときに攻撃者のアプリケーションが起動されるようにし、同時にターゲットのプライベート プロバイダーにアクセスします。プライベート ファイルを盗んだり上書きしたりするために取得されます。攻撃アイデアの詳細については、以下の参考記事をご覧ください。

  • •ブロードエニウェア: ブロードキャスト コンポーネントの権限バイパスの脆弱性 (バグ: 17356824) [47]

  • • PendingIntent リダイレクト: Android および人気アプリ向けのユニバーサル権限昇格方法 - BlackHat EU 2021 トピックの詳細 (パート 1)[48]

  • • PendingIntent リダイレクト: Android システムおよび人気のあるアプリ向けのユニバーサル権限昇格方法 - BlackHat EU 2021 の問題の詳細な説明 (パート 2)[49]

FLAG_MUTABLE Android 12 以降では、PendingIntent を作成するとき、または FLAG_IMMUTABLE変更できるかどうかを明示的に指定する必要があります 。

ディープリンク

ほとんどのオペレーティング システムには、ディープリンク、つまりカスタム スキーマを通じて特定のアプリケーションを開くという概念があります。たとえば、https://evilpan.com/ をクリックするとデフォルトのブラウザを起動してターゲット ページを開くことができ、tel://10086 をクリックするとダイヤル インターフェイスが起動し、weixin://qr/xxx をクリックすると WeChat が起動するなどです。他のシステムとは関係なく、Android ではこれは主に暗黙的なインテントによって実現されます。

アプリケーションが同様のカスタム プロトコルを登録したい場合は、アプリケーション マニフェスト ファイルでそれを宣言する必要があります。

<intent-filter>
  <action android:name="android.intent.action.VIEW"/>
  <category android:name="android.intent.category.DEFAULT"/>
  <category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="weixin" android:host="qr"/>
</intent-filter>

このタイプの暗黙的インテントはリンクをクリックすることで直接トリガーできるため、攻撃者に人気があります。対応するインテントを処理するコンポーネントがユーザーから渡されたコンテンツのフィルタリングに失敗すると、1 クリックの脆弱性が発生する可能性があります。関連するケースについては、記事「Android の特別な攻撃対象領域 (2) - 危険なディープリンク」を参照してください。

ウェブビュー

Andorid システムでは、Webview[50] は主にアプリケーション独自の Activety で Web ページ コンテンツを表示するために使用され、開発者がカスタム コントロールを実装するための追加インターフェイスをいくつか提供します。スケーラビリティが高いほど、エラーが発生する可能性が高くなります。特に Android クライアント開発が減少しており、Java 開発も「ビッグ フロント エンド」の方向に発展している現在ではなおさらです。もともとネイティブ アプリケーションを使用して実装されていた多くのロジックは、h5 やアプレットなどの Web ページに徐々に移行されてきました。このようにして、WebView の攻撃対象領域も大幅に拡大しました。

onReceivedSslError 従来の Webview のセキュリティ問題は主に、中間者攻撃やsetAllowFileAccessFromFileURLs ローカルのプライベート ファイルの漏洩につながる SSL エラーのオーバーライドや無視など、安全でない構成に関連しています 。しかし現在、脆弱性は Web ページ内の Java コードと JavaScript コード間のブリッジである JSBridge に多く存在しています。

Webview または JS エンジンのサンドボックス機能により、Web ページ自体の Javascript コードは、JavaScript からのブロードキャストの送信、アプリケーション ファイルへのアクセスなど、ネイティブ アプリケーションでのみ実行できる多くの操作を実行できません。ビジネスの複雑さにより、多くのロジックを Java レイヤーまたはネイティブ レイヤーに実装する必要があるため、JSBridage を使用する必要があります。従来の JSBridge が Webview.addJavascriptInterface 実装されています。簡単な例は次のとおりです。

class JsObject {
    @JavascriptInterface
    public String toString() { return "injectedObject"; }
}
webview.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JsObject(), "injectedObject");
webView.loadData("", "text/html", null);
webView.loadUrl("javascript:alert(injectedObject.toString())");

Java 層は、loadUrl を直接使用して JS コードを実行することにより、JavaScript にデータを返します。もちろん、この方法で Bridge を登録するだけでなく、 console.log データ送信を使用したり、  onConsoleMessage Java 層でデータを受信するためにコールバックを使用したりするなど、アプリケーション固有の実装が多数あります。しかし、いずれにせよ、これは攻撃対象領域の増加につながり、大規模なアプリケーションでは Web ページの呼び出しに数百の jsapi を登録することもあります。

歴史的な脆弱性の観点から見ると、Webview 脆弱性の主な原因は、jsapi ドメイン名検証の問題と、スペース上の理由で拡張されない Bridge コード自体の脆弱性です。

追記

この記事では主に Android の 4 つの主要コンポーネントを通じて関連する一連の論理的問題を紹介し、著者が知っている歴史的な脆弱性を可能な限り取り上げます。個人の認識レベルが限られているため、100 万もの内容を見逃すことは常に避けられませんが、それでも記事の長さは予想を 10 億ポイント上回っています。古いものを見直して新しいものを学ぶという観点から、この種のロジックの脆弱性を掘り出すための最良の戦略は、静的分析ツールを使用してより多くのシンク パターンを収集し、スキャンするための効果的なルールを作成することです。条件がない場合は、(rip)grep も可能です。

参考文献

  • • Galaxy S8 を食い荒らすギャラクシー・リープフロッグ[51]

  • • チェーンスポッティング: ロジック バグによるエクスプロイト チェーンの構築[52] (11 個のエクスプロイトで Samsung S8 を破壊する方法[53])

  • • Huawei Mate 9 Pro Mobile Pwn2Own 2017[54]

  • • TikTok Android アプリの危険な脆弱性を検出 - Overcured[55]

  • • Mystique 脆弱性ホワイトペーパー - JD Research Institute 情報セキュリティ研究所 [56]

  • • XIAOMI の Android アプリのハッキング - パート 1[57]

  • • Jandroid による Pwn2Own の自動化[58]

Supongo que te gusta

Origin blog.csdn.net/ab6326795/article/details/131206584
Recomendado
Clasificación