![]() |
Уязвимости С/С++ кода. Buffer Overflow.
Kids Buffer Overflow Paper. Автор: darkeagleБродя по многочисленным форумам, смотря рассылки и т.д. Я наткнулся на один очень частный вопрос. Звучит он примерно так: "Я не пойму технику переполнения буфера, объясните, пожалуйста!". В данном материале я бы хотел рассмотреть технику полностью. Весь материал будет рассчитан для ОС Linux. Я постараюсь затронуть тему локального и удаленного переполнения буфера. Постараюсь внятно объяснить все. Я думаю, этот материал будет понятен даже новичку. Итак, пора приступить к изучению. Переполнение буфера это, пожалуй, самая распространенная ошибка как в больших приложениях, так и в маленьких утилитах. Впервые техника переполнения буфера была предпринята в нашумевшем черве конца 80-х годов - черве Роберта Морриса. С тех пор, данная уязвимость стала такой популярной, что на данный момент число эксплоитов, которые написаны на основе данной уязвимости, перевалило уже за отметку более 2-х тысяч. Из всех уязвимостей, которые на данный момент известны миру, переполнение буфера занимает 1-ое место. Ежедневно обнаруживается огромное количество ошибок на основе переполнения буфера. Для примера, подпишитесь на рассылку новостей bugtraq, и составьте процентное соотношение обнаруженных уязвимостей. У меня вышло примерно 35-40 % уязвимостей основанных на переполнении буфера. А ведь это только публичные данные! Представьте, что находится в закрытых источниках, там примерно такое же соотношение. Что-то я уж заговорился :) Давайте перейдем к обсуждение данной ошибки. Скажу, что для изучения данного материала, у Вас должны быть хотя бы начальные знания языка Си под Linux. Для дальнейшей работы нам понадобятся следующие инструменты: gcc, gdb, gedit (но можно и другой редактор). Теперь перейдем к непосредственному объяснению техники переполнения. Допустим, Вы написали утилиту, которая принимает входную строку (первый аргумент). Далее она вызывает системный вызов утилиты "ls" и ищет файл/директорию. В случае если файл/директория найдены, то программа оповещает пользователя о том, что такой файл/директория существуют в системе. Давайте посмотрим на пример такой программки. Код:
[========================================CODE#1 util.c=================================]Код:
[root@localhost boft]# gcc util.c -o utilКод:
[root@localhost boft]# touch something.fileТеперь давайте попробуем ввести название файла более 267 символов. Потом объясню, почему именно более 267 символов. Итак: Код:
[root@localhost boft]# ./util `perl -e 'print "A"x268'`Идем далее... Программа ничего нам не вывела, а выскочило странное сообщение "Segmentation fault (core dumped)". Чтобы оно могло значить??? А значит оно одно. Наша программа повела себя нестандартно и что-то там произошло. А что именно я попытаюсь сейчас объяснить. В системе Unix (как и в Win32) для хранения данных используется "стек". Именно в нем хранятся различные значения переменных (да и они сами там хранятся) в момент запуска программы. После закрытия программы все данные выгружаются из "стека". Стек можно сравнить со складом :) Конечно это довольно грубое объяснение, но все же. Так вот, в нашей программе мы используем несколько буферов. А именно буфер для хранения значения переменной "filename" и буфер переменной "cmd". Буфер "cmd" нас не интересует. А вот буфер переменной "filename" нам более интересен. А все потому, что именно данная переменная используется как имя файла/директории, истоки которого берутся из первого входного аргумента при запуске программы. Копирование строки происходит путем стандартной в языке Си функции strcpy(); Синтаксис ее таков: strcpy(строка_в_которую_нужно_ опировать_данные, строка_из_которой_следует_к опировать); Так вот строка в коде: Код:
strcpy(filename, argv[1]);Но взглянем выше, и мы увидим следующее: Код:
char filename[255];Переполнение буфера - это буфер, значение которого определяется ранее в программе и в последующий момент выходит за рамки определяемости (т.е. переполняется). Несколько запутанно и некорректно. Но каждый может для себя составить определение этому словосочетанию :) Для меня же понятнее мое определение. Двигаемся дальше. Я думаю все линуксоиды знают очень хорошую и нужную утилиту gdb. Это утилита является встроенным отладчиком в системах Unix. gdb расшифровывается как GNU Debugger. Теперь давайте запустим нашу утилиту в этом отладчике и попробуем ввести длинное имя файла/директории. Код:
[root@localhost boft]# gdb utilСмотрим пример: Код:
(gdb) r `perl -e 'print "A"x268'`BBBBМы указываем ему 4 символа "B", поэтому строка 0x42424242 in ?? () означает что после полного переполнения адрес по которому обратится функция будет указывать на адрес "B" в шестнадцаричном -hex формате. А теперь взглянем на следующий пример: Код:
(gdb) r `perl -e 'print "A"x268'`BBBAКод:
(gdb) r `perl -e 'print "A"x268'`BBBAстек (адрес вершины стека = 0xbfffffff) __ || ДАННЫЕ || || \/ АДРЕС Т.е. в случае с нашим переполнением программа себя ведет в стеке так: Идут данные... Если все в порядке, то программа обычно завершает свою работу и выгружается из стека. В случае переполнения ДАННЫЕ превышают норму и уже АДРЕС будет указывать не на выход из функции ( в нашем случает это return в main() ), а на что-то другое ( в нашем случае это последние 4 символа в аргументе. ) Давайте взглянем на следующее. Я думаю, вы еще не закрыли gdb. Введите следующую команду. Код:
(gdb) i rКод:
ebp 0x41414141 0x41414141Т.е. попробуйте ввести такое в нашу утилиту: Код:
`perl -e 'print "A"x267'`Взглянем на значения регистров: Код:
(gdb) i rА попробуйте ввести такое значение: Код:
`perl -e 'print "A"x268'`Теперь давайте взглянем на другой регистр. Название ему EIP. Код:
eip 0x41424242 0x41424242РЕГИСТРЫ. Вообще регистры это некое подобие строителей внутри процессора. Они как бы получают данные и складывают их в компьютере. Т.е. в случае со строителями они строят дом/гараж и т.д. Они получают данные и складывают их, а далее некая программа пытается прочесть информацию из этих регистров. Количество регистров в архитектуре процессора x86 большое. И с каждым разом все увеличивается и увеличивается. Они бывают как 16-ти разрядные, так и 32-х. Сейчас я хочу рассказать более детально об основных регистрах процесорра. регистр EIP - это регистр содержит в себе адрес функции, на который должна перепрыгнуть программа в какое-либо действие. В нашем случае адрес eip был равен BBBA = 0x41424242. А что расположено по этому адресу? А ничего. В дальнейшем мы разберем эту тему. регистр ESP - это регистр, с помощью которого можно бегать по стеку. Т.е. обращаться к какому либо адресу в стеке. регистр EBP - это регистр, который дает нам возможность прямого обращения к данным, находящимся в стеке. Эти три регистра считаются основными. Понимание их значений является, по сути, основным фундаментом в понимании техники переполнения буфера. Итак, пора двигаться далее... |
ТЕХНИКА ПЕРЕПОЛНЕНИЯ. Думаю, вы уже наглотались теории по самые уши :) Ну ничего осталось совсем чуть-чуть. Я сейчас постараюсь максимально внятно объяснить процесс переполнения, а далее нам останется только осуществить все на практике. И мы уже будем на коне! Итак, поехали... Процесс переполнения происходит следующим образом: Вы определяете размер буфера и его крайний край :) (т.е. значение при котором регистр ebp затрется). Далее. Подготавливаете "мусорный буфер". Мусорный буфер - это данные, которые просто заполнят стек ненужной информацией для переполнения буфера. Далее Вы наглядно это увидите. Потом мы копируем шеллкод в буфер. О том, что такое шеллкод я Вам сейчас поведаю. Шеллкод - это некий код, переведенный в машинные инструкции. Почему именно "шеллкод", так это, потому что часто после его исполнения на компьютере предоставляется доступ к оболочке Unix (т.е. к shell-оболочке). Шеллкод может быть локальным и удаленным. Локальный шеллкод - это код для локальных программ, которые исполняются на локальной машите. Т.е. пользователь работает за компьютером, в котором имеется уязвимая программа. После эксплуатации, которой, пользователю сразу представятся права уязвимой программы. Т.е. допустим, программа запущена с правами супер-пользователя (root), а у пользователя допустим права games (игровые). Когда пользователь (атакующий) успешно проэксплуатирует программу, у него появятся права супер-пользователя в локальной машине. Удаленный шеллкод - это код для удаленных демонов (программ серверов). Т.е. например Вы обнаружили уязвимость в каком либо сервере. При подключении на который, передается длинная строка, а далее сервер завершает работу с ошибкой переполнения буфера. В данном случае пользователь не имеет прав на удаленной машине. Поэтому, написав эксплоит, который бы переполнял буфер и исполнял удаленный шеллкод с правами запущенного сервера. Часто удаленные шеллкоды после исполнения открывают на удаленной машине порт, после подключения на который, предоставляется командная строка (шелл) с правами запущенного сервера. Итак, с шеллкодом мы разобрались. Двигаемся далее. После копирования шеллкода в буфер, мы должны указать адрес нашего шеллкода, чтобы после переполнения, уязвимая программа обращалась на инструкцию заданную в шеллкоде. То бишь на инструкцию появления командной строки. Если все это представить в уме, то это примерно высветится так: Код:
стек <data><...shellcode...><retaddr>В нашем случае нам нужно разместить наш адрес по следующим параметрам: 268+4 = 268+1(269)+1(270)+1(271)+1(272). Наш адрес ляжет в радиус 268 -- 272. Получается как раз 4 символа (байта) и 8 (4 символа в hex) байт в качестве указания адреса на который будет передано управление после переполнения. Итак, думаю довольно сухомятки. Пора приступить к реалиям. Для начала давайте снова запустим нашу программу в отладчике gdb. Код:
(gdb) r `perl -e 'print "A"x1000'`Давайте взглянем внутрь него: Код:
(gdb) x/200x $espИтак, что мы видим. А видим мы следующее. Слева у нас как раз те адреса возврата на данные, которые расположены справа. То бишь на данные символов "A". Из этого может следовать, что после переполнения наша уязвимая утилита обращается по одному из этих адресов, в которых имеется значение "A". На ум сразу приходит, что после того как шеллкод будет расположен, он успешно должен исполниться, после того как мы успешно засунем адрес возврата на наш код. Итак, пора всю нашу занудную теорию перенести в практические действия. Сейчас я приведу код эксплуататора для нашей утилиты, и мы подробно разберем его. В качестве адреса я взял один из вышеприведенных адресов, значение у которого 0x41414141. Код:
[========================================CODE#2 ex_util.c=================================]В начале мы объявляем переменные. Переменную RET, для того чтобы в ней хранить наш адрес. Далее идет переменная-индекс. Она нужна для запуска цикла добавления адреса на наш шеллкод. Следующая переменная "buf", нужна для того, чтобы полностью подготовить наш код. Вида <data><shellcode><retaddr>. Переменная "p" - это указатель на наш буфер данных. Она нужна для того, чтобы посимвольно добавить адрес возврата на код. Итак, далее идет уже код. В первой строке кода мы присваиваем переменной RET адрес на наш код. Далее переменной "p" указываем на то, что она теперь стаем указателем на наш буфер. Потом мы заполняем наш буфер "мусором" для того чтобы переполнить буфер данными, а далее положить шеллкод после "мусора". Далее идет цикл for (...). В нем мы, как уже было сказано, добавляем адрес с "краев" на 4 байта вперед для того, чтобы адрес поместился полностью. Следующая строка говорит нам о том, что бы запускаем нашу утилиту с заполненным буфером в качестве первого входного аргумента. Вот в принципе и все! :) В данном примере я показал простейшее переполнение на основе входного параметра. Так же мне хотелось бы Вам еще показать переполнения, основанные через "Переменные окружения" и удаленные переполнения буфера. |
ПЕРЕПОЛНЕНИЕ БУФЕРА ЧЕРЕЗ "ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ". Я думаю, многие знают, что такое Переменная Окружения. Если нет, то переменные окружения это некие хранилища информации :). Они используются для того, чтобы хранить какую либо информацию. Но очень часто при написании больших программ, программисты допускают ошибки переполнения буфера при передачи этих самых данных через эти самые переменные ; -). Давайте рассмотрим пример написания такой программки. Код:
[========================================CODE#3 env.c=================================]sprintf(буфер_куда_копировать, формат_копирования, откуда_копировать_данные); Скажу лишь то, что параметр "формат_копирования", может быть отпущен, но в этом случае возникает другая ошибка программирования. Название ей Ошибки При форматировании Строк. Но об этом читайте в других источниках. Синтаксис функции getenv() таков: переменная_куда_копировать _значение = getenv(название_переменной_ок ужения); Итак, мы рассмотрели пример уязвимой программы. Напишем пример программы, которая покажет нам, как переполняется в этом случае буфер. Но сначала откомпилируйте эту программу. Код:
[========================================CODE#4 env_dos.c=================================]Он таков: setenv(имя_переменной_окружен я, значение, значение_перезаписи - 1 да, 0 - нет); Все. Откомпилируйте программу. Код:
[root@localhost boft]# gcc env.c -o envКод:
[root@localhost boft]# gdb env -core core.2827Код:
(gdb) x/100x $espКод:
[========================================CODE#5 ex_env.c=================================]Код:
[root@localhost boft]# gcc ex_env.c -o ex_envУДАЛЕННОЕ ПЕРЕПОЛНЕНИЕ БУФЕРА. Я думаю, многие видели в security рассылках сообщение об очередной ошибке в каком-либо демоне. И в advisory написано, например, что тип атаки является "Удаленным" (Remote). Вначале статьи мы описали принцип локального переполнения. Сейчас я хочу показать Вам пример удаленного переполнения. Мы напишем уязвимый демон. А далее напишем для него эксплоит. Итак, рассмотрим пример уязвимого сервера. Код:
[========================================CODE#6 vsrv.c=================================]Код:
[root@localhost boft]# gcc vsrv.c -o vsrvКод:
[root@localhost boft]# telnet 127.0.0.1 2278Код:
[========================================CODE#7 vsrv_dos.c=================================]Код:
[root@localhost boft]# gcc vsrv_dos.c -o dosКод:
[root@localhost boft]# ./vsrv 2278Код:
[root@localhost boft]# gdb vsrv -core core.3390Настало время написать эксплоит. Код:
[========================================CODE#8 exp_vsrv.c=================================]Код:
[root@localhost boft]# gcc exp_vsrv.c -o expВот в принципе и все. Вообще переполнение удаленное и локальное мало чем отличается. ЗАКЛЮЧЕНИЕ. В этом материале я постарался рассказать очень подробно тему переполнения. Я думаю, она очень понятна даже для человека, который вообще не знал об этой уязвимости. В заключении хотелось бы также отметить то, что я разработал утилиту, которая генерирует эксплоит автоматически. Скачать ее вы можете на сайте http://unl0ck.info. На данный момент это версия 0.3. В будущем планируется добавить новые возможности. Хотелось бы поблагодарить следующих людей: stine, f00n, nekd0, forsyte, eitr0n, msm, mssunny. Без этих людей жизнь в Сети была бы однообразна. По поводу каких-либо вопросов пишите на darkeagle@list.ru http://unl0ck.info P.S. Все примеры программ в статье Вы можете скачать http://unl0ck.info/boft.tgz От себя: Об этом должен знать любой кодер =) |
Старье это, годится для ядер 2.4.х, нового ничего. Хотя новичкам пригодится
|
Только все же не C++, а C.
В C++ не принято использовать подобный код. |
| Время: 14:46 |