-------------------------[ Перезапись указателя на окно памяти ]
--------[ klog ]
----[ Введение
Если буферы могут быть переполнены, то путем перезаписи критических данных,
хранимых в адресном пространстве атакуемого процесса, мы можем изменить
порядок выполнения процесса. Это не новость. Эта статья не окажет большой
помощи в использовании переполнения буферов и не расскажет о самой уязвимости.
Она просто демонстрирует, что подобную уязвимость можно использовать даже в
таких сложных условиях, когда буфер может быть переполнен всего на один байт.
Во многих неприятных ситуациях существуют всякие тайные уловки где основной
целью является атака на доверяющий процесс, включая и такие, где сбрасываются
права доступа, но мы будем рассматривать только случаи переполнения с 1 байтом.
----[ Объект нашей атаки
Давайте напишем уязвимую программу с правами суперюзера, которую мы
назовем "suid". Она написана таким образом, что позволяет переполнение
буфера всего на 1 байт.
Как мы все знаем, процессор сначала запихивает в стек %eip, как того требует
инструкция CALL. Затем, наша маленькая программка засовывает %ebp, что можно
увидеть в строке *0x8048134. Затем, создается локальное окно памяти путем
уменьшения %esp на 0x104. Это значит, что наши локальные переменные занимают
0x104 байта (0x100 занимает строка и 4 байта занимает целочисленная
переменная). Обратите внимание, что переменные физически выравниваются по 4
байта, т.е. буфер в 255 байт займет столько же места, что и буфер в 256
байт. Теперь мы можем сказать, как выглядит наш стек в момент переполнения
буфера:
сохраненный_eip
сохраненный_ebp
char buffer[255]
char buffer[254]
...
char buffer[000]
int i
Это означает, что переполняющий байт перезапишет сохраненный указатель окна
памяти, который был помещен в стек в начале func(). Но как можно использовать
этот байт, чтобы изменить последовательность выполнения программы? Давайте
взглянем на то, что происходит с образом %ebp. Мы уже знаем, что он
восстанавливается в конце func(), что можно увидеть в *0x804817e. Но что
дальше?
Великолепно! После вызова func() в конце main(), %ebp восстанавливается в
%esp, строка *0x80481b1. Это означает, что мы можем установить %esp. Это
означает, что мы можем установить %esp в произвольное значение. Но помните,
что значение не по-настоящему произвольное, вы можете изменить только
последний байт в %esp. Давайте проверим, правы ли мы.
Breakpoint 2, 0x80481b4 in main ()
(gdb) info register esp
esp 0xbffffd45 0xbffffd45
(gdb)
Да, похоже что мы правы. После переполнения буфера одной буквой 'A'
(0x41), %ebp перемещается в %esp, который увеличивается на 4, поскольку
%ebp извлекается из стека перед RET. Это дает нам 0xbffffd41 + 0x4 =
0xbffffd45.
----[ Подготовка.
Что нам дает изменение указателя стека? Мы не можем изменить сохраненное
значение %eip напрямую, но можем заставить процессор думать, что он находится
где-то в другом месте. Когда процессор возвращается из процедуры он просто
вынимает первое слово из стека, считая что это оригинальный %eip. Но если
мы меняем %esp, мы можем заставить процессор вынуть любое значение из стека
и считать, что это %eip, таким образом изменить последовательность выполнения.
Давайте спроектируем переполнение буфера используя следующую строку:
Для того, чтобы сделать это, нам сначала нужно определить, какое значение мы
хотим придать %ebp (и посредством этого %esp). Давайте взглянем на что будет
похож стек, когда произойдет переполнение буфера:
сохраненный_eip
сохраненный_ebp (с 1 измененным байтом)
&код
код | char буфер
пустые операции /
int i
Теперь, мы хотим чтобы %esp указывал на &код, чтобы адрес кода был вынут в
%eip когда процессор вернется из main(). Теперь, когда мы мы знаем, как мы
хотим атаковать нашу уязвимую программу нам нужно извлечь информацию из процесса
во время работы в ситуации переполненного буфера и адрес указателя на наш
код (&код). Давайте выполним программу так, как если бы мы хотели переполнить
ее строкой из 257 символов. Чтобы сделать это мы должны написать фальшивый
эксплоит который воспроизведет ситуацию в которой мы атакуем уязвимый процесс.
Breakpoint 1, 0x804813d in func ()
(gdb) info register esp
esp 0xbffffc60 0xbffffc60
(gdb)
Есть. Теперь у нас есть значение %esp сразу после создания окна памяти. С
помощью этого значения мы теперь можем предположит, что наш буфер будет
расположен по адресу 0xbffffc60 + 0x04 (размер 'int i') = 0xbffffc64, и что
указатель на наш код будет располагаться по адресу 0xbffffc64 + 0x100 (размер
'char buffer[256]') - 0x04 (размер нашего указателя) = 0xbffffd60.
----[ Время начать атаку
Наличие этих значений позволит нам написать полную версию эксплоита, включая
сам код, указатель на код и перезаписывающий байт. Значение, которым нам надо
переписать последний байт сохраненного %ebp будет 0x60 - 0x04 = 0x5c,
поскольку, как вы должны помнить, мы вынимаем %ebp сразу перед возвращением
из main(). Эти четыре байта компенсируют то, что %ebp удаляется из стека.
Что касается указателя на наш код, то на самом деле нам не нужно, чтобы он
указывал на точный адрес. Все, что нам надо,это чтобы процессор вернулся в
середину пустых операций (noops) между началом переполняемого буфера
(0xbffffc64) и нашим кодом (0xbffffc64 - sizeof(код)), как и в обычном
переполнении буфера. Давайте будем использовать 0xbffffc74.
Таким образом первые точки останова позволит нам просмотреть содержимое %ebp
до и после извлечения из стека. Эти значения соответствуют оригинальному и
переписанному значениям.
Здесь мы хотим отследить перемещение нашего перезаписанного %ebp в %esp и
содержимое %esp до возвращения из main(). Давайте выполним программу.
(gdb) c
Continuing.
Breakpoint 1, 0x804817e in func ()
(gdb) info reg ebp
ebp 0xbffffd64 0xbffffd64
(gdb) c
Continuing.
Breakpoint 2, 0x804817f in func ()
(gdb) info reg ebp
ebp 0xbffffd5c 0xbffffd5c
(gdb) c
Continuing.
Breakpoint 3, 0x80481b3 in main ()
(gdb) info reg esp
esp 0xbffffd5c 0xbffffd5c
(gdb) c
Continuing.
Breakpoint 4, 0x80481b4 in main ()
(gdb) info reg esp
esp 0xbffffd60 0xbffffd60
(gdb)
Во-первых мы просматриваем настоящее значение %ebp. После извлечения из
стека, мы можем увидеть, как оно заменяется значением, которое было
перезаписано последним байтом нашей переполняющей строки, 0x5c. После
этого, %ebp переписано в %esp, и, в конечном итоге после того, как %ebp
извлекается вновь из стека, %esp увеличивается на 4 байта. Это дает
окончательное значение 0xbffffd60. Давайте взглянем, как все происходит.
Мы видим, что 0xbffffd60 это настоящий адрес указателя, указывающего в
середину пустых операций непосредственно перед нашим кодом. Когда процессор
будет возвращаться из main(), он извлечет этот указатель в %eip b перейдет
по точному адресу 0xbffffc74. Вот тогда и начнется выполнение нашего кода.
(gdb) c
Continuing.
Program received signal SIGTRAP, Trace/breakpoint trap.
0x40000990 in ?? ()
(gdb) c
Continuing.
bash$
----[ Выводы
Несмотря на то, что способ неплох, некоторые проблемы остаются неразрешенными.
Изменение выполнения программы с помощью всего одного байта перезаписываемых
данных несомненно является возможным, но при каких условиях? По сути дела,
воспроизведение ситуации атаки может быть сложной задачей в чужеродном
окружении или, хуже того, на удаленном компьютере. Это может потребовать от
нас угадать точный размер стека атакуемого процесса. Плюс к этому, добавьте
необходимость того, что переполняемый буфер должен следовать непосредственно
сразу за указателем на окно памяти, да и выравнивание по 32-битной границе
так же необходимо учитывать. Что же насчет атак больших защищенных архитектур?
Мы не сможем переписать сколь либо важный байт информации, если только у нас
нет возможности достичь этого адреса...
Можно сделать выводы, что это почти невозможная для атаки ситуация. Не смотря
на то, что я буду чрезвычайно удивлен, услышав, что кому-либо удалось применить
этот метод к реальной уязвимости, он, конечно, доказывает нам, что нет таких
вещей, как большая или маленькая уязвимость. Любое переполнение уязвимо, все
что нужно - это найти как именно.