FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages

LLVMのバックエンドを作るための第一歩 (11. LLVMが独自ターゲットマシンを認識する仕組み)

f:id:msyksphinz:20190425001356p:plain
LLVM Compiler Infrastructure

MYRISCVXTargetMachineはターゲットマシンを定義するファイルである。

また、MYRISCVXTargetMachineを継承したクラスとして複数のターゲットマシンを作ることができる。 MIPSなどのバイエンディアンアーキテクチャでは、リトルエンディアンとビックエンディアンの両方が定義されるが、RISC-Vではリトルエンディアンしか存在しないので1種類しか不要だ。 ここでは32ビット版と64ビット版のターゲットマシンを作りたいので、MYRISCVX32TargetMachineMYRISCVX64TargetMachineを定義する。

f:id:msyksphinz:20190525161248p:plain
  • MYRISCVXTargetMachine.h
/// MYRISCVX32TargetMachine - MYRISCVX32 little endian target machine.
///
class MYRISCVX32TargetMachine : public MYRISCVXTargetMachine {
  virtual void anchor();
 public:
  MYRISCVX32TargetMachine(const Target &T, const Triple &TT, StringRef CPU,
                          StringRef FS, const TargetOptions &Options,
                          Optional<Reloc::Model> RM,
                          Optional<CodeModel::Model> CM,
                          CodeGenOpt::Level OL, bool JIT);
};

/// MYRISCVX64TargetMachine - MYRISCVX64 little endian target machine.
///
class MYRISCVX64TargetMachine : public MYRISCVXTargetMachine {
  virtual void anchor();
 public:
  MYRISCVX64TargetMachine(const Target &T, const Triple &TT, StringRef CPU,
                          StringRef FS, const TargetOptions &Options,
                          Optional<Reloc::Model> RM,
                          Optional<CodeModel::Model> CM,
                          CodeGenOpt::Level OL, bool JIT);
};

これらのターゲットマシンをLLVM側に認識させるために、LLVMInitializeMYRISCVXTarget()という関数を作る。この関数の名前ルールは決まっており、llcで名前が決まっている。

  • lib/Target/MYRISCVX/MYRISCVXTargetMachine.cpp
extern "C" void LLVMInitializeMYRISCVXTarget() {
  // Register the target.
  //- Little endian Target Machine

  RegisterTargetMachine<MYRISCVX32TargetMachine> X(getTheMYRISCVX32Target());
  RegisterTargetMachine<MYRISCVX64TargetMachine> Y(getTheMYRISCVX64Target());
}

LLVMInitialize(Target名)Target()という初期化用のメソッドはllvm::InitializeAllTargets()から呼ばれる。

  • include/llvm/Support/TargetSelect.h
/// InitializeAllTargets - The main program should call this function if it
/// wants access to all available target machines that LLVM is configured to
/// support, to make them available via the TargetRegistry.
///
/// It is legal for a client to make multiple calls to this function.
inline void InitializeAllTargets() {
  // FIXME: Remove this, clients should do it.
  InitializeAllTargetInfos();

#define LLVM_TARGET(TargetName) LLVMInitialize##TargetName##Target();
#include "llvm/Config/Targets.def"
}

まずは、InitializeAllTargetInfos()が呼ばれる。

namespace llvm {
  /// InitializeAllTargetInfos - The main program should call this function if
  /// it wants access to all available targets that LLVM is configured to
  /// support, to make them available via the TargetRegistry.
  ///
  /// It is legal for a client to make multiple calls to this function.
  inline void InitializeAllTargetInfos() {
#define LLVM_TARGET(TargetName) LLVMInitialize##TargetName##TargetInfo();
#include "llvm/Config/Targets.def"
  }

llvm/Config/Targets.defはCMakeにてビルド用のコンフィグレーションを作成したときに自動的に生成される。 LLVM_TARGETS_TO_BULIDおよびDLLVM_EXPERIMENTAL_TARGETS_TO_BUILDで指定したターゲットについて、初期化用の関数が呼ばれるという仕組みだ。

f:id:msyksphinz:20190525161329p:plain
CMakeのコンフィグレーションからターゲットアーキテクチャが生成される仕組み
  • build-myriscvx/include/llvm/Config/Targets.def
#ifndef LLVM_TARGET
#  error Please define the macro LLVM_TARGET(TargetName)
#endif

LLVM_TARGET(X86)
LLVM_TARGET(Mips)
LLVM_TARGET(MYRISCVX)  # これが展開されて`LLVMInitializeMYRISCVXTargetInfo()`となる。
LLVM_TARGET(RISCV)

#undef LLVM_TARGET

これによりMYRISCVXとしてはLLVMInitializeMYRISCVXTargetInfo()およびLLVMInitializeMYRISCVXTarget()が呼ばれる。

LLVMInitializeMYRISCVXTarget()を見てみるとRegisterTargetMachineにてターゲットをLLVMに登録する。 この際getTheMYRISCVX32Target()およびgetTheMYRISCVX64Target()という関数が呼ばれているが、 これはlib/Target/MYRISCVX/TargetInfo/MYRISCVXTargetInfo.cppで定義されている。getTheMYRISCX{32,64}Target()を一度呼び出すとそのインスタンスが生成され、以降は同じインスタンスが呼び出させる、という仕組みだ。

