[ Обновленные темы · Новые сообщения · Участники · Правила форума · Поиск · RSS ]
  • Страница 1 из 1
  • 1
Переполнение буфера
eXceedДата: Вторник, 12.08.2008, 23:32 | Сообщение # 1
Генералиссимус
Группа: Гости
Сообщений: 5466
Репутация: 616
Статус: Offline
Уже много лет известна ошибка под названием Buffer Overflow. Но, тем не менее, до сих пор она является самой распространенной в программном обеспечении. Для того чтобы понять весь механизм атак на переполнение буфера, мы должны кратко ознакомиться с основными принципами архитектуры процессоров х86. Все процессоры архитектуры x86 имеют наборы регистров с определенным назначением и названием.
Так, например, регистр EAX применяется для пользовательских данных, регистр ECX используется как счетчик в циклах и повторяющихся операциях и т.д. Основные регистры и их назначения были унаследованы от 286-го процессора, но была расширена разрядность каждого регистра (до 32 бит) и в название добавили букву "E" (extended, в переводе с англ. – расширенный). Кроме базовых, доступных пользователю регистров, существуют так называемые системные регистры. Запись в них напрямую запрещена, и они используются для контроля за выполнением программы. Примерами таких регистров могут служить EBP и ESP, использующиеся в операциях со стеком, EIP, представляющий собой указатель на инструкцию, которую процессор будет выполнять следующей. И еще один регистр, о котором нам желательно знать, – регистр флагов EFLAGS, это, по сути, 32 бита, которые используются как переключатели-флаги при работе процессора.

Теперь мы должны ознакомиться с понятием стека.

Стек представляет собой непрерывную область памяти, адресация на которую происходит с помощью регистров ESP (указатель стека) и SS (указатель на сегмент стека). Именно в стеке хранится тот загадочный буфер, переполнение которого так пугает всех разработчиков ПО. Расположение буфера внутри стека таит в себе огромную опасность: компилятор помещает буфер переменной в стек, а до этого, чуть раньше, записывается адрес возврата из процедуры. Стек, таким образом, имеет следующий вид:

Код атакующего
|
Адрес возврата
|
Локальная переменная

Но такой расклад несет только полбеды: стек работает с данными по принципу "первым пришел, последним ушел" (FILO). То есть это обычная пирамида: чтобы добраться до самого низа, нужно разобрать все строение – чтобы взять данные, положенные в стек первыми, нужно вынуть всю информацию из стека. На языке команд процессора эти операции носят названия PUSH («запихать») и POP («достать»). Именно с помощью таких операндов происходит вся основная работа с содержимым стека.

Теперь мы можем ознакомиться с буфером и как он располагается в стеке.

Представим себе простую программу C, в которой используется переменная, объявленная как char buff[10];, то есть переменная buff имеет размер строго в 10 байт, что явно определено при ее объявлении. Больше 10 байт данная переменная принять не сможет, хотя меньше – пожалуйста. И программа, имеющая так объявленную переменную, будет работать долго и стабильно до тех пор, пока в переменную buff будут помещаться строки длиной не более 10 символов. Но это в идеале и при условии, что программа работает в правильных руках ;-). А что произойдет, если попытаться присвоить переменной buff строку длиной более 10 символов? А произойдет самое интересное! Представим, что мы откомпилировали программу, содержащую такую переменную, и начали исследовать ее в отладчике. Самое главное, на что следует обратить внимание, – это на стек и на его строение в уязвимой программе.

Если попробовать прогнать в отладчике тестовую программу, то мы увидим, что адрес возврата из процедуры, в которой происходит объявление процедуры buff, лежит в одном сегменте с буфером самой переменной buff. Что нам это дает? На первый взгляд, ничего особенного, но если вспомнить, что стек «растет» вниз, то есть имеет идеологию FILO, то становится очевидной потенциальная уязвимость: при попытке записи в переменную buff больше 10 байт произойдет затирание области памяти, никак не относящейся к данной переменной! Сначала будет затерта область служебной информации, а потом, если передать достаточно длинную строку, произойдет перезапись адреса возврата из процедуры. Причем в качестве адреса возврата будут выступать не случайные адреса, а первые несколько байт строки, переполнившей буфер.

Как это выглядит в жизни.

Отличительной чертой таких атак является то, что они не имеют привязки к какой-либо платформе. Уязвимости этого типа находят и в Windows, и в Linux, и в FreeBSD и многих других. В жизни выглядит это так: *nix-системы при возникновении такой критической ситуации выполнят аварийное завершение программы и выдадут на консоль предупреждение, аналогичное "Segmentation fault, core dump" (как говорится, упал в кору). Перевод этого высказывания системы чего-то конкретного не даст: ошибка сегментации, кусок памяти, вызвавший сбой, сохранен на винте. Однако появление такого сообщения вовсе не означает, что произошло именно переполнение буфера, поэтому говорить о стопроцентной ошибке программистов в этих случаях, по меньшей мере, глупо ;-). Очень часто *nix-системы "сбрасывают дамп в кору" при других ошибках, вовсе не связанных с переполнением. Иначе дело обстоит в среде мелкомягких. При переполнении буфера в Windows пользователь получит системное предупреждение и аварийную остановку программ. Но предупреждение ОС в данной системе более информативно: "The instruction at "0x31313131" referenced memory at "0x31313131". The memory could not be read". Как видите, все достаточно понятно: мы видим попытку глючной программы обратиться к запрещенному адресу памяти.

Как используют BufferOverflow в ПО.

Подмена адреса возврата в привилегированной программе является следствием удачного переполнения буфера. Обычно новый адрес содержит ссылку на функцию, которая вызывает командный интерпретатор (shell) системы с высокими привилегиями. Для Windows вызывается на выполнение всем известная cmd, а для *nix – /bin/sh. На этом работа эксплоита закончена, ошибка переполнения буфера была использована. Дальнейшие действия, как атакующего, так и системы, уже не имеют отношения к нашей теме.

Теория атаки.

Переполнение буфера – одна из самых критических ошибок в ПО. Хотя она полностью изучена и документирована, багтрак-сайты снова и снова пестрят сообщениями об очередном buffer overflow. А причина устойчивости этой уязвимости кроется в самой архитектуре системы. Можно смело говорить о большой вероятности переполнения буфера, если система обладает следующими параметрами:
– адрес возврата из функции помещается в стек (абсолютно все распространенные ОС и компиляторы);
– параметры функции передаются через стек (все ОС, а для Windows это – основной способ передачи параметров в API-функции);
– локальные переменные лежат в стеке (Windows- и *nix-компиляторы делают именно так);
– стек имеет идеологию FILO (так устроена платформа х86, а значит все системы, работающие на данной архитектуре, имеют такой тип стека);
– данные стека могут быть командами (одна из основ архитектуры х86 – единое адресное пространство для данных и кода);
– имеются процессы с высокими привилегиями (справедливо для любой ОС; одна из основ разграничения пользователей, базис сетевых ОС);
– в программах используются функции с ошибками.


bda-expert.ru — это система форумов, где можно общаться быстро и свободно, где любая точка зрения имеет право на жизнь.

Сообщение отредактировал eXceed - Вторник, 12.08.2008, 23:34
 
eXceedДата: Вторник, 12.08.2008, 23:44 | Сообщение # 2
Генералиссимус
Группа: Гости
Сообщений: 5466
Репутация: 616
Статус: Offline
Тренируемся на кошках.

Для того, чтоб не читать нудную теорию, я решил показать на примере как это происходит.

Для реализации нашей задумки нам потребуется:
1. Компилятор языка C/C++. Я использовал Microsoft Visual Studio 2005, но с Runtime Library от Microsoft Visual С++ 6.0(Необходимо для того, чтоб видеть коды ошибок во время испытаний)
2. WinHex(для работы с HEX значениями)
3. OllyDBG(отладчик, можно использовать любой)
4. IDA Pro 4(дизассемблер)
5. Трезвый рассудок и прямые руки

Для начала нам стоит ознакомиться с тестовым стендом. Это будет небольшая программа. Я надеюсь с кодом разберетесь wink

Code
#include <stdio.h>
#include <string.h>

int root()
{
      printf("You have a root!\n");

      return 0;
}

int main()
{
      char login[16];
      char passwd[16];

      printf("login: ");
      gets(login);
      printf("passwd: ");
      gets(passwd);
      if (!strcmp(login, "bob") && ~strcmp(passwd, "god"))
      {
       printf("Hello, bob!\n");
      }

      return 0;
}

Забиваем это в среду разработки и компилируем. Для тех, кто использует Visual Studio рекомендую собирать Debug проект.
Запускаем нашу программу. Пробуем ввести логин вида: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
и пароль вида: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Нажимаем ввод. Что получилось? Windows ругнулась сообщением вида: Инструкция по адресу 0x41414141 обратилась к памяти по адресу 0x41414141... Откуда взялось это значение? Постойте! А ведь 0x41 это HEX ASCII значение буквицы "а".
Значит, во-первых, переполнение произошло в буфере логина, а во-вторых, данный тип переполнения допускает передачу управления на произвольный код, поскольку регистр – указатель команд переметнулся на содержащийся в хвосте буфера адрес. Случайным образом по адресу 0x41414141 оказался расположен бессмысленный мусор, возбуждающий процессор вплоть до исключения, но этому горю легко помочь!
Для начала нам предстоит выяснить, какие по счету символы логина попадают в адрес возврата. В этом нам поможет последовательность в стиле "qwerty...zxcvbnm", вводим ее и... система сообщает, что "инструкция по адресу 0x7a6c6b6a обратилась к памяти". Запускаем WinHex и набиваем эти "7A 6C 6B 6A" на клавиатуре. Получается "zlkj". Значит, в адрес возврата попали 17-й, 18-й, 19-й и 20-й символы логина (на архитектуре x86 младший байт записывается по меньшему адресу, то есть машинное слово, образно выражаясь, становится к лесу передом, а к нам задом).

Быстренько дизассемблируем программу и находим там функцию root!
При нормальном развитии событий она никогда не получает управления. Если, конечно, не подсунуть адрес ее начала вместо адреса возврата. А какой у root'а адрес? Смотрим – 00401150h. Перетягиваем младшие байты на меньшие адреса и получаем 50 11 40 00. Именно в таком виде адрес возврата хранится в памяти. Хорошо, что ноль в нем встретился лишь однажды, аккурат оказавшись на его конце. Пусть он и будет тем нулем, что служит завершителем всякой ASСII-строки. Символам с кодами 50h и 40h соответствуют буквицы "P" и "@". Символу с кодом 11h соответствует комбинация >Ctrl-Q< или >Alt<+>0, 1, 7< (нажмите Alt, введите на цифровой клавиатуре 0, 1 и 7, отпустите Alt).

Задержав дыхание, вновь запускаем программу и вводим "qwertyuiopasdfghP^Q@", пароль можно пропустить. Собственно говоря, символы "qwertyuiopasdfgh" могут быть любыми, главное, чтобы "P^Q@" располагались в 17-й, 18-й и 19-й позициях. Нуль, завершающий строку, вводить не надо: функция gets засунет его самостоятельно.

Если все сделано правильно, то программа победоносно выведет на экран "Your have root", подтверждая, что атака сработала. Правда, по выходе из root'а программа немедленно грохнется, так как в стеке находится мусор, но это уже неважно, ведь функция root отработала и стала не нужна.

Пишем примитивный шелл код.

Вообще говоря, организовать удаленный shell не так-то просто: необходимо, как минимум, установить TCP/UDP-соединение, попутно обманув доверчивый firewall, создать пайпы, связать их дескрипторами ввода/вывода терминальной программы, а самому работать диспетчером, гоняя данные между сокетами и пайпами.

Если среди читателей наберется кворум, эту тему можно будет осветить во всех подробностях, пока же ограничится локальным shell'ом.

Вновь запускаем нашу демонстрационную программу, срываем буфер, вводя строку "AAA...", но вместо того чтобы нажать "ОК" в диалоге критической ошибки приложения, давим "отмену", запускающую отладчик (для этого он должен быть установлен). Конкретно нас будет интересовать содержимое регистра ESP в момент сбоя. На моей машине он равен 0012FF94h, у тебя это значение может отличаться. Вводим этот адрес в окне дампа и, прокручивая его вверх/вниз, находим нашу строку "ААААА". В моем случае она расположена по адресу 0012FF80h.

Теперь мы можем изменить адрес возврата на 12FF94h, и тогда управление будет передано на первый байт переполняющегося буфера. Остается лишь подготовить shell-код. Чтобы вызвать командный интерпретатор в осях семейства NT, необходимо дать команду WinExec("CMD", x). В 9x такого файла нет, но зато есть command.com, который есть анахронизм. На языке ассемблера этот вызов может выглядеть так:

00000000: 33C0 xor eax,eax

00000002: 50 push eax

00000003: 68434D4420 push 020444D43 ;" DMC"

00000008: 54 push esp

00000009: B8CA73E977 mov eax,077E973CA ;"wesE"

0000000E: FFD0 call eax

00000010: EBFE jmps 000000010

Здесь мы используем целый ряд хитростей и допущений, подробный разбор которых требует целой книги. Если говорить кратко, то 77E973CAh – это адрес API-функции WinExec, жестко прописанный в программу и добытый путем анализа экспорта файла KERNEL32.DLL утилитой DUMPBIN. Это грязный и ненадежный прием, так как в каждой версии оси адрес функции свой и правильнее было бы добавить в shell-код процедуру обработки экспорта, описанную в следующей статье. Почему вызываемый адрес предварительно загружается в регистр EAX? Потому что call 077E973CAh на самом деле ассемблируется в относительный вызов, чувствительный к местоположению call'а, что делает shell-код крайне немобильным.

Почему в имени файла "CMD" (020444D43h, читаемое в обратном порядке) стоит пробел? Потому что в shell-коде не может присутствовать символ нуля – он служит завершителем строки. Если хвостовой пробел убрать, то получится 000444D43h, а это уже не входит в наши планы. Вместо этого мы делаем XOR eax, eax, обнуляя EAX на лету и запихивая его в стек для формирования нуля, завершающего строку "CMD ". Но непосредственно в самом shell-коде этого нуля нет!

Поскольку в отведенные нам 16 байт shell-код влезать никак не хочет, а оптимизировать его уже некуда, мы прибегаем к вынужденной рокировке и перемещаем shell-код в парольный буфер, отстоящий от адреса возврата на 32 байта. Учитывая, что абсолютный адрес парольного буфера равен 12FF70h (у вас он может быть другим!), shell-код будет выглядеть так (просто переводим hex-коды в ASCII-символы, вводя непечатные буквицы через alt+num):

login :1234567890123456>alt-112<>alt-255<>alt-18<

passwd:3>alt-192<PhCMD T>alt-184<>alt-202<s>alt-233<w>alt-255<>alt-208<>alt-235<>254<

Вводим это в программу. Логин срывает стек и передает управление на парольный буфер, где лежит shell-код. На экране появляется приглашение командного интерпретатора. Все! Теперь с системой можно делать что угодно!

Заключение.

Повторяя выше описанные действия вы поймете как этот механизм работает. Попробуйте повторить =) Вам понравиться =)

В следующий раз мы продолжим беседовать на эту тему.

з.ы. Ознакомьтесь со статьей: UNIX Assembly Codes Development for Vulnerabilities Illustration Purposes" – великолепное руководство по технике переполнения буферов и захвату контроля удаленной машиной.


bda-expert.ru — это система форумов, где можно общаться быстро и свободно, где любая точка зрения имеет право на жизнь.

Сообщение отредактировал eXceed - Среда, 13.08.2008, 00:09
 
eXceedДата: Четверг, 28.08.2008, 00:16 | Сообщение # 3
Генералиссимус
Группа: Гости
Сообщений: 5466
Репутация: 616
Статус: Offline


bda-expert.ru — это система форумов, где можно общаться быстро и свободно, где любая точка зрения имеет право на жизнь.
 
  • Страница 1 из 1
  • 1
Поиск:

close