简单的活着

Record

Posted on By Mista Cai

Record

高俊亚学长

linux-source/include/uapic/linux/kvm.h里面kvm_run这个结构体需要跟Qemu里面的kvm_run保持一致。

里面的成员fault_gfn原本放到最后面,但是它们后面的成员不太一样,因此我放到前面了。所以把linux内核的fault_gfn放到最前面,然后执行./recomplish.sh,再启动Qemu就没问题了。

会议纪要

12-11 kvm双视图权限设置

学校:

  • 我刚才问的双视图包含的映射内容不一致,不是说GPA->HPA这个关系的不一致,我考虑的是,如果监控视图不是在驱动未加载前创建的,那么监控视图创建是通过window驱动使用vmfunc切换视图,主动触发not present而创建的吗?那这样的话,那么两个视图的映射的pte页表项的数目,而不是映射的内容。是不是产生不一致。

华为:

  • 这个不一致没有什么影响,只要保证gpa->hpa映射关系是一致的就可以了。为什么要保证页表里面的pte数量也一样呢?

学校:

  • 那监控视图的创建时机呢?是在驱动加载的时候才去创建的吗?

华为:

  • kvm_mmu_get_page这个函数你们改了,但是patch里面没有看到这个函数内部是怎么改动的?
  • 在你使用这个视图之前,随便你什么时候创建。

学校:

  • kvm_mmu_get_page只改动了一个地方,加了个ept_index
hlist_add_head(&sp->hash_link,
                        &vcpu->kvm->arch.mmu_page_hash_list[ept_index][kvm_page_table_hashfn(gfn)]);

华为:

alloc_page_for_kernel_code_space

alloc_page_for_malware_space

alloc_page_for_driver_space

  • 这三个函数是一样的么?为什么要自己通过超级调用去创建映射呢,你切到这个视图的时候没有建立映射的都会通过not presnt异常由kvm去创建好,完全没必要自己主动去创建。
  • 你是在哪里设置页表权限的?哪里去处理你设置权限之后产生的页异常的? 我看了半天没看到~~

学校:

  • 我以为在驱动加载的时候,在正常的视图里面是通过not present建立的映射关系,但是在监控视图里面并没有其关于驱动的映射关系,所以我打算利用这个超级调用,将驱动在监控视图的映射关系和权限给设置好。这三个函数还没写完整,页表权限的设置可能是我理解错误了,我以为在___direct_map中调用的mmu_set_spte传入的ACC_ALL修改成无执行权限的标识就行了,好像不成功。

华为:

  • 你只设置不去处理页异常?那切到这个视图执行的时候里面就会触发页异常,肯定出错了啊~ 你也没写windows驱动处理#VE

学校:

  • 我现在在做的是触发#VE和和捕获#VE是否能成功,所以我现在是打算在系统初始化的时候,也同时初始化监控视图,但是监控视图的任何内存区域暂时是都没有执行权限的。然后我再加载监控驱动,让驱动告知hypervisor驱动的起始地址和大小,然后在监控视图里面赋予其执行权限。再通过监控驱动调用vmfunc,切到监控视图,此时因为监控视图没有执行权限,所以会造成ept violation,我在handle_ept_violation利用exit_qualification判断是否为exec fault,若是,则inject #VE。然后在监控驱动里面看能否捕获到这次产生的#VE

2018-12-16 #VE捕获

学校

  • 师兄,我想先看看#VE触发能否成功,所以直接用not present触发#VE,然后在切换到另一个视图,让它在监控视图里面继续运行,所以没有对权限进行相关的修改 。

华为

  • #VE被触发要同时满足:

    1、“EPT-violation #VE” VM-execution control set 1.

    2、63 bit of EPT paging-structure entries set 0.

    3、CR0.PE = 1

    4、logical processor is not in the process of delivering an event through IDT

    5、32 bits at offset 4 in virtualization-exception information are all 0.

    6、set physical address of virtualization-exception information area in vmcs’s virtualization-exception information address.

  • 第6步你在Hypervisor里面是怎么设置的,把相关的代码贴给我看下

