libvmiのサンプルコードを読んでみました

VMI(Virtual Machine Introspection)っていうOut-VMからIn-VMの情報を取得するやり方を調べていて、
XenとかKVMとかで使えるlibvmiっていうライブラリをちょっと読んでみました。
libvmiが何をしてくれるかというとvmmからGuest OSへのアドレス指定の際に必要になるGuest OS内の仮想アドレス->物理アドレスの変換やシンボル
RekallというMemory Forensic Toolのprofileをつかってアドレスを指定できたりします。

アーキテクチャ

http://libvmi.com/assets/images/intro-detail.png

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レジスタの値として仮想アドレスから物理アドレスに変換します。