  • lib/Target/MYRISCVX/TargetInfo/MYRISCVXTargetInfo.cpp
extern "C" void LLVMInitializeMYRISCVXTarget() {
  // Register the target.
  //- Little endian Target Machine

  RegisterTargetMachine<MYRISCVX32TargetMachine> X(getTheMYRISCVX32Target());
  RegisterTargetMachine<MYRISCVX64TargetMachine> Y(getTheMYRISCVX64Target());
}
  • lib/Target/MYRISCVX/TargetInfo/MYRISCVXTargetInfo.cpp
namespace llvm {
Target &getTheMYRISCVX32Target() {
  static Target TheMYRISCVX32Target;
  return TheMYRISCVX32Target;
}

Target &getTheMYRISCVX64Target() {
  static Target TheMYRISCVX64Target;
  return TheMYRISCVX64Target;
}
}

一方でLLVMInitializeMYRISCVXTargetInfo()では以下を実行する。ターゲットアーキテクチャを登録する。

extern "C" void LLVMInitializeMYRISCVXTargetInfo() {
  RegisterTarget<Triple::myriscvx32> X(getTheMYRISCVX32Target(), "myriscvx32",
                                       "MYRISCVX (32-bit)", "MYRISCVX");
  RegisterTarget<Triple::myriscvx64> Y(getTheMYRISCVX64Target(), "myriscvx64",
                                       "MYRISCVX (64-bit)", "MYRISCVX");
}

LLVMのバックエンドを作るための第一歩 (10. サブターゲットを追加するためのSubtargetFeature)

f:id:msyksphinz:20190425001356p:plain
LLVM Compiler Infrastructure

前回に引き続きLLVMのバックエンドを作るために必要なファイルを読み解いていく。

MYRISCVXSubTarget.{h,cpp}

サブターゲットはMYRISCVXの中でもアーキテクチャのバリエーションを付けるために使用されるもので、その名の通りサブのターゲットだ。 llcにおいてアーキテクチャ-march=myriscvx32で指定するが、サブターゲットは-mcpu=で指定する。 例えば、同じMIPSアーキテクチャの中でも、ISAのバージョン違いや実装の違いによってさまざまなバリエーションがある。

  • lib/Target/Mips/Mips.td
  def FeatureNoABICalls  : SubtargetFeature<"noabicalls", "NoABICalls", "true",
                                  "Disable SVR4-style position-independent code">;
  def FeaturePTR64Bit    : SubtargetFeature<"ptr64", "IsPTR64bit", "true",
                                  "Pointers are 64-bit wide">;
  def FeatureGP64Bit     : SubtargetFeature<"gp64", "IsGP64bit", "true",
                                  "General Purpose Registers are 64-bit wide">;
  def FeatureFP64Bit     : SubtargetFeature<"fp64", "IsFP64bit", "true",
                                  "Support 64-bit FP registers">;
  ...
./bin/llc -march=mips -mcpu=help
...

Available features for this target:

  mips1                    - Mips I ISA Support [highly experimental].
  mips16                   - Mips16 mode.
  mips2                    - Mips II ISA Support [highly experimental].
  mips3                    - MIPS III ISA Support [highly experimental].
  mips32                   - Mips32 ISA Support.
...
  noabicalls               - Disable SVR4-style position-independent code.
...

このサブターゲットを定義するためのクラスMYRISCVXSubTargetMYRISCVX.tdから生成された自動生成クラスMYRISCVXGenSubtargetInfoから生成される。

RISC-Vの場合、アーキテクチャに大きなバリエーションがある訳ではないのだが、ISAのどのモジュールが使用できるのか、例えば乗除算命令は使用できるのか、Compressed命令は使用できるのか、浮動小数点命令は使用できるのかなどのバリエーションが作れそうだ。

  • lib/Target/MYRISCVX/MYRISCVXSubtarget.h
    class MYRISCVXSubtarget : public MYRISCVXGenSubtargetInfo {
  ...
       protected:
      bool is_enable_M = false;
      bool is_enable_A = false;
      bool is_enable_F = false;
      bool is_enable_D = false;
  ...
  • lib/Target/MYRISCVX/MYRISCVXSubtarget.cpp
  MYRISCVXSubtarget &
  MYRISCVXSubtarget::initializeSubtargetDependencies(StringRef CPU, StringRef FS,
                                                     const TargetMachine &TM) {
    if (TargetTriple.getArch() == Triple::myriscvx32) {
      if (CPU.empty() || CPU == "generic") {
        CPU = "cpu-rv32";
      }
    } else if (TargetTriple.getArch() == Triple::myriscvx64) {
      if (CPU.empty() || CPU == "generic") {
        CPU = "cpu-rv64";
      }
    } else {
      errs() << "!!!Error, TargetTriple.getArch() = " << TargetTriple.getArch()
             <<  "CPU = " << CPU << "\n";
      exit(0);
    }
  
    // Parse features string.
    ParseSubtargetFeatures(CPU, FS);
    // Initialize scheduling itinerary for the specified CPU.
    InstrItins = getInstrItineraryForCPU(CPU);
  
    return *this;
  }

このSubtargetFeatureではアーキテクチャのバリエーションを指定するので、今回は32ビットモードと64ビットモードのサブターゲットを追加してみる。 デフォルトでは32ビットで、オプションで64ビットに設定されるようにする。

