libvmiのサンプルコードを読んでみました
VMI(Virtual Machine Introspection)っていうOut-VMからIn-VMの情報を取得するやり方を調べていて、
XenとかKVMとかで使えるlibvmiっていうライブラリをちょっと読んでみました。
libvmiが何をしてくれるかというとvmmからGuest OSへのアドレス指定の際に必要になるGuest OS内の仮想アドレス->物理アドレスの変換やシンボル
RekallというMemory Forensic Toolのprofileをつかってアドレスを指定できたりします。
アーキテクチャ
1. VMMがlibvmiにシンボル名でデータを問い合わせます
2. アーキテクチャごとに用意されたシンボルとアドレスのテーブルからアーキテクチャに対応した仮想アドレスを取得します
3, 4. libvmiがGuest OSのPD(Page Directory)とPT(Page Table)を参照して、Guest OSの仮想アドレスをGuest OSの物理アドレスに変換します
5. 変換したGuest OSの物理アドレスを参照してGuest OSのデータを取得します
libvmiのサンプルソースを読む
examples/dump-memory.c
Guest OSの物理メモリをダンプするプログラムです。
while (address < size) { /* write memory to file */ if (PAGE_SIZE == vmi_read_pa(vmi, address, memory, PAGE_SIZE)) { /* memory mapped, just write to file */ size_t written = fwrite(memory, 1, PAGE_SIZE, f); // omitted } else { /* memory not mapped, write zeros to maintain offset */ size_t written = fwrite(zeros, 1, PAGE_SIZE, f); // omitted } address += PAGE_SIZE; }
vmi_read_paはlibvmiが提供するAPIで第1引数のaddressでGuest OSの物理アドレスを指定して、そこから第4引数のsize分データを第3引数のmemoryに読み取ります。
vmi_read_paは最終的にlibvmi/read.cのvmi_read()を呼ぶのですが、vmi_read_paは指定したアドレスを物理アドレスとして扱うので、特にアドレスの変換は行っていません。
access_context_t ctx = { .translate_mechanism = VMI_TM_NONE, .addr = paddr };
仮想アドレスやシンボルを使ってデータを読み取りたい場合は、内部でaccess_context_tの.addrや.translateを変更するのですが、これは別のAPIが提供されているのでライブラリのユーザーは気にしなくてよいです。
examples/va-pages.c
Guest OSのプロセスごとにPT(Page Table)をダンプするプログラムです。
SETUP_REG_EVENT(&cr3_event, CR3, VMI_REGACCESS_W, 0, cr3_callback); vmi_register_event(vmi, &cr3_event); while(!interrupted){ printf("Waiting for events...\n"); status = vmi_events_listen(vmi,500); if (status != VMI_SUCCESS) { printf("Error waiting for events, quitting...\n"); interrupted = -1; } }
プロセスの切替時にCR3レジスタが書き換わるので、
書き込みがあったときにコールバックが呼ばれるようにレジスタイベントを登録してます。
SETUP_REG_EVENTの第4引数に0を設定していますが、これはevent->reg_event.equalのフィルタを使いませんという意味です。
event->reg_event.equalに0以外が指定してあると指定した値に書き換わるときのみevent->reg_event.valueに値がセットされます。
event_response_t cr3_callback(vmi_instance_t vmi, vmi_event_t *event) { va_pages = vmi_get_va_pages(vmi, event->reg_event.value); GSList *loop = va_pages; while(loop) { page_info_t *page = loop->data; // Demonstrate using access_context_t access_context_t ctx = { .translate_mechanism = VMI_TM_PROCESS_DTB, .addr = page->vaddr, .dtb = event->reg_event.value, }; uint64_t test; if(VMI_FAILURE == vmi_read_64(vmi, &ctx, &test)) { // emitted } loop=loop->next; } free_va_pages(); return 0; }
次にコールバック部分です。vmi_get_va_pages()を呼び出してます。
vmi_get_va_pages()はIntel CPUでNO PAEのときは最終的にlibvmi/arch/intel.cのget_va_pages_nopae()を呼び出します。
この関数はPD(Page Directory)とPT(Page Table)を辿ってページの情報を取得します。
// emitted for(pgd_index = 0; pgd_index < PTRS_PER_NOPAE_PGD; pgd_index++, pgd_location += entry_size) { uint32_t pgd_entry = pgd_page[pgd_index]; if(ENTRY_PRESENT(vmi->os_type, pgd_entry)) { // emitted uint32_t pte_location = ptba_base_nopae(pgd_entry); // emitted uint32_t pte_index; for(pte_index = 0; pte_index < PTRS_PER_NOPAE_PTE; pte_index++, pte_location += entry_size) { uint32_t pte_entry = pt_page[pte_index]; if(ENTRY_PRESENT(vmi->os_type, pte_entry)) { page_info_t *p = g_malloc0(sizeof(page_info_t)); p->vaddr = pgd_base_vaddr + pte_index * VMI_PS_4KB; p->paddr = get_paddr_nopae(p->vaddr, pte_entry); p->size = VMI_PS_4KB; p->x86_legacy.pgd_location = pgd_location; p->x86_legacy.pgd_value = pgd_entry; p->x86_legacy.pte_location = pte_location; p->x86_legacy.pte_value = pte_entry; ret = g_slist_prepend(ret, p); } } } }
コールバックのこの部分
access_context_t ctx = { .translate_mechanism = VMI_TM_PROCESS_DTB, .addr = page->vaddr, .dtb = event->reg_event.value, };
今度は.addr = page->vaddrが指定されているので、
vmi_read_64(vmi, &ctx, &test)で指定したアドレスを仮想アドレスとして扱います。
event->reg_event.valueの値をCR3レジスタの値として仮想アドレスから物理アドレスに変換します。