
ブランチターゲットバッファと投機的実行を悪用し、サイドチャネルを介してランダム化されたアドレスを漏洩させることで、Intel CPU上のASLRをバイパスする概念実証エクスプロイト。
ASLR(Address Space Layout Randomization)は、メモリ破壊攻撃の悪用を困難にするために使われる緩和策です。例えば、バッファオーバーフローの脆弱性があるシナリオでは、Return Oriented Programming(ROP)攻撃を行おうとする攻撃者は、チェーン内のガジェットのアドレスを知る必要があります。悪用対象のバイナリのコードセグメントがランダム化されていれば、攻撃者が正しいアドレスを選ぶことははるかに困難になり、悪用は実行不可能になります。
次の例は、アドレスがどのようにランダム化されるかを示しています:
#include <stdio.h>
void DoNothing();
void (*codePtr)() = DoNothing;
void DoNothing(){}
int main(int argc,char **argv){
printf("Destination %p\n",codePtr);
DoNothing();
}
実行のたびに値がランダム化されます:
Destination 0x563714256149
Destination 0x556d8e2f1149
Destination 0x5618c8bdd149
Destination 0x55ee623b0149
最後の12ビット149は常に同じですが、関数の位置はおおよそ0x550000000000から0x570000000000の間のどこにでもなり得ます。つまり、29ビットがランダム化され、0x200 0000 0000(2.2 TB)のアドレス空間を占有することになります。
各命令の処理は困難なタスクです。単一の命令の処理には以下のような段階があります:
CPUの命令スループットを向上させるために、命令の各タスクはプロセッサの特定のユニットによって実行されます。すべてのユニットが並行して動作することで、CPUはより高いクロック速度で実行できます。これがパイプラインの考え方です。
命令A、B、Cがサイクル1〜5で実行される様子。例えばサイクル3では、フェッチ、デコード、実行の各ユニットが同時にアクティブになります
しかし、命令は互いに完全に独立しているわけではありません。例えば、次のようなシーケンスを考えます:
A. add ax,[bx]
B. jz $+1
C. mov dl,[rsi]
D. nop
この場合、命令Aは最良でもサイクル3の実行段階でしか完了しません。しかし、フェッチユニットは次にメモリからフェッチする命令を決定する必要があり、命令C(mov dl,[rsi])をスキップすべきかどうかを判断しなければなりません。
このシナリオでは、CPUは命令Aの完了を待つという選択肢があります。例えばadd演算が0を返す場合、命令Aの完了は3クロックサイクル目にしか起こらないため、その後でメモリ上の正しい命令をフェッチすることになります:
これはパイプラインに遅延をもたらします。CPUは命令の実行が完了するまで待たなければならないからです。この例では遅延は1クロックサイクルですが、add ax,[bx]命令はメモリ操作を必要とし、前述のように完了までに数百サイクルかかることがあり、プロセッサに大きな性能コストをもたらします。
より高速な選択肢は、正しい実行経路を「推測」することです。CPUは分岐が取られるかどうかを投機的に実行できます。その時点以降、実行は投機された経路から続行され、A命令の完了後に経路が正しいと証明された場合にのみ値がコミットされます。経路が間違っていると証明された場合は、結果は破棄され、投機実行前の状態に戻されます。
実行経路を元に戻す際の唯一の問題は、CPUのマイクロアーキテクチャ状態は元に戻せないことです。そのため、CPUが命令C(mov dl,[rsi])の実行を投機すると、rsiが指すデータがキャッシュに移動されます。この影響は、後でサイドチャネル攻撃を用いて測定できます。

2ビット条件分岐予測子。 https://en.wikipedia.org/wiki/Branch_predictor
予測しなければならないのは条件分岐命令だけではなく、間接分岐も同様です。CPUはcall [rdi]のような命令の分岐先を推測するメカニズムを持たなければなりません。
Spectre v2の脆弱性は、間接分岐予測子を悪用して他のプロセスで一時的実行(transient execution)を達成できることを示しています:
https://spectreattack.com/spectre.pdf から引用
コンテキストAのコール命令を、コンテキストBの別のコールと同じ仮想アドレスに配置することで、攻撃者はCPUを訓練して、コンテキストB上で攻撃者が選んだ位置のコードを実行させることができます。これはReturn Oriented Programming(ROP)に似たコード再利用攻撃です。
標的となる被害者には、サイドチャネル攻撃を使って秘密情報を漏洩できる「Spectreガジェット」と呼ばれるコードが存在しなければなりません。Spectre攻撃を成功させるには、攻撃者もSpectreガジェットの位置を知っている必要があります。したがって、ユーザー対ユーザーの攻撃では、被害者をASLRで保護することがこの種の攻撃に対する緩和策として使われていました。しかし、Jump Over ASLRのようなマイクロアーキテクチャ攻撃を用いてASLRを抽出する手法もあります。ただし、この手法は直接分岐予測子の衝突に依存してASLRをバイパスするため、漏洩できるビット数に制限があります。
この予測子の内部メカニズムを以下に示します:

https://spectreattack.com/spectre.pdf から引用
これらのコンポーネントには以下のものがあります:
古典的なSpectre v2攻撃の構成は次のようになります:

getenv + secret[0]*4096を実行するSpectreガジェットは、libcを共有メモリとして使用することで位置0のsecretの値を漏洩させることができます。Exec ASLR(別名Reverse Branch Target Buffer Poisoning)は、Spectre-BTI脆弱性を使用してASLRをバイパスする新しい手法です。この攻撃は、古典的なSpectre-BTIシナリオでは攻撃者だけがBTBを汚染できるのではなく、被害者も攻撃者プロセス内で分岐予測ミスを引き起こし、攻撃者をASLRで保護されたアドレスへの投機的ジャンプに導くことができるという事実を悪用します。そして、実行中のアドレスを漏洩させるサイドチャネルを使用することで、攻撃者は完全な分岐先アドレスを取得し、標的プロセスのASLRをバイパスできます。
Exec ASLR攻撃の構成は次のようになります:

この種の攻撃では、Spectreガジェットを見つける必要も、サイドチャネルのための共有メモリを持つ必要もありません。唯一の要件は悪用する間接分岐が存在することであり、Spectre v2に必要なすべてのガジェットは攻撃者プロセス内に配置されます。この攻撃の唯一の新しい要件は、被害者の分岐先アドレスを自分のプロセスにマップできることです。そのため、この攻撃は例えばKASLRに対しては機能しません。
これはブルートフォース攻撃でもありません。1回の試行で複数のアドレスを同時にテストできますが、メモリが同時に保持できる「リークガジェット」の数には制限があります。これにより、Jump Over ASLRと比較して攻撃にかかる時間が大幅に短縮され、毎秒約100アドレスから毎秒数千億アドレスまで向上します。
リークガジェットはprobeArrayを使用して、ガジェット自身がどこで実行されたかを攻撃者に通知します。probeArrayと漏洩させたいRIPのインデックスを引数として受け取り、probeArray[0]とprobeArray[4096]のどちらにアクセスすべきかを決定する分岐のないプログラミング手法を実行します。
lea rax,[rip - 7] ;load current address
shr rax,cl ;selects the bit using cl arg
and rax,1
shl rax,12 ;loads probearray
mov dl,[rsi+rax] ;or probearray+4096
前述のとおり、BHBはBTBエントリを選択するために使用されます。BTBの衝突を見つけてこの脆弱性を悪用するには、攻撃者は最後に実行されたN個(<skylakeの場合は29個)の分岐を知っている必要があります。私たちのテストでは、間接呼び出しの前にBHBの状態を既知の値に設定するためにforループを使用しました。以下は脆弱な被害者コードの例です:
#include <stdio.h>
void DoNothing();
void (*codePtr)() = DoNothing;
void DoNothing(){
return;
}
int main(){
printf("Destination = %p\n",codePtr);
while(1){
for(int i=0;i<200;i++){}
codePtr();
}
}
両方のコンテキストでBHBの状態が同じになるようにするため、攻撃者は被害者のforループとcallに対応するバイトをシェルコードの形式でコピーします。シェルコードは、BHBの状態を同じにするために必要な20LSBのアライメントに一致し得るすべての256ポジションにコピーされます。
Victim Code
0x5594c566a152 <+28>: mov eax,0xc8
0x5594c566a157 <+33>: dec eax
0x5594c566a159 <+35>: jne 0x1157 <main+33>
0x5594c566a15b <+37>: nop
0x5594c566a15c <+38>: nop
0x5594c566a15d <+39>: nop
0x5594c566a15e <+40>: lea rdi,[rip+0x2ecb]
0x5594c566a165 <+47>: call QWORD PTR [rdi]
--> Executes to
0x5594c5669135: ret
Attacker Code
… //eax=200 rsi=probeArray, cl=0
0x6a157 dec eax
0x6a159 jne 0x455555500157
…
0x6a165 jmp QWORD PTR [rdi]
--> Misspredicts to
0x5594c566a135: lea rax,[rip - 7]
0x5594c566a13c: shr rax,cl
0x5594c566a13f: and rax,1
0x5594c566a133: shl rax,12
0x5594c566a137: mov dl,[rsi+rax]
すべての可能な位置に同時にガジェットを配置しようとすると、問題があります。私たちのテストでは、ASLRは分岐先を0x550000000000から0x570000000000の間のどこかに配置します。つまり、マップすべき仮想アドレス空間は2.2TB、すなわち5億3700万個のリークガジェットに相当します。しかし、システムのRAMはわずか8GBです。
COWを使用して2TBのRAMをマップすることも可能ですが、このアプローチではあまり成功しませんでした。おそらく、Translation Lookaside Buffer(TLB)に過度のプレッシャーがかかり、未変換アドレスへの投機が遅すぎるのだと思います。
テストでは、1GBのメモリ内ページを作成し、リークガジェットで満たしました。その後、remapシステムコールを使用してページを2TBの範囲全体にシフトしました。
観察されたもう1つの問題は、投機されたアドレスは実際に実行されたことがないため、おそらくTLBに存在しないという事実でした。しかし、Intelのマニュアルには次のように記載されています:
したがって、「投機誘発」ページウォークの可能性を高めるために、正しいアドレスが解決されるのを可能な限り困難にしました。これは分岐先アドレスにポインタチェーンを使用することで実現されます。アイデアは、CPUのフロントエンドが分岐先を投機し、リオーダーユニットがポインタチェーンの読み取りを完了する前にリークガジェットを実行するというものです。
Improved Caller - Frontend fetched instructions
mov rcx,%1 ;mask arg for gadget
lea rsi,[%2] ;probe array ptr arg
lea rdx,[%0]
mov rdx,[rdx] ;pointer chain
mov rdx,[rdx]
mov rdx,[rdx]
...
mov rdx,[rdx]
mov rdx,[rdx]
check [rdx] ;mispredicts to gadget
lea rax,[rip - 7];speculative execution
shr rax,cl
and rax,1
shl rax,12
mov dl,[rsi+rax]
Improved Caller - Reorder unit scheduled instructions
mov rcx,%1 ;mask arg for gadget
lea rsi,[%2] ;probe array ptr arg
lea rdx,[%0]
mov rdx,[rdx] ;pointer chain
mov rdx,[rdx]
; <pagewalk occurs some where here>
... ;Out of order + speculation
lea rax,[rip - 7]
shr rax,cl
and rax,1
shl rax,12
mov dl,[rsi+rax]
...
mov rdx,[rdx]
mov rdx,[rdx]
check [rdx] ;the execution path reverted
このアウト・オブ・オーダー+投機実行の手法により、攻撃における望ましい予測ミス率が改善されました。
Spectre V2攻撃を実行するには、攻撃者は被害者と同じコアでコードを実行し、同じ分岐予測ユニット(BPU)を共有する必要があります。<= SkylakeのCPUでは、ハイパースレッディングを使用してコアの同一配置(コア・コレジデンシー)を達成できることを確認しました。
IcelakeおよびCascade Lake CPUは、スレッド間でBPUを分離するSingle Thread Indirect Branch Predictor(STIBP)と呼ばれる緩和策を実装しています。そのため、被害者と攻撃者を同じスレッドで実行し、usleepを使用して被害者プロセスと攻撃者プロセスを交互に切り替える必要があります。これらの世代のキャッシュ(おそらくTLBも)が非常に優れており、同時にはるかに多くのガジェットをキャッシュできるため、攻撃者はこのCPUでは遅くなります。
この手法は、Google Cloudで利用可能なすべてのIntel CPU(N1世代とN2世代の両方)でテストされました。
Cascade LakeとIce Lakeではエクスプロイトにいくつかの違いがありますが、すべてのテストで10秒未満に99%を超える精度でアドレスを復元できました。
緩和策は、ユーザー対ユーザー攻撃を防ぐためのSpectre V2と同じです。
Indirect Branch Prediction Barrier(IBPB)はBPUのフラッシュを可能にし、コンテキストスイッチ時に使用できます。Linuxでは、IBPBはprctlシステムコールをPR_SET_SPECULATION_CTRLオプション付きで使用することで利用できます。
Windowsでの同等の緩和策はまったくわかりません。教えてください。
https://cos.ufrj.br/uploadfile/publicacao/3061.pdf
https://www.youtube.com/watch?v=Qj4z-KvnkxU
https://docs.google.com/presentation/d/10t-oo-c26x9ydx1_FYgmhy204rxfmQ92eboPlCnA2y4/edit?usp=sharing
https://googleprojectzero.blogspot.com/2018/01/reading-privileged-memory-with-side.html https://eprint.iacr.org/2013/448.pdf https://spectreattack.com/spectre.pdf https://www.cs.ucr.edu/~nael/pubs/micro16.pdf http://download.vusec.net/papers/bhi-spectre-bhb_sec22.pdf https://www.kernel.org/doc/html/latest/userspace-api/spec_ctrl.html Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3. Santa Clara, USA: Intel Corporation, 2016, iSBN 325384-060US
| 操作 \ クロックサイクル | 1 | 2 | 3 | 4 | 5 |
|---|
| フェッチ | A | B | C | ||
| デコード | A | B | C | ||
| 実行 | A | B | C |
| 操作 \ クロックサイクル | 1 | 2 | 3 | 4 | 5 | 6 |
|---|
| フェッチ | A | B | D | |||
| デコード | A | B | D | |||
| 実行 | A | B | D |
| 操作 \ クロックサイクル | 1 | 2 | 3 | 4 | 5 | 6 |
|---|
| フェッチ | A | B | (S) C | |||
| デコード | A | B | (S) C | |||
| 実行 | A | B | (S) C |