Arm アーキテクチャ v8.3-A: ポインタ認証コードの解説
1. はじめに
Armv8.3-A から導入されたセキュリティ (ポインタ改ざん検知) に関する ISA 拡張機能の一つである Pointer Authentication Code (PAC) / ポインタ認証コード
について解説しています。もしかしたらPAC-RET
見たいなキーワードの方がよく目にするかも知れません。
iPhone / macOS の場合、Apple A12 Bionic 以降では利用可能な技術であり、すでにいくつかのアプリでは利用されている様子です。基本はユーザプログラム側で利用される技術ではありますが、Linux カーネルでも一部関数で利用されている様子です(当然、Arm版Windowsでも同様)。
2. PAC が必要な背景と PAC で防ぐことが出来る脆弱性
PAC を利用することで、ROP (Return-Oriented Programming) 攻撃や JOP (Jump-Oriented Programming) 攻撃の対策 (改ざんの検知) が可能です。
ROP 攻撃
ROP 攻撃は、スタックに格納された関数のリターンアドレスを改ざんすることによって既存のコードを不正に実行させようとするプログラミング技法です。通常スタックにはリターン先の番地(番地)が格納されていますが、悪意があるハッカーはバッファーオーバーフローを起こすようなプログラムを対象に攻撃し、スタックの目的のポインタを書き換えて乗っ取ってしまうというものです。
これについて詳しく説明し始めると非常に長くなるため、詳細については以下を参照して下さい。
3. PAC の仕組み
前述のような悪意があるプログラムは自分が実行したいコードが配置してある場所(ポインタ)にプログラムカウンタ (PC) を持っていこうとします。これを防ぐために、リターンもしくはジャンプする前にそのポインタが不正なものでないかどうかを確認するようにした(関数ポインタとリターンアドレスの改ざん防止)のが PAC です。
PAC は、専用命令が用意されており、その命令を利用してリターン/ジャンプ先として利用する関数ポインタなどのポインタに認証コードを埋め込み、ジャンプ命令の実行直前にそのポインタが改竄されていないかどうか認証を行います。目的のポインタを認証し、問題がなければそのまま処理を続け、仮に不正なポインタであれば例外が発生し、プログラムは異常終了します。
このように認証処理(とポインタの認証コードの生成処理)を行うことで、セキュリティ対策を行います。では、詳細を説明していきます。
サンプルコード
関数の最初に LR (リンクレジスタ) に認証コードを埋め込み、処理が終わって関数コール元のアドレスに戻る前に LR の内容を認証するという単純な流れです。
foo:
paciasp # LR の上位ビットに認証コードを追加(埋め込み)
stp fp, lr, [sp, #-FRAME_SIZE]! # LR をスタックに退避
mov fp, sp
... # < function body >
ldp fp, lr, [sp], #FRAME_SIZE # LR の内容を復元
autiasp # LR を認証し、元のポインタに戻す
ret lr
ポインタのどこに認証コードを埋め込むのか?
認証コード(値)は、通常利用されていない(実際には40ビットまでしかアドレスとして利用しない) 64 ビット仮想アドレスの上位ビットに格納されます。
Note: https://events.static.linuxfound.org/sites/events/files/slides/slides_23.pdf から抜粋
認証コード生成アルゴリズム
アルゴリズムは CPU の実装依存みたいですが、基本的には認証コードの生成には、対象のポインタ、後述の秘密鍵(キー)、そしてコンテキスト (例えばスタックポインタなど、命令で指定可能だが、認証する場合には同じコンテキストを指定する必要がある)から生成する QARMA アルゴリズムが推奨されているみたいです。
Note: https://events.static.linuxfound.org/sites/events/files/slides/slides_23.pdf から抜粋
秘密鍵(キー)
用途に合わせて 5 種類の秘密鍵(以降、単にキーと呼ぶ)を持っています。各キーは 128 ビットで、カーネルモード以上 (EL1/EL2/LE3) でアクセス可能な専用レジスタに保存されています。
A と B の 2 種類があるのは、ユーザプログラムとカーネル用で使い分けるなどのためでしょうか?
- APIAKey, APIBKey (通常のユースケース (関数ポインタ) 用のキー)
- APDAKey, APDBKey (データ参照用のキー?)
- APGAKey (汎用キー。PACGA命令で利用される)
PAC 命令
基本的なものとして、ポインタに認証コードを埋め込むpac*<a|b>*
、そして認証コードを検証してポインタを元に戻す aut*<a|b>*
系命令が用意されており、各命令は基本的に2種類のキー(A/B) 毎に用意されています(PACGA命令だけはKey-Aのみ)。
また、CPUがこれらの命令をサポートしていないターゲットでも動かす事を考慮して、NOP
命令扱いになる後方互換性が考慮されています。例えば pacia
だと paciasp
。
認証コードを埋め込む命令
- PACIA, PACIZA, PACIA1716, PACIASP, PACIAZ
- PACIB, PACIZB, PACIB1716, PACIBSP, PACIBZ
- PACDA, PACDZA
- PACDB, PACDZB
- PACGA
認証する命令
- AUT*
認証コードを除去して生ポインタを得る命令
- XPAC*
プログラムのパフォーマンス影響
実際ユーザプログラム(+カーネル)で PAC を有効にした場合のパフォーマンスへの影響はなんとも言えませんが、認証処理の命令 (認証コードを生成するのに1クロックサイクルで終わるとは思わない...) を入れることにはなるため、少なからず影響があると思います。
4. コンパイラオプション
PAC は GCC および Clang/LLVM の両方でサポートされています。-mbranch-protection
オプション (デフォルトは PAC 無効) で PAC を利用可能です。
-mbranch-protection=bti+pac-ret
パラメータ | 用途 |
---|---|
none | PAC無効 |
bti |
BTI (Branch Target Identification) を有効にする (本記事では説明していない内容) |
pac-ret | PACを有効にする。ただし、リンクレジスタにアドレスが保存されないような場合(例えばリーフ関数/末尾再帰)には保護されない |
leaf | リーフ関数でもPACを有効にする |
b-key | PAC-retで利用されるキーにKey-Bを利用する (Key-Aの代わりに) |
詳細は以下を参照して下さい。
Discussion