前言

基于windows的内存分页机制,进程间的隔离让我们无法直接读写其他进程的地址空间,又因为copy on write让我们即使patch了系统dll也无法影响其他进程。但是这一根内存条就插在主板上,就不能有个方法能看到内容么?
在裸机上如果没有定制的外设是无解了,但是在虚拟化的框架下,这一切都显得再正常不过。
思路
在虚拟化的框架下,客户机的“内存条”实际上是宿主机上申请的一段普通内存,如果我们能够找到GVA与HVA之间的映射关系,就可以绕开任何限制,直接在宿主机上读写任意位置的客户机内存。
步骤一: 通过CR3寄存器获取页表基址
1 2 3 4 5 6 7 8 | machine_->Pause();bool success = false;for (auto vcpu : machine_->vcpus()) { struct kvm_sregs sregs; if (ioctl(vcpu->fd(), KVM_GET_SREGS, &sregs) < 0) MV_PANIC("KVM_GET_REGS failed");... |
我们先暂停vm后,通过KVM_GET_SREGS即可获取cr3寄存器。
步骤二: 获取KPCR
gs寄存器在R0下存放的是KPCR,在R3下存放的是TEB
1 2 3 4 5 6 7 8 | ...if ((sregs.cs.base & 3) != 0) { MV_LOG("current vcpu=%d is in usermode", vcpu->vcpu_id()); continue;}auto kpcr = (uint8_t*)GuestVAToHostAddress((uint64_t)sregs.gs.base, sregs.cr3);... |
步骤三: 通过KPCR结构计算偏移即可得到KTHREAD
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | //0xb080 bytes (sizeof)struct _KPCR{ union { struct _NT_TIB NtTib; //0x0 struct { union _KGDTENTRY64* GdtBase; //0x0 struct _KTSS64* TssBase; //0x8 ULONGLONG UserRsp; //0x10 struct _KPCR* Self; //0x18 struct _KPRCB* CurrentPrcb; //0x20 struct _KSPIN_LOCK_QUEUE* LockArray; //0x28 VOID* Used_Self; //0x30 }; }; union _KIDTENTRY64* IdtBase; //0x38 ULONGLONG Unused[2]; //0x40 UCHAR Irql; //0x50 UCHAR SecondLevelCacheAssociativity; //0x51 UCHAR ObsoleteNumber; //0x52 UCHAR Fill0; //0x53 ULONG Unused0[3]; //0x54 USHORT MajorVersion; //0x60 USHORT MinorVersion; //0x62 ULONG StallScaleFactor; //0x64 VOID* Unused1[3]; //0x68 ULONG KernelReserved[15]; //0x80 ULONG SecondLevelCacheSize; //0xbc ULONG HalReserved[16]; //0xc0 ULONG Unused2; //0x100 VOID* KdVersionBlock; //0x108 VOID* Unused3; //0x110 ULONG PcrAlign1[24]; //0x118 struct _KPRCB Prcb; //0x180}; //0x700 bytes (sizeof)struct _KPRCB{ ULONG MxCsr; //0x0 UCHAR LegacyNumber; //0x4 UCHAR ReservedMustBeZero; //0x5 UCHAR InterruptRequest; //0x6 UCHAR IdleHalt; //0x7 struct _KTHREAD* CurrentThread; //0x8 struct _KTHREAD* NextThread; //0x10 struct _KTHREAD* IdleThread; //0x18 UCHAR NestingLevel; //0x20 UCHAR ClockOwner; //0x21 union { UCHAR PendingTickFlags; //0x22 struct { UCHAR PendingTick:1; //0x22 UCHAR PendingBackupTick:1; //0x22 }; }; UCHAR IdleState; //0x23 ULONG Number; //0x24 ULONGLONG RspBase; //0x28 ULONGLONG PrcbLock; //0x30 CHAR* PriorityState; //0x38 CHAR CpuType; //0x40 CHAR CpuID; //0x41 union { USHORT CpuStep; //0x42 struct { UCHAR CpuStepping; //0x42 UCHAR CpuModel; //0x43 }; }; ULONG MHz; //0x44 ULONGLONG HalReserved[8]; //0x48 USHORT MinorVersion; //0x88 USHORT MajorVersion; //0x8a UCHAR BuildType; //0x8c UCHAR CpuVendor; //0x8d UCHAR LegacyCoresPerPhysicalProcessor; //0x8e UCHAR LegacyLogicalProcessorsPerCore; //0x8f ULONGLONG TscFrequency; //0x90 ULONG CoresPerPhysicalProcessor; //0x98 ULONG LogicalProcessorsPerCore; //0x9c ULONGLONG PrcbPad04[4]; //0xa0 struct _KNODE* ParentNode; //0xc0 ULONGLONG GroupSetMember; //0xc8 UCHAR Group; //0xd0 UCHAR GroupIndex; //0xd1 UCHAR PrcbPad05[2]; //0xd2 ULONG InitialApicId; //0xd4 ULONG ScbOffset; //0xd8 ULONG ApicMask; //0xdc VOID* AcpiReserved; //0xe0 ULONG CFlushSize; //0xe8 ULONGLONG PrcbPad11[2]; //0xf0 struct _KPROCESSOR_STATE ProcessorState; //0x100 struct _XSAVE_AREA_HEADER* ExtendedSupervisorState; //0x6c0 ULONG ProcessorSignature; //0x6c8 ULONG ProcessorFlags; //0x6cc ULONGLONG PrcbPad12a; //0x6d0 ULONGLONG PrcbPad12[3]; //0x6d8}; |
步骤四: 遍历进程链表active_process_links
通过偏移计算KTHREAD->KPROCESS->EPROCESS->active_process_links获得这个进程双链表结构,通过遍历active_process_links我们可以获取当前系统所有进程,包括每个进程的CR3。
步骤五: win10 分页解析

关于windows的分页机制,文档很多不赘述直接贴出部分代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | void* MDebugger::GuestVAToHostAddress(uint64_t gva, uint64_t cr3) { void* hva; uint64_t gpa; auto pml4_gpa = cr3 & 0xFFFFFFFFFF000; auto pml4 = (uint64_t*)machine_->device_manager()->TranslateGuestMemory(pml4_gpa); auto pml4e_index = (gva >> 39) & 0x1FF; auto pml4e = pml4[pml4e_index]; MV_ASSERT((pml4e & 0x80) == 0); if (!(pml4e & 0x1)) { MV_ERROR("page fault happened"); return nullptr; } auto pdpt_gpa = pml4e & 0xFFFFFFFFFF000; auto pdpt = (uint64_t*)machine_->device_manager()->TranslateGuestMemory(pdpt_gpa); auto pdpte_index = (gva >> 30) & 0x1FF; auto pdpte = pdpt[pdpte_index]; if (!(pdpte & 0x1)) { MV_ERROR("page fault happened"); return nullptr; } if ((pdpte & 0x80) == 0) { auto pd_gpa = pdpte & 0xFFFFFFFFFF000; auto pd = (uint64_t*)machine_->device_manager()->TranslateGuestMemory(pd_gpa); auto pde_index = (gva >> 21) & 0x1FF; auto pde = pd[pde_index]; if (!(pde & 0x1)) { MV_ERROR("page fault happened"); return nullptr; } if ((pde & 0x80) == 0) { auto pte_gpa = pde & 0xFFFFFFFFFF000; auto pt = (uint64_t*)machine_->device_manager()->TranslateGuestMemory(pte_gpa); auto pte_index = (gva >> 12) & 0x1FF; auto pte = pt[pte_index]; if (!(pte & 0x1)) { MV_ERROR("page fault happened"); return nullptr; } auto offset = gva & 0xFFF; gpa = (pte & 0xFFFFFFFFFF000) + offset; hva = machine_->device_manager()->TranslateGuestMemory(gpa); } else { auto offset = gva & 0x1FFFFF; gpa = (pde & 0xFFFFFFFE00000) + offset; hva = machine_->device_manager()->TranslateGuestMemory(gpa); } } else { auto offset = gva & 0x3FFFFFFF; gpa = (pdpte & 0xFFFFFC0000000) + offset; hva = machine_->device_manager()->TranslateGuestMemory(gpa); } return hva;} |
总结
在能够获取所有进程cr3后,加上页表解析,可以轻松的在host上读写任意guest内存。当我们尝试在host抹掉guest进程notepad中的ntdll的PE头后,由于系统dll的特殊性加上直接修改了物理内存,系统也随之陷入一片混乱 

项目地址
https://github.com/tenclass/mvisor
使用右ctrl+F8唤起debug窗口,目前仅支持win10 guest。