SELinux の検証

Android では OEM に対し、SELinux の実装を徹底してテストすることを強くおすすめしています。SELinux を実装するメーカーは、まずデバイスのテストプールに新しいポリシーを適用する必要があります。

新しいポリシーを適用したら、getenforce コマンドを実行して、SELinux が正しいモードで実行されていることを確認します。

このコマンドにより、Enforcing モードか Permissive モードのどちらかのグローバル SELinux モードが出力されます。各ドメインの SELinux モードを確認するには、対応するファイルを調べるか、/platform/system/sepolicy/tools/ の適切なフラグ(-p)を使用して最新バージョンの sepolicy-analyze を実行する必要があります。

拒否の読み取り

エラーを確認します。エラーはイベントログとして dmesglogcat にルーティングされ、デバイス上でローカルで確認できます。メーカーは、dmesg への SELinux の出力をデバイス上で調べ、公開リリース前に permissive モードで設定を調整して、最終的に enforcing モードに切り替える必要があります。SELinux ログメッセージには avc: が含まれているため、grep で簡単に見つけることができます。cat /proc/kmsg を実行して、残っている拒否ログをキャプチャしたり、cat /sys/fs/pstore/console-ramoops を実行して前回のブートから拒否ログをキャプチャしたりできます。

ログの消去を回避するため、SELinux エラー メッセージは起動完了後にレート制限されます。関連するメッセージをすべて表示するには、adb shell auditctl -r 0 を実行してレート制限を無効にします。

メーカーはこの出力を確認することで、システム ユーザーまたはシステム コンポーネントによる SELinux ポリシー違反を簡単に特定できます。特定した不正な動作を修正するには、ソフトウェアと SELinux ポリシーのいずれかまたは両方に変更を加えます。

具体的には、このログメッセージには、どのプロセスが enforcing モードで違反となるか、そしてその違反の理由が示されます。次に例を示します。

avc: denied  { connectto } for  pid=2671 comm="ping" path="/dev/socket/dnsproxyd"
scontext=u:r:shell:s0 tcontext=u:r:netd:s0 tclass=unix_stream_socket

この出力は次のように解釈できます。

  • 上記の { connectto } は、実行されたアクションを表します。末尾の tclassunix_stream_socket)と合わせて確認することで、何に対して何が実行されたかが大まかにわかります。このケースでは、何かが unix ストリーム ソケットへの接続を試みていました。
  • scontext (u:r:shell:s0) は、どのコンテキストでこのアクションが開始されたかを示します。このケースでは、シェルとして実行された何かがアクションを実行しています。
  • tcontext (u:r:netd:s0) は、アクションのターゲットのコンテキストを示します。このケースでは、netd がオーナーである unix_stream_socket がコンテキストになっています。
  • 上の行の comm="ping" を確認すると、拒否が生成された時点で何が実行されていたかについて、さらにヒントが得られます。このケースでは、有益なヒントが示されています。

別の例を示します。

adb shell su root dmesg | grep 'avc: '

出力:

<5> type=1400 audit: avc:  denied  { read write } for  pid=177
comm="rmt_storage" name="mem" dev="tmpfs" ino=6004 scontext=u:r:rmt:s0
tcontext=u:object_r:kmem_device:s0 tclass=chr_file

この拒否で重要となる要素を以下に示します。

  • アクション - 試行されたアクションがかっこ内に示されています(read write または setenforce)。
  • 実行元 - scontext(ソース コンテキスト)のエントリは、実行元(このケースでは rmt_storage デーモン)を表します。
  • オブジェクト - tcontext(ターゲット コンテキスト)のエントリは、アクションの対象となったオブジェクトを表します。このケースでは kmem です。
  • 結果 - tclass(ターゲット クラス)のエントリは、アクションの対象となったオブジェクトのタイプを示します。このケースでは chr_file(キャラクター デバイス)です。

ユーザー スタックとカーネル スタックをダンプする

イベントログに含まれる情報だけでは拒否の原因を特定できない場合もあります。多くの場合、カーネルやユーザー空間を含めた呼び出しチェーンを収集すると、拒否の原因を詳しく把握できます。

最近のカーネルでは、avc:selinux_audited というトレースポイントが定義されています。Android simpleperf を使用して、このトレースポイントを有効にし、呼び出しチェーンをキャプチャします。

サポートされている構成

  • Linux カーネル 5.10 以降、具体的には Android 共通カーネル ブランチの mainlineandroid12-5.10 がサポートされています。android12-5.4 ブランチもサポートされています。simpleperf を使用すると、デバイスでトレースポイントが定義されているかどうかを判断できます(adb root && adb shell simpleperf list | grep avc:selinux_audited)。他のカーネル バージョンの場合は、dd8166230969bc の commit を選択できます。
  • デバッグしているイベントを再現できる必要があります。起動時のイベントは simpleperf ではサポートされていませんが、サービスを再起動することでイベントをトリガーできる場合があります。

呼び出しチェーンをキャプチャする

まず、simpleperf record を使用してイベントを記録します。

adb shell -t "cd /data/local/tmp && su root simpleperf record -a -g -e avc:selinux_audited"

その後、拒否の原因となったイベントをトリガーし、記録を停止します。この例では、Ctrl-c を使用してサンプルをキャプチャしています。

^Csimpleperf I cmd_record.cpp:751] Samples recorded: 1. Samples lost: 0.

最後に、simpleperf report を使用すると、キャプチャしたスタック トレースを検査できます。次に例を示します。

adb shell -t "cd /data/local/tmp && su root simpleperf report -g --full-callgraph"
[...]
Children  Self     Command  Pid   Tid   Shared Object                                   Symbol
100.00%   0.00%    dmesg    3318  3318  /apex/com.android.runtime/lib64/bionic/libc.so  __libc_init
       |
       -- __libc_init
          |
           -- main
              toybox_main
              toy_exec_which
              dmesg_main
              klogctl
              entry_SYSCALL_64_after_hwframe
              do_syscall_64
              __x64_sys_syslog
              do_syslog
              selinux_syslog
              slow_avc_audit
              common_lsm_audit
              avc_audit_post_callback
              avc_audit_post_callback

この呼び出しチェーンは、カーネルとユーザー空間の呼び出しチェーンを統合したものです。ユーザー空間から拒否が発生するカーネルに至るまで、トレースを開始することでコードフローを理解しやすくなります。simpleperf の詳細については、Simpleperf の実行可能コマンド リファレンスをご覧ください。

permissive への切り替え

SELinux の適用は、userdebug ビルドまたは eng ビルドで adb を使用して無効にできます。それには、まず adb root を実行して ADB を root に切り替えます。次に、以下のコマンドを実行して SELinux の適用を無効にします。

adb shell setenforce 0

または、(デバイス起動時の初期に)カーネル コマンドラインで次のようにします。

androidboot.selinux=permissive
androidboot.selinux=enforcing

あるいは、Android 12 の bootconfig を使用して次のようにします。

androidboot.selinux=permissive
androidboot.selinux=enforcing

audit2allow の使用

audit2allow ツールは、dmesg の拒否を対応する SELinux ポリシー ステートメントに変換します。そのため、SELinux の開発が大幅に効率化されます。

これを使用するには、次のコマンドを実行します。

adb pull /sys/fs/selinux/policy
adb logcat -b events -d | audit2allow -p policy

その場合も、権限を追加する際にはその権限が過度なものではないか注意する必要があります。たとえば、先ほど示した rmt_storage 拒否を audit2allow にフィードすると、次のような SELinux ポリシー ステートメントが結果として示されます。

#============= shell ==============
allow shell kernel:security setenforce;
#============= rmt ==============
allow rmt kmem_device:chr_file { read write };

これでは、カーネルメモリに書き込む権限が rmt に付与され、大きなセキュリティ ホールが生まれてしまいます。多くの場合、audit2allow ステートメントは出発点にすぎません。これらのステートメントを導入した場合は、必要に応じてソースドメインとターゲットのラベルを変更し、適切なマクロを利用して、適切なポリシーを完成させます。調査した拒否によっては、ポリシーの変更ではなく、不適切なアプリの変更が必要になることもあります。