티스토리 뷰

CTF write-up

RealWorld CTF kid vm

marshimaro aSiagaming 2018.08.07 21:17
keyword : vm escape, kvm, kvm escape

# Fail


내 맥북에서는 VM fusion에 올릴 용량이 없어서 Docker로 작업중인데, kvm module이 없어서 fail...
다른 VM 환경으로 옮겨가서 풀었다.

# KVM analysis

Linux KVM 모듈을 이용하여, custom vm을 형성한다. -> 사실상 Qemu의 축소판 느낌
kvm을 이용하게될 때, 처음의 코드는 아래의 pseudo 코드와 같은 initialize 역할을 하게 된다.


sample code는 아래와 같다.

struct vm {
        int sys_fd;
        int fd;
        char *mem;
};

void vm_init(struct vm *vm, size_t mem_size)
{
        int api_ver;
        struct kvm_userspace_memory_region memreg;

        vm->sys_fd = open("/dev/kvm", O_RDWR);
        if (vm->sys_fd < 0) {
                perror("open /dev/kvm");
                exit(1);
        }

        api_ver = ioctl(vm->sys_fd, KVM_GET_API_VERSION, 0);
        if (api_ver < 0) {
                perror("KVM_GET_API_VERSION");
                exit(1);
        }

        if (api_ver != KVM_API_VERSION) {
                fprintf(stderr, "Got KVM api version %d, expected %d\n",
                        api_ver, KVM_API_VERSION);
                exit(1);
        }

        vm->fd = ioctl(vm->sys_fd, KVM_CREATE_VM, 0);
        if (vm->fd < 0) {
                perror("KVM_CREATE_VM");
                exit(1);
        }

        if (ioctl(vm->fd, KVM_SET_TSS_ADDR, 0xfffbd000) < 0) {
                perror("KVM_SET_TSS_ADDR");
                exit(1);
        }

        vm->mem = mmap(NULL, mem_size, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
        if (vm->mem == MAP_FAILED) {
                perror("mmap mem");
                exit(1);
        }

        madvise(vm->mem, mem_size, MADV_MERGEABLE);

        memreg.slot = 0;
        memreg.flags = 0;
        memreg.guest_phys_addr = 0;
        memreg.memory_size = mem_size;
        memreg.userspace_addr = (unsigned long)vm->mem;
        if (ioctl(vm->fd, KVM_SET_USER_MEMORY_REGION, &memreg) < 0) {
                perror("KVM_SET_USER_MEMORY_REGION");
                exit(1);
        }
}

struct vcpu {
        int fd;
        struct kvm_run *kvm_run;
};

void vcpu_init(struct vm *vm, struct vcpu *vcpu)
{
        int vcpu_mmap_size;

        vcpu->fd = ioctl(vm->fd, KVM_CREATE_VCPU, 0);
        if (vcpu->fd < 0) {
                perror("KVM_CREATE_VCPU");
                exit(1);
        }

        vcpu_mmap_size = ioctl(vm->sys_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
        if (vcpu_mmap_size <= 0) {
                perror("KVM_GET_VCPU_MMAP_SIZE");
                exit(1);
        }

        vcpu->kvm_run = mmap(NULL, vcpu_mmap_size, PROT_READ | PROT_WRITE,
                             MAP_SHARED, vcpu->fd, 0);
        if (vcpu->kvm_run == MAP_FAILED) {
                perror("mmap kvm_run");
                exit(1);
        }
}

int run_vm(struct vm *vm, struct vcpu *vcpu, size_t sz)
{
        struct kvm_regs regs;
        uint64_t memval = 0;

        for (;;) {
                if (ioctl(vcpu->fd, KVM_RUN, 0) < 0) {
                        perror("KVM_RUN");
                        exit(1);
                }

                switch (vcpu->kvm_run->exit_reason) {
                case KVM_EXIT_HLT:
                        goto check;

                case KVM_EXIT_IO:
                        if (vcpu->kvm_run->io.direction == KVM_EXIT_IO_OUT
                            && vcpu->kvm_run->io.port == 0xE9) {
                                char *p = (char *)vcpu->kvm_run;
                                fwrite(p + vcpu->kvm_run->io.data_offset,
                                       vcpu->kvm_run->io.size, 1, stdout);
                                fflush(stdout);
                                continue;
                        }

                        /* fall through */
                default:
                        fprintf(stderr, "Got exit_reason %d,"
                                " expected KVM_EXIT_HLT (%d)\n",
                                vcpu->kvm_run->exit_reason, KVM_EXIT_HLT);
                        exit(1);
                }
        }

check:
        if (ioctl(vcpu->fd, KVM_GET_REGS, &regs) < 0) {
                perror("KVM_GET_REGS");
                exit(1);
        }

        if (regs.rax != 42) {
                printf("Wrong result: {E,R,}AX is %lld\n", regs.rax);
                return 0;
        }

        memcpy(&memval, &vm->mem[0x400], sz);
        if (memval != 42) {
                printf("Wrong result: memory at 0x400 is %lld\n",
                       (unsigned long long)memval);
                return 0;
        }

        return 1;
}

vm_init()을 통해 vm context가 형성된다.
크게 3가지의 ioctl이 사용되는데, kvm_ioctl, kvm_vm_ioctl, kvm_vcpu_ioctl, etc)... 가 있다.

  1. kvm_ioctl에서는 kvm_create_vm이 중요함.
  2. kvm_vm_ioctl에서는 guest memory를 구현하고, vcpu, device 등을 구현함.
  3. kvm_vcpu_ioctl에서는 kvm_run이 중요하다.




# analysis

리버싱 시에, kvm에 대한 ioctl command가 중요한데, #define으로 되어있어서 해당 값들을 찾아내야 한다.


비트맵을 통해서 정보를 나타내기때문에, bit shifting이 어떻게 되는지 보고 판단해야 한다.
KVM_RUN의 경우에는 nr == 0x80이다.

몇가지 요소들만 살펴보면

/*
* ioctls for /dev/kvm fds:
*/
#define KVM_GET_API_VERSION       _IO(KVMIO,   0x00)
#define KVM_CREATE_VM             _IO(KVMIO,   0x01) /* returns a VM fd */
#define KVM_GET_MSR_INDEX_LIST    _IOWR(KVMIO, 0x02, struct kvm_msr_list)
#define KVM_S390_ENABLE_SIE       _IO(KVMIO,   0x06)


/*
* ioctls for vcpu fds
*/

#define KVM_RUN                   _IO(KVMIO,   0x80)
#define KVM_GET_REGS              _IOR(KVMIO,  0x81, struct kvm_regs)
#define KVM_SET_REGS              _IOW(KVMIO,  0x82, struct kvm_regs)
#define KVM_GET_SREGS             _IOR(KVMIO,  0x83, struct kvm_sregs)
#define KVM_SET_SREGS             _IOW(KVMIO,  0x84, struct kvm_sregs)
#define KVM_TRANSLATE             _IOWR(KVMIO, 0x85, struct kvm_translation)
#define KVM_INTERRUPT             _IOW(KVMIO,  0x86, struct kvm_interrupt)
/* KVM_DEBUG_GUEST is no longer supported, use KVM_SET_GUEST_DEBUG instead */
#define KVM_DEBUG_GUEST           __KVM_DEPRECATED_VCPU_W_0x87
#define KVM_GET_MSRS              _IOWR(KVMIO, 0x88, struct kvm_msrs)
#define KVM_SET_MSRS              _IOW(KVMIO,  0x89, struct kvm_msrs)
#define KVM_SET_CPUID             _IOW(KVMIO,  0x8a, struct kvm_cpuid)
#define KVM_SET_SIGNAL_MASK       _IOW(KVMIO,  0x8b, struct kvm_signal_mask)
#define KVM_GET_FPU               _IOR(KVMIO,  0x8c, struct kvm_fpu)
#define KVM_SET_FPU               _IOW(KVMIO,  0x8d, struct kvm_fpu)
#define KVM_GET_LAPIC             _IOR(KVMIO,  0x8e, struct kvm_lapic_state)
#define KVM_SET_LAPIC             _IOW(KVMIO,  0x8f, struct kvm_lapic_state)
#define KVM_SET_CPUID2            _IOW(KVMIO,  0x90, struct kvm_cpuid2)
#define KVM_GET_CPUID2            _IOWR(KVMIO, 0x91, struct kvm_cpuid2)

kvm을 통해 vm을 만드는 과정은 아래의 url에 잘 나와있다.


vm을 형성하는 과정은 대략적으로 이렇다. ( 핸들은 리눅스에서 fd를 의미하고있습니다 )

  • kvm 디바이스를 오픈하여 kvm에 대한 핸들을 가져온다.
  • vm context를 형성하고 vm과 호환성 체크라던가 extension 체크를 진행한다.
  • 그리고 ioctl상의 KVM_CREATE_VM command를 실행한다. -> vm에 대한 핸들이 반환됨.
  • vm상에서 실행될 코드를 mmap을 통해 할당하고 copy해준다.
  • vm의 physical address등 kvm_userspace_memory_region을 설정해준다.

IDA상에서 struct 아래와 같이 정의해준다.

struct kvm_userspace_memory_region
{
    __int64 slot;
    void *guest_phys_addr;
    __int64 memory_size;
    void *userspace_addr;
};

example)

  • 그리고 vcpu를 vmfd의 ioctl을 통해 형성해준다.



  • 이후 루틴들은 아래의 define 값을 참고해서 봐야한다.


  • kvm_run struct에 대한 size를 가져오고, mmap해준다.



  • 여타 다른 세그먼트 레지스터 등, context 설정을 마무리하고, kvm_run을 실행한다.
  • vm_exit 핸들러 등의 여러가지 작동들과 함께 vm을 컨트롤한다.

vm_exit의 핸들러로 작동하는 switch-case문에서는 kvm_run에 대하여 아래의 링크에서 struct를 가져와서 정의해주면 된다.



위와 같이 리버싱이 조금더 편해진다 !
간단하게만 보면, io 23번 port를 이용하여 in/out instruction을 통해 통신하게 된다.


guest에 대한 메모리 구조는 위와 같이 되어있다.
vm_code를 확인해보면 아래와  같다.