На днях мы изучали технику переполнения буфера. Было перечитано множество мануалов, инструкций и так далее, пока мы не выяснили все, что нам требовалось на первое время - для написания эксплоита к тестовой программе. И этой информацией мы хотим поделиться с вами. Возможно, будут какие то неточности или маленькие ошибки, так как мы тоже только поняли это, и еще многое предстоит понять, но мы постараемся дать вам всю необходимую информацию.
Желательно знать что делают регистры процессора, язык Ассемблер и язык С.
Также необходимо иметь под рукой:
Отладчик (OllyDbg) Компилятор С (Dev-CPP) Прямые руки и желание понять тему.
[ Полный вперед ]
Итак, когда происходит переполнение буфера?
Это случается при отсутствии фильтрации длины входящих параметров (очень похоже на SQL инъекции). Вообще эта тема очень похожа на SQL инъекции в WEB приложениях - только вместо инъекции SQL запросов мы будем делать инъекцию кода в стек.
Пример уязвимой программы:
/* b0f.exe
Vulnerable for local buffer overflow attacks user b0f_expl.exe to test this */
#include <stdio.h> #include <stdlib.h>
int main(int argc, char *argv[]) { char buf[10];
printf("Print your nick: "); scanf("%s", &buf); // there no checks for buffer limit printf("Hello, %s!\n", buf);
return 0; }
Здесь видно что выделяется буфер длиной 10 байт (char buf[10]), запрашивается ввод пользователем ника (scanf("%s", &buf)), введенная строка пишется в буфер и в конце выводится строка "Hello, [СТРОКА_ВВЕДЕННАЯ_ПОЛЬЗОВАТЕЛЕМ]".
Почему эта программа уязвима?
Потому что выделяется буфер длиной в 10 байт и никак не проверяется длина строки введенная пользователем!
Что же это нам может дать?
Попробуем отправить не 10 а большее количество байт.
C:\Documents and Settings\compaq\Рабочий стол\Reload>b0f Print your nick: vvvvvvvvvvvvvvvvvvvvvvvvvvvv1234 Hello, vvvvvvvvvvvvvvvvvvvvvvvvvvvv1234! ^C
Ага! Происходит ошибка, смотрим значение offset в отчете: 76767676. Что это такое? А это строка vvvv в hex значении! Значит есть переполнение буфера с затиранием адреса возврата из функции (он будет равен 0x76767676). Что это нам может дать:
- Сделать переход на любой другой адрес (например, чтобы обойти авторизацию или чтобы залить в стек шеллкод) - Сделать классическое Denial of Service (ведь адрес возврата затерт на несуществующий и функция не может возвратиться обратно, то есть произойдет вылет программы)
Но атака DoS это детские шалости ведь тут достаточно просто затереть адрес возврата а получить рутовые права и/или shell гораздо интереснее! ;) Но увы для второго варианта придется приложить некоторые усилия.
Итак, вкратце объясню теорию о действиях программы при входе и выходе из процедур/функций.
То есть получается что выполняется следующая последовательность:
начало f1() выполнение "код1" выполнение "код2" прыжок на f2 выполнение "код3" выполнение "код4" возвращение на "код5" выполнение "код5" выполнение "код6"
Теперь выясним как же сохраняется "возвращение на ..." из функции f2:
Перед тем как сделать "прыжок на f2" происходит следующая операция:
PUSH EBP MOV EBP, ESP ADD ESP, -36
Разъясню:
Командой PUSH EBP мы кладем на вершину стека (а стек как известно растет сверху вниз) значение регистра EBP (сохраняем его). Затем кладем в регистр EBP значение ESP (регистр ESP хранит адрес верхушки стека). В конце мы вычитаем из стекового указателя ESP 36 бит (ведь при каждой команде стек снижается).
Далее:
вход в функцию f2 выполнение "код3" выполнение "код4"
А дальше идет операция возврата из функции ("возвращение на код5"):
MOV ESP, EBP POP EBP RET
Сначала кладем в ESP (напомню - это указатель верхушки стека, а мы восстанавливаем его значение) значение EBP (помните, перед входом в функцию f2 мы клали в него значение ESP перед входом в функцию f2, простите за каламбур). Дальше мы снимаем с верхушки стека значение EBP (мы его сохранили перед входом в процедуру f2). И в конце мы делаем "возвращение на код5" функцией RET, которая берет значение следующего после вызова функции f2 функцией f1 адреса из регистра EBP.
Фуух, вроде отмучились с теорией. Надеюсь все понятно.
Тогда чтоже мы делаем когда переполняем буфер? А мы затираем как раз адрес возврата, и при желании то, что следует за ним! Это происходит из за такого строения стека, по принципу "все в одну кучу", поэтому мы и можем затереть значения регистров.
Расскажу одну вещь. В стеке все читается справа налево и сверху вниз, поэтому в будущем эксплоите мы должны писать адрес допустим не 0x01234567 а 0x67452301.
Ну, теперь нам надо определиться с тем, на что изменить адрес возврата.
Вообще, например при авторизации, мы можем изменить адрес возврата на код последующих после вызова функции авторизации инструкций, тем самым мы обойдем авторизацию. Также можно прыгнуть на код, заведомо опасный, например работающий с командной строкой. А еще мы можем сами "залить" в стек shellcode и записать в адрес возврата адрес шеллкода, и программа перейдет на него. Вот этим то мы и займемся.
Теперь надо определиться, куда заливать шеллкод. Его можно залить в буфер с мусором (до перезаписи адреса возврата), и после перезаписи адреса возврата. Поскольку выделенный буфер у нас маленький для шеллкода, то мы будем заливать шеллкод после перезаписи адреса возврата.
Запись шеллкода в мусоре не сильно отличается от записи шеллкода после перезаписи адреса возврата.
Итак, теперь нам надо узнать, по какому адресу запишется наш шеллкод. Для этого открываем отладчик (например OllyDbg), жмем F3, выбираем нашу уязвимую программу и жмем несколько раз F8 для пошагового выполнения инструкций, пока не наткнемся на вызов функции scanf. Пропишем следующее в отлаживаемой программе:
vvvvvvvvvvvvvvvvvvvvvvvvvvvvaddrshellcode
Видим, что произошло исключение. Смотрим регистры:
Как мы видим, ESP у нас указывает на начало нашей строки "shellcode", где мы и разместим наш будущий шеллкод. Значение ESP в данном случае у нас и будет служить адресом возврата в эксплоите.
А теперь пишем эксплоит. В нем сформируем злонамеренную строку следующего вида:
vvvvvvvvvvvvvvvvvvvvvvvvvvvvaddrshellcode
Где "addr" будет адрес возврата а shellcode - код нашего шеллкода.
Сам шеллкод можно либо написать самому либо где то скачать, например с milw0rm.com.
Я выбрал шеллкод, издающий процессорный сигнал, в связи с тем, что я могу поместить свой код ~70 байт - таково "расстояние" от Entry Point программы до конца нашего буфера. А шеллкод делающий "beep", как раз и входит в это ограничение.
Пишем сплоит:
/* b0f_expl.exe
buffer overflow exploit for b0f.exe (winxp sp2 shellcode) */
#include <stdio.h> #include <stdlib.h>
/* milw0rm.com BEEP shellcode */ unsigned char scode[] = "\x41\x41\x41\x41\x41" // fuck... "\x41\x41\x41\x41\x41" // ...fuck... "\x41\x41\x41\x41\x41" // ...this is all the fuck... "\x41\x41\x41\x41\x41" // ...and this too... "\x41\x41\x41\x41\x41" // ...doh, when it will be ended... "\x41\x41\x41" // ...yeah, the fuckin end of fuck "\x80\xFF\x22\x00" // our RET address "\x55\x89\xE5\x83\xEC\x18\xC7\x45\xFC" "\x53\x8A\x83\x7C" //Address \x53\x8A\x83\x7C = SP2 "\xC7\x44\x24\x04" "\xD0\x03" //Length \xD0\x03 = 2000 (2 seconds) "\x00\x00\xC7\x04\x24" "\x01\x0E" //Frequency \x01\x0E = 3585 "\x00\x00\x8B\x45\xFC\xFF\xD0\xC9\xC3";
Тут видно, что сначала генерируем строку для передачи в буфер (не забываем указать адрес "наоборот"), потом открываем программу и пишем в нее строку. И затем издается beep =)
Есть одно примечание - шеллкод создан под WinXP Service Pack 2, и на остальных системах он работать не будет.
[ Злоключение ]
Ну вот, в принципе, и все, что мы хотели сказать по данному поводу. Мы не описали множество приемов и техник переполнения буфера, например локальное, или залитие шеллкода в мусорный буфер, или техники обхода подводных камней, но эта статья была написана "для старта" новичкам, хотя мы и сами новички. Возможны недочеты и ошибки, мы рады будем узнать о них. Спасибо во внимание, и да пребудет с вами священный EAX ;)
Уязвимая программа и эксплоит: Helkern Разъяснение Helkern'у ранее до описываемых событий вопросов по асму: Anvil aka Alasin Написание статьи: Helkern && Alasin