Вопрос или проблема
Периодически, когда я размышляю вслух, и люди подслушивают меня, я вынужден объяснять, что такое переполнение буфера. Поскольку я не могу придумать хорошую метафору, я трачу около 10 минут, объясняя, как работают (уязвимые) программы и выделение памяти, а затем у меня остается всего 2 предложения о самом эксплойте (“Итак, переполнение буфера заполняет буфер бессмыслицей и перезаписывает указатель, чтобы он указывал на то, что я хочу”). К этому моменту большинство людей становится депрессивными… Как можно просто объяснить переполнение буфера обычным людям? Если возможно, пожалуйста, включите компонент “переполнения”, но также хотя бы введение в то, почему это значит, что 공격ующий может получить то, что он хочет. Имейте в виду, что люди со средним (и ниже среднего) уровнем интеллекта должны понимать, о чем идет речь, поэтому, хотя вы совершенно свободно можете (на самом деле, поощряется) объяснить, что представляет собой каждая часть вашей метафоры (аналогии?), не полагайтесь на какие-либо супер-технические описания…
ПС, связанный вопрос, объясняющий техническими терминами, что делает переполнение буфера: Что такое переполнение буфера?
Лицензия Stack Exchange на отображение этого ответа была автоматически прекращена из-за нарушения лицензии со стороны Stack Exchange. См. раздел 7.
Идея занять больше места, чем у вас есть, и таким образом выливаться в другую область, достаточно проста для визуализации. Но, вероятно, не совсем ясно, как это может привести к тому, что злодей выполнит свой код.
Это довольно просто объяснить, если вы понимаете это достаточно хорошо. Просто убедитесь, что вы затрагиваете важную информацию. Более или менее в следующем порядке:
-
“Стек” — это место, где вы можете хранить временную информацию. “Указатель стека” определяет, где заканчивается стек. Когда функция выполняется, она перемещает указатель стека, чтобы обеспечить себе память для работы, а когда заканчивается, возвращает указатель назад на место, откуда его взяла.
-
Стек растет назад. Чтобы выделить 100 байт в стеке, вы вычитаете 100 из указателя стека вместо того, чтобы добавлять. Если стек предыдущей функции начинался с 1000, и я хочу 100 байт, то мой стек начинает с 900.
-
Это означает, что если вы используете больше пространства, чем выделили, вы не просто продолжите писать в пустоту, вы фактически начнете перезаписывать предыдущие значения в стеке.
-
Когда моя функция запускается, самое верхнее значение, оставленное мне на стеке предыдущей функцией, — это возвращаемый адрес, куда мне следует вернуться, когда моя функция закончится.
-
Это означает, что если моя функция переполнит свой стек, первое, что она перезапишет, — это возвращаемый адрес. Если атакующий осторожен в том, чем заполняет стек, он может указать любой возвращаемый адрес, который хочет.
-
Когда моя функция завершается, код, находящийся по этому возвращаемому адресу, будет выполнен следующим.
Простой пример
В Smashing the Stack for Fun and Profit, где этот метод был изначально описан, была представлена самая простая и понятная техника. Представьте, что функция считывает ваше имя, а затем возвращается. Таким образом, ваш стек выглядит следующим образом:
Указатель стека Пред. указатель стека
+----------------------------------+--------------+................
| Ваше имя здесь | Возвращаемый адрес | Старый стек ...
+----------------------------------+--------------+................
Но злодей делает свое имя достаточно длинным, чтобы переполнить пространство. И не только это, вместо того, чтобы ввести настоящее имя, он вводит какой-то Злой Код, немного заполнения и адрес этого Злого Кода.
+----------------------------------+--------------+................
| [ Злой Код ]xxxxxxxxxxxxxxxxxxxxxxЗлой адрес | Старый стек ...
+----------------------------------+--------------+................
▲──────────────────────────────────┘
Теперь вместо того, чтобы возвращаться к предыдущему вызову, вы переходите прямо к [Злому Коду]
. Теперь вы выполняете его код вместо вашего приложения. После этого почти конец игры.
Меры предостережения и другие техники
Две из техник, используемых для уменьшения эффективности переполнения стека, — это DEP и ASLR.
DEP (“Защита от выполнения данных”) работает, помечая стек как неисполняемый. Это означает, что [Злой Код]
на стеке не будет работать, потому что выполнение кода на стеке больше не разрешено. Чтобы обойти это, атакующий находит фрагменты существующего кода, которые сделают то, что он хочет. И вместо того, чтобы просто перезаписывать свой возвращаемый адрес, он создает цепочку возвращаемых адресов через стек для всех функций, которые хочет запустить поочередно. Это называется “Программирование, ориентированное на возвращение”, или ROP. Эта цепочка возвратов называется “Цепочка ROP”. Это действительно сложно сделать. Но есть инструменты для помощи.
ASLR (“Случайное расположение адресного пространства”) работает, рандомизируя расположение всех интересных функций. Теперь создание цепочки ROP не так просто — каждый раз, когда программа запускается, все адреса находятся в разных местах. Поэтому, когда атакующий попытается перезаписать возвращаемый адрес на свой Злой адрес, он не будет знать, какие числа использовать, потому что код всегда в разных местах.
Ни DEP, ни ASLR сами по себе не предлагают большой защиты, но вместе они делают успешную эксплуатацию очень трудной. Хотя некоторые обходы иногда существуют, но обход, который работает везде, нет. Если вы сможете обойти DEP + ASLR, это будет довольно уникальный успех.
Другие ответы по-прежнему достаточно технически ориентированы, так что я предлагаю это.
Представьте, что у вас есть класс детского сада. Есть ящики для каждого ученика, чтобы они могли положить в них свои обувь. Каждый ящик вмещает одну обувь. Поэтому для каждого ученика вы предоставляете два ящика.
Каждому ученику назначаются два смежных ящика. Затем учитель вызывает учеников по очереди, чтобы они положили свою обувь в ящики, к которым они назначены.
Когда учитель вызывает Плохого Билли, Плохой Билли хочет намять Глупую Салли. Ящики Билли — это номера 5
и 6
, а ящики Салли — числа 7
и 8
. Билли кладет свою обувь в 5
и 6
, а затем переполняет свой определенный лимит и кладет слизистую жабу в Саллину ячейку номер 7
.
Поскольку учитель не обеспечивает никаких защит на установленный лимит для использования ящиков в смежном порядке, Билли может переполнить свой лимит и повредить Саллыну ячейку. Теперь, когда Салли идет за своей обувью, она вместо этого получит слизистую жабу фу!
+-------------------+--------------------+-------------------+--------------------+
| ЯЩИК 5 | ЯЩИК 6 | ЯЩИК 7 | ЯЩИК 8 |
+-------------------+--------------------+-------------------+--------------------+
| | | | |
| Левый Билли | Правый Билли | Левый Саллы | Правый Саллы |
+-------------------+--------------------+-------------------+--------------------+
Билли ввел три предмета, где было указано, что он должен положить только 2, так работает переполнение стека на высоком уровне, кто-то вмешивается в хранилище, на которое у него нет прав, и когда это хранилище читается, это не то, что вы ожидали.
+-------------------+--------------------+------------+--------------------+
| ЯЩИК 5 | ЯЩИК 6 | ЯЩИК 7 | ЯЩИК 8 |
+-------------------+--------------------+------------+--------------------+
| | | | |
| Левый Билли | Правый Билли | Слизистая жаба | Правый Саллы |
+-------------------+--------------------+------------+--------------------+
Переполнение буфера могло бы быть предотвращено, если бы учитель более внимательно следил, чтобы каждый ученик использовал только то количество хранения, которое ожидалось.
Я попробую это без использования аналогий.
Компьютер в основном состоит из памяти, это важная часть, содержимое памяти — это инструкции, которые говорят компьютеру, что делать, и данные, которые используют эти инструкции и могут использовать или изменять. Часто необходимо хранить данные переменной длины. Например, если программе нужно отслеживать адрес электронной почты, он может быть очень коротким ([email protected]) или очень длинным ([email protected]). Некоторые программы не очень хорошо отслеживают максимальную длину своих записей данных. Поэтому если программа была разработана с максимумом, скажем, 100 символов для адреса электронной почты, и кто-то подал ей адрес электронной почты с более чем 100 символами, программа просто продолжит записывать оставшуюся часть адреса в память после конца выделенного пространства. Важно помнить, что вся память — это все, программа сама находится в памяти рядом с записями данных.
Кто-то, кто точно знает, как работает эта программа, мог бы дать ей очень тщательно подготовленный адрес электронной почты, который был очень длинным и имел специальные символы в конце. Идея заключается в том, что когда программа сохраняет адрес электронной почты в памяти, она слепо запишет эти специальные символы в ту часть памяти, где программа думала, что находятся другие части самой программы, а затем, когда она попытается выполнить эти части, она вместо этого выполнит любое программное обеспечение, на которое эти специальные символы переводятся в компьютерный код. Таким образом, кто-то сможет заставить компьютер выполнить все, что он хочет, просто тщательно подготовив данные, которые он передал программе.
Хороший вопрос. Вот аналогия, которая не самая технически точная, но должна донести идею.
Представьте себе кулинарную книгу на бумаге с тремя дырками в папке (память) и очень глупого повара (процессор, т.е. CPU).
- Люди могут добавлять или удалять страницы из папки (загружать или выгружать программы и данные в память)
- Повар просто следует каждой инструкции на странице, на которой он находится
- Повар начинает с начала (загрузчик) и продолжает, пока не встретит инструкцию “закрыть книгу”
- Даже если инструкция – перевернуться на другую страницу (перевернуть на страницу 394)
Итак, обычно вы пишете на первой странице “Переверните на страницу 200 (вафли)”, открываете папку и кладете туда вафли на страницу 200. Затем заставляете повара начинать – повар должен делать вафли!
Но подождите… есть нападающий! Они написали заметки на полях вашего рецепта для вафель (вне буфера) – и повар выполняет эти инструкции, хотя они явно рукописные.
Повар никогда не был предупрежден, чтобы делать только то, что напечатано на оригинальном листе (в нормальном буферном пространстве) – повар также сделает все, что после этого (в памяти после буфера).
Возможно, повар добавит уксус в вафли (повредит ваши файлы). Возможно, повар перевернет на триста девяносто четвертую страницу и просто оставит сырое яйцо лежать, без дела, пока оно не испортится и не заплесневеет (отключит ваш антивирус). Возможно, повар выбросит все из кухни (удалит все ваши файлы), или поставит замок на дверь вашей кухни, чтобы вы не могли войти (вымогательский софт), или откроет окно (установит троян / задняя дверь), чтобы нападающий мог влезть в окно.
Я всегда объясняю это как разрыв ведра. Ведро нужно, чтобы защитить содержимое от внешней среды и наоборот, но вы используете содержимое, чтобы добраться до внешней стороны ведра и, следовательно, получить доступ к областям системы, к которым вы не должны иметь доступ.
Как вам это?
Данные в компьютере хранятся в виде длинного списка чисел, как дорожки на музыкальном кассете. В отличие от музыки, которая воспроизводится от начала до конца, компьютерам нужно перескакивать с одной дорожки на другую, поэтому им нужен “список треков”, чтобы знать, где начинается каждая из них.
Списки треков просты для музыки, так как каждая песня имеет известную длину. С компьютером количество данных, которые нам нужно хранить, может быть неизвестно, например, если они поступают из Интернета. Если дорожка, которую мы используем, заполняется, нам нужно переключиться на другую, неиспользуемую. Если мы этого не сделаем, например, если неправильно предположим, что нам никогда не предоставят больше определенного количества данных, мы можем использовать слишком много ленты и “записать” на следующую дорожку. Когда программа попытается прочитать следующую дорожку, она получит часть наших данных, а не то, что там было раньше.
Это может быть опасно, поскольку перезаписанные данные могли быть набором инструкций для выполнения. Если это так, то компьютер теперь будет выполнять инструкции, загруженные прямо из Интернета!
Ответ или решение
Объяснить понятие переполнения буфера простыми словами можно с использованием аналогий, чтобы сделать эту концепцию более доступной для понимания. Вот один из способов объяснить это явление, используя метафору с коробками и водой:
### Пример с коробками и водой
Представьте, что у вас есть несколько коробок (это плоские области памяти в компьютере), в которые вы можете складывать разные вещи. Допустим, каждая коробка имеет своё ограничение по объему – она может вмещать только определенное количество предметов, например, только три мячика.
Теперь, если вы действительно хотите положить туда четыре мячика и не обращаете внимания на ограничения, вы просто их запихиваете. Четыре мячика не помещаются, и в результате один из них вываливается из коробки и начинает попадать в соседнюю коробку. Это и есть нечто похожее на переполнение буфера: данные (в данном случае мячики) выходят за пределы отведенного пространства и могут повредить или изменить содержимое других “коробок”.
### Как это связано с безопасностью
Теперь давайте предположим, что в одной из соседних коробок есть что-то ценное или важное (например, инструкции по выполнению каких-то задач). Если вы заполнили свою коробку неправильно и мячик попал в соседнюю коробку, он мог бы случайно испортить эти важные инструкции. Это значит, что теперь вместо выполнения правильной задачи, компьютер может выполнять что-то совершенно другое, что вы не собирались делать.
Атакующая сторона может воспользоваться этой ситуацией, чтобы подставить свои собственные «инструкции» или код, используя переполнение буфера для выполнения своих злонамеренных действий. Это можно сопоставить с тем, когда кто-то добавляет вредоносные команды в ваш набор инструкций просто потому, что у вас не было должной защиты.
### Итог
Таким образом, переполнение буфера — это ситуация, когда программа записывает больше данных, чем было предусмотрено, и это приводит к тому, что данные начинают «затапливать» соседние области памяти. Эта уязвимость может быть использована злоумышленниками для выполнения своих собственных программ в системе, что может привести к различным нежелательным последствиям, как-то к краже данных или повреждению систем.
Хорошая защита от переполнения буфера заключается в том, чтобы всегда внимательно следить за тем, сколько данных вы записываете и какие области памяти вы используете, чтобы предотвратить случайные ошибки и возможности для атак.