Показать сообщение отдельно

  #2  
Старый 09.07.2008, 06:24
Dober'man
Banned
Регистрация: 16.07.2007
Сообщений: 79
Провел на форуме:
801879

Репутация: 337
Отправить сообщение для Dober'man с помощью ICQ
По умолчанию

После защиты StackGuard'ом перед адресом возврата располагается константа 000AFF0Dh (в терминологии StackGuard'а — canary word), целостность которой проверяется перед выходом из функции. Суть в том, что комбинацию символов, слагающие canary word – \x00\x0A\xFF\x0D, очень трудно "воспроизвести" с помощью строковых функций, поскольку в языке Си символ нуля трактуется как "конец строки". Функция gets – одна из тех немногих, что обрабатывает ноль как обыкновенный символ, поскольку в качестве завершителя строки использует символ "возврата каретки".
При работе с ASCIIZ-строками "подделать" canary word невозможно! Адрес возврата можно считать надежно защищенным. Ведь, чтобы "дотянуться" до него, переполняющемуся буферу необходимо пересечь (и затереть) canary word! Разработчики торжествуют, а хакеры рвут себе вены и вешаются. Или… все-таки нет? Начнем с того, что на уникод все эти ограничения не распространяются и canary word подделывается без труда (кстати говоря, плотная версия Stack Guard'а в качестве сторожевого слова использовала 00000000h, что уникодом уже не воспроизводится, но может быть введено с помощью функции gets, которая сегодня практически никем и нигде не используется). К тому же, приложения, обрабатывающие двоичные данные функциями типа memcpy, так же остаются беззащитными.
Локальные переменные и указатель кадра стека вообще никак не защищены и могут быть беспрепятственно атакованы. Если среди этих переменных присутствует хотя бы один указатель на функцию, вызываемую после переполнения, хакер сможет подменить его адрес, передавая управление на свой shell-код. Конструкция типа "int *x; int a; … x = a;", которая к числу экзотических никак не относится, позволяет атакующему модифицировать любые указатели на функции, в том числе и адрес возврата (правда, в этом случае необходимо знать точное положение вершины стека на момент атаки, что не всегда возможно, поэтому хакеры предпочитают модифицировать таблицу импорта в Windows, а в UNIX – секцию got).
Рассмотрим самый сложный случай, когда никаких переменных в нашем распоряжении нет, а есть только сохраненный регистр кадра стека, который мы и будем атаковать. Фатальной ошибкой StackGuard'а явилось то, что он не учел "побочных эффектов" инструкции leave, которая работает так: mov esp, ebp/pop ebp, позволяя хакеру воздействовать на кадр материнской функции. Если в каком-то месте стека или кучи атакующему удастся "сложить" конструкцию 000AFF0Dh &shell-code, ему остается всего лишь подменить сохраненный EBP на адрес "своего" canary-word. Тогда при выходе из материнской функции управление будет передано на shell-код! Атаки этого типа называются ret2ret и давно описаны в хакерской литературе, однако, какого-либо практического приложения они так и не получили, поскольку, в оптимизированных эпилогах (ключ -O2) вместо инструкции leave компилятор использует более быстродействующую конструкцию add esp,x/pop ebp, и побочный эффект воздействия на ESP исчезает. В оптимизированном эпилоге, хакер может воздействовать только на стековый кадр материнской функции, "подсовывая" ей те значения локальных переменных, которые он захочет. Для успешной реализации атаки этого, обычно, оказывается вполне достаточно.
В версии 2.0 защита адреса возврата была как бы усилена — в нем появился случайный canary word, хранящийся в read-only памяти и "шифрующий" адрес возврата по XOR. Угадать 32-битный canary word — нереально, но это и не нужно! Достаточно подсунуть заведомо ложное значение. Тогда, убедившись, что стек переполнен и хакеры хакерствуют как крысы в амбаре, Stack-Guard передаст управление функции __canary_death_handler, которая завершает выполнение программы, устраивая настоящий DoS. Но лучше DoS, чем захват управления!
Весь фокус в том, что указатель на __canary_death_handler размещается в глобальной таблице смещений — GOT и может быть атакован путем воздействия на локальные переменные через уязвимый указатель кадра стека. Если такие переменные действительно есть (а куда бы они подевались?), хакер просто перенаправляет __canary_death_handler на свой shell-код!
В последующих версиях Stack-Guard'а canary world "переехал" на одну позицию вверх, взяв под свою защиту и указатель кадра, однако, дальнейшего развития проект не получил и постепенно сдулся.
Microsoft Visual Studio .NET
Озабоченная последними хакерскими атаками, Microsoft реализовала в своем новом компиляторе Visual Studio .NET (бывший Visual Studio C++) некоторую разновидность Stack-Guard'а в далеко не лучшей его "инаугурации". Никогда не разрабатывающая собственных продуктов, а только "ворующая" уже готовые (авторитетный товарищ Берзуков в своей софт-панораме об этом только и говорит, сходите на www.softpanorama.org/Bulletin/News/Archive/news078.txt, почитайте — там много интересного), Microsoft, как это часто и бывает, сама не поняла, что стащила и у кого. Ладно, все это лирика. Перейдем к фактам.
При компиляции с ключом /GS компилятор добавляет в код security cookie — так москали кличут пыво, то есть, так в терминологии Microsoft называется случайный 32-битный canary word, хранящийся в writable-памяти и инспектируемый функцией check_canary при выходе из функции:
Код:
function _prologue:
	push ebp		; // сохраняем прежний указатель кадра
	mov ebp, esp		; // открываем новый кадр
	sub esp, 9Ch		; // резервируем место для лок. переменных и canary
	push edx		; \
	push esi		;  + - сохраняем регистры которые будут изменены
	push edi		; /
	
	mov eax, [canary]	; // копируем глобальный canary в eax
	xor eax, [esp+10h]	; // XOR'им адрес возврата с canary
	mov [ebp-10h],eax	; // кладем результат на стек, защищая указатель кадра

; // тело функции
; (не совсем такое же, как и в прошлый раз,
;  но различия между компиляторами к делу не относятся)

function_epilogue:
	mov ecx, [epb-10h]	; // копируем поXOR'ый canary в ecx
	xor ecx, [ebp+10h]	; // XOR'им адрес возврата и кладем его ecx
	call check_canary	; // вызываем функцию проверки canary
	
	pop edi			; \
	pop esi			;  + - восстанавливаем регистры
	pop ebx			; /
	mov esp, ebp		; // закрываем кадры стека небезопасным путем
	pop ebp			; // (Microsoft повторяет ошибку Stack Guard'а)
	ret			; // выходим в материнскую функцию

check_canary:			; // функция проверки canary
	cmp ecx, [canary]	; // сравниваем переданный ecx с глобальным canary
	jnz canary_changed	; // если не совпадают - завершаем программу
	ret			; // все ок, продолжаем выполнение программы
Листинг 6 дизассемблерный листинг функции f(), откомпилированной Microsoft .NET с ключом /GS (добавленные защитой строки выделены жирным шрифтом)
Canary word защищает не только адрес возврата, но и кадр, что очень хорошо, правда в оптимизированном коде, генерируемый этим же самым компилятором, локальные переменные адресуются непосредственно через ESP и дополнительный регистр им не нужен, поэтому, фактически защищается только один адрес возврата. Остальные переменные остаются незащищенными, что открывает простор для махинаций с указателями. В частности, хакер может перезаписать глобальную переменную canary своим значением — тогда его проверка пройдет нормально. Это даже упрощает (!) атаку: в незащищенной системе существует проблема ввода "запрещенных" символов, которую не всегда возможно обойти. Операция XOR позволяет генерировать любые символы! В частности, чтобы сформировать символ нуля, достаточно положить в canary и зашифрованный адрес возврата два одинаковых символа. Как известно X XOR X = 0. Остальные символы генерируются аналогичным способом.
Самое интересное, что Microsoft переняла ошибку ранних версий Stack-Guard, причем даже не его ошибку, а особенность поведения компилятора GCC, позволяющую атакующему воздействовать на регистр ESP через модификацию указателя кадра стека. Microsoft Visual C++ 6.0 закрывал кадр стека безопасной конструкций ADD ESP,XXX, а .NET вместо этого использует MOV ESP, EBP. И хотя указатель кадра защищен canary word, это еще не повод ослаблять защиту! Canary word генерируется не совсем случайным путем и угадать его с нескольких попыток вполне реально, ну а инструкция XOR позволит подделать любой символ. Короче говоря, если бы в Microsoft думали головой…
Stack-Shield
Несмотря на схожесть в названии со своим собратом, Shack-Shield действует совсем по другому принципу. Это еще одно расширение к gcc, последнюю версию которого можно скачать с http://www.angelfire.com/sk/stackshield, но иного типа. Если Stack-Guard реализован как патч к компилятору, "исправляющий" function_prologue и function_epilogue, то Stack-Shield "захватывает" ассемблерные файлы, сгенерированные компилятором (в UNIX-мире они имеют расширение .s), обрабатывает их, выплевывая защищенный ассемблерный файл, возвращаемый компилятору для окончательной трансляции в двоичный код. Такая схема дает Stack-Shiled'у намного большие возможности и мыщъху сразу же захотелось посмотреть как он ими воспользовался и можно ли его одолеть.
Соблазненный процессорными архитектурами с разнесенным стеком (один стек для хранения адресов возврата, другой — для локальный переменных), создатель Stack-Guard'а попытался "проэмулировать" на x86 нечто подобное. Для этой цели он использовал глобальный массив retarray на 256 адресов: эпилог копирует текущий адрес на вершину массива, определяемую указателем retprt, а пролог "стягивает" этот адрес с вершины и передает ему управление. Эта эмуляция далека от идеала, но сохраненный в стеке адрес возврата в ней вообще не используется и выполнение программы продолжится даже после того, как он будет затерт, что предотвращает DoS (впрочем, поскольку локальные переменные искажены, программа все равно рухнет).
Код:
function_prologue:
	push eax		; // сохраняем регистры, которые изменяет Stack-Shied
	push edx		
	mov eax, offset retpt	; // копируем в eax смещение указателя массива retpt
	cmp rettop, eax	; // смотрим - есть ли еще место?
	jbe .LSHIELDPROLOG	; // если места нет,отказываемся от записи нового адреса
	mov edx, [esp+8]	; // заносим в edx адрес возврата со стека
	mov [eax], edx		; // сохраняем его в массиве адресов возврата
	
.LSHIELDPROLOG:
	add [retptr],4		; // увеличиваем указатель массива возвратов
				; // на первый взгляд это явный баг,
				; // но на самом деле - оптимизация!
	
	pop edx			; // восстанавливаем регистры назад
	pop eax			;  
	
	push ebp		; // сохраняем старый указатель карда стека
	mov ebp, esp		; // открываем новый кадр
	sub esp, 98h		; // резервируем место под локальные переменные
	
; // тело функции
; // (такое же как в случае с Stack-Guard)

function_epilogue:
	leave			; // закрываем кадр стека небезопасным путем
	push eax		; // сохраняем регистры
	push edx
	add [retptr], -4	; // уменьшаем указатель массива возвратов
	mov eax, offset retptr	; // заносим в eax смещение массива возвратов
	cmp eax, rettop	; // как на счет свободного места?
	jbe .LSHIELDEPILOG	; // если места нет, значит и выталкивать нечего
	mov edx, [eax]		; // снимаем сохраненный адрес со стека возвратов
	mov [esp+8],edx	; // восстанавливаем стековый адрес не проверяя его
	
.LSHIELDEPILOG:
	pop edx			; // восстанавливаем регистры
	pop eax			; 
	ret			; // выходим в материнскую функцию
Листинг 7 дизассемблерный листинг функции f(), защищенной Stack-Shield'ом с настройками по умолчанию (добавленные защитой строки выделены жирным шрифтом)
При компиляции с ключом -d, Stack-Shiled вставляет дополнительную проверку, сравнивая адреса возврата на стеке и retarray'е. В случае расхождения вызывается функция SYS_exit, завершающая программу в аварийном режиме.
Ключи -r и -g задействуют механизм "Ret Range Checking", проверяющий границы адресов возврата и останавливающий программу, если они выходят за пределы некоторой заранее заданной величины (т. е. находятся в куче или стеке). Таким образом, даже если хакер перезапишет retarray (а он находится в записываемой области памяти), подсунуть указатель на shell-код ему уже не удастся, правда, он может беспрепятственно вызывать функции библиотеки libc, передавая им любые аргументы (атака типа return-to-libc).