Edited at
BitVisorDay 18

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

これはBitVisor Advent Calendar 2018の18日目の記事です.

こんにちは,@morimolymolyです.

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


どんな記事を書くといいの? って思う人は例えばこんな記事とかどうでしょう?

* BitVisor 初心者体験談

...

* ○○やりたいんだがよくわからん助けて


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


EPTとは

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

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


MSR VPID AND EPT CAPABILITIES

まずはMSRのIA32_VMX_EPT_VPID_CAP MSRを読んでプロセッサが対応している機能などを確認します.

BitVisorのソースではcore/vt_init.cvpid_initept_init関数に実装があります.

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の初期化関数が呼ばれます.

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)が呼ばれます.

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を呼びます.

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を呼び出します.

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を呼び出します.

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を行うにはこの記事が参考になる.


実装

さて,実際にTLB Shootdownとトラップする機構を実装してみた.

レポジトリ

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

VMCALLの実装は

core/vmcall.c

core/vmcall3.c

で,VMCALLをするためのLKMは

vmcall/以下に実装した.

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

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ループに陥る.

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にアップロードした.


検証方法

vmcallのLKM内のトラップするアドレスを書き換えたあと,make && make testで実行する.

そうすると実行フラグを落としたはずの関数が実行されてしまうことがわかる.


たすけて…

HyperVillageのslackか当記事コメントでどうかよろしくおねがいします



解決したら全部バッチリまとめた記事を書きます……


参考

https://qiita.com/RKX1209/items/38c8403a73c91bd782bc

https://www.slideshare.net/DeepTokikane/ept-tlb

https://qiita.com/deep_tkkn/items/0220df8d569b5a6a8bc3