BitVisorのEPTを使って任意のアドレスにトラップを仕掛けたいができない

これは[BitVisor Advent Calendar 2018](https://qiita.com/advent-calendar/2018/bitvisor)の18日目の記事です．
こんにちは，@morimolymolyです．

今日はBitVisorのEPTを使ってカーネル空間の関数をトラップしようとしてできない，というような記事を書きます．:thinking_face:

> どんな記事を書くといいの? って思う人は例えばこんな記事とかどうでしょう?
* BitVisor 初心者体験談
...
* ○○やりたいんだがよくわからん助けて

ということでわからないので助けてください！！(メーリスでやれ？)

# EPTとは
EPTとは物理メモリ仮想化のためのVT-xで用意されている機構です．ゲスト物理アドレス(GPA)をホスト物理アドレス(HPA)に変換する際に使用します．

EPTを有効にするにはいくつかの段階を踏む必要があります．

## MSR VPID AND EPT CAPABILITIES
まずはMSRの`IA32_VMX_EPT_VPID_CAP MSR`を読んでプロセッサが対応している機能などを確認します．
BitVisorのソースでは`core/vt_init.c`の`vpid_init`と`ept_init`関数に実装があります．

```c
static void
vpid_init (void)
{
	u64 ept_vpid_cap;

	asm_rdmsr64 (MSR_IA32_VMX_EPT_VPID_CAP, &ept_vpid_cap);
	if (!(ept_vpid_cap & MSR_IA32_VMX_EPT_VPID_CAP_INVVPID_BIT))
		return;
	if (!(ept_vpid_cap &
	      MSR_IA32_VMX_EPT_VPID_CAP_INVVPID_SINGLE_CONTEXT_BIT))
		return;
	current->u.vt.vpid = 1; /* FIXME: VPID 1 only */
}

static void
ept_init (void)
{
	u64 ept_vpid_cap;

	asm_rdmsr64 (MSR_IA32_VMX_EPT_VPID_CAP, &ept_vpid_cap);
	if (!(ept_vpid_cap & MSR_IA32_VMX_EPT_VPID_CAP_PAGEWALK_LENGTH_4_BIT))
		return;
	if (!(ept_vpid_cap & MSR_IA32_VMX_EPT_VPID_CAP_EPTSTRUCT_WB_BIT))
		return;
	current->u.vt.ept_available = true;
	if (!(ept_vpid_cap & MSR_IA32_VMX_EPT_VPID_CAP_INVEPT_BIT))
		return;
	if (!(ept_vpid_cap & MSR_IA32_VMX_EPT_VPID_CAP_INVEPT_ALL_CONTEXT_BIT))
		return;
	current->u.vt.invept_available = true;
}

```

## EPTの設定
`vt_vminit`(core/vt_init.c) -> `vt_paging_init`(core/vt_paging.c) -> `vt_ept_init`(core/vt_ept.c)という流れでEPTの初期化関数が呼ばれます．

```c
void
vt_ept_init (void)
{
	struct vt_ept *ept;
	int i;

	ept = alloc (sizeof *ept);
	alloc_page (&ept->ncr3tbl, &ept->ncr3tbl_phys);
	memset (ept->ncr3tbl, 0, PAGESIZE);
	ept->cleared = 1;
	for (i = 0; i < NUM_OF_EPTBL; i++)
		alloc_page (&ept->tbl[i], &ept->tbl_phys[i]);
	ept->cnt = 0;
	ept->cur.level = EPT_LEVELS;
	current->u.vt.ept = ept;
	asm_vmwrite64 (VMCS_EPT_POINTER, ept->ncr3tbl_phys |
		       VMCS_EPT_POINTER_EPT_WB | VMCS_EPT_PAGEWALK_LENGTH_4);
}
```

`tbl`がEPTのエントリで，`cur`でエントリを走査していきます．
EPTPにEPTのテーブルの物理アドレスをセットして設定は完了です．

## BitVisorでのEPTの扱い
ゲストでEPT Violation(EPTエントリがマップされていなかったり，アクセス制限に引っかかるようなアクセスをシた場合におこる)が起こった場合，`do_ept_violation`(core/vt_main.c)が呼ばれます．

```c
static void
do_ept_violation (void)
{
	ulong eqe;
	u64 gp;

	asm_vmread (VMCS_EXIT_QUALIFICATION, &eqe);
	asm_vmread64 (VMCS_GUEST_PHYSICAL_ADDRESS, &gp);
	vt_paging_npf (!!(eqe & EPT_VIOLATION_EXIT_QUAL_WRITE_BIT), gp);
}
```

Exit QUalificationとEPT Violationが発生したゲスト物理アドレスを取り出して，`vt_paging_npf`を呼びます．


```c
void
vt_paging_npf (bool write, u64 gphys)
{
#ifdef CPU_MMU_SPT_DISABLE
	if (current->u.vt.vr.pg)
		panic ("EPT violation while spt disabled");
#endif
	if (ept_enabled ())
		vt_ept_violation (write, gphys);
	else
		panic ("EPT violation while ept disabled");
}
```

これはSPT(Shadow Paging Table)とEPTとを抽象化するレイヤです．SPTはEPTが導入される前に用いられていたメモリ仮想化のテクニックです．今回はSPTではなく`ept_enabled`なので`vt_ept_violation`を呼び出します．

```c
void
vt_ept_violation (bool write, u64 gphys)
{
	struct vt_ept *ept;

	ept = current->u.vt.ept;
	mmio_lock ();
	if (vt_ept_level (ept, gphys) > 0 &&
	    !mmio_range (gphys & ~PAGESIZE2M_MASK, PAGESIZE2M) &&
	    !vt_ept_map_2mpage (ept, gphys))
		;
	else if (!mmio_access_page (gphys, true))
		vt_ept_map_page (ept, write, gphys); // こんかいはここ
	mmio_unlock ();
}
```
MMIOのトラップはここで処理をしているみたいですが，今回は`vt_ept_map_page`にいきます．
`vt_ept_map_page`からマップを行う`vt_ept_map_page_sub`を呼び出します．

```c
static void
vt_ept_map_page_sub (struct vt_ept *ept, bool write, u64 gphys)
{
	bool fakerom;
	u64 hphys;
	u32 hattr;
	u64 *p;

	cur_move (ept, gphys);
	p = cur_fill (ept, gphys, 0);
	hphys = current->gmm.gp2hp (gphys, &fakerom) & ~PAGESIZE_MASK;
	if (fakerom && write)
		panic ("EPT: Writing to VMM memory.");
	hattr = (cache_get_gmtrr_type (gphys) << EPTE_MT_SHIFT) |
		EPTE_READEXEC | EPTE_WRITE;
	if (fakerom)
		hattr &= ~EPTE_WRITE;
	*p = hphys | hattr;
}
```
`cur_move`で現在のEPTエントリを更新，`cur_fill`でPTEを取得．(この中でPML4E,PDPTE,PDEの設定もしている)
`hphys = current->gmm.gp2hp (gphys, &fakerom) & ~PAGESIZE_MASK;`でフラグのぶん11bitマスク．あとは適当にフラグを設定してPTEに書き込むだけです．

# TLB Shootdown
もし，任意のアドレスでEPT Violationを起こしたい場合は，`vt_ept_map_page_sub`でフラグを落とした設定をすればよい．ここで気をつけなければいけないのはTLBである．TLBにはEPTのキャッシュも存在していて，これはコアごとに存在する．つまり一度フラッシュしておかなければ，別のコアでは古いキャッシュ情報が使用されてしまう恐れがある．ここでTLB Shootdownというテクニックを用いる．

TLB Shootdownを行うには[この記事](https://qiita.com/deep_tkkn/items/0220df8d569b5a6a8bc3)が参考になる．

# 実装
さて，実際にTLB Shootdownとトラップする機構を実装してみた．
[レポジトリ](https://github.com/morimolymoly/TrapVisor)

具体的には，GPAとGVAでトラップできるようにVMCALLを追加した．

VMCALLの実装は
`core/vmcall.c`
`core/vmcall3.c`
で，VMCALLをするためのLKMは
`vmcall/`以下に実装した．

今回は`commit_creds`をトラップすることにして，`vmcall/cc`にこれを呼び出すLKMを書いた．


```c
static void
do_ept_violation (void)
{
	ulong eqe;
	u64 gp, va;
	ulong rax;
	current->vmctl.read_general_reg(GENERAL_REG_RAX, &rax);
	asm_vmread (VMCS_EXIT_QUALIFICATION, &eqe);
	asm_vmread64 (VMCS_GUEST_PHYSICAL_ADDRESS, &gp);
	asm_vmread64 (VMCS_GUEST_LINEAR_ADDR, &va);
	if(is_trapped(gp) || is_trapped_gva(va)){
		vt_paging_npf (!!(eqe & EPT_VIOLATION_EXIT_QUAL_WRITE_BIT), gp, false, true);
	}else{
		vt_paging_npf (!!(eqe & EPT_VIOLATION_EXIT_QUAL_WRITE_BIT), gp, false, false);
	}
}
```
GPAがトラップするアドレスなら`is_trapped`，GVAがトラップされるなら`is_trapped_gva`からtrueが帰ってくる．`vt_paging_npf`にはそれぞれtrapとexecという引数を追加して，trapする場合はPTEに実行フラグをexecの内容で書き込む．今回はトラップされるアドレスなら実行フラグが問答無用でオフになるので，該当するアドレス空間が参照された段階で無限EPT Violationループに陥る．

```c
static void
vt_ept_map_page_sub (struct vt_ept *ept, bool write, u64 gphys, bool execute, bool trap)
{
	bool fakerom;
	u64 hphys;
	u32 hattr;
	u64 *p;

	cur_move (ept, gphys);
	p = cur_fill (ept, gphys, 0);
	// フラグのために11 bit maskしている
	hphys = current->gmm.gp2hp (gphys, &fakerom) & ~PAGESIZE_MASK;
	if (fakerom && write)
		panic ("EPT: Writing to VMM memory.");
	if(trap){
		if(execute){
			hattr = (cache_get_gmtrr_type (gphys) << EPTE_MT_SHIFT) |
			EPTE_READ | EPTE_WRITE | EPTE_EXEC;
		}else{
			hattr = (cache_get_gmtrr_type (gphys) << EPTE_MT_SHIFT) |
			EPTE_READ | EPTE_WRITE;
			printf("TV: Removed wrx from 0x%llx\n", gphys);
			printf("TV: Host address is 0x%llx\n", (u64)current->gmm.gp2hp (gphys, &fakerom));
			printf("PTE: 0x%llx\n", hphys | hattr);
		}
	}else{
		hattr = (cache_get_gmtrr_type (gphys) << EPTE_MT_SHIFT) |
		EPTE_READEXEC | EPTE_WRITE;
	}
	if (fakerom)
		hattr &= ~EPTE_WRITE;
	*p = hphys | hattr;
}
```

# 問題点
というわけでVMCALLを使ってカーネル空間の関数をKASLR無効でいくつかトラップしてみたが，EPT Violationが発生しなかった．TLB Shootdownはエントリが追加される前に走っているのは確認できた．
ログは[gist](https://gist.github.com/morimolymoly/3909769584084609a0f53eaa67a186e5)にアップロードした．

# 検証方法
vmcallのLKM内のトラップするアドレスを書き換えたあと，`make && make test`で実行する．
そうすると実行フラグを落としたはずの関数が実行されてしまうことがわかる．


# たすけて…
HyperVillageの[slack](http://bit.ly/2pn6ewY)か当記事コメントでどうかよろしくおねがいします:bow:
解決したら全部バッチリまとめた記事を書きます……

# 参考
https://qiita.com/RKX1209/items/38c8403a73c91bd782bc
https://www.slideshare.net/DeepTokikane/ept-tlb
https://qiita.com/deep_tkkn/items/0220df8d569b5a6a8bc3