FFRIエンジニアブログ

株式会社FFRIセキュリティのエンジニアが執筆する技術者向けブログです

セキュリティキャンプの報告と Arm 版 Windows における Arm Pointer Authentication Code (PAC)

はじめに

基礎技術研究室、リサーチエンジニアの中川です。

今年も FFRI セキュリティから押場・桑原・私の 3 人がセキュリティ・キャンプに講師として参加しました。 昨年は新型コロナウイルスの影響もあり、時期が 10 月にずれ、完全オンラインでの開催となりました。 今年については 8 月開催に戻りましたが、形態は変わらずオンラインのままです。

テーマとしては昨年と同様、脆弱性防御機構を扱いました。

https://www.ipa.go.jp/jinzai/camp/2021/zenkoku2021_program_list.html#list_s-z6

最近の OS には多くの脆弱性防御機構が備わっています。 例えば Windows では ASLRDEP などが、最近では eXtreme Flow Guard (XFG) などが導入されています。

また、最近の興味深い傾向として CPU レベルの脆弱性防御機構が導入されている点が挙げられます。 例えば、return address の改ざんを検知する Shadow Stack やポインタの完全性を担保する PAC などです。 こうした CPU レベルの脆弱性防御機構は、低いパフォーマンスオーバーヘッドと脆弱性防御機構としての強力さの両立を可能とすることから、近年注目を集めています。 実用化例もすでにあり、近年の Intel/AMD の CPU では Shadow Stack が実装され、Arm でも Arm PAC がすでに実装され実際に利用されています。

弊社では脆弱性防御機構を自社開発していることもあり、こうした脆弱性防御機構の近年の動向については継続的にリサーチを続けています。 調査した結果は、ホワイトペーパーなどとしてもまとまっておりますので、技術的詳細に興味のある方はこちらもご参照ください。

講義では過去のソフトウェアレベル・CPU レベルの脆弱性防御機構を概観した後で、受講者で防御機構を自作するところまでを行ってもらいました。 期間としては短かったのですが、非常に良い成果が出ました。興味のある方はぜひとも以下のスライドをご参照ください。

今回の記事では、キャンプで扱った脆弱性防御機構に関連したちょっとした話題について紹介します。 最近解析して発見した Arm 版 Windows における Arm PAC の利用事例についてです。

先日公開したホワイトペーパーに、Arm 版 Windows 10 に今後 Arm PAC が導入される可能性が高いということを書きました。

この予想は現実となり、今年の 5 月に配信された Insider Preview 版 Windows 10 から Arm PAC が導入されました。 これは Windows 11 にも引き継がれています。

本記事では Arm 版 Windows における利用事例について、独自に行なったリバースエンジニアリング結果を踏まえながら説明します。

Arm PAC

Arm PAC については優れた記事がありますので (文献[1][2])、簡単に説明するにとどめます。

Arm PAC は Armv8.3 にて導入されたポインタの改ざんを検知するための ISA 拡張です。 ポインタ (return address や関数ポインタなど) の使われていない上位ビットに認証コードを埋め込み、この認証コードの検証を通じてポインタの完全性を担保します。

図 1 PAC の埋め込みに使われるポインタの上位ビット (文献[1]の図より転載)

命令としては PAC を埋め込む pac* 系の命令と PAC 付きのポインタを認証する aut* 系の命令の 2 種類が追加されています。 (ここで「系」と書いているのは、Arm PAC で利用可能なキーが A key・B key と複数あり、それぞれで命令が用意されているためです。)

簡単な擬似コードを通じて説明します。Arm PAC による return address の改ざんを検知する例です。

<myFunc>:
    pacibsp   # B key と return address の値を入力とし、コンテキストとして stack pointer を指定して PAC を生成し、その値を return address の上位ビットに埋め込み
    <push lr> # stp 命令などで return address を stack に退避

    ...

    <pop lr>  # ldp 命令などで return address の値を lr レジスタに復帰
    autibsp   # B key と return address の値を入力とし、コンテキストとして stack pointer を指定して認証コードを検証 (失敗した場合、不正なポインタが lr に代入)
    ret

