Rapid Refresher
Прежде всего, вы должны понимать, что основными единицами защиты в современных ОС являются процесс и страница памяти.Процессы являются доменами защиты памяти;это уровень, на котором ОС применяет политику безопасности, и, таким образом, они строго соответствуют работающей программе.(Там, где они этого не делают, это либо потому, что программа работает в нескольких процессах, либо потому, что программа используется совместно в какой-то среде; последний случай потенциально может быть «интересным с точки зрения безопасности», но это «другая история».)Страницы виртуальной памяти - это уровень, на котором оборудование применяет правила безопасности;каждая страница в памяти процесса имеет атрибуты, которые определяют, что процесс может делать со страницей: может ли он читать страницу, может ли он писать на нее и может ли он выполнять программный код на ней (хотя третий атрибут довольноредко используется, чем, возможно, должно быть).Скомпилированный программный код отображается в памяти на страницах, которые могут быть как читаемыми, так и исполняемыми, но недоступными для записи, тогда как стек должен быть читаемым и записываемым, но не исполняемым. Большинство страниц памяти вообще не читаемы, не доступны для записи и не выполняются; ОС позволяет процессу использовать только столько страниц, сколько ему явно требуется, и именно этим управляют библиотеки выделения памяти (malloc()
и др.)для вас.
Анализ
При условии, что каждый кадр стека меньше страницы памяти [1] , так что, когда программа перемещается по стеку, она записывает в каждыйНа странице ОС (т. е. привилегированная часть среды выполнения) может, по крайней мере, в принципе надежно обнаружить переполнение стека и завершить программу, если это произойдет.По сути, все, что происходит при таком обнаружении, - это наличие страницы, на которую программа не может записать в конце стека;если программа пытается выполнить запись в нее, аппаратное обеспечение управления памятью перехватывает ее, и ОС получает возможность вмешаться.
Потенциальные проблемы могут возникнуть, если ОС может быть обманута, если она не установит такую страницу или есликадры стека могут стать настолько большими и редко записанными, что защитная страница будет перепрыгнута.(Сохранение большего количества защитных страниц поможет предотвратить второй случай с небольшими затратами; вынуждает выделение стека с переменным размером - например, alloca()
- всегда записывать в пространство, которое они выделяют, прежде чем вернуть управление программе, и таким образом обнаруживать разбитый стек,предотвратил бы первое с некоторой стоимостью с точки зрения скорости, хотя записи могли бы быть достаточно редкими, чтобы сохранить стоимость довольно маленькой.)
Последствия
Каковы последствия этого?Ну, ОС должна правильно делать с управлением памятью.(Ссылка @ Michael иллюстрирует, что может случиться, когда это происходит неправильно.) Но также опасно позволять злоумышленнику определять размеры выделения памяти, когда вы не форсируете запись для всего выделения немедленно;Массивы переменного размера alloca
и C99 представляют собой особую угрозу.Более того, я был бы более подозрительным к коду C ++, так как он имеет тенденцию делать намного больше выделения памяти на основе стека;это может быть хорошо, но есть большая вероятность того, что что-то пойдет не так.
Лично я предпочитаю в любом случае сохранять размеры стеков и размеры стековых фреймов небольшими и делать все выделения с переменным размером в куче.Частично это наследие работы с некоторыми типами встроенных систем и с кодом, который использует очень большое количество потоков, но это значительно упрощает защиту от атак переполнения стека;ОС может надежно их перехватить, и тогда у злоумышленника есть отказ в обслуживании (раздражающий, но редко фатальный).Я не знаю, является ли это решением для всех программистов.
[1] Типичные размеры страниц: 4 КБ в 32-разрядных системах, 16 КБ в 64-разрядных системах.Проверьте системную документацию, чтобы узнать, что находится в вашей среде.