首页
社区
课程
招聘
[原创][HCTF-2018] Rev-Spiral -出题思路 及VT在ctf中的初尝试
2018-11-12 19:07 9399

[原创][HCTF-2018] Rev-Spiral -出题思路 及VT在ctf中的初尝试

2018-11-12 19:07
9399

0x00 序

  • Auth: vvv_347
  • Date: 11/12/2018
  • From: Vidar-Team
 

本题的主要思路是用VT中的VMEXIT事件构建了VM(此处指加密逻辑)的Dispatch。
对于大部分逆向选手来讲,VT可能有点陌生,其实这也是十年前的技术了。
下面先简单介绍下VT。

0x01 破

Intel VT-x(简称VT) 是Intel芯片支持的硬件虚拟化(Hardware Enabled Virtualization)技术,
与之相对的,AMD芯片支持的HEV技术叫做AMD-V(or SVM)。

000.VT-x中的VMM Hypervisor到底是什么

我们知道有Type-1和Type-2两类Hypervisor
Type-1跑在硬件上,Type-2跑在OS上。

 

众所周知,日常使用的应用程序跑在Ring3上,驱动内核跑在Ring0,
而HEV中构建出一个新的特权层,它的权限要等于或者大于OS的权限,称为"Ring -1"层。

 

这个特权层我们称为VMX,而VMX又分为两种模式,各自跑着不同的逻辑:
VMX root: 此权限下跑着的逻辑代码我们命名为VMM(Virtual Machine Monitor),或者叫做Hypervisor。
VMX non-root: 此权限下跑着的逻辑代码我们称为客户机。

 

下图为VMX root层跑着的VMM:

 

那VMM的作用是什么呢,答:实现虚拟硬件与真实硬件的通信与一些事件处理。
当然这么说会很让人困惑,举个现实的例子:
Vmware中跑了一个Win7 x64,又跑了一个Win xp,这两个虚拟机应该是互相是不知道对方的,或者说是不应该干扰对方的。
如何实现这个功能,就需要VMM来进行调度。

 

这里其实就引出的新的名词,我们把上面提到的虚拟机称为VM(Virtual Machine)。
然后就引出了对立的概念,以及存在类似的名词替代。

  • VMM <------------------> VM
  • host <------------------> guests
  • 宿主机 <------------------> 客户机
 

以上的名词在列上都可替代使用,具体画个图是这样的(先不管VM指令与事件部分):

 

大体来看,就是VMM在管理着两个VM(Guest0 和 Guest 1)。

 

回到开始的问题:
既然叫做硬件虚拟化,那么是不是所有的VT都是Type-1呢?其实不一定

 


我们知道KVM,Xen都是Hypervisor,也就是VMM,但前者是Type-2的,后者是Type-1的,这又是为什么呢?
这其实是由于HEV启动Guests(即VM,客户机;后面不赘述了)的方式不同,一共有3种

第1种:存在HostOS,后启动Hypervisor的guests的启动过程


简单来说,KVM这个Hypervisor是先启动HostOS(也就是我们熟知的Linux等OS,MBR最开始引导的OS),
后启动Hypervisor。
所以KVM是Type-2的Hypervisor。
其中这种很有意思,我们知道Hypervisor的权限要大于或等于OS,
在Type-2中,HostOS相当于创建了比自己权限更高的Hypervisor。
此处我认为权限虽然更高,但未必可以管理HostOS。

第2种:存在HostOS,先启动Hypervisor的guest的启动过程


可以发现,MBR引导的是Hypervisor,而不是HostOS,
HostOS(即Domain0)是由Hypervisor引导的。
所以,Hypervisor在先,HostOS在后。
即Type-1的Hypervisor。

第3种,不存在HostOS,启动Hypervisor的guest