  • lib/Target/MYRISCVX/MYRISCVX.td
//===----------------------------------------------------------------------===//
// MYRISCVX subtarget features and instruction predicates.
//===----------------------------------------------------------------------===//

def FeatureRV64 : SubtargetFeature<"64bit", "HasRV64", "true", "RV64 support">;

サブターゲットの記述には要素が4つ並んでいる。

  • Featureの名前
  • Featureの属性(実装時に追加される変数の名前)
  • Featureの値(属性に設定される値)
  • Featureの説明。

この記述を加えることによって、tdファイルから生成されるMYRISCVXGenSubtargetInfo.incにはSubtargetFeatureに基づく変数制御が追加される。

  • lib/Target/MYRISCVX/MYRISCVXGenSubtargetInfo.inc
// Sorted (by key) array of values for CPU features.
extern const llvm::SubtargetFeatureKV MYRISCVXFeatureKV[] = {
  { "64bit", "RV64 support", { MYRISCVX::FeatureRV64 }, { } },
};

// Sorted (by key) array of values for CPU subtype.
extern const llvm::SubtargetFeatureKV MYRISCVXSubTypeKV[] = {
  { "cpu-rv32", "Select the cpu-rv32 processor", { }, { } },
  { "cpu-rv64", "Select the cpu-rv64 processor", { MYRISCVX::FeatureRV64 }, { } },
};
...
// ParseSubtargetFeatures - Parses features string setting specified
// subtarget options.
void llvm::MYRISCVXSubtarget::ParseSubtargetFeatures(StringRef CPU, StringRef FS) {
  LLVM_DEBUG(dbgs() << "\nFeatures:" << FS);
  LLVM_DEBUG(dbgs() << "\nCPU:" << CPU << "\n\n");
  InitMCProcessorInfo(CPU, FS);
  const FeatureBitset& Bits = getFeatureBits();
  if (Bits[MYRISCVX::FeatureRV64]) HasRV64 = true;
}

したがって、MYRISCVXSubtargetクラスにはメンバ変数HasRV64を追加する必要がある。

  • lib/Target/MYRISCVX/MYRISCVXSubtarget.h
  class MYRISCVXSubtarget : public MYRISCVXGenSubtargetInfo {
...
        bool HasRV64 = false;

セキュリティプロセッサMorpheusの論文を読む。ランダム化のための"Churn"とは。

少し前のニュースで、ASPLOS19でRISC-Vをベースとした新しいセキュアプロセッサの発表が行われたというニュースを見た。

Morpheus: A Vulnerability-Tolerant Secure Architecture Based on Ensembles of Moving Target Defenses with Churn

dl.acm.org

セキュリティ界隈には全く詳しくないのだが、面白そうなので読んでみることにした。

結論から言うと「RISC-Vベース」というのは単なる釣りで、特にRISC-Vは関係ない。別に何のアーキテクチャでも良いのだが、サンプルとしてRISC-Vを使用した、というだけだった。

しかし、普段あまり関わりのない領域の論文を読むのは面白い。プロセッサのセキュリティ対策としてどのようなものがあるのか、勉強になった。

長いので、読むのが面倒な人向け。

  • 様々なランダム化機構を埋め込む。あと、「意味的におかしな」コードがあればそれの実行を行わない。
    • このため、「本当に意味的におかしな」プログラムがあればその性能は低下する。ランダムパタン等かな?
  • チャーンという定期的に再ランダム化させる機構により、攻撃者が時間をかけてシステムの攻撃目標を見つける(これをProbeと呼ぶ)のを防ぐ。
    • このあたり、引用文献が非常に多いので、ある程度既存な技術が混じっていることは間違いないと思う。それを上手く組み合わせ、チャーンを組み込むのがポイント。
  • RTLを実装したのかと思ったら違った。いろんなニュースでは、「マイクロアーキテクチャの発表」だとか、「チップのデモ」などと書いてあるが、論文にはその言及はなかった。
    • だいたい、ダイ写真やプロセスの情報も出てこない。
  • プログラムにセマンティクスを入れる必要があるため、コンパイラに手を入れる必要がある。
    • これはマイナスポイント。よっぽど特殊な、軍事向けなどの特殊なチップとしてなら使えそう。
  • 性能低下は、どの程度暗号化を行うかに依存する。

Morpheusは、システムの攻撃を防ぐための機能を搭載したプロセッサ。ここでは、大きく分けて以下の3つの機能を解説する。

  • Domain Tagging : セマンティックと命令を元にデータの意味をタグ付けする。
  • Pointer Displacement : 命令とデータの場所をランダムに分離し、攻撃を防ぐ。
  • Domain Encryption : Tag付けしたDomainを暗号化する。
  • Churn (チャーン、と読む) : 語源は「かくはん」。定期的にランダム値を切替、攻撃者のシステム解析を遅らせる。

1. Introduction

これまでにあるCPUによる悪意のあるコードを実行されることを防ぐ仕組みは以下の通り。

