Переполнение буфера в Windows NT/2000/XP
Пусть что угодно говорят поклонники *nix-систем, но защита в последних версиях операционных систем семейства Windows сделала громадный скачок вперёд. И так как языки C и C++ очень распространены и на платформах Windows, то атаки на переполнение буфера стали реальной угрозой безопасности. Атаки такого типа известны уже очень давно, со времен операции «Полуденный бес», которая проводилась в США, начиная с мая 1990 года, и была направлена против хакеров. Её проводила «Секретная Служба». Переполнение буфера, правда, на операционные системы семейства *nix использовалось хакерами по полной программе. Но реальная угроза данных атак на операционные системы семейства Windows появилась гораздо позднее, в 1995-2002 годах.
Смысл атак на переполнение буфера заключается в том, что существуют программы, запущенные с большими привилегиями, чем у пользователя, и благодаря переполнению буфера для пользователя стало возможно получить эти привилегии (уязвимой программы).
Мы рассмотрим одну из самых распространённых атак на переполнение буфера. Это так называемая атака на «срыв стека». В большинстве своём данные атаки возможны во многих программах, написанных на языках программирования C или C++.
Поэтому мы рассмотрим пример уязвимой программы, а также разработаем для неё эксплоит (программу, которая реализует данную уязвимость).
Начнём с разработки простой программы, содержащей данную уязвимость:
void vuln_func(char *stroka)
// функция, в результате вызывающая переполнение буфера
void main (int argc, char *argv[])
// вызов уязвимой функции
printf("Parameter is : %s",argv[1]);
Откомпилируем её и выясним, что делает эта программа. Если запустить её без параметров, то она выведет следующее:
Parameter is : 0(null)
Запустим её следующим образом:
Тогда данная программа выведет:
Parameter is : aaaaaaa
Но если в качестве параметра передать количество «a» большее, чем 100, то данная программа просто ничего не выведет в отличие от Windows NT, которая выводила сообщение об ошибке. Поэтому реализовать атаку данного типа под операционной системой Windows XP будет несколько сложнее. Теперь давайте возьмём отладчик и посмотрим, что же происходит при вызове нашей уязвимой функции.
Будем рассматривать пошагово:
- При вызове функции vuln_func в стек заносится адрес следующей инструкции (printf).
- Рассмотрим вид стека перед вызовом функции lstrcpyA:
- Рассмотрим вид стека после вызова функции lstrcpyA:
Так как наша переменная росла к адресу возврата, и когда ей стало не хватать места, она молча и без вопросов переписала адрес возврата нашими любимыми буквами «aaaaaaaa. ». Раз мы можем переписать адрес возврата, значит, мы можем и заставить нашу программу выполнять наш код.
Для этого вместо букв «a» мы будем использовать символ с кодом 90 («x90»), что означает инструкцию ассемблера nop (задержка процессора на один такт). Также нам необходимо точно определить, какими байтами (по счёту) перезаписывается адрес возврата, чтобы мы знали, куда записывать наш новый адрес возврата.
Обычно это делается методом «грубой силы», а потом по сообщению об ошибке определяются байты, по которым был совершён переход. Но данный метод в нашем случае не уместен, т.к. в Windows XP сообщение об ошибке не вываливается. Поэтому нам придётся вручную при помощи отладчика подсчитывать, какие по счёту nop перетёрли адрес возврата, это несложно и нетрудоёмко, т.к. в стеке все приравнивается к двойному слову (4 байта). В нашем случае это оказались 4 байта, начиная со 104.
Теперь, когда мы знаем, какими байтами перетирается адрес возврата, мы должны подменить его таким образом, чтобы он передал управление нашему коду, а если быть более точным, то он должен передать управление на наши nop. Реально очень удобно просто передать управление в отладчике на наши инструкции, но если это удалённое переполнение буфера или у вас нет под рукой отладчика? При помощи прямой подмены адреса на наш вычисленный адрес стека не получится, потому что адрес стека начинается с нуля, а наш код не должен состоять из нулей.
Поэтому оптимальным вариантом будет обнаружить в памяти используемых программой библиотек или в памяти самой программы обнаружить байты, соответствующие инструкции «jmp esp» - ff e4. В статье Андрея Колищака (1) предлагался вариант использования инструкции «call esp», но с этим вариантом я должен не согласиться, так как при переходе на эту инструкцию с последующим её исполнением мы перетираем байты, находящиеся по адресу esp, т.е. мы сами себе портим жизнь (при переходе на esp нам будет очень сложно запустить наш код). Поэтому оптимальным вариантом будет поиск в памяти байт «ff e4». В результате поиска мы обнаружили данные байты в библиотеке USER32.dll и запомнили адрес (в каждой версии операционной системы он может быть отличным).
Теперь мы должны заняться формированием shell-кода – это будет строка Command Promt. Реально мы должны запустить программу cmd.exe. Чтобы не морочить вам голову «кривыми» аналогами данной программы на C, начнём её писать сразу на ассемблере, но перед этим мы должны кое в чём разобраться.
- Наш эксплоит будет зависеть от USER32.dll, что означает на других версиях операционной системы Windows XP, по всей видимости, он работать не будет.
- Исходя из предыдущего пункта, мы видим, что не имеет смысла встраивать в наш shell-код обнаружение адреса ядра и функции GetProcAddress.
Поэтому для экономии места мы в эксплоите жёстко зафиксируем адрес нужной нам функции WinExec.
Аналог того, что будет делать наш эксплоит на С, будет выглядеть примерно так:
Для определения адресов функций, которые необходимы для корректной работы эксплоита, напишем ещё одну маленькую программу.