пятница, 25 июля 2014 г.

Heap Overflow на примере одного Wargame

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

Итак, для начала, что имеется : процессор MSP430 (его архитектура команд довольно проста, для того чтобы понять суть происходящего, желательно прочитать вот этот мануал)

Так же имеется некоторая система аутентификации.

Дизассемблерный листинг приводить не буду, если интересно, просто дойдите до этого уровня на сайте игры, а весь рассказ будет в картинках :)

Пожалуй, начнем. Дамп по базовому адресу для динамически выделяемой памяти.
Сразу отмечаем несколько интересных значений (помните, порядок байт little-endian)
0x2408 - Адрес первого доступного блока
0x1000 - Общий допустимый размер выделяемой памяти
0x0001 - Память еще не выделялась
После первого выполнения функции malloc(0x10) дамп памяти будет уже выглядеть следующим образом (было выделено 0x10 байт памяти)
После второго выполнения malloc(0x10) (выделено еще 0x10 байт)
И после третьего вызова malloc(0x20) (выделено дополнительно 0x20 байт)
Можно заметить, что служебная информация делится на два типа : двусвязный список выделенных блоков и метаинформация, являющаяся заключительным блоком в цепочке.
Отличительной особенностью метаинформации является третья пара байт в структуре элемента списка. Рассмотрим эту структуру (размер каждого поля 2 байта).
0xADDR - Первым идет адрес предыдущего выделенного блока
0xADDR - Затем следующего выделенного блока
0xVALE - Третьим элементом является объем выделенной памяти в формате : (amount << 1) | 1
Однако, для метаинформации третье значение является объемом доступным для выделения.
0xVALE - Объем доступной для выделения памяти в формате : amount << 1
Именно по младшему биту и происходит отличие метаинформации от элемента списка.

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

Из-за уязвимости в мехнизме обработки пользовательских данных мы получаем возможность перезаписать служебную информацию второго блока и блока содержащего метаинформацию.
Попытаемся реализовать ROP (return on pointer).

Итак, дизассемблированная функция free будет выглядеть следующим образом

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

Загружаем r13 значением, равным удвоенному размеру выделенного блока.
450e:  1d4f 0400      mov    0x4(r15), r13
4512:  3df0 feff      and    #0xfffe, r13
4516:  8f4d 0400      mov    r13, 0x4(r15)
Загружаем r12 значением, равным удвоенному размеру предыдущего блока, r14 ссылкой на предыдущий элемент.
451a:  2e4f           mov    @r15, r14
451c:  1c4e 0400      mov    0x4(r14), r12
И вот тут начинается интересное, дело в том, что служебный блок элемента списка мы контролируем, иными словами можем указать любой адрес в качестве адреса предыдущего блока, и любой размер текущего блока, ну а дальше то что нам и было нужно.
4524:  3c50 0600      add    #0x6, r12
4528:  0c5d           add    r13, r12
452a:  8e4c 0400      mov    r12, 0x4(r14)
r14 - указанный нами адрес, r13 - указанный нами размер блока, r12 - предыдущее значение по указанному нами адресу.
В стеке интересующий нас адрес возврата имеет смещение 0x439A, значит нужно отнять от него значение смещения размера блока (в данном случае равное 4) и получим адрес, который станет адресом предыдущего блока (0x4396). Предыдущее значение адреса возврата равняется 0x4440, размер служебной информации 0x6, также нужно отметить блок как элемент списка, для этого установив младший бит в 1, функция на которую нужно передать управление имеет адрес 0x4564, исходя из нехитрой математики получаем
block size = (0x4564 - 0x4440 - 6) | 1 = 0x11F
Остается одна небольшая хитрость, если за нашим блоком следует метаинформация, то произойдут дополнительные действия, которых нам нужно избежать, но так как содержимое метаинформации мы так же контролируем, достаточно будет пометить содержащий ее блок, как элемент списка и тогда, как ранее упоминалось, он будет пропущен.

В результате получаем следующие строки на вход (каждый символ здесь представлен в виде ascii hex-code)
name - 61616161616161616161616161616161964334241F01
pass - 626262626262626262626262626262621E2408249F1F
Дамп памяти будет выглядеть так
Выделены ключевые изменения.
А так будет выглядеть память перед возвратом из функции (выделен адрес возврата)

Конечно, это упрощенный пример и на практике все сложнее, но он дает базовые понятия о данном типе уязвимостей.