stack-guards_2
переполнение буфера —\\ активные средства защиты
крис касперски
умные в споре ищут истину, глупцы — выясняют, кто умнее
народное
на рынке имеется множество средств (как коммерческих, так и бесплатных), обещающих решить проблему переполняющихся буферов раз и навсегда, но хакеры ломают широко разрекламированные защитные комплексы один за другим. почему? давайте заглянем под капот Stack-Guard, Stack-Shield, Pro-Police и MicrosoftVisualStudio .NET, сравнив заявленные возможности с реальными
введение
Ошибки переполнения вездесущи — это факт. Буквально каждые несколько дней обнаруживается новая дыра, а сколько дыр остаются необнаруженными — приходится только гадать. Как с ними борются? Арсенал имеющихся средств довольно разнообразен и простирается от аппаратных защит типа NX/XD битов1), до статических анализаторов наподобие Spilnt.
В последнее время в обиход вошел термин «secureprogramming» и вышло множество книг по безопасности, настоятельно рекомендующих использовать динамические средства защиты типа Stack-Guard, внедряющие в компилируемую программу дополнительный код, проверяющий целостность адреса возврата перед выходом из функции и предпринимающий другие действия, затрудняющие атаку.
Расплатой за «безопасность» становится снижение производительности (впрочем, довольно незначительное) и необходимость перекомпиляции всего кода. Но это только внешняя сторона проблемы. Понадеявшись на широко разрекламированные защитные средства, разработчики расслабляются и… начинают строчить небрежный код, который Stack-Guard (Stack-Shield/Pro-Police) все равно «исправит». Но что именно он правит? Давайте задвинем рекламу в сторону и посмотрим на защиту глазами хакера, который ломится не в дверь (там замок), и не в окно (там — сигнализация), а проникает через никем не охраняемую вентиляционную/канализационную трубу или даже дымоход.
Все защитные механизмы, имеющиеся на рынке, спроектированы так, что дрожь берет. Сразу видно, что их создатели никогда не атаковали чужие системы, не писали shell-код и даже не общались с теми, кто всем этим занимается. Защита не только не останавливает атакующего, но в некоторых случаях даже упрощает атаку!
типы переполнения и типы защит
Существует множество типов ошибок переполнения, подробно рассмотренных в статье «ошибки переполнения буфера извне и изнутри как обобщенный опыт реальных атак»). Это: переполнение кучи (работающее как оператор POKE[запись значения в указанную ячейку памяти]), целочисленное переполнение, ошибки форматированного вывода (PEEK[чтение содержимого произвольной ячейки памяти] и POKE в одном лице) и переполнение локальных стековых буферов.
Стековое переполнение — не только не единственное, но даже не самое популярное. Оператор new языка Си++ размещает переменные в динамической памяти, поэтому, актуальность атак на кучу все растет, а к стеку интерес снижается. Ложка — хороша к обеду. После драки кулаками не машут. Защитники стека явно опоздали и теперь подтасовывают факты и разводят рекламу. Вотцитатаиздокументациина Stack-Guard: «…emits programs hardened against «stack smashing» attacks. Stack smashing attacks are the most common form of penetration attack. Programs that have been compiled with StackGuard are largely immune to stack smashing attack» («Stack-Guard закаляетпрограммыпротивсрывастека – наиболеепопулярноготипаудаленныхатак. Программы, откомпилированные со Stack-Guard'ом приобретают крепкий иммунитет против этого»). На самом деле, Stack-Guard всего лишь затрудняет подмену адреса возврата, то есть противодействует подклассу стековых атак, причем, противодействует весьма неумело. Тоже самое можно сказать и про остальные защиты, устанавливая которые мы не должны забывать, что они сражаются лишь с определенным типом атак, а на остальные просто не обращают внимания.
Поскольку, из рекламных проспектов (по недоразумению называемых «технической документацией») ничего конкретного выяснить невозможно, используем дизассемблер, достоверно показывающий, что делает та или иначе защита и чем она реально занимается.
Рисунок 1 по сути, стек это просто очень высокая труба
stack-guard
Первым, кто бросил вызов переполняющимся буферам, был Stack-Guard, представляющий собой заплатку для компиляторов gcc и eggs, распространяемую по лицензии GPL. Раньше его было можно скачать с www.cse.ogi.edu/DISC/projects/immunix/StackGuard или immunix.org, но сейчас эти ссылки мертвы, а проект заброшен. C тех пор как Immunix скупила Novell, Stack-Guard больше не поддерживается, во всяком случае найти какие бы то ни было упоминания о нем на официальном сайте мне не удалось. Исходный код сохранился только у «коллекционеров», как например: www.packetstormsecurity.org/UNIX/utilities/stackguard. Тут может возникнуть вопрос: «Если stack-guard Устарел и мертв, какой смысл его исследовать?». На самом деле, смысл есть. Stack-Guard — простейший защитный механизм, расковыряв который, мы сможем разобраться и со всеми остальными, тем более, что между ними наблюдается стройная эволюционная преемственность.
Возьмем следующую программу, с умышленно допущенной ошибкой переполнения, и посмотрим, сможет ли Stack-Guard ее защитить.
дочерняя функция f(char *msg) { объявляем локальные переменные
inta; charbuf[0x66];
копируем аргумент в буфер без контроля длины, что на определенном этапе приводит к его переполнению
a = *strcpy(buf, msg);
выходим из функции returna; } материнскаяфункция
int main(int argv, char argc) { int x; x = f(argc[1]); } Листинг 1 демонстрационная программа с переполняющийся буфером, которую мы будем защищать Откомпилируем файл компилятором gcc с настойками по умолчанию (то есть без оптимизации) и загрузим полученный elf в дизассемблер, чтобы посмотреть как выглядит стандартный пролог/эпилог функции f(). function_prologue: pushebp; сохраняем старый указатель карда movebp, esp; открываем новый кадр стека subesp, 98h; резервируем место под локальные переменные ; тело программы moveax, [ebp+arg_0]; копируем аргумент в регистр eax mov[esp+98h+var_94], eax; кладем eax в стек ; (выглядит как засылка eax в лок. переменную ; но в действительности это такая передача ; аргументов, необычно но компилятору удобно) leaeax, [ebp+var_88]; получаем указатель на лок. переменную var_88 mov[esp+98h+var_98], eax; кладем его в стек call_strcpy; вызываем _strcpy(&arg_0[0], &var_88[0]) movsxeax, byte ptr [eax]; eax = *2)