学校

enum vmcs_field{
    ....
   VIRTUAL_EXCEPTION_INFO    =    0x0000202a,
    VIRTUAL_EXCEPTION_INFO_HIGH = 0x0000202b,
  ...
}

int kvm_emulate_hypercall(struct kvm_vcpu *vcpu)
{
    unsigned long nr, a0, a1, a2, a3, ret;
    int op_64_bit, r;

    r = kvm_skip_emulated_instruction(vcpu);

    if (kvm_hv_hypercall_enabled(vcpu->kvm))
        return kvm_hv_hypercall(vcpu);

    nr = kvm_register_read(vcpu, VCPU_REGS_RAX);
    a0 = kvm_register_read(vcpu, VCPU_REGS_RBX);
    a1 = kvm_register_read(vcpu, VCPU_REGS_RCX);
    a2 = kvm_register_read(vcpu, VCPU_REGS_RDX);
    a3 = kvm_register_read(vcpu, VCPU_REGS_RSI);

    trace_kvm_hypercall(nr, a0, a1, a2, a3);
    op_64_bit = is_64_bit_mode(vcpu);
    if (!op_64_bit) {
        nr &= 0xFFFFFFFF;
        a0 &= 0xFFFFFFFF;
        a1 &= 0xFFFFFFFF;
        a2 &= 0xFFFFFFFF;
        a3 &= 0xFFFFFFFF;
    }

    if (kvm_x86_ops->get_cpl(vcpu) != 0) {
        ret = -KVM_EPERM;
        goto out;
    }
    switch (nr) {
    case KVM_HC_VAPIC_POLL_IRQ:
        ret = 0;
        break;
    case KVM_HC_KICK_CPU:
        kvm_pv_kick_cpu_op(vcpu->kvm, a0, a1);
        ret = 0;
        break;
    case kVM_HC_STORE_VE_GPA:
        printk(KERN_ERR ">>>>>>>>>> the ve gpa is %x<<<<<<\n",a0);
        kvm_vcpu_store_ve_gpa(vcpu,a0);
        ret =0;
        break;
    default:
        ret = -KVM_ENOSYS;
        break;
    }
out:
    if (!op_64_bit)
        ret = (u32)ret;
    kvm_register_write(vcpu, VCPU_REGS_RAX, ret);
    ++vcpu->stat.hypercalls;
    return r;
}

void kvm_vcpu_store_ve_gpa(struct kvm_vcpu *vcpu,gpa_t gpa)
{
    kvm_pfn_t pfn=gpa_to_pfn(vcpu,gpa);
    vmcs_write16(EPTP_INDEX,vcpu->arch.mmu.current_ept_index);
    vmcs_write64(VIRTUAL_EXCEPTION_INFO,pfn<<12);
}

u64 gpa_to_pfn(struct kvm_vcpu *vcpu,gpa_t gpa)
{
    kvm_pfn_t pfn;
    int r;
    
    gfn_t gfn = gpa >> PAGE_SHIFT;
    int write = 0;
    bool map_writable;
    bool prefault=false;
    smp_rmb();
    try_async_pf(vcpu, prefault, gfn, gpa, &pfn, write, &map_writable);
    return pfn;
}

华为

  • HookVE这个函数感觉有问题,下面是我的代码,给你参考一下
IdtEntry = idt_entry(IdtBase, X86_TRAP_PF);
cs = IdtEntry->u.Selector;
IdtEntry = idt_entry(IdtBase, X86_TRAP_VE);
pack_entry(IdtEntry, cs, (ULONG64)__gate_entry);
static __inline VOID pack_entry(PKIDTENTRY64 entry, USHORT selector, ULONG64 addr)
{
     entry->u.OffsetLow = addr & 0xFFFF;
     entry->u.Selector = selector;
     entry->u.Type = GATE_INTERRUPT;
     entry->u.Dpl = 0;
     entry->u.IstIndex = 0;
     entry->u.Present = 1;
     entry->u.OffsetMiddle = (addr >> 16) & 0xFFFF;
     entry->u.OffsetHigh = (addr >> 32);
     entry->u.Reserved0 = 0;
     entry->u.Reserved1 = 0;
}
#define GATE_INTERRUPT       0xE
static __inline PKIDTENTRY64 idt_entry(ULONG64 base, USHORT n)
{
     PKIDTENTRY64 table = (PKIDTENTRY64)base;
     return &table[n];
}
  • 你的gpa_to_pfn这个函数也,怎么说呢,很无语。下面是我的代码:
