この記事はBitVisor Advent Calendar 17日目の記事です.(またしても遅刻すみません...
BitVisorのEPTエントリーをいじって特定のドライバが実行された時のみBitVisor側に制御を戻すようにしてみます。
#EPT改造でできる事
BitVisorはゲストOSの物理アドレスとホストの物理アドレスの対応をEPT(Extended Page Table)で行っています。
これらEPTのエントリにはそれぞれR(Read),W(Write),X(Execute)のパーミッションフラグが設定でき、これによりゲストOSのページングのフラグと合わせてアクセス権限を設定できます。これを利用すると、ゲストOSから透過な場所で権限設定を行うことができるため、マルウェア解析において、例えばrootkitからも見えないアクセス保護を行う事ができます。(結構昔からある研究ですね)
さて、今回はこれをBitVisorにも実装し、ゲストOSの特定のドライバ領域のEPTエントリーからX(Execute)フラグを外す事で、その領域を実行しようとするとBitVisor側に制御が戻るようにしてみます。
#BitVisorのEPTマッピング処理
BitVisorではEPT violationした際、core/vt_ept.cでEPTの各エントリーを作成するようになっています。注意すべき点としては、(まあ当然ですが)それらのエントリーはtbl(phys)という1024個の配列から取ってこられ、もし足りなくなると一旦クリアしてやりなおす仕組みになっている事です。
つまり、今回特定のEPTエントリーからX flagを外したいのですが、仮にどこかのタイミングでX flagを外したとしても、しばらくしてエントリーがいっぱいになるとクリアされてしまう可能性があるという事です。
よってクリアされた後もX flagを外した状態で再割当てしてもらうように、EPTの割当関数vt_ept_map_page_sub()内で、特定の領域内ならX flagを抜いてエントリーを作成するように改造しました。
diff --git a/bitvisor/../../bitvisor/core/vt_ept.c b/bitvisor/core/vt_ept.c
index 341e1e2..518e9ce 100644
--- a/bitvisor/../../bitvisor/core/vt_ept.c
+++ b/bitvisor/core/vt_ept.c
static void
vt_ept_map_page_sub (struct vt_ept *ept, bool write, u64 gphys)
{
@@ -154,8 +173,16 @@ vt_ept_map_page_sub (struct vt_ept *ept, bool write, u64 gphys)
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 (in_hook_point (&hook_point, gphys)) {
+ hattr = (cache_get_gmtrr_type (gphys) << EPTE_MT_SHIFT) |
+ EPTE_READ | EPTE_WRITE; // witout EXEC permission
+ printf("remove x-bit from 0x%llx\n", gphys);
+ }
+ else
+ hattr = (cache_get_gmtrr_type (gphys) << EPTE_MT_SHIFT) |
+ EPTE_READEXEC | EPTE_WRITE;
+
if (fakerom)
hattr &= ~EPTE_WRITE;
*p = hphys | hattr;
@@ -238,6 +265,37 @@ vt_ept_map_page (struct vt_ept *ept, bool write, u64 gphys)
}
void
+vt_ept_set_hook (u64 gphys, u32 size)
+{
+ struct vt_ept *ept;
+ u32 i;
+ u64 gp;
+ u64 *m;
+ hook_point.mod_areas = (u64*)alloc(sizeof *m * size);
+ hook_point.size = size;
+ hook_point.available = 1;
+
+ ept = current->u.vt.ept;
+
+ /* Clearing EPT entries, to hook */
+ vt_ept_clear_all_slow();
+
+ printf("vt_ept_set_hook 0x%llx(%d)\n", gphys, size);
+
+ /* remap address range of kernel module
+ * TODO: 2M page
+ */
+ m = hook_point.mod_areas;
+
+ for (i = 0; i < hook_point.size; i++) {
+ read_gphys_q(gphys + sizeof(u64) * i, (void*)&m[i], 0);
+ printf("ares[%d] = 0x%llx\n", i, m[i]);
+ vt_ept_map_page(ept, false, m[i]);
+ }
+
+}
+
#コア間のキャッシュコヒーレンシ
さて、EPTエントリーをいじる事はできましたが、もうひとつ気を付けなければならないのがTLB(Translation Lookaside Buffer)の扱いです。EPTによる変換は各コアのTLBに載っているため、かりにX flagを抜いてもTLBにある古いエントリーが使われ続けると意味がありません。
そこでX flagを抜く前に、全コアに対してTLBをフラッシュするように命令します。(いわゆるTLB shootdownです)
そういえばちょうど少し前にkernelvm勉強会でdeep_tkknさんがBitVisorでTLB shootdownの話をされてたなぁと思い、その時の発表資料を参考にさせて頂きました。
またtwitterで愚痴っていた所、deep_tkknさんにTLB shootdownのパッチを見せていただけました。
圧倒的感謝m(_ _)m
実装方法としては、BitVisorは各コアに対してIPIでNMIを送ってハンドルする機能があるのでそれを利用して、NMIとしてshootdownメッセージを送り、各コアにTLB flushさせる物です。
これでうまく行きました。
#ドライバの物理アドレスの扱い
さてこれで全てうまく行ったように見えますが、まだ駄目です。
なぜかというと、そもそもEPT violationした時に取得できるのはゲストの物理アドレスなのですが、ドライバのロードされている領域の物理アドレスは、実は連続している保証がありません。
これはLinuxの場合ドライバの領域確保がvmallocで行われているからです。
[参考]
- Linuxのカーネルモジュールがロードされるアドレス範囲 http://kernhack.hatenablog.com/entry/2016/10/06/010004
よってドライバの領域を網羅するには、非連続な物理アドレスをリストで繋いでBitVisorに渡すか、連続な領域にドライバをコピーして仮想アドレスをremapするかです。
今回はとりあえず前者で実装しました。
以前の記事で作ったLinux用エージェントを改良し、vmallocの物理アドレスをリストに繋いでvmcallで渡すようにしました。
[参考]
- BitVisorにVMCALLを追加してドライバを検知してみる http://qiita.com/RKX1209/items/91e45c5f1b9b9c7211f8
改造した主な部分です。
static void
gen_phys_list (void *vmod, unsigned long size)
{
/* [vmod, vmod+size] area is physically not-contiguous,
because it's allocated by vmalloc() in module_alloc. */
u64 p = vmod, phys;
mod_area_phys = (u64*)kmalloc(sizeof(u64) * (size / PAGE_SIZE), GFP_KERNEL);
for (p = vmod; p < (u64)vmod + size; p += PAGE_SIZE) {
phys = page_to_phys(vmalloc_to_page((void*)p));
pr_info("module: 0x%llx => [0x%llx,0x%llx]\n", p, phys, phys+PAGE_SIZE);
mod_area_phys[(p - (u64)vmod) / PAGE_SIZE] = phys;
}
}
static void
vmcall_drvhook (void *vmod, unsigned long size)
{
call_vmm_function_t drvf;
call_vmm_arg_t drv_a;
call_vmm_ret_t drv_r;
char drvhook[] = "drvhook";
vmmcall_get_function(drvhook, &drvf);
pr_info("drvhook number=%d\n",drvf.vmmcall_number);
gen_phys_list(vmod, size);
drv_a.rbx = (intptr_t)mod_area_phys;
drv_a.rcx = (intptr_t)__pa((void*)mod_area_phys);
drv_a.rdx = size / PAGE_SIZE;
call_vmm_call_function(&drvf, &drv_a, &drv_r);
}
page_to_phys(vmalloc_to_page)で物理アドレスをとってつなげてます。もちろんつなげるリストそれ自体は連続している必要があるので、kmallocで確保しています。
#実行
では実際に動作させてみます。
Processor 3 tlb flush
vt_flush_all_tlb
sending to processor 2
complete processor 2
Processor 2: tlb flush
vt_ept_set_hook 0xa6be1200(4)
ares[0] = 0xd5443000
remove x-bit from 0xd5443000
ares[1] = 0xd3cc1000
remove x-bit from 0xd3cc1000
ares[2] = 0x98f1d000
remove x-bit from 0x98f1d000
ares[3] = 0x9b054000
remove x-bit from 0x9b054000
(3)ept: 0xd5444000(write)
remove x-bit from 0xd5444000
(3)ept: 0xd5443040(exec)
X-bit violation: 0xd5443040
見づらいログですが、remo x-bit fromでドライバの領域(areas[0] ~ [4])からX flagを抜き、その後適当にioctlを発行してドライバを実行すると、X-bit violationで該当アドレスで戻ってきたというログです。
#その他
まあ上記のコードスニペットを見れば分かるのですが、ここでは全ての実装は載せてません。
いずれ公開しようと思います。(卒論が無事終わったら.....