  • NX-bit : 特定のメモリ領域(に置かれたデータ)に付与する実行不可属性、またはその属性付与機能
  • ASLR : Address Space Layout Randomization

今回は、プロセッサにチャーンと呼ばれる機構を導入してセキュリティの強化を行う。 チャーン(churn)とは、システムが動作している最中に透過的に移動するターゲットプログラムの値を再ランダム化するメカニズムのこと。 基本的な考え方は、システム中にランダムな値を動的に組み込んで攻撃者がターゲットの場所を読み取りにくくするのだが、チャーンと呼ばれる定期的にランダム値を再ランダム化する機構により、よりターゲットを探索しにくくするという機構である。

Morpheusでは、セキュリティの機構として大きく分けて以下の2つを理解する必要がある。

  1. ポインタ Displacement : アドレス空間中のコードとデータをランダムかつ独立に配置する。
  2. ドメイン暗号化 : コード、コードポインタ、データポインタを強力な暗号化によりランダム化する。

上記の2つを組み合わせて、 - ensembles of moving target defenses(EMTDs) を導入する。これは、上記のセキュリティ機構を組み合わせて、未確定な要素をランダム化する機構。 - Morpheusアーキテクチャを導入する。RISC-Vベースのプロセッサであり、制御フロー攻撃をEMTDとチャーンを使って防御するアーキテクチャである。

Morpheusを使うと、504ビットのエントロピーと50msのチャーン期間によって平均1%の性能低下(最大7%)がSPEC06およびMiBenchベンチマークで観測された。 Morpheusにより制御フロー攻撃に対する強力な防御が可能であることを示した。これには、ROPおよびBack-Call-Site攻撃も含まれる。 これらの分析により、Morpheusアーキテクチャ潜在的な制御フロー攻撃に対して効率的な防御が行えることを示した。

2. 脆弱性の例と、それに対抗するセキュアアーキテクチャ

以下ののコードは、ROP(Return Oriented Programming)を使用した攻撃コードだ。 strcpyにより配列の境界を越えた場所にデータを書き込み、vulnerable()の戻り値を上書きすることでtarget()にジャンプできるようにする。

void target() {
  printf("You overflowed successfully, gg");
  exit(0);
}
void vulnerable(char* str1) {
  char buf[5];
  strcpy(buf, str1);
}
int main() {
  vulnerable("ffffffffffffffff\xf0\x03\x02\x01");
  printf("This only prints in normal control flow");
}

この時の特徴は、攻撃対象のコードはセマンティック的に意味を持たないコードで構成されている。 つまり、普通はデータフロー的に意味のあるフローで流れているはずが、突如としてデータフロー的に意味のない命令が挿入され、データが改ざんされる。これをとらえれば、攻撃を検知することができる、という訳だ。

例えば、上記のプログラムにおける未定義な部分は、

  • 境界を越えた配列アクセスbuf[]
  • 戻りアドレスの場所 : コードオブジェクトのアドレス

そこで、防御の方法としてMoving Target Defense(攻撃対象となる領域を移動させる)。つまり、未定義なセマンティクスに対してランダムな領域に移動を行う。これにより、攻撃者が攻撃の際に前提に置く動作を防ぐことができる。

図2. Moving Target Defenseにより、攻撃者がシステムの未定義の値を事前調査する必要が生じることを示している。通常であれば、「システムの事前調査(Probe)」→「攻撃の用意(Wepononize)」→「攻撃(Attack)」→「漏洩(Exploited)」となるところを、

  • EMTDsを導入 : 「システムの事前調査(Probe)」に非常に時間がかかる→「攻撃の用意(Wepononize)」→「攻撃(Attack)」→「漏洩(Exploited)」となり攻撃に時間がかかる。
  • EMTDs + チャーン : システムの事前調査をしてもチャーンにより定期的に再ランダム化されるため、いつまでたってもProbeが完了しない。
f:id:msyksphinz:20190521222402p:plain
本論文Figure 2.より抜粋。

例えば、上記のROPの攻撃であれば、

  • 攻撃者は、システム内でtarget()が存在するアドレスをスキャンする。
  • strcpy()を行うための所望のアドレスと文字列を算出し挿入する。

というステップが必要になる。これはASLRによりヒープ・スタックの場所をランダム化することができるが、ASLRでは一時的な障壁に過ぎない。 例えば、AnC攻撃は150秒で完全な48ビットx86_64仮想アドレスを検知することができる。 エントロピーが大きければ、システムの事前調査(Probe)に必要な時間が増加すると言う事を示している。 つまり、エントロピーを増大させることによりProbe時間を増大させ、攻撃が成功することを防ぐ、という防御手段が成り立つ。

3. 脅威モデル

本論文での脅威モデルはDoSおよびサイドチャネルアタックは対象としない。

4. Morpheus セキュアアーキテクチャ

f:id:msyksphinz:20190521222746p:plain
本論文Figure 3.より抜粋。

64ビットRISC-V命令セットをベースとする。防御対象としては、1. 命令コード、2. コードポインタ、3. データポインタとする。

MorpheusのMoving Target Defensesの機構には、大きく分けて以下の3つが存在する。

さらに、チャーンユニット(churn unit : 4.4章) をコアに追加する。4.1章では、攻撃検知器(attack detector)を搭載する。

イデアその1.ドメインタギング

CPUの動作を4つのドメインに分類する。