void vmx_set_ve_info(struct kvm_vcpu *vcpu, gfn_t gfn)  
  {  
      kvm_pfn_t pfn = gfn_to_pfn(vcpu->kvm, gfn);  
      kvm_get_pfn(pfn);   //pin the page.  
        
      vmcs_write64(VIRT_EXCEPTION_INFO, pfn << PAGE_SHIFT);  
  }  

2018-12-26~2-25 QEMU和KVM交互

学校

  • 袁师兄,您好。我对于Qemu和kvm交互的两个接口不是很能理解,请问一下

    1.在kvm中的handle_v2e_ept_violation函数中处理恶意代码产生的EPT violation具体要做什么,如果只是转发给Qemu的话,gpa和exit_qulification这个两个参数有什么用呢。

    1. Qemu接口int kvm_handle_v2e (CPUState *cpu,struct kvm_run *run)中的“模拟执行”指的是利用tcg,具体来说tcg_cpu_exec这个函数来执行吗。

    希望袁师兄抽空能指点我一下。

华为

  • 1、 EPT violation 有很多,根据gpa和 exit_qulification,查找access_bitmap,就可以判断这次异常是不是恶意代码执行产生的,即是不是需要转给QEMU,不然你怎么判断是否需要转给QEMU呢?

  • 2、 模拟执行后面再考虑,现在先把前面这个转给QEMU的这条路打通,后面可不仅仅只是调用这个函数执行一些这么简单。

    PS:赶紧把前面这些接口实现好,更新一下代码。一步一步来,先别考虑那么多。

    1、修改kvm_mmu_get_page,在分配新的MMU页的时候就把该页对应的所有页表项里面的63bit都设置上。

    2、修改tdp_page_fault、关闭大页映射:

    force_pt_level = !check_hugepage_cache_consistency(XXX);

    force_pt_level = true; //强制使用小页映射

    3、windows驱动里面很多可以参考xen winpv driver 的代码,比如hast tableMmGetPhysicalMemoryRangesAuxKlibQueryModuleInformation等等我都是参考的这个项目里面的代码。

    4、windows驱动尽量逻辑简单,用稳定的windows接口实现。通过注册进程创建和模块加载回调函数来获取进程和模块的信息。

    4、ve info里面的第三个字段exit_qualification很有用,通过这个字段可以判断当前产生VE的原因,以及当前物理页对应的EPT页表项里面的权限设置情况。

/* #VE (ept violation) bits (Exit qualification) and suppress bit  */
#define EPT_VE_READFAULT          0x1           /* It is a read fault */
#define EPT_VE_WRITEFAULT         0x2           /* It is a write fault */
#define EPT_VE_FETCHFAULT         0x4           /* It is a fetch fault */
#define EPT_VE_READABLE           0x8           /* EPTE is readable */
#define EPT_VE_WRITABLE           0x10          /* EPTE is writable  */
#define EPT_VE_EXECUTABLE         0x20          /* EPTE is executable  */

前三个用来判断当前触发VE的原因,后三个用来判断当前EPTE里面的权限设置情况(比如是DATA、还是系统代码,不会是恶意代码,因为恶意代码会走EPT violation)

__ept_handle_violation主要就是根据当前进程信息、模块信息、以及veinfo->exit,来动态调整当前物理页的权限。因为现在是转给QEMU执行,所以struct cpu_user_regs作用不大了,可以不用太关注。