没有了HostOS,MBR只能引导Hypervisor来创建guests。
现在还没有这样的虚拟机,所以称为Unknown Virtual Machine。
如果出现了,估计就是Type-3了吧(X

 

所以,Hypervisor的两类,准确说是根据MBR的引导来决定的:
MBR先引导Hypervisor:Type-1
MBR先引导HostOS:Type-2

 

这样我们搞清了一些术语与VTx的初窥外观,知道了VTx在整个Ring中是什么样子的。
结合下实际,我们日常用的Vmware,在当前这个版本肯定是使用了Vtx技术,(没有Vtx技术时,Vmware都是用软件来模拟硬件)。
我们先启动Win10,在R3上启动了Vmware14,此时就启动了Type-2型的Hypervisor,来调度着我们打开的Ubuntu16.04和Win7 x64。对于Ubuntu和Win7,或者对于我们来说,VMM层是透明的,我们以为这两个虚拟机都跑在真实的硬件上。

001.VMM与VM的具体交互


还是这张图,这是VMM与VM交互的一个概述

  • 1.指令VMXON,进入VMM
  • 2.指令VMLAUNCH,启动VM
  • 3.事件#VMEXIT,退出VM,回到VMM
  • 4.指令VMRESUME,回到VM
  • 5.指令VMXOFF,退出VMM
 

其中,比较重要的有3个部分,进入VMM,#VMEXIT,退出VMM。

01.进入VMM(开启虚拟机):

  • 1.查是否支持VT
  • 2.设置CR4.VMXE[bit 13]=1
  • 3.分配VMXON
  • 4.初始化VMXON
  • 5.执行VMXON ===> 进入VMM
  • 6.分配VMCS(Virtual Machine Control Structure),即VM控制块
  • 7.初始化VMCS: 指令: VMCLEAR,设置启动状态为空(clear)
  • 8.加载VMCS: 指令: VMPTRLD,设置启动状态为激活
  • 9.填充VMCS
  • a.指令VMXLAUNCH,启动虚拟机

02.#VMEXIT 事件处理:

  1. VMM中VMXLAUNCH启动VM
  2. VM中发生#VMEXIT事件(无条件产生与有条件产生两种,CPUID指令属于无条件产生),回到VMM
  3. VMM处理后,指令VMRESUME回到VM,这个事件叫做#VMENTRY

03.退出VMM:

  虽然没有图,但是当VMXOFF后,如果需要继续运行HostOS,必须人工还原OS运行环境。
很多时候是指人工将VMCS保存的虚拟机环境填充为当前环境。

002.VT中的指令与结构

01. 指令可以分为3类:

1. 维护VMCS结构体的指令


2. 与VMM相关的指令

3. 管理VT相关TLB的控制指令

02. 结构

上面我们在进入VMM中提到了,在VMXLAUNCH之前还需要填充VMCS结构,这个东西真的十分庞大,共有6个组成部分

003.EPT的产生原因及概念

现在稍微考虑详细一点,会发现一个问题:
回顾在学逆向,Win32时,我们知道每个进程独有一个4G的虚拟地址空间(or 线性地址空间),
以进程的视角来考虑,看起来自己把4G的内存全占了,实际上是因为在x86保护模式中:
虚拟地址(or 线性地址) ===[分页机制]===> 物理地址
那么在VTx中,这个分页机制就会变得更加复杂一些,
因为反过来想,如果每个VM都以为自己在真实的硬件上跑,肯定以为自己"独占了"硬件的物理地址。
所以VT中引入了EPT(Extended Page Table)机制,扩展页表翻译技术。

 


简单来说,需要EPT两个东西:CR3和EPTP,
CR3 指向---> gPT(guest Page Table),作用:VM线性地址 trans_to VM物理地址
EPTP 指向---> EPT页表,作用:VM物理地址 trans_to 真实物理地址

0x02 急

本题的所有代码已经开源:https://github.com/vvv-347/hctf2018_Rev_spiral
VT部分的代码参考了小宝,传送门:https://bbs.pediy.com/thread-211973.htm

 

本题分为两个部分,spiral.exe和spiral_core.sys。
spiral.exe主要目的是加载驱动,加密则是附带的功能。
spiral.exe检查输入第1部分成功后,会将输入的第2部分写到申请的内存中,同时计算checksum,将spiral_core.sys写到本地并加载。

 

spiral_core.sys则是有主要的加密逻辑。

000.SetVMCS:

首先在SetVMCS中,有关键的Guest与Host的入口地址

Vmx_VmWrite(GUEST_RIP,Asm_GetGuestReturn());// Guest入口地址
Vmx_VmWrite(HOST_RIP,(ULONG)&Asm_VMMEntryPoint);//Host入口地址

001.VMM(Host):

extern "C" void VMMEntryPoint()
{
    ULONG ExitReason;
    ULONG ExitInstructionLength;
    ULONG GuestResumeEIP;

    ExitReason = Vmx_VmRead(VM_EXIT_REASON);
    ExitInstructionLength = Vmx_VmRead(VM_EXIT_INSTRUCTION_LEN);

    g_GuestRegs.esp = Vmx_VmRead(GUEST_RSP);
    g_GuestRegs.eip = Vmx_VmRead(GUEST_RIP);
    g_GuestRegs.cr3 = Vmx_VmRead(GUEST_CR3);
    if(Board_Init == FALSE)
    {
        //Log("VMM_handler_board_init",0);
        HandleBoardInit();
        Board_Init = TRUE;
    }

    switch(ExitReason)
    {
    case EXIT_REASON_CPUID:
        {
            HandleCPUID();
            break;
        }
    case EXIT_REASON_INVD:
        {
            HandleInvd();
            break;
        }
    case EXIT_REASON_VMCALL:
        {
            HandleVmCall();
            break;
        }
    case EXIT_REASON_MSR_READ:
        {
            HandleMsrRead();
            break;
        }
    case EXIT_REASON_MSR_WRITE:
        {
            HandleMsrWrite();
            break;
        }
    case EXIT_REASON_CR_ACCESS:
        {
            HandleCrAccess();
            break;
        }
    default:
        break;
    }

Resume:
    GuestResumeEIP = g_GuestRegs.eip+ExitInstructionLength;
    Vmx_VmWrite(GUEST_RIP,GuestResumeEIP);
    Vmx_VmWrite(GUEST_RSP,g_GuestRegs.esp);
}

初次进入VMM时,会对大小为81的数组进行一次移位变换(实际上是个数独board),
之后根据VMEXIT事件分发到各自的Handle。

002.VM(Guest逻辑):

Asm_SetupVMCS Proc
    cli
    mov GuestESP,esp

    mov EntryEAX,eax
    mov EntryECX,ecx
    mov EntryEDX,edx
    mov EntryEBX,ebx
    mov EntryESP,esp
    mov EntryEBP,ebp
    mov EntryEDI,edi
    mov EntryESI,esi
    pushfd
    pop EntryEflags
    mov eax, 0CAFEBABEh
    cpuid
    mov eax, 04437h
    invd

    call Asm_RunToVMCS

    push EntryEflags; <----------执行Guest逻辑
    popfd
    mov eax,EntryEAX
    mov ecx,EntryECX
    mov edx,EntryEDX
    mov ebx,EntryEBX
    mov esp,EntryESP
    mov ebp,EntryEBP
    mov esi,EntryESI
    mov edi,EntryEDI

    mov esp,GuestESP
    sti

    mov eax, 0DEADBEEFh
    cpuid                ; vm_opcode解码, 同时触发宿主机VM的board初始化解码(优先)
    mov eax, 04437h
    invd                ; vm_opcode乱序
    mov eax, 0174h
    mov ecx, 0176h
    rdmsr                ; sokudo_board解码
    ret
Asm_SetupVMCS Endp

VM(Guest)的入口点是Asm_RunToVMCS返回地址,即之前所有的VMEXIT事件指令(cpuid, invd)都不会触发。
真正进入VM(Guest)后,执行第一次特权指令后会进入VMM执行上面提到的数独第一次decode。

003.VM in VM(Guest)

本次一共涉及4个类型的Handle:

  • HandleCPUID: vm_opcode解码
  • HandleInvd: vm_opcode顺序调整
  • HandleMsrRead: 数独Item移位
  • HandleVmCall: VM实现的主要方式
 

VM(Guest)继续执行:

mov eax, 0DEADBEEFh
cpuid                ; vm_opcode解码, 同时触发宿主机VM的board初始化解码(优先)
mov eax, 04437h
invd                ; vm_opcode乱序
mov eax, 0174h
mov ecx, 0176h
rdmsr                ; sokudo_board解码

执行Asm_SetupVMCS的后半段,来到主加密逻辑RegVM_in_VMX

VOID RegVM_in_VMX(){
    //Log("进入RegVm_in_VMX",0);
    Vmx_ReadMsr(0x176);
    Vmx_Invd(0x4433);
    Vmx_VmCall(0x30133403);
    Vmx_VmCall(0x3401cc01);
    Vmx_VmCall(0x36327a09);
    Vmx_VmCall(0x3300cc00);
    Vmx_VmCall(0x3015cc04);
    Vmx_VmCall(0x35289d07);
    Vmx_VmCall(0x3027cc06);
    Vmx_VmCall(0x3412cc03);
    Vmx_VmCall(0x3026cd06);
    Vmx_VmCall(0x34081f01);
    Vmx_VmCall(0x3311c302);
    Vmx_VmCall(0x3625cc05);
    Vmx_VmCall(0x3930cc07);
    Vmx_VmCall(0x37249405);
    Vmx_VmCall(0x34027200);
    Vmx_VmCall(0x39236b04);    
    Vmx_VmCall(0x34317308);
    Vmx_VmCall(0x3704cc02);
    Vmx_Invd(0x4434);
    Vmx_VmCall(0x38531f11);
    Vmx_VmCall(0x3435cc09);
    Vmx_VmCall(0x3842cc0a);
    Vmx_VmCall(0x3538cb0b);
    Vmx_VmCall(0x3750cc0d);
    Vmx_VmCall(0x3641710d);
    Vmx_VmCall(0x3855cc0f);
    Vmx_VmCall(0x3757cc10);
    Vmx_VmCall(0x3740000c);
    Vmx_VmCall(0x3147010f);
    Vmx_VmCall(0x3146cc0b);
    Vmx_VmCall(0x3743020e);
    Vmx_VmCall(0x36360f0a);
    Vmx_VmCall(0x3152cc0e);
    Vmx_VmCall(0x34549c12);
    Vmx_VmCall(0x34511110);
    Vmx_VmCall(0x3448cc0c);
    Vmx_VmCall(0x3633cc08);
    Vmx_Invd(0x4437);
    Vmx_VmCall(0x3080cc17);
    Vmx_VmCall(0x37742c16);
    Vmx_VmCall(0x3271cc14);
    Vmx_VmCall(0x3983cc19);
    Vmx_VmCall(0x3482bb17);
    Vmx_VmCall(0x3567bc15);
    Vmx_VmCall(0x3188041a);
    Vmx_VmCall(0x3965cc12);
    Vmx_VmCall(0x32869c19);
    Vmx_VmCall(0x3785cc1a);
    Vmx_VmCall(0x3281cc18);
    Vmx_VmCall(0x3262dc14);
    Vmx_VmCall(0x3573cc15);
    Vmx_VmCall(0x37566613);
    Vmx_VmCall(0x3161cc11);
    Vmx_VmCall(0x3266cc13);
    Vmx_VmCall(0x39844818);
    Vmx_VmCall(0x3777cc16);
    Vmx_VmCall(0xffeedead);
}

再一次对数独进行移位变换,此时数独移位decode全部完成。
除了最后的Check的vm_call,因为含有正反输入,所以共54位。
54位分成3组,每组18个vm_call,且每组由Invd分割,Invd的目的是重新生成vm_opcode顺序。

void HandleVmCall() {
    ULONG vm_code = ((g_GuestRegs.eax) >> 24) & 0xff;
    ULONG dst_pos_orig = ((g_GuestRegs.eax) >> 16) & 0xff;
    ULONG src_spec = ((g_GuestRegs.eax) >> 8) & 0xff;
    ULONG src_pos = (g_GuestRegs.eax) & 0xff;
    ULONG JmpEIP;

    ULONG dst_pos = ((dst_pos_orig >> 4) & 0x0f) * 9 + (dst_pos_orig & 0x0f);
    if (src_spec == 0xcc) {
        src_addr = input_d;
    }
    else {
        src_addr = input_d_rev;
    }
    if (vm_code == r_vm_opcode[0]) {
        //Log("MOV: value of eax", vm_code);
        dst_addr[dst_pos] = src_addr[src_pos];
        show_byte_value_and_pos(dst_addr[dst_pos], dst_pos);
    }
    else if (vm_code == r_vm_opcode[1]) {
        //Log("ADD: value of eax", vm_code);
        dst_addr[dst_pos] += src_addr[src_pos];
        dst_addr[dst_pos] &= 0xff;
        show_byte_value_and_pos(dst_addr[dst_pos], dst_pos);
    }
    else if (vm_code == r_vm_opcode[2]) {
        //Log("SUB: value of eax", vm_code);
        dst_addr[dst_pos] -= src_addr[src_pos];
        dst_addr[dst_pos] &= 0xff;
        show_byte_value_and_pos(dst_addr[dst_pos], dst_pos);
    }
    else if (vm_code == r_vm_opcode[3]) {

        //Log("DIV: value of eax", vm_code);
        dst_addr[dst_pos] /= src_addr[src_pos];
        dst_addr[dst_pos] &= 0xff;
        show_byte_value_and_pos(dst_addr[dst_pos], dst_pos);
    }
    else if (vm_code == r_vm_opcode[4]) {
        //Log("MUL: value of eax", vm_code);
        dst_addr[dst_pos] *= src_addr[src_pos];
        dst_addr[dst_pos] &= 0xff;
        show_byte_value_and_pos(dst_addr[dst_pos], dst_pos);
    }
    else if (vm_code == r_vm_opcode[5]) {
        //Log("XOR: value of eax", vm_code);
        dst_addr[dst_pos] ^= src_addr[src_pos];
        dst_addr[dst_pos] &= 0xff;
        show_byte_value_and_pos(dst_addr[dst_pos], dst_pos);
    }
    else if (vm_code == r_vm_opcode[6]) {
        //Log("MIX: value of eax", vm_code);
        ULONG num = src_addr[src_pos];
        num += src_addr[src_pos - 1];
        num -= src_addr[src_pos + 1];
        dst_addr[dst_pos] ^= num;
        dst_addr[dst_pos] &= 0xff;
        show_byte_value_and_pos(dst_addr[dst_pos], dst_pos);

    }
    else if (vm_code == r_vm_opcode[7]) {
        //Log("SHIFT: value of eax", vm_code);
        ULONG num3 = src_addr[src_pos] << 4;
        dst_addr[dst_pos] ^= num3;
        dst_addr[dst_pos] &= 0xff;
        show_byte_value_and_pos(dst_addr[dst_pos], dst_pos);
    }
    else if (vm_code == r_vm_opcode[8]) {
        //Log("OR: value of eax", vm_code);
        dst_addr[dst_pos] |= src_addr[src_pos];
        dst_addr[dst_pos] &= 0xff;
        show_byte_value_and_pos(dst_addr[dst_pos], dst_pos);
    }
    else if (vm_code == r_vm_opcode[9]) {
        //Log("MIX2: value of eax", vm_code);
        ULONG num2 = src_addr[src_pos];
        num2 += src_addr[src_pos - 2];
        num2 -= src_addr[src_pos + 2];
        num2 ^= src_addr[src_pos - 1];
        num2 ^= src_addr[src_pos + 1];
        dst_addr[dst_pos] ^= num2;
        dst_addr[dst_pos] &= 0xff;
        show_byte_value_and_pos(dst_addr[dst_pos], dst_pos);
    }
    else if (vm_code == 0xdd) {
        //Log("Stop_VT: value of eax", vm_code);
        JmpEIP = g_GuestRegs.eip + Vmx_VmRead(VM_EXIT_INSTRUCTION_LEN);
        Vmx_VmxOff();
        Asm_AfterVMXOff(g_GuestRegs.esp, JmpEIP);
    }
    else if (vm_code == 0xff) {
        check_enc();
    }
    else {
        Log("DEFAULT: value of eax", vm_code);
    }
}

VM部分就是逆向题中的日常了,
Reg4字节,分为4个部分

  • 1.vm_opcode
  • 2.数独位置
  • 3.选择正向还是反向输入
  • 4.输入位置
 

这样解题思路也很清晰了:
1.先解出数独的结果(手算,z3)。
2.4次Invd会将vm_opcode乱序,可动态或者手动的到vm_opcode每次变换后的顺序。
3.根据vm_opcode对应的Handle,计算出flag。

 

解密脚本详见赞助方bysec的hctf2018官方wp。

0x03 终

本人对于vt的理解只是知道浅显原理,如有失误请各位看雪大手子斧正。
代码有两处bug,第1个是check部分应该是i,不过没有影响逻辑。
第2个是flag的第11位因为正反的vmHandle都是<<4后xor,而且输出位又&0xff,事后发现太zz了:\
最后感谢参加hctf的各位,明年见!


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2019-2-2 09:59 被kanxue编辑 ,原因:
上传的附件:
收藏
点赞8
打赏
分享
最新回复 (9)
雪    币: 32410
活跃值: (18735)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2018-11-15 15:46
2
0
文章很精彩
雪    币: 6314
活跃值: (824)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
二娃 2018-11-15 16:13
3
1
回复可以给你5个币?
雪    币: 19584
活跃值: (60093)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2018-11-15 16:30
4
0
二娃 回复可以给你5个币?
是的。你发好的主题帖,大家也会支持你的。
雪    币: 1330
活跃值: (3667)
能力值: ( LV9,RANK:220 )
在线值:
发帖
回帖
粉丝
黑手鱼 3 2018-11-15 23:14
5
1
辛苦了,我真好需要这个
雪    币: 1087
活跃值: (641)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
小宝来了 1 2018-11-19 16:34
6
0
听学长介绍,特地来这里支持一下~
雪    币: 1361
活跃值: (552)
能力值: ( LV11,RANK:187 )
在线值:
发帖
回帖
粉丝
vvv_347 3 2018-11-20 08:42
7
0
小宝来了 听学长介绍,特地来这里支持一下~
感谢小宝大大的文章!~
雪    币: 1222
活跃值: (48)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
找到组织了 2019-8-27 16:00
8
0
膜拜学长
雪    币: 31
活跃值: (24)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
cook1es 2020-3-30 00:24
9
0
雪    币: 83
活跃值: (1042)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
killpy 2 2020-3-30 14:57
10
0
mark
游客
登录 | 注册 方可回帖
返回