  • Code (C)
  • Code Pointer(CP)
  • Data Pointer(DP)
  • Other Data(D)

パイプラインはデータとともにタグを流す。最初のタグはコンパイラから生成する。このため、改造したLLVMコンパイラが必要になる。 つまり、パイプラインにデータとともにその意味(これは命令か?データか?ポインタか?)を流すわけである。 一方でマイクロアーキテクチャはタグを格納するためのストレージを持っている。

MorpheusはLLVMベースのコンパイラ拡張を使用し、実行時に正確なドメインを記録する。 コンパイラはCのソースファイルをコンパイルし、LLVM中間表現に変換し、バックエンドは各命令についてラベルを生成してメモリオブジェクトを初期化する。 この時、メモリオブジェクトの最初のタグを生成する。

  • 全てのアーキテクチャレジスタに2ビットのタグを接続する。
  • Morpheusは64ビットのalignedされたワードに対してタグを付加する。

そして命令が実行されるたびにタグを継承する。 例えば、ポインタへの算術演算命令はポインタを生成する。Attack Detectorにより、怪しいポインタの計算や型の異なる演算を検出し、チャーンのサイクル毎に例外を発生させる。 ただし、攻撃目的のプログラムでなくてもまれにこのような動作をすることがあり、その時に性能は低下する。

イデアその2. ポインタディスプレースメント防御

全ての制御フロー攻撃は、メモリオブジェクトがどこに配置されているのかという情報を必要とする。 これらの値がどこに配置されているのかをより不明確にするために、MorpheusはPointer displacementという機構を持っている。 これは、2つのランダムな分離したアドレス空間(DAS_CDAS_D)を、仮想アドレス空間VAS上に配置している。 図4に示すように、コードはDAS_C上の[tex:d{CODE}]および、DAS_D上の[tex:D{DATA}]に別々に配置される。

プログラムは、そのライフタイム中にコードを実行するとき、 [tex:d{CODE}] と [tex:d{DATA}] がランダムに決定されるのでそのアドレスを読み取られる可能性が低くなる?

f:id:msyksphinz:20190521222924p:plain
本論文 Figure 4. より抜粋。

イデアその3.ドメイン暗号化防御

Domain encryptionでは、メモリ中のコード・コードポインタ・データポインタを強力に暗号化する。 これらの要素は固有のドメイン鍵で強力に暗号化されれる。そして、コード・データ・ポインタのタグにより、どの鍵を使って暗号化されるのかが異なる。 私たちのMorpheusの実装では協力なブロック暗号化を導入した。これをQARMAと呼ぶ。

ドメイン暗号化に使用される鍵($K_C, K{CP}, K{DP}$)はマイクロアーキテクチャ上のレジスタに格納され、ソフトウェアから見ることはできない。 各プロセスは固有の鍵を持っている。最後に、カーネルが2種類の鍵を持っている。1つが特権メモリのための鍵で、もう一つはユーザ空間のための鍵である。

f:id:msyksphinz:20190521223145p:plain
本論文 Figure 5.より抜粋。

イデアその4. Moving Target Defensesのチャーン

Morpheusでは、上記で説明したドメイン暗号化とポインタディスプレースメントによる防御はCPUの実行時に再ランダム化される。 チャーンユニットはこの再ランダム化の役割を行う。 さらに、チャーンユニットはアドレス空間の安全なコンテキストスイッチングをサポートする。

チャーンの再生成が行われるときに、パイプラインはストールしフラッシュされる。

5. Morpheus アーキテクチャについて

まず、Gem5シミュレータを使ってMorpheusのプロトタイプを作成した。アーキテクチャはGem5 MinorCPU 4-stage インオーダコアとした。

Morpheusの暗号化レイヤはQARMA_7-64\sigma_1 : 3.25ns (9-cycle latency at 2.5GHz)。さらに、チャーン操作をモデル化するためにDRAMSim2を使用する。 チャーンユニットはGem5とDRAMSim2とのやり取りを行い、タグのスキャンとポインタとコードのアップテートを行う。

5.2 セキュリティ分析

ペネトレーションテストの結果 : RIPEとよばれる制御フロー攻撃のテストスイートを実行した。さらに、自作の攻撃スイートを作成してテストした。 この結果、Morpheusアーキテクチャはすべての攻撃を停止させることができた。

  • Morpheusの攻撃分析

Morpheusに実装した防御機構の効果を調査するために、以下の4種類でProbeに必要な時間を計測した。