不用设置全部物理内存为DATA权限,也不要#VE了,直接在驱动里面监控到恶意代码的加载,然后设置恶意代码的不可执行权限,恶意代码执行时即触发EPT violation进hypervisor,然后转给QEMU模拟执行。

1-11

QEMU加速器(Accelerator)包括TCG和KVM两种:

1、TCG:通过动态二进制翻译进行软件模拟,所有的VCPU都使用同一个QEMU线程(qemu/cpus.c: qemu_tcg_cpu_thread_fn)进行TCG模拟执行(因为目前的TCG设计还不支持多线程)。

2、KVM:通过硬件辅助虚拟化进行硬件加速,每个VCPU都有一个独立的QEMU线程(qemu/cpus.c: qemu_kvm_cpu_thread_fn)。

除了VCPU对应的线程之外,QEMU还有一个主线程(iothread),负责处理各种设备IO,以及一些worker thread负责处理一些blocking的任务。因为QEMU的核心代码(core QEMU)并不是完全被设计成允许多线程执行的,所以各个线程在执行的时候有个全局的mutex保证并发同步。参考:http://blog.vmsplice.net/2011/03/qemu-internals-overall-architecture-and.html

我们现在需要扩展QEMU的KVM加速器,能够动态地由KVM加速器切换到TCG加速器执行(恶意代码执行的时候):

1、qemu_kvm_cpu_thread_fn 负责KVM加速器中的VCPU线程,该线程主要是在一个while循环中不断调用kvm_cpu_exec,该函数调用kvm_vcpu_ioctl通知kvm Hypervisor开启硬件辅助虚拟化执行,当执行时产生一些IO事件需要QEMU进行处理时,该函数即返回到kvm_cpu_exec,通过switch (run->exit_reason),根据‘exit_reason’知道退出的原因并进行处理。

2、qemu_tcg_cpu_thread_fn 负责TCG加速器中的所有VCPU的执行,该线程和KVM类似,同样通过一个while循环不断调用tcg_exec_all,该函数循环执行每个VCPU,调用tcg_cpu_exec->cpu_exec,即开始动态二进制翻译执行。

3、CPUState这个数据结构代表VCPU的状态,TCG和KVM都是基于该数据结构。

我们在扩展时,是基于KVM加速器,即每个VCPU都有单独的执行线程,TCG线程并没有执行也没有初始化。我们需要修改KVM线程的执行逻辑和TCG的功能代码:

1、增加一个exit_reason,表示因为恶意代码的执行触发EPT violation 退出KVM,在这个点开始进行模拟执行,先需要调用kvm_cpu_synchronize_state(cpu); 同步一下KVM和QEMU两边的CPUState状态,然后调用tcg_cpu_exec开始TCG动态翻译执行,但要注意,KVM里面每个VCPU都是有单独的线程,所以调用tcg_cpu_exec的时候需要用mutex同步,避免多VCPU并发执行该函数。

2、调用tcg_cpu_exec之前,需要先完成tcg必须的初始化(tcg_exec_init),因为TCG此时是没有初始化的。

3、QEMU会使用tcg_enabled这个函数来判断当前是使用KVM还是TCG,但是对于我们来说,只有当恶意代码在模拟执行时,TCG才是开启的,其它时候TCG都是关闭的(KVM开启),所以要注意QEMU调用tcg_enabled的地方,我们的修改是否会影响原本QEMU的逻辑。

4、需要修改cpu_exec,在TB翻译的时候,如果此时已经执行到正常代码(非恶意代码),即需要停止TCG模拟执行,回到KVM执行,然后调用kvm_arch_put_registers将QEMU的cpu状态同步到KVM。

5、其它需要特殊处理的地方。

1-18

你们更新的代码我大致看了一下,整体是没有大问题,我把我的一下思路和建议总结一下:(PS:两边QEMU版本不一致,我这边是2.6.2,这个是产品的版本,不能改。你们使用的2.11.2,这个版本QEMU已经开始实现TCG的多线程支持,所以两个版本代码有点差别了,最好能统一版本,如果比较麻烦也没关系,但要注意的是,我这边所有的分析都是基于2.6.2)

1、你们使用了两个变量kvm_allowed/tcg_allowed表示KVM模式和TCG模式。我的建议是,在逻辑上我们应该认为始终QEMU都是处于KVM模式,TCG只是在KVM模式中的“很短的一个过程”,我建议在CPUState里面增加一个字段表示当前VCPU是否处于“这个过程”当中:

  //include/qom/cpu.h  
  struct CPUState {  
      /*< private >*/  
      DeviceState parent_obj;  
      /*< public >*/  
    
      int nr_cores;  
      int nr_threads;  
      int numa_node;  
   
     struct QemuThread *thread;  
     ...  
     bool throttle_thread_scheduled;  
   
     bool kvm_on_emulation;  
   
     /* Note that this is accessed at the start of every TB via a negative 
        offset from AREG0.  Leave this field at the end so as to make the 
        (absolute value) offset as small as possible.  This reduces code 
        size, especially for hosts without large memory offsets.  */  
     uint32_t tcg_exit_req;  

因为对于多VCPU的系统,VCPU1可能处于TCG按需模拟状态,而VCPU2处于KVM模式中执行,所以在CPUState里面增加字段表示会更好。

tcg_enabled()这个函数应该一直是返回false,所以需要修改一下这个函数:

  bool tcg_enabled(void)  
  {  
      //return tcg_ctx.code_gen_buffer != NULL;  
      return tcg_enable_flag;  
  }  

不再通过tcg_ctx.code_gen_buffer来判断tcg是否开启(因为在KVM模式下我们也需要分配这个内存),而是用一个单独的变量来表示,TCG模式下这个变量为true,KVM模式下这个变量为false。

另外,qemu_cpu_kick这个函数也需要调整一下,主线程可能会kick VCPU(让VCPU跳出TCG模拟执行或者KVM VMX执行),如果这个时候VCPU1处于TCG、VCPU2处于KVM,这两个VCPU的kick方式是不相同的:

  //cpus.c  
  void qemu_cpu_kick(CPUState *cpu)  
  {  
      qemu_cond_broadcast(cpu->halt_cond);  
      if (tcg_enabled() || cpu->kvm_on_emulation) {  
          qemu_cpu_kick_no_halt();  
      } else {  
          qemu_cpu_kick_thread(cpu);  
      }  
 }  

2、内存相关的初始化,你们通过if (1||tcg_enabled()) 对TCG相关的AddressSpace进行了初始化,我认为不需要进行相关的初始化,这样可能改变了KVM的状态,除非我们能够完全确认这些代码不会改变KVM原本的状态,而且是TCG必须的初始化,我们才需要去执行它,比如对于tcg_ctx等变量的初始化。

3、kvm_handle_v2e这个函数你们一直都是返回0的,应该根据cpu_exec的返回值进行返回,返回非0时是会跳出kvm_cpu_exec的循环,回到qemu_kvm_cpu_thread_fn里面的循环的,这个是必要的,比如该VCPU被kick的时候,就会跳出来执行qemu_kvm_wait_io_event.

1-25

我这边也尝试动态切换calc.exe程序到QEMU模拟执行,同样也遇到了同一个物理页反复触发EPT violation切到QEMU,calc.exe程序一直卡死在那里,总结一下我的调试结果:

IN:
0x00000000ff12b9b8: sub  $0x28,%rsp
0x00000000ff12b9bc: callq 0xff12af28

切到QEMU执行的第一个TB为如上所示两行代码,第一行能够正常执行,第二行代码执行会产生页异常并进入内核,然后会被切回KVM执行。但是很奇怪,切回KVM之后立马又会再次触发EPT violation进入QEMU,RIP一直都是第二条指令的地址,即0x00000000ff12b9bc,

然后QEMU同样再次触发页异常,切回KVM,如此反复一直卡死在这个地方,在触发页异常的地方我把log打印出来如下:

IN:

0x00000000ff12b9bc: callq 0xff12af28



MMU fault: addr=18f748 **mmu_idx** = 0 w=1 u=0 eip=00000000ff12b9bc

pml4e == 2d000002246f867, fee8000, 0

pdpe == 1400000103f6867, 2246f000, 0

pde == 13000000fbf7867, 103f6000, 0

**pte == 8350000010502867**, fbf7c78, 0

prot == 0

EXCP0E_PAGE: error_code = 3, vaddr = 18f748

check_exception old: 0xffffffff new 0xe

   1: v=0e e=0003 i=0 cpl=3 IP=0033:00000000ff12b9bc pc=00000000ff12b9bc SP=002b:000000000018f750 CR2=000000000018f748

RAX=00000000778e6520 RBX=0000000000000000 RCX=000007fffffdd000 RDX=00000000ff12b9b8

RSI=0000000000000000 RDI=0000000000000000 RBP=0000000000000000 RSP=000000000018f750

R8 =000007fffffdd000 R9 =00000000ff12b9b8 R10=0000000000000000 R11=0000000000000000

R12=0000000000000000 R13=0000000000000000 R14=0000000000000000 R15=0000000000000000

RIP=00000000ff12b9bc RFL=00010206 [-----P-] CPL=3 II=0 A20=1 SMM=0 HLT=0

ES =002b 0000000000000000 ffffffff 00c0f300 DPL=3 DS  [-WA]

CS =0033 0000000000000000 00000000 0020fb00 DPL=3 CS64 [-RA]

SS =002b 0000000000000000 ffffffff 00c0f300 DPL=3 DS  [-WA]

DS =002b 0000000000000000 ffffffff 00c0f300 DPL=3 DS  [-WA]

FS =0053 00000000fffe0000 00003c00 0040f300 DPL=3 DS  [-WA]

GS =002b 000007fffffde000 ffffffff 00c0f300 DPL=3 DS  [-WA]

LDT=0000 0000000000000000 000fffff 00000000

TR =0040 fffff80000b96080 00000067 00008b00 DPL=0 TSS64-busy

GDT=   fffff80000b95000 0000007f

IDT=   fffff80000b95080 00000fff

CR0=80050031 CR2=000000000018f748 CR3=000000000fee8000 CR4=000406f8

DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000

DR6=00000000fffe0ff0 DR7=0000000000000400

CCS=0000000000000004 CCD=000000000018f750 CCO=EFLAGS

EFER=0000000000000d01

从log里面我们可以看到,应该是callq这条指令会把返回地址压栈,即去写栈内存,从而触发了内存写的页异常,但是正常情况此时的堆栈应该是映射了而且是可写的,不会触发页异常才对,因此我把页异常触发时的页表内容打印出来,可以看到图中红色加粗部分的pte内容,即页表中该堆栈页是Prent的并且可读可写,结合QEMU中的代码分析(target-i386/helper.c: x86_cpu_handle_mmu_fault),最后发现是mmu_idx这个值有问题,应该是1(MMU_USER_IDX)而不应该是0(MMU_KSMAP_IDX),因为这个错误的mmu_idx,本来不会产生页异常的指令在QEMU执行时产生了页异常,然后切换KVM中,内核处理该异常又回到该指令重新执行,再次进入QEMU又触发页异常,如此反复。

具体为什么mmu_idx会出错,原因还不清楚~


问题定位到了,原因是CPUState->env->hflags 里面有个标志位HF_SOFTMMU_MASK,在切到TCG的时候需要设置上,不然后续在TB翻译代码块的时候,mmu_index会被错误的设置成0(target-i386/translate.c: gen_intermediate_code):

/* select memory access functions */

  dc->mem_index = 0;

  if (flags & HF_SOFTMMU_MASK) {           //如果这个标志位没有设置上,就不会进这个里面,mem_index == 0

        dc->mem_index = cpu_mmu_index(env, false);

}

PS:hflags为hiden flags,是QEMU使用的内部标志。

我这边目前看起来原型还比较稳定,没有大问题。