myFunc という関数の先頭で pacibsp が実行され return address に PAC が埋め込まれます。 この時、生成にあたっては return address の他、キーとなる 128 bit のシステムレジスタ、コンテキストとなる汎用レジスタが入力として指定されます (図 2)。

図 2 認証コードを埋め込むまでの流れ (文献[2]の図より転載)

関数から return する前には PAC 付きの return address の値が autibsp 命令によって検証されます。 この時、pacibsp 命令を実行した時と同じキーとコンテキストの値を指定する必要があります。

仮に、関数実行の過程で stack に存在する return address の値が改ざんされた場合、autibsp 命令の検証に失敗します。 失敗した場合には不正なポインタが生成され、ret 命令以降でプログラムがクラッシュし、実行が中断されます。 このように、return address の完全性を PAC で担保することで ROP などの攻撃を防ぎます。

Arm PAC は M1 Mac に使われている M1 Chip、iPhone に使われている A12 以降の Chip に実装されています。 macOS や iOS のシステムバイナリは Arm PAC による防御策が既に有効になっています。

Arm 版 Windows における Arm PAC

次に、Arm 版 Windows において Arm PAC がどのように利用されているのかを見ていきます。 以下では Arm 版 Windows 11 (version 22000.100) の結果を示しますが、Insider 版 Windows 10 においてもほぼ同じ結果が得られています。

システム DLL

早速 %SystemRoot%\System32\kernelbase.dll を見てみましょう (図 3)。 Arm PAC 命令の逆アセンブルをサポートしている逆アセンブラであれば何でも良いのですが、ここでは Ghidra を使います。 (確認した限り radare2 は未サポートで IDA Pro についてはサポート済みのようです。)

図 3 kernelbase.dll の CreateFileA 関数の逆アセンブル結果

すると、関数の先頭に PAC を埋め込む pacibsp 命令が、末尾に認証用の autibsp 命令が見られます。 確かに Arm 版 Windows で Arm PAC が有効になっていることがわかります。

ここで、macOS で Arm PAC が有効になっているバイナリと比較をすると興味深いことがわかります。 図 3 では関数の末尾が autibspret になっていますが、この部分は macOS のシステムバイナリだと retab になっていることがわかります (図 4)。 retabautibspret を 1 命令で行うために用意された命令で、命令数の削減のために用いられます。

図 4 macOS の echo バイナリの逆アセンブル結果

では、Windows でなぜ retab を使っていないのでしょうか。 これは Arm PAC をサポートしていない CPU での実行を可能とするためと考えられます。

Arm PAC には後方互換性を持つ命令がいくつかあり (autibsppacibsp など)、これらの命令は Arm PAC 未サポートの CPU では nop として扱われます。 一方、retab など一部の命令は後方互換性を持ちません (図 5 の Backwords-compatible の列)。

図 5 後方互換性を持つ命令と持たない命令の一覧 (文献[3]より抜粋)

そのため、Arm PAC 未サポートの CPU で実行した場合、未定義の命令として扱われ、プログラムがクラッシュしてしまいます。

確認する限り、現時点で Arm 版 Windows 搭載端末のほとんどが Arm PAC 未サポートの状態ですので、retab を使わないようにしていると考えられます。

一方で、M1 Mac に含まれるシステムバイナリの場合だと Arm PAC をサポートしない CPU で実行されることはありません。 そのため、命令数の削減のメリットがある retab を使うようにしていると考えられます。

この辺り、ハードウェアからソフトウェアを一貫して作ってユーザーに提供している Apple と、サードパーティベンダーと協力して提供している Microsoft の違いが見えますね。

カーネル

カーネルにも変更が加えられ、Arm PAC キーの初期化処理が追加されています。 また、一部の関数には pacibspautibsp が見られるなど、カーネル側でも Arm PAC が有効になっています。

