
CVE-2021-39749のPoC。Android 12L Betaで任意のActivityを起動できるようにするもの。
これはCVE-2021-39749のPoCであり、Android 12L Betaで、他のアプリのpermissionやexported設定に関係なく、そのアクティビティを起動できるようにするものです。
Android 12Lでは、TaskFragmentOrganizerへのアクセスは(意図的に)MANAGE_ACTIVITY_TASKS権限を必要としなくなりました
ここで提供されるアプリを使用するには、Hidden API Checksを無効にする必要があります。adb shell settings put global hidden_api_policy 1で無効にできます。これらはセキュリティ境界ではなく、既知のアプリベースのバイパスが存在します
このバグ(および元のレポートで言及された関連するいくつかのバグ)を修正するコミットは次のとおりです。
startActivityInTaskFragmentがBinder.getCallingUid()に依存しなくなったResolverActivityでrelinquishTaskIdentityが有効になったTaskFragmentのSurfaceControlが提供されなくなったActivityRecord#appTokenをTaskFragmentOrganizerに送信するかどうかの決定が、pidベースからuidベースに変更されたandroid-12.1.0_r4をチェックアウトし、最初の3つのコミット(または最初の2つ。アプリは(ShutdownActivityを起動することで)デバイスをシャットダウンできますが、「ズームしてアルファを設定」チェックボックスは機能しません)をリバートできます。
(そのリストの最初のコミットは、リバートしようとするとテストでマージ競合が発生しますが、これらは無視できます)
Binder.getCallingUid()Binder.getCallingUid()メソッドは、現在処理中のBinderトランザクションを送信したプロセスのuidを返します。そのuidはスレッドローカル変数に格納されます。トランザクションを処理するコードは、Binder.clearCallingIdentity()を呼び出して、その変数を自身のプロセスのuidに設定し、トランザクション処理中に後で呼び出されるメソッドに対して、権限チェックがBinderトランザクションの呼び出し元ではなく自身に対して行われるべきであることを示すことができます。
Binder.clearCallingIdentity()の後に常に呼び出されるため、常に自身のプロセスのuidを返すBinder.getCallingUid()が存在することがあります。これは意図的に行われる場合もあります。例えば、ActivityTaskManagerService#startDreamActivityなどです(ただし、これはProcess.myUid()やOs.getuid()を行うかなり回りくどい方法ですが)
私は自分用に、(Sootベースの)静的解析ツールを作成しました。これは、Binder.clearCallingIdentity()の後にのみ発生し得る、そのようなBinder.getCallingUid()呼び出し(およびその他の権限チェック)を報告します。(Sootが提供するJimple/Shimple IRを処理するカスタムロジックがありますが、Sootでそれを行うより良い方法があるかもしれません。しかし、それが現在私が持っているものです)
Android 12L Betaで、そのツールはActivityStartController#startActivityInTaskFragmentに1つ発見しました(注:Betaリリースはオープンソースではないため、当時ソースコードは利用できませんでしたが、Shimpleは一般的に読み取り可能なので、SootをJavaデコンパイラとしても使用していました)
startActivityInTaskFragmentの呼び出し方静的解析レポートの一部として、onTransact()実装(Binder呼び出しが開始される場所)からstartActivityInTaskFragmentへの呼び出し階層を取得しました。
IWindowOrganizerControllerのaidl生成コード内のonTransactWindowOrganizerController#applyTransaction(CallerInfo引数なし)WindowOrganizerController#applyTransaction(CallerInfo引数あり)WindowOrganizerController#applyHierarchyOpActivityStartController#startActivityInTaskFragmentapplyTransactionへのBinder呼び出しがTaskFragmentOrganizerクラスに存在することを発見し、すべてのBinder呼び出しを直接行うよりも便利なラッパーとして使用することにしました(どちらも公開APIではないため、いずれにせよリフレクションを使用する必要がありました)
まず、メソッド「2.」はenforceTaskPermissionを呼び出します。これはAndroid 12.0では、取得できないsignatureのみのMANAGE_ACTIVITY_TASKS権限をチェックしていましたが、Android 12Lではルールが緩和され、特定のトランザクションは権限なしで実行できるようになりました。startActivityInTaskFragmentの実行に必要な操作はどれも権限を必要としないことが判明しました(トランザクションにTaskFragmentOrganizerが関連付けられている場合)
したがって、HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENTを実行したいと考えています。そのためには、TaskFragmentをmLaunchTaskFragmentsに登録する必要があります。そうしないと、"Not allowed to operate with invalid fragment token"例外が報告されます。
HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENTを通じて、そのようなTaskFragmentを登録できます。これはcreateTaskFragment()を呼び出します。
(PoCコードでは、これらのトランザクションはSecondActivityで送信されます。HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENTはinitOrganizerAndFragment()によって送信され、HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENTはstartActivityInOrganizerによって送信されます)
これにより、startActivityInTaskFragmentを呼び出せるようになり、ここで起動されたアクティビティのインテントはシステムuidから送信されたものと見なされますが、それ自体では何もできないことが判明しました。システムによって起動されたアクティビティはURIグラントを実行できず、別のアプリのアクティビティを起動しようとすると、canEmbedActivityチェックによって停止されるためです。
canEmbedActivityのバイパスcanEmbedActivityをもう一度見てみましょう。taskFragment.getTask().effectiveUidがシステムのuidであるか、起動されたアプリのuidと一致する場合、埋め込みが許可されます。effectiveUidがシステムであるタスクに存在する必要があります。
また、createTaskFragment()に戻ります。TaskFragmentの作成は、rootActivity.getUid() != ownerActivity.getUid()の場合にのみ許可されていました。これは、アクティビティが存在するタスクのバックスタックの最下部にある必要があることを意味します。
新しいTaskを起動する必要があります(Intent.FLAG_ACTIVITY_NEW_TASKを使用)。これにはシステムuidに属するアクティビティが含まれ(Task#effectiveUidがAID_SYSTEMに設定されます)、その後、そのアクティビティが(同じタスク内で)アクティビティを起動し、finish()自身を呼び出します(これにより、アクティビティがそのタスクのルートになり、createTaskFragment()を使用できるようになります)
そのようなアクティビティの1つがChooserActivityです。Chooserは通常、「共有」オプションを選択した後にユーザーが使用したいアプリを選択するために使用されます。ただし、ChooserActivityにはAndroidManifest.xmlでandroid:relinquishTaskIdentity="true"が設定されています。これは、別のアクティビティを起動すると、Task#effectiveUidが新しく起動されたアプリのuidで上書きされることを意味します。
(relinquishTaskIdentityは、タスク内の最初のアプリによって使用され、システムアプリに対してのみ機能するため、relinquishTaskIdentityを自分で使用してシステムアプリを起動し、タスクのTask#effectiveUidを上書きすることはできません)
アクティビティを起動してfinish()自身を呼び出すことができるもう1つのそのようなアクティビティは、ResolverActivityです。これは、複数のアクティビティに解決される暗黙のIntentを起動するときに使用されます。Resolverは(Chooserとは異なり)選択を記憶するオプションを提供します。これにより、(電話のユーザーとして)この2つを区別できます。ResolverActivityにはrelinquishTaskIdentityが設定されていませんでしたが、Resolverは利用可能なオプションを見つけるために独自のIntentを使用します(ChooserはExtrasで提供されたIntentを受け取ります)。これは、Resolverが選択したアクティビティを起動するときに使用するIntentフラグが、Resolverの起動に使用されたものと同じになるため、悪用にとって問題であることが判明しました。
Intent.FLAG_ACTIVITY_NEW_TASKを設定しない場合、ResolverはeffectiveUidがすでに永続的に設定されているタスク内で起動されます。Intent.FLAG_ACTIVITY_NEW_TASKを設定する場合、Resolverは選択をさらに別のTaskに起動し、そのTaskのeffectiveUidは起動されたアプリに属するものに設定されます。これらの問題の解決策は、両方を使用することです。
ChooserActivityを起動します。そのIntentに以下を提供します。
Intent.FLAG_ACTIVITY_NEW_TASK。Chooserが新しいタスクで起動されるようにします(effectiveUidはシステムのものになりますが、次のアクティビティ起動までです)。Intent.EXTRA_INTENTを、どのアクティビティにも一致しないIntentに設定し、Chooserに残るオプションがIntent.EXTRA_INITIAL_INTENTSからのみになるようにします。Intent.EXTRA_INITIAL_INTENTSに、Chooserに起動させたいIntentを含む1要素の配列を含めます(オプションが1つだけの場合、ChooserとResolverの両方がプロンプトをスキップして、唯一のオプションをすぐに起動し、finish()自身を呼び出します)。ResolverActivityがChooserActivityによって起動されます。
relinquishTaskIdentityが設定されていなかったため、はシステムに設定され、このタスクで起動される次のアクティビティに関係なく、その状態が維持されます。(PoCアプリでは、これらの手順の準備はFirstActivityで実行されます)
TaskFragmentOrganizerは、onTaskFragmentAppearedコールバックを通じてSurfaceControlを受け取り、そのSurfaceControlを使用して、起動されたアクティビティを拡大縮小したり、透明にしたりできます。その間もタッチイベントを受け取り、隠されているとは見なされません(そのため、タップジャック防止要素をタップできます)。
PoCアプリで「ズームしてアルファを設定」チェックボックスをオンにすると、これを確認できます。
これは、上部の修正リストのコミット「3.」によって修正されています
もう1つの点は、TaskFragment内で実行されているアクティビティのActivityRecord#appTokenがTaskFragmentOrganizerコールバックに渡されることです。このリストは、同じプロセス内のアクティビティのトークンのみを含むようにフィルタリングされていましたが、チェックはTaskFragmentOrganizerのpidと、appTokenを取得できるアクティビティのpidを比較することによって行われていました。実際に確認したわけではありませんが、アプリケーションがTaskFragmentOrganizerを作成し、最初に作成に使用したプロセスを終了し、そのpidを別のアプリのアクティビティのpidとして再利用して、そのappTokenを取得できる可能性があると思います。ここに、検証をpidベースからuidベースに切り替えるコミット(上記の修正リストの「4.」)があります(このコミットは私のレポートとは独立して行われたように見えますが(ただし、その後に行われました))
攻撃者がアクティビティのappTokenを取得すると、onActivityResult()呼び出しを注入でき(ターゲットアプリが自分でstartActivityForResult()を呼び出していない場合でも)、(activityStopped()を呼び出すことで)savedInstanceStateを改ざんできる可能性があります。ただし、攻撃者がターゲットアプリケーションがそのメソッドを呼び出す競争に勝ち、追加の呼び出しがクラッシュによる状態の喪失を引き起こさない場合に限ります。
この場合にそれが可能かどうかは確認していませんが、以前、CVE-2020-0001(ええ、派手な番号を手に入れました)で、savedInstanceStateの改ざんとonActivityResult()の注入を使用して、システム設定アプリをだまして、ユーザーの操作なしでAccessibilityServiceを有効にすることができましたが、それはまた別の機会の話です。
Task#effectiveUidIntent.FLAG_ACTIVITY_NEW_TASKがないため、次のアクティビティは同じタスク内で起動されます。<intent-filter>にのみ一致するため、Resolverはすぐにアクティビティの起動に進みます。ResolverActivityがアクティビティを起動します。
effectiveUidがAID_SYSTEMであるタスクにいるため、canEmbedActivity()はすべてを許可します。finish()自身を呼び出しているため、タスク内のルートアクティビティであり、createTaskFragment()を使用できます。