  • \overline{EP} : Encryption無効、Pointer Displacement無効
  • E\overline{P} : Encryption有効、Pointer Displacement無効
  • \overline{E}P : Encryption無効、Pointer Displacement有効
  • EP : Encryption有効、Pointer Displacement有効

この時の結果が以下のグラフのようになる。\overline{EP}の時は一瞬でProbeが完了するが、EP になると完了するのに時間がかかるベンチが多い。そしてベンチの特性によって、効果のある防御手法が異なっている。

f:id:msyksphinz:20190521223533p:plain
本論文 Figure 7. より抜粋。
  • チャーン実装によるオーバヘッド

チャーンを定期的に実装することで攻撃者がシステム内部を解析することを防ぐが、このチャーンの定期実行時間を変化させてCPUの性能にどう影響するかを計測する。チャーンの期間が短いと、高い頻度でキャッシュのフラッシュやパイプラインのクリアが行われるためオーバヘッドが大きい。その結果が次のグラフ。

f:id:msyksphinz:20190521223627p:plain
本論文 Figure 8.より抜粋。

やはりチャーンの期間が短いと性能に対するオーバヘッドが大きい。

xSIG 2019にてRISC-Vとオープンハードウェアについてチュートリアル講演を行います

2019/05/27(月)~2019/05/28(火)に開催されるxSIGと呼ばれるワークショップにて、RISC-Vについてチュートリアル講演を行います。

xsig.hpcc.jp

xSIGというのは、「クロス・シグ」と読むらしく、「The 3rd cross-disciplinary Workshop on Computing Systems, Infrastructures, and Programming」つまり「コンピューティングシステム・インフラ・プログラミングの分野間交流ワークショップ」ということでしょうか。

昔SACSISという学会に少しだけお世話になったことがありますが、その後継のようですね。

チュートリアル講演ということで、依頼を受けてみたら90分もしゃべるものでした。 去年のEmbedded Technologyのテクニカルセッションでも90分しゃべりましたが、1時間の講義と考えるととてもつらいですね。。。 大学の先生が毎回の講義で90分もしゃべっているのを考えると本当にすごいと思います。

f:id:msyksphinz:20190518190158p:plain

ってかプログラムから基調講演・チュートリアルのページをみると、私の発表だけ異様な気がします。 他の方がすごい人なのに、一人だけ本名じゃないし、私の所だけふざけたプロフィール写真が載っているのは大変申し訳ない...

参加するためには情報処理学会のページから予約する必要があるようですが、なんと学生は無料です!これは参加するしかないね!!

という訳で、優秀な学生の皆さんや大学の先生方、お手柔らかにお願いします。

LLVMのバックエンドを作るための第一歩 (9. MCTargetDescとABIInfo)

f:id:msyksphinz:20190425001356p:plain
LLVM Compiler Infrastructure

前回に引き続きLLVMのバックエンドを作るために必要なファイルを読み解いていく。

MCTargetDesc/MYRISCVXMCTargetDesc.{h,cpp}

MYRISCVXMCTargeTDescでは明確なクラスを定義するわけではない。 その代わりに、これまでTarget Descriptionから生成したソースコードをincludeしていく。 MYRISCVXMCTargetDesc.hではenumなどのレジスタ番号を定義する。一方で、MYRISCVXMCTargetDesc.hは実装をincludeする。

f:id:msyksphinz:20190520003950p:plain
  • lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.h
// Defines symbolic names for MYRISCVX registers.  This defines a mapping from
// register name to register number.
#define GET_REGINFO_ENUM
#include "MYRISCVXGenRegisterInfo.inc"

// Defines symbolic names for the MYRISCVX instructions.
#define GET_INSTRINFO_ENUM
#include "MYRISCVXGenInstrInfo.inc"

#define GET_SUBTARGETINFO_ENUM
#include "MYRISCVXGenSubtargetInfo.inc"
  • lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.cpp
#define GET_INSTRINFO_MC_DESC
#include "MYRISCVXGenInstrInfo.inc"

#define GET_SUBTARGETINFO_MC_DESC
#include "MYRISCVXGenSubtargetInfo.inc"

#define GET_REGINFO_MC_DESC
#include "MYRISCVXGenRegisterInfo.inc"

MCTargetDesc/MYRISCVXABIInfo.{h,cpp}`

MYRISCVXABIInfoはABIに関するクラスだ。 MYRISCVXについては、1つのABIとしてLP32を定義した。 このABIを使用するための各種メソッドを定義していく。

  • lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXABIInfo.h
  class MYRISCVXABIInfo {
   public:
    enum class ABI { Unknown, LP32 };

   protected:
    ABI ThisABI;

   public:
    MYRISCVXABIInfo(ABI ThisABI) : ThisABI(ThisABI) {}

    static MYRISCVXABIInfo Unknown() { return MYRISCVXABIInfo(ABI::Unknown); }
    static MYRISCVXABIInfo LP32() { return MYRISCVXABIInfo(ABI::LP32); }
    static MYRISCVXABIInfo computeTargetABI();

    bool IsKnown() const { return ThisABI != ABI::Unknown; }
    bool IsLP32 () const { return ThisABI == ABI::LP32; }
    ABI GetEnumValue() const { return ThisABI; }

    /// The registers to use for byval arguments.
    ArrayRef<MCPhysReg> GetByValArgRegs() const;

    /// The registers to use for the variable argument list.
    ArrayRef<MCPhysReg> GetVarArgRegs() const;

    /// Obtain the size of the area allocated by the callee for arguments.
    /// CallingConv::FastCall affects the value for O32.
    unsigned GetCalleeAllocdArgSizeInBytes(CallingConv::ID CC) const;

    /// Ordering of ABI's
    /// MYRISCVXGenSubtargetInfo.inc will use this to resolve conflicts when given
    /// multiple ABI options.
    bool operator<(const MYRISCVXABIInfo Other) const {
      return ThisABI < Other.GetEnumValue();
    }

    unsigned GetStackPtr() const;
    unsigned GetFramePtr() const;
    unsigned GetNullPtr() const;

    unsigned GetEhDataReg(unsigned I) const;
    int EhDataRegSize() const;
  };

Intel CPUの脆弱性"ZombieLoad"の論文を読んでみる

Intelがまた出した。Meltdown / Spectre系のCPUの脆弱性として新たに発表された"ZombieLoad"である。

これもまた論文が発表されている。これらの論文は、最新のCPUの技術を勉強するにあたって非常に有用なものだ。ZombieLoadの論文を読んで、どのような脆弱性であるのかを読み解いていくことにした。

参考にしたのは、"ZombieLoad: Cross-Privilege-Boundary Data Sampling" という論文だ。以下からダウンロードできる。

arxiv.org

すべてを理解できたわけではないが、ZombieLoadの本質は、Intel CPUのハードウェア的なバグであるというところであると言える。アーキテクチャ上のバグではないため、AMD、Armでは現れない。Intelの実装が単純に問題だった、ということになりそうだ。

そもそもMeltdown / Spectreの分類はどのようになっているのかというところから復習する。

  • Spectre : 分岐予測が予測に失敗する事象を悪用する
  • Meltdown : CPU例外の後の一時的な実行を悪用する

ということを思い出しておく。

この中でZombieLoadはMeldwon型の脆弱性であると言える。ロードストアユニットのバッファに入っている脆弱性をついたものだ。

ちなみに、このZombieLoad単体では攻撃対象のアドレスを制御することはできない。しかし、ZembieLoadと別の攻撃方法を組み合わせることで、より詳細な攻撃が可能になるということだ。

現代のCPUは、命令はアウトオブオーダで実行される。しかし、プログラムの順番を維持しなければならないという制約から、パイプラインの途中ではアウトオブオーダで実行されるとしても最後はインオーダで完了するための機構が備わっている。ALUやLSUの内部はアウトオブオーダで実行されるのだが、特にメモリアクセスについては順番を守る必要がある。このため、LSUの内部はメモリアクセスの順番を守るための様々なバッファが搭載されている。これらの一つに、問題があるということなのだ。

ZombieLoadのキモは、あるロード命令が例外を発生した場合、そのロード命令より前のメモリ操作に属する古い値を投機的にロードしてしまう、ということだ。

f:id:msyksphinz:20190518235016p:plain

ZombieLoadについて説明するにあたり重要なCPU内ユニットとして、メモリオーダバッファ(Memory Order Buffer)がある。Intel CPUの場合、データロードを行うユニットは2つ、データストアを行うためのユニットが1つ搭載されている。メモリオーダバッファはロードバッファとストアバッファの内部を管理し、メモリの依存関係を解決してどのメモリアクセス命令を発行してよいのかを決定する。

ZombieLoadが攻撃対象とするロードバッファは、ロード命令が発行されると、その情報を格納されるバッファだ。

  1. ロード命令が発行されると、ロードバッファにキューイングされる。
  2. ロード操作がメモリに発行され、データが返されると、対応するロードエントリを開放する。
  3. ロード命令がリタイアする。

上記の手順でロード命令は処理される。

しかしここで問題なのは、Intel CPUの場合はロード命令が複雑な複雑なマイクロアーキテクチャの状態に入った場合、そのロード命令の後にロードデータを使う命令は、ロードバッファに格納されている古いデータを読むことがある、ということが観測されたのである。ロード命令が実行中断される前に、漏洩した値が後続の命令によって投機的に使用される可能性がある、ということである。

これだけだとなかなか攻撃手法として使うことができないが、さらに従来のサイドチャネル攻撃と組み合わせることでより強力な攻撃手法として成立させることができる。

  • いったい何が原因なのか?

今回のこのZombieLoadは、他のサイドチャネル攻撃と異なり、ハードウェア的な原因が明らかになって以来ということが問題となっている。つまり、アーキテクチャの問題ではなく、Intel CPUの実装的なバグではないかということである。

この問題の原因を特定するために、以下の実験を行っている。

  1. あるページをキャッシュ不可に設定し、キャッシュからそのページをフラッシュする。
  2. そのページに対するロード命令は、すべてのキャッシュを回避されメモリに直接アクセスされ、データは直接フィルバッファに転送される。
  3. キャッシュにデータのコピーがないことを確認するために、秘匿情報をキャッシュ可能なページに書き込む。
  4. 再度と秘匿データをロードすると、キャッシュ不可能な場所からデータをロードするが、ここでリークが発生する。

  5. 攻撃のシナリオ

攻撃するタイプとしては、2種類が考えられる。

  • Variant 1: Kernel Mapping : カーネルとユーザが同じ物理ページを異なるアドレスから参照する際、異なるプロセスであっても下位のアドレスがあっているため、Faultページロードが発生した際にデータのリークが発生する。
  • Variant 2: Microcode-Assisted Page-Table Walk : ページフォルトが発生した際の、マイクロコードによるページテーブルウォークが起きる。今回のシナリオあは、物理ページに対して仮想アドレス v v_2マッピングされている場合、一方のアドレス v_2 側でアクセスビットが無効になっていたとしても、ページテーブルウォークなどのマイクロコーデでの動作が有効になるとデータ漏洩が発生する。

今回ほZombieLoadでは、Meltdownと違い特定のアドレスを漏洩させることはできず、あくまで基本はLSUのフィルバッファに格納されているデータがそのまま読めている、ということだ。しかし様々な手法を使うことで、ケーススタディでは以下の秘匿データを習得することができている。

  • AES-NI鍵データ
  • SGXでガードされた鍵データ
  • VM間の交換チャネル
  • ウェブブラウザの動作のモニタリング

などの攻撃が可能だということらしい。

LLVMのバックエンドを作るための第一歩 (8. Calling Conventionとレジスタ定義)

f:id:msyksphinz:20190425001356p:plain
LLVM Compiler Infrastructure

MYRISCVXCallingConv.td

MYRISCVXのCalling Convention、つまり呼び出し規約について設定するTarget Descriptionファイルだ。MYRISCXVの呼び出し規約は、RISC-Vのものをそのまま使用しようと思う。

ここでは、関数呼び出しの時のCallee Savedレジスタの定義だけを入れておく。この記述は、MYRISCVXRegisterInfoの実装時に使用する。

  • lib/Target/MYRISCVX/MYRISCVXCallingConv.td
  def CSR_LP32 : CalleeSavedRegs<(add SP, FP, S1, (sequence "S%u", 2, 11))>;

MYRISCVXRegisterInfo.{h, cpp}

MYRISCVX.tdレジスタを定義した。MYRISCXVRegisterInfo.{h, cpp}は、定義したレジスタのテンプレートを使った実際の実装を記述することになる。 したがって、MYRISCVXRegisterInfoクラスは、MYRISCVXRegisterInfo.tdから自動生成されたMYRISCVXGenRegisterInfoクラスを継承している。

f:id:msyksphinz:20190518012001p:plain
MYRISCVXRegisterInfoクラスの継承関係。

lib/Target/MYRISCVX/MYRISCVXRegisterInfo.hで定義しなければならないメソッドは以下の通りだ。

  • getCalleeSavedRegs() : Callee Savedなレジスタのリストを返す。これはMYRISCVXRegisterInfo.tdで定義したのでC++に変換されている。

    • lib/Target/MYRISCVX/MYRISCVXCallingConv.td
  def CSR_LP32 : CalleeSavedRegs<(add SP, FP, S1, (sequence "S%u", 2, 11))>;
  const MCPhysReg *
  MYRISCVXRegisterInfo::getCalleeSavedRegs(const MachineFunction *MF) const {
    return CSR_LP32_SaveList;
  }
  • getCallPreservedMask() : 役割としてはgetCalleeSavedRegs()と似ている。レジジスタ割り当てに使用しない予約済みのレジスタを示する。ビットマスクとして表現される。
  const uint32_t *
  MYRISCVXRegisterInfo::getCallPreservedMask(const MachineFunction &MF,
                                             CallingConv::ID) const {
    return CSR_LP32_RegMask;
  }
  BitVector MYRISCVXRegisterInfo::
  getReservedRegs(const MachineFunction &MF) const {
    //@getReservedRegs body {
    static const uint16_t ReservedCPURegs[] = {
      MYRISCVX::ZERO, MYRISCVX::RA, MYRISCVX::FP, MYRISCVX::SP, MYRISCVX::GP, MYRISCVX::TP
    };
    BitVector Reserved(getNumRegs());
  
    // ResservedCPURRegsの配列に相当するビットに1を設定していく。
    for (unsigned I = 0; I < array_lengthof(ReservedCPURegs); ++I)
      Reserved.set(ReservedCPURegs[I]);
  
    return Reserved;
  }
  • requiresRegisterScavenging() : レジスタスカベンジャ(レジスタが足りない場合の空きレジスタの探索処理)を実行するかどうかを指定する。今回は単純にtrueを返す。

  • trackLivenessAfterRegAlloc() : レジスタ割り当て後のLiveness(レジスタの生存範囲)を記憶するかどうかを指定する。今回は単純にtrueを返す。

  • eliminateFrameIndex() : 抽象的なフレームインデックスを削除するためのコードを記述する。ここではまだ実装しない。

  void MYRISCVXRegisterInfo::
  eliminateFrameIndex(MachineBasicBlock::iterator II, int SPAdj,
                      unsigned FIOperandNum, RegScavenger *RS) const {
  }
  • getFrameRegister() : フレームレジスタ(フレームポインタ)を返す。
  unsigned MYRISCVXRegisterInfo::
  getFrameRegister(const MachineFunction &MF) const {
    return MYRISCVX::FP;
  }