キーの初期を行っていると思われる関数について簡単に見ていきます (図 6)。

図 6 nt!KiConfigPointerAuthKernelEntry 関数の逆アセンブル結果

コメントとして示していますが、汎用レジスタの内容を B キーのシステムレジスタ (APIBKeyHi_EL1APIBKeyLo_EL1) に書き込む命令が実行されていることがわかります。

エントリーポイント付近ではグローバル変数の値を、これらのシステムレジスタに代入する処理も見られます。

図 7 APIBKeyHi_EL1APIBKeyLO_EL1 の初期化処理の一部 (nt!entry から処理を抜粋)

具体的にキーとしてどういった値が代入されるのか、ブートごとに異なる値が設定されるのかは気になるところです。 しかし、残念ながら Arm PAC をサポートしていない端末だとキーの値として null が入るようになっており、どのような値で初期化されるのかは確認できませんでした。

キーがどのような値で初期化されるのかは Arm PAC をサポートした実機端末が入手可能となった後、再度調査する予定です。

Visual Studio で Arm PAC を有効にしてビルドする方法

Microsoft からは公になっていませんが、Visual Studio 2019 ではすでに Arm PAC を有効にするためのコンパイルフラグが導入されています。 Control Flow Guard (CFG) を有効にする /guard:cf を拡張する形で /guard:signret というコンパイルフラグが追加されています。 (現状では GUI 経由での選択ができないようになっており、隠しオプション扱いです。)

早速試してみましょう。 以下のサンプルプログラムを /guard:signret を有効にしてビルドします。

#include <Windows.h>
#include <stdio.h>
#include <string.h>

__declspec(noinline)
void func(const char* in) {
    char buffer[0x100];
    memcpy(buffer, in, 0x100);
    printf("%s\n", buffer);
}

int main() {
    func("Hello");
}

ビルド成果物である実行ファイルを Ghidra で開くと、関数の先頭と末尾に pacibspautibsp がそれぞれ追加されていることがわかります (図 8)。

図 8 main 関数の逆アセンブル結果

コンパイルフラグの名前からも想像が付きますが、現在では return address の改ざん検知でしか Arm PAC を利用できません。 仮想関数テーブルや関数ポインタの改ざんに対しては CFG やその後継である XFG で対策することを念頭に置いているのではと考えられます。

おわりに

今回はセキュリティ・キャンプの報告と Windows における Arm PAC の利用状況について調査した結果を紹介しました。

こうした CPU レベルの脆弱性防御機構は最近の Intel/AMD CPU でも導入されており、CET Shadow Stack や Indirect Branch Tracking (IBT) などがあります。

はじめにでも触れましたが、Microsoft はこれらの CPU レベルの脆弱性防御機構を積極的に利用しています。 例えば、Windows では Shadow Stack による return address の改ざん検知を行う機構がすでに導入されており、一部のアプリケーションではすでに有効になっています。 現在は、64 bit アプリケーションのみとなっていますが、Windows カーネルをリバースエンジニアリングしている限り、カーネル側でも今後有効になる動きも見られます。 また、32 bit アプリケーションでも今後有効になる可能性が高いと考えられます。

このように Windows では (Arm 版も含み) 非常に多くの脆弱性防御機構が導入され、今後もこの傾向が続いていくと考えられます。

しかし、過去の歴史を振り返ればこうした防御機構を攻略する技術が考案されてきました。 今回紹介した Arm PAC についても、iOS での話となりますが、バイパスした上でエクスプロイトを成功させた事例がすでに報告されています (文献[4])。 Windows における Arm PAC にもこうした欠陥がないか、今後さらなる研究が必要です。

参考文献

[1] Providing protection for complex software

[2] ARMv8.3 Pointer Authentication

[3] PAC it up: Towards Pointer Integrity using ARM Pointer Authentication

[4] Everything has Changed in iOS 14, but Jailbreak is Eternal