この記事は Artem Gonchar と Travis Biehnによって執筆されました。
モバイル端末では、信頼できる実行環境として、ARM TrustZoneの利用が拡大しています。モバイル決済、端末の完全性、モバイル端末管理、その他の問題を解決するために信頼性のある実行環境が求められます。 CyRC の活動の一環で、TEE(Trusted Execution Environments)の適用されたセキュリティについて調査をしてみました。そして、OnePlus 7 Pro Advanced の利用者の登録した指紋データが晒されるバグを発見しました。(vulnerability advisory for CVE-2020-7958を参照)。
そこには、既知の、そして未知のソフトウェアコンポーネントが組み合わせられており、TrustZone上で指紋の識別を行なっていた。我々の解析レポートを読んでいただければ、各コンポーネントがどのように相互作用しているのか、どのようにそれらを攻撃できるのかが明らかになるでしょう。
Android 6の登場以前は、指紋認証の実装の標準は規定されていませんでした。セキュリティ研究者たちは、指紋画像を抽出し、デバイスが認証チェックのために保持していた登録済みの参照指紋を改ざんする方法をすばやく見つけていたものです。たとえば有名なBlack Hat での発表では、当時、どのように指紋認証の仕組みが安全でないまま主要なAndroidスマートフォンに実装されているかを説明しています。これらの実装は、個人の生体認証データを危険にさらし、結果として、何百万ものユーザーに害を及ぼす可能性がありました。
Android 6の登場に際し、Google社はCDD(Compatibility Definition Document)で指紋認証の要件について踏み込み、定義しました。それはその後増え続け、広範な 生体認証センサーのグループが存在しています。
標準化されたやり方は、Trusted Execution Environment (TEE)内での生体センサーの利用に伴うすべての機微な操作の実行のためのやり方です。この仕様によれば、指紋センサーとの通信はCPUのSecure mode内でのみ行われなければなりません。つまり、センサーはAndroid OS (Rich Execution Environment(REE)) からアクセスできないことを保証するということです。
Android 端末の”root”になったとしたら、ブートローダーのロックを解除し、フラッシュメモリー上のいくつかのパーティションを変更することができます。これらの変更は、実行時の自身の権限”root”に昇格させることも可能にします。しかし、“ブートローダー・アンロック”は厳密には正しい表現ではありません。Android端末のブートについてはARM trusted firmware modelを参照してください。 いくつかのブート段階があることがわかります。このフレーズ自体は、どのシステムのどのブートローダーの段階にも適用できますが、Androidにおける“ブートローダーのアンロック” は、ブート段階のほぼ最後のステージを示すのが普通で、端末の”Secure World”でAndroid OS のイメージをブート前に検証する段階のことです。もちろんTEEはこのステージの前にブートされています。
ARM CPUのセキュリティの境界は、異なるセキュリティの状態で、異なる例外レベル(ELs)の実行が可能となっています。これは、カーネル空間とユーザ空間を分離するのと同じ仕組みを拡張したものです。TEEはAndroid OSカーネルよりも特権的なレベルとセキュリティ状態で実行されます。TEEのメモリーはREEからはアクセスできず、すべての通信はAndroid OS カーネルからのSecure Monitor Callsを通じて行われます。
Android OSを起動する後段のブートローダーのロックを解除しても、TEEを起動する前段のブートローダーのロックが解除されるわけではありません。 これで、”ルート化”によってAndroid OSが侵害されたとしても、TEEは安全なままです。実際に、コンテンツ保護システム (DRM)、モバイル決済システム、生体認証、ハードウェア支援による暗号APIなどで使用されています。
Google社は、CDDの中で、どのようなデータをTEEで保護し、生体認証によってTEEで行うアクションを規定しています。このことで、たとえ”ルート化”された端末であったとしても、TEEは指紋の安全を維持するのです。
もちろん、実装には常にバグがつきものです。いままでも、メモリーを破壊してしまうバグを悪用してTEEで任意のコードを実行することに成功しているわけです。同様に、ロックされた端末上のカーネルで任意のコードを実行する方法を見つけることができるでしょう。もちろん、ベンダーがそういったバグを修正するためのセキュリティアップデートを速やかに提供するでしょう。ベンダーにとってこれがいかに速く、重要なのかを理解するためには、Google Project Zeroのwriteup for the Bad Binder vulnerabilityを参照ください。
より多くのコードを持つということは、より多くのバグを持つことになります。TEEの利点の一つは、特権コードで実行されるコードが少ないということです。そう、Android OSのカーネルレベルで。このことはアタックサーフェスを減らし、セキュアにするのが少しだけ楽になるということでもあります。
OnePlus 7 Proは Qualcomm Secure Execution Environment (QSEE)を使用しています。これは端末を動かすチップセットの製造元の半導体メーカーであるQualcomm社のTEE OSです。この端末にはいくつもの指紋認証用のコンポーネントがREEとTEEで動作します。ここには、抽象化された複数のレイヤーが積み重なっています。
非ルート化端末では、Android framework によって公開されている生体認証APIにしかアクセスができません。
しかし、ルート化された端末では、REE内のどこからでも攻撃することが可能になります。
さらに、libgf_ud_halライブラリに存在する機能を呼び出すことができます。リバースエンジニアリングによってREEの指紋処理のライブラリーにはAndroid framework APIをサポートするのに必要な機能以外の多くの機能が含まれていることを発見しました。基本的なやり方として、多くの場合、この機能を削除しても問題はなく、これらのライブラリに含まれるプロダクションコード以外のコードはオーバーヘッドなどの問題をもたらすだけで、なんら利点はありません。
また、共有メモリバッファを確保して入力をTEEに渡すrawトランスポートを呼び出すこともできます。このような機能は通常libQSEECom APIやTEE固有の似たようなものが相当します。
前者の場合、libgf_ud_halライブラリ内の関数は起動したいコマンド固有の構造体をバッファに書き込み、rawトランスポートを呼び出します。後者の場合、TEE内でtrustletが理解できるよう、バッファにどのような構造体を書き込むのかを知らなければなりません。
そして、Android OS カーネル内の長い呼び出しチェーンを通してバッファが横断し、EL3のSecure Monitor、S-EL1の TEE OS、そして最終的にS-EL0のtrustletに到達します。
いったん、バッファがtrustletに到達すると、ルーティング関数(コマンドハンドラー)で参照されます。この関数はバッファの最初の数バイトを検査し、trustletのどの関数を実行する必要があるかを調べます。そして、ここからが非常に興味深い点です。
理論上は、製品ファームウェアイメージを実行している製品デバイスで必要な関数は「fingerprint HAL」が使用する機能セットのみで、指紋の登録、指紋の検証、指紋の登録解除、他に少しばかりの関数です。多くの場合、trustletの本番ビルドにはデバッグや、工場での試験用ロジックが残されたままです。
本番ビルドにデバッグコードが残っていると、攻撃者にとって少しばかり有利になります。trustletのデバッグコードが残っていることで、セキュアなクライアントコンピューティングの保障ができなくなることになりかねないのです。
多くのREEアプリケーションは、ライブラリーを介してtrustletと通信します。しばしば、デバッグ呼び出しがREEライブラリから取り除かれていることがありますが、trustletには関数が残ったままなのです。つまり、ここではtrustletをリバースエンジニアすることで、関数を利用するためのバッファを構築することなのです。
こう考えてみましょう:webアプリから許可された関数へのリンクを取り除いても、それはAPIエンドポイントを取り去ったことにはなりません。つまり、いまだに攻撃者は関数にアクセスできるわけです。もしAPIエンドポイントを取り除いたとしたら、それはただのリンクではないなら、その関数にアクセスすることはできなくなります。これはwebアプリの APIエンドポイントに対するアクセス管理策です。 この場合も、 libgf_ud_hal のコードはwebアプリのフロントエンドで、trustlet内のコードはAPIエンドポイントに相当します。この場合、 libQSEEComAPI は通信層ということになります。(例;HTTPクライアントライブラリ)。
trustletの開発者は、本番ビルドのtrustletがREEに重要な関数を晒さないようにしなければならないのです。
例えば、次のようなデバッグコードをtrustletの中に発見しました。これはセンサーからのデータをダンプし、REEに渡すことができます。
__int64 __fastcall gf_sz_dump_capture_data(unsigned __int8 **buf_out, _DWORD *buf_sz_out)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
ret = 1004;
if ( !buf_out || !buf_sz_out )
goto LABEL_6;
buf = cpl_malloc(43020LL, 0LL);
if ( !buf )
{
// … error handling
return ret;
}
gf_sz_fill_bmpdata(buf, 1);
*buf_out = buf;
ret = 0;
*buf_sz_out = 2634620;
return ret;
}
この関数は、構造体へのポインターを受け取り、バッファを生成し、バッファをセンサーからの生データで満たすために別の関数を呼び出します。そして、このバッファへのポインターを受け取った構造体に書き込みます。
本番ビルドは、これらのデバッグコードを除去して利用できなくする必要があります。コマンドハンドラーを #ifdef でラッピングすることで、本番ビルドのtrustletから問題のデバッグコードを取り除くことができるわけです。
あるいは、OnePlus社が7 Proのルーティングコンポーネントの中で行なったように対処するか。こちらについては、このあと解説していきます。
OnePlus 7 Proに発見した問題は、工場でのテストコマンドのハンドラーが残っていたというものでした。
REE内の指紋認証サブシステムにはlibgf_ud_hal.sos共有オブジェクトが含まれています。この共有オブジェクトは以下のいずれかの方法で手に入れることができます。
下図は、コンポーネントの階層のどこに合致するかを示しています。
libgf_ud_hal.so共有オブジェクトには、メソッドgoodix::SZCustomizedProductTest::factoryCaptureImage()が含まれていました そして、リバースエンジニアリングによって次の擬似コード*を復元しました。
*注記:擬似コードの復元は、非常に多くのリバースエンジニアリングによって、意味のないv1、v2、v3…vnを経ることで実現しました。投稿しているコードはリバースエンジニアによって得られたものです。
__int64 __fastcall goodix::SZCustomizedProductTest::factoryCaptureImage(goodix::SZCustomizedProductTest *this, GF_SZ_TEST_RAWDATA *raw_data_out, unsigned __int16 ae_expo_start_time, unsigned __int8 uchar, unsigned __int16 ushort2)
{
goodix::command::FactoryCaptureImage *Command; // x0 MAPDST
unsigned int rv; // w21
const char *errfmt; // x0
if ( raw_data_out )
{
Command = malloc(0x1502Cu);
if ( Command )
{
memset(Command, 0, sizeof(goodix::command::FactoryCaptureImage));
Command->ae_expo_start_time = ae_expo_start_time;
Command->field_1C = uchar;
Command->field_1E = ushort2;
Command->Parent.target = 1003;
Command->Parent.cmd_id = 17;
rv = goodix::HalBase::invokeCommand(&this->HalBase, &Command->Parent, 0x1502C);
if ( !rv )
{
memcpy(raw_data_out, &Command->captureImageResponseBuffer,
sizeof(GF_SZ_TEST_RAWDATA));
free(Command);
return rv;
}
free(Command);
}
else
{
__android_log_print(6, "[GF_HAL][SZCustomizedProductTest]", "[%s] out of memory,
cmd", "factoryCaptureImage");
rv = 1001;
}
}
else
{
__android_log_print(6, "[GF_HAL][SZCustomizedProductTest]", "[%s] param is erro",
"factoryCaptureImage");
rv = 1004;
}
errfmt = gf_strerror(rv);
__android_log_print( 6, "[GF_HAL][SZCustomizedProductTest]", "[%s] exit. err=%s, errno=%d", "factoryCaptureImage", errfmt, rv);
return rv;
}
この関数はバッファ(raw_data_out)を受け取り、指紋センサーからのイメージデータを受け取ります。このバッファは、少なくとも86024バイトのサイズで、出力画像に呼応します。— GF_SZ_TEST_RAWDATA構造体のサイズはtrustlet側のリバースエンジニアによって得られました。
露出時間(ae_expo_start_time)などの他の引数がこの関数に渡され、TEEコマンドに含まれます。
TEEコマンドは構造体で、コマンドごとに異なりますが、重要なのは以下のルーティング情報が含まれているということです。
注記:この点において、TEEについてまだ何も説明していません。この段階ではTEEから何層かの抽象化層を隔てているからです。しかし、指紋センサーのコンセプトについては説明しました。このライブラリはセンサーベンダーが提供するSDKの一部である可能性があり、OnePlusなどの端末ベンダーは、使用しているハードウェアセンサーに固有の最小限の変更と再構成を適用します。
いったん、コマンドの構造体が準備できたら、goodix::HalBase::invokeCommand()関数はQualcommの提供するTEEコミュニケーションライブラリlibQSEEComAPI.soと通信するために使用されます。
このライブラリはTEEベンダーによって異なります。この時点で、以前に作成された指紋センサーコマンドは、基本的に、いくつかのデータを含む抽象的なバッファーにすぎません。 このライブラリは、TEE通信用のカーネルドライバーと通信し、このバッファーを正しいtrustletに渡します。もし、コマンド構造体を準備するためのgoodix::factoryCaptureImageが無い場合でも、trustletをリバースしてlibQSEEComAPI.soを直接呼び出し、コマンドをtrustletに配信することで適切なものを構築できます。
この場合、呼び出し可能な高水準関数を持つことになります。なぜかというと、いまだにすべてのパラメーターとそれらを呼び出した際の結果を完全に理解していないからです。そして、簡単な呼び出しから始めてみました。
goodix::SZCustomizedProductTest::factoryCaptureImage(SZCustomizedProductTest _object, image_buffer , ae_expo_start_time, 1, 0);
この呼び出しには、SZCustomizedProductTestインスタンスおよびae_expo_start_timeへのリファレンスが必要です。
後者はgoodix::SZCustomizedProductTest::getSensorInfoを呼び出し、ライブラリにある実際の呼び出しの動作を模倣します。
SZCustomizedProductTestのインスタンスの生成は、HalContextインスタンスへの正しいリファレンスが必要です。
幸運なことに、goodix::HalBase::invokeCommand()は指紋センサーの通常の操作中に頻繁に呼び出されており、 goodix::HalBaseのインスタンスにはHalContextの正しいポインターが含まれていたのでこれを利用することができました。我々の「エクスプロイト」のロジックは次のようなものになりました。
上記全てはルート化した端末で実行できました。また、Fridaのようなツールを使うことで、動的なtrustletの悪用を実践できます。
ここでは、簡単なエクスプロイトがなぜ機能したのか、そしてどのように修正したのかについて解説します。
OnePlus 7 Proの指紋認証trustletには、次のようにルーティング関数があることを、エラーハンドラーの吐き出すシンボルを見ることで特定できました。
app_main.tz_app_cmd_handler -> gf_modules.gf_modules_cmd_entry_point
ここで「モジュール」のリストが反復され、モジュールごとに、モジュールのメタデータエントリに、処理されるコマンドの「Target ID」と一致する「Target ID」があるかどうかを確認するチェックが実行されます。存在する場合、モジュールのエントリポイントアドレスがメタデータ構造から読み取られて呼び出されるため、コマンドの処理を続行できます。
__int64 __fastcall gf_modules_cmd_entry_point(cmd_routing_info *cmd_routing_info, unsigned int buffer_size)
{
// ...
loop_idx_stopgap = -1LL;
for ( module_table_entry = g_module_table;
(*module_table_entry)->target_id != cmd_routing_info->target_id; ++module_table_entry )
{
if ( ++loop_idx_stopgap > 2 )
return 0;
}
errno = ((*module_table_entry)->entry)(cmd_routing_info, buffer_size);
*cmd_routing_info[1].gap0 = errno;
strerr = gf_strerror(errno);
gf_dump_log(3LL, "[gf_modules][%s] err = %d, errno = %s", "cmd_entry_point", errno, strerr);
// ...
return errno;
}
また、trustlet内に、g_dump_moduleを呼び出すシンボルを見つけました。
それでもいいのです。イメージをダンプするコンポーネントはコマンドをルーティングするロジックには含まれていないということなのです。
しかし、別の興味深いモジュールが見つかりました、g_product_test_moduleです。テストコードに都合の良い機能が含まれていることは滅多にあることではありません。バイナリーの中の文字列を検索し、何かイメージをダンプするであろう、sz_factory_test_capture_imageという名前の関数を探し出したのです。
それでもいいのです。イメージをダンプするコンポーネントはコマンドをルーティングするロジックには含まれていないということなのです。
しかし、別の興味深いモジュールが見つかりました、g_product_test_moduleです。テストコードに都合の良い機能が含まれていることは滅多にあることではありません。バイナリーの中の文字列を検索し、何かイメージをダンプするであろう、sz_factory_test_capture_imageという名前の関数を探し出したのです。.
クロスリファレンスのグラフから分かるのは、これが製品のテスト用のモジュールと関連があるということです。
この時点で、テーブル内のエントリは次の構造で記述されていることが分かりました。
struct module_fnc_table
{
__int64 (__fastcall *start)();
__int64 (__fastcall *stop)();
__int64 (__fastcall *entry)();
int target_id;
int null;
};
“start”や“stop”関数は、ブートストラップや破棄のために使用されます。“entry”関数は、渡されたコマンドを処理するために使用されます。
製品テストモジュールの“entry”機能を詳しくみていけば、それがg_product_test_ctxというグローバルの名前付きシンボルを利用して関数テーブルを解決するラッパー関数だということが分かります。
__int64 __fastcall gf_product_test_cmd_entry(__int64 a1, unsigned int a2)
{
return gf_product_test_cmd_entry(g_product_test_ctx->ptest_fnc_table, a1, a2);
}
次に、解決された関数テーブルを使用して正しい関数を呼び出す実際のコマンドを呼び出します。
__int64 __fastcall gf_product_test_cmd_entry(ptest_fnc_table_0 *a1, __int64 a2, unsigned int a3)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
if ( a1 && a2 && a3 > 27 )
{
if ( a1->costomer_get_mt_info )
{
rv = a1->costomer_get_mt_info(a1, a2, a3);
if ( rv )
{
v4 = gf_strerror(rv);
gf_dump_log(1LL, "[gf_product_test][%s] exit. err=%s, errno=%d", "gf_product_test_cmd_entry", v4, rv);
この時点で、モジュールの”start”関数への参照があることは非常に役に立ちました。 続く構造などの複雑な構造をリバースエンジニアリングする場合、初期化ルーチンは非常に重要です。これは、通常、多数の構造体メンバーを使用し、構造体メンバーの目的を理解するのに役立つ有用なエラーメッセージが表示されるためです。
製品テストモジュールの“start”ルーチンによって、REEからイメージ・キャプチャ・テストを起動できることを理解しました。
“start”ルーチンはgf_product_test_createを呼び出します。関数テーブルの為にいくつかのメモリーをリバースし、最初のエントリーを埋め、gf_sz_product_test_ctorを呼び出して作業を続けます。私たちは、可能性のあるランタイムエラーをログに記録するためにバイナリに存在するエラーメッセージ文字列に残された有用な名前を使用して、構造体の関数ポインターに名前を付け続けました。
ptest_fnc_table_2 *__fastcall gf_sz_product_test_ctor(ptest_fnc_table_1 *a1)
{
ptest_fnc_table_2 *result; // x0
a1->gf_sz_product_test_cmd_entry = gf_sz_product_test_cmd_entry;
a1->sz_product_test_data_preview = sub_16AE8;
a1->sz_product_test_fusion_preview = sub_16EE4;
a1->sz_product_test_get_version = sub_17420;
a1->sz_product_test_find_sensor = sub_16CB0;
a1->sz_product_test_set_capture_param = sub_17574;
a1->sz_product_test_get_config = sub_17350;
a1->sz_product_test_capture_base = sub_15094;
a1->sz_product_test_set_enroll_template_count = sub_176C8;
a1->get_bmp_data = sz_product_test_get_bmp_data;
a1->sz_product_test_update_cfg = sub_17900;
a1->sz_product_test_update_fw = sub_17A20;
a1->sz_product_test_untrust_enroll_enable = sub_177E0;
result = gf_sz_factory_test_ctor();
a1->ptest_sz_fnc_table = result;
return result;
}
このプロセスを続行し、“entry”関数に繰り返し戻り、そこから間接的に呼び出される関数をたどることにより、最終的に、ここに到りました。
__int64 __fastcall gf_sz_factory_test_cmd_entry(ptest_fnc_table_1 *a1, __int64 a2, unsigned int a3)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v5 = 0;
v6 = a1->ptest_sz_fnc_table;
switch ( *(a2 + 8) )
{
case 16:
v7 = (v6->sz_factory_test_get_chip_info)(a2, a3);
goto LABEL_32;
case 17:
v8 = v6->sz_factory_test_capture_image;
goto LABEL_22;
case 18:
v7 = (v6->sz_factory_test_set_auto_exposure_time)(a2, a3);
goto LABEL_32;
// ...
最初のモジュールエントリから、ターゲットIDが0x03EB、つまり1003であることがわかります。必要な関数のコマンドIDは17です。 sz_factory_test_capture_image は、REEがライブラリ関数を持ち合わせていない場合に、結果データがどのように構造化されているかを理解します。
__int64 __fastcall sz_factory_test_capture_image(goodix::command::FactoryCaptureImage *x0_0, unsigned int a2)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v15 = 0LL;
v20 = 0LL;
v21 = 0LL;
v18 = 0LL;
v19 = 0LL;
v16 = 0LL;
v17 = 0LL;
if ( x0_0 && a2 > 0x1502B )
{
rv = sz_set_exposure_time(x0_0->ae_expo_start_time);
if ( !rv )
{
gf_get_timestamp(&v16);
LODWORD(v21) = 4;
rv = gf_sensor_capture_image(g_sensor_ctx.fnc_table, &v17, 40u);
if ( !rv )
{
ResponseBuffer = &x0_0->captureImageResponseBuffer;
if ( !cpl_memcpy(ResponseBuffer->data, image_data, 2 * dword_39F47C * dword_39F478) )
{
ResponseBuffer->image_data_size = image_data_size;
gf_get_timestamp(&v15);
rv = 0;
ResponseBuffer->profiling_seconds = (v15 - v16) / 1000uLL;
return rv;
}
gf_dump_log(1LL, "[gf_shenzhen_factory_test][%s] memory copy error", "sz_factory_test_capture_image");
修正は非常に簡単でした。ID 17のコマンドハンドラーを本番ビルドのtrustletから取り除いたのです。これは、REEからgoodix::SZCustomizedProductTest::factoryCaptureImageの呼び出しに失敗するということです。
このように、TEEを利用するような重要な機能は、複数のベンダーやハードウェア側とソフトウェア側の両方の広範な知識ベースが前提条件の、極めて複雑なプロセスなのです。複数のSDKを統合するのは簡単ではありません。SDKにはREEとTEE両方のコンポーネントが含まれ、このようなニッチなバグを見つけるのは容易ではありません。OnePlusが適切な担当者を迅速に特定してこの問題を解決できるように伝えたいと思いました。調査の観点では、この方向に目を向けるように促してくれたJohn Kozyrakisに感謝します。
この記事が、trustletの内部動作、さまざまなコンポーネントが連携してTEE(Trusted Execution Environment)を提供する方法、およびそれらを攻撃する方法について、みなさんの理解の一助になることを願っています。