Коллектив авторов - Защита от хакеров корпоративных сетей
Программа, уязвимая к переполнению буфера
Главная цель – привести в этой секции пример программы, уязвимой к переполнению буфера. Рассмотренная в этой секции программа очень похожа на последний пример, но вместо постоянной строки входных данных она использует ввод данных пользователя. Это позволило загружать в регистр EIP нужные данные.
Исходный текст программы, уязвимой к переполнению буфера
На последующих рисунках, начиная с рис. 8.17, представлена программа, предназначенная для считывания входных данных из файла в локальную переменную, размещенную в области стека. В результате присваивания этой переменной входных данных происходит переполнение буфера. Управляя входными данными программы, появляется идеальная возможность изучить возможности использования переполнения буфера. В программе вызывается специально написанная для примера функция bof(), которая открывает файл «badfile», считывает из него входные данные программы размером 1024 байта, записывает их в восьмибайтовый буфер и закрывает файл. При записи данных в буфер происходят переполнение буфера и порча данных стека, а по завершении функции bof() в регистр EIP загружается значение из файла «bad-file». Исследуем работу этой программы в Linux и Windows, приводя для обеих платформ соответствующие примеры.
Рис. 8.17. Пример программы, уязвимой к переполнению буфера
Дизассемблерование
На рисунке 8.18 представлен дизассемблерный вид функции bof(). Дизассемблерный вид всей программы на рисунке не показан, поскольку она аналогична предыдущей программе и отличается от нее только функцией bof(). При большом размере файла «badfile» во время работы функции fread() произойдет переполнение буфера, а команда ret функции bof() загрузит в регистр EIP величину из входных данных.
Рис. 8.18. Дизассемблированный вид функции bof()Дамп стека после переполнения
Главное предназначение этой программы заключается в анализе уязвимостей переполнения буфера, поэтому на рис. 8.19 показан дамп стека после выполнения функции fread(). Для примера был создан файл «badfile» с двадцатью символами «Л». После выполнения функции fread() область стека изменена так же, как и в предыдущей программе, но дополнительно появилась возможность управлять записью данных в буфер с помощью файла «badfile». Запомним, что в функции определена дополнительная переменная стека – указатель дескриптора файла (дескриптор файла – уникальный идентификатор, присваиваемый системой Windows файлу в момент его открытия или создания и существующий до момента его закрытия), которая размещена в старших адресах памяти стека сразу за областью буфера.
Рис. 8.19. Дамп стека после выполнения функции fread()
Программа переполнения буфера
После ознакомления с примером программы, уязвимой к переполнению буфера при чтении файла «badfile», пришло время познакомиться с программой, извлекающей из этого пользу, – программой переполнения буфера. Программа переполнения буфера написана на ANSI C, поэтому она может быть откомпилирована любым компилятором ANSI C. Для приведенных в книге примеров использованы компиляторы Visual C++ for Windows NT и GCC for Linux.
Вначале рассмотрен более простой случай – программа переполнения буфера для Linux, а затем для Windows NT и отмечены различия используемых способов переполнения буфера для других платформ.
Основные принципы построения программ переполнения буфераВ секции подробно описаны принципы построения работоспособной программы переполнения буфера для различных платформ. В предыдущем примере было показано, каким образом переполнение буфера используется для контроля содержимого регистра EIP. Теперь нужно разобраться, как этим можно воспользоваться вообще и для управления компьютером в частности.
Контролируя содержимое регистра EIP, можно выполнить нужный программный код. Обычно это достигается путем прямого или косвенного указания на специально написанный для этих целей программный код – программный код полезной добавки, или payload-код. Программный код полезной добавки описываемой программы переполнения буфера очень прост. Он только демонстрирует возможность осуществления задуманного. Более изощренные примеры программного кода полезной нагрузки будут рассмотрены позднее.
В основе современных программ переполнения буфера лежат несколько идей, но будут рассмотрены только некоторые из них, применимые к большинству типов рассматриваемых программ.
Создание программ переполнения буфера подразумевает несколько этапов. Во-первых, нужно получить доступ к буферу данных, то есть найти способ размещения в нем данных. Во-вторых, для того чтобы выполнить нужный код, следует найти способ контроля содержимого регистра EIP. Таких способов несколько. И наконец, в-третьих, нужен программный код полезной нагрузки, выполняющий возложенные на него функции.
Структура программы переполнения буфераПервый этап разработки программы переполнения буфера заключается в поиске способа переполнения буфера. Обычно это несложная задача, решаемая автоматизированными сетевыми средствами записи в буфер или записью в файл нужных данных, которые позже прочитает программа, уязвимая к переполнению буфера. Но иногда не все так просто.
Загрузчики и программный код полезной нагрузки. В военном деле широко используются два связанных понятия: средства доставки и полезный груз. Аналогичные понятия применимы и для переполнения буфера. Говоря о переполнении буфера, подразумевают наличие средства доставки – загрузчика (injection vector) и полезного груза – программного кода полезной нагрузки. Загрузчик – выполнимый программный код, который позволяет управлять указателем на текущую команду удаленной машины. Это код целиком определяется компьютером, на котором он будет выполняться, и преследуемыми целями. Главная задача загрузчика заключается в том, что он должен заставить выполниться программный код полезной нагрузки. Программный код полезной нагрузки подобен вирусу: он должен работать везде, в любое время и независимо от того, как он попал на удаленную машину. Если программный код полезной нагрузки не удовлетворяет перечисленным требованиям, то он неработоспособен. Рассмотрим условия его создания.
Условия работоспособности программного кода полезной нагрузки. Проще всего загрузчик и программный код полезной нагрузки разместить в одном стеке, но так обычно не делается. При использовании стека для хранения загрузчика и программного кода полезной нагрузки следует позаботиться об их взаимодействии и учесть ограничения на допустимый размер программного кода полезной нагрузки. Если программный код полезной нагрузки загружается в память раньше загрузчика, то следует убедиться, что они не конфликтуют друг с другом. Если программы перекрываются, то в программном коде полезной нагрузки следует предусмотреть команду перехода, которая позволяет обойти код загрузчика и продолжить выполнение программного кода полезной нагрузки. Если вопросы их взаимодействия трудноразрешимы, то рекомендуется размещать программный код полезной нагрузки отдельно от загрузчика.
Все программы вводят данные пользователя и хранят их где-нибудь. Любой буфер в программе является кандидатом на хранение программного кода полезной нагрузки. Вопрос только в том, как заставить процессор выполнить его.
Чаще всего для размещения программного кода полезной нагрузки используются:
• файлы на диске, которые загружаются в память;
• доступные локальному пользователю переменные окружения;
• передаваемые через Web-запрос общие переменные окружения;
• доступные пользователю поля сетевого протокола.
После размещения программного кода полезной нагрузки в памяти компьютера остается правильно загрузить в регистр EIP-адрес кода. При размещении программного кода полезной нагрузки не в области стека появляется ряд преимуществ, и ранее невозможное становится возможным. Например, сразу исчезает ограничение на размер кода. До сих пор для установления контроля над компьютером используется ошибка занижения или завышения на единицу числа подсчитываемых объектов (off-by-one error).
Способы передачи управления программному коду полезной нагрузкиВ последующих секциях рассматриваются способы передачи управления программному коду полезной нагрузки. Особое внимание уделяется поиску не зависимых от платформы решений и способам подмены сохраненного в стеке содержимого регистра EIP, которые позволяют выполнить нужный код. Для этого мало знать адрес размещения программного кода полезной нагрузки в памяти.