Броня для Висты: создание безопасного кода для Windows Vista
🕛 18.02.2008, 18:01
О нововведениях в Windows Vista не говорил только ленивый. Набили оскомину и рассуждения о том, насколько трудно разработчикам софта обеспечить совместимость их программных продуктов с новой концепцией безопасности, реализованной в этой ОС. Но хватит нытья, пора заставить работать систему на себя, использовав средства обеспечения безопасности Windows Vista в своем ПО. Сегодня мы поговорим о том, как новая система помогает бороться с атаками на переполнение.Новая концепция безопасности
По сравнению с Windows XP, Vista более устойчива перед такими ошибками (читай: атаками), как переполнение буфера. Ряд технологий позволяет избежать этой досадной неприятности или хотя бы смягчить ее последствия. Ты легко можешь реализовать поддержку всех этих новых средств защиты программного кода в своих проектах. При этом все, что от тебя потребуется, - это включить соответствующие опции компоновщика.
Однако вышесказанное вовсе не означает, что теперь для разработчиков программного обеспечения настало безмятежное золотое время и больше не надо ломать голову. Любое из предлагаемых Microsoft средств защиты при желании относительно легко можно обойти. Тем не менее как средство защиты от случайных ошибок и действий пионеров рассмотренные ниже способы, безусловно, будут эффективны. Тем более что от тебя, как от разработчика, не требуется никаких особых усилий - в большинстве случаев достаточно указать соответствующе ключи компоновщика на этапе сборки проекта.
Мы рассмотрим такие технологии защиты от переполнения, как ASLR, случайная адресация стека и кучи, NX, GS и SafeSEH. Некоторые из них являются принципиально новыми, другие представляют собой улучшенные версии технологий, входящих в состав Windows XP SP2 и Windows Server 2003. Большинство из рассмотренных технологий имеют реализацию не только от самой Microsoft, но и от других производителей как программного обеспечения, так и железа.
Введение в ASLR
Технология случайного распределения адресного пространства, или ASLR (Address Space Layout Randomization), нацелена на то, чтобы защитить системный API от различной нечисти. Принцип действия этой технологии ясен уже из названия - случайному распределению подвергаются адреса загрузки системных библиотек, начальный указатель стека и начальный указатель кучи. В Windows XP все эти адреса были статичными и, соответственно, были хорошо известны создателям эксплойтов.
Если говорить откровенно - а у меня нет никаких причин излишне льстить парням из Редмонда, хотя... если они мне хорошо заплатят, то такие причины моментально найдутся :) - так вот если говорить откровенно, то в ASLR не было бы никакой необходимости, не будь архитектура операционных систем семейства Windows, мягко говоря, немного странной. Вот что я имею в виду. Все системные файлы в Windows-системах загружаются в память с заранее определенным смещением. Именно этой особенностью и пользуются авторы эксплойтов, поскольку всегда заранее точно известно, по какому адресу находится дырявая функция, для которой веселые парни приготовили маленький, но очень полезный патчик. Будь архитектуры Windows изначально ориентированы на случайное распределение адресного пространства, многих проблем удалось бы избежать.
ASLR включает в себя случайное распределение следующих элементов:
* адреса загрузки исполняемых файлов и системных библиотек, * начальный адрес стека, * начальный адрес кучи.
В общем виде атака на переполнение сводится к двум вещам: поиск участков кода, в которых возможно возникновение неотслеживаемых исключительных ситуаций, и поиск диапазонов адресного пространства, в которых может быть расположен вредоносный код с последующей передачей ему управления. Одним из обязательных условий успешного завершения атаки является идентичность адресного пространства программы на машине жертвы и адресного пространства программы на машине хакера. В результате случайного распределения адресного пространства в Windows Vista дырявый модуль, расположенный по определенному адресу, при следующей загрузке системы или на другой машине окажется совершенно в ином месте. Аналогично с модификацией кода через загрузку посторонних DLL - для того чтобы подцепить к программе свою библиотеку, хакеру нужно отыскать функцию LoadLibrary. Но при каждом запуске программы адрес, по которому она расположена, будет разный!
Возможно, у тебя возник вопрос о том, что произойдет в результате многократного перезапуска системы. Иначе говоря, как долго Vista сможет назначать загружаемым библиотекам и функциям неповторяющиеся адреса? На этот интересный вопрос есть вполне конкретный ответ. При каждой загрузке системной библиотеки или исполняемого файла происходит случайная адресация из 256 возможных вариантов. В результате шанс загрузить эксплойт по месту назначения равен 1/256.
Допустим, у нас есть жутко полезная программа, состоящая всего из одной функции и нескольких строк кода, выводящих на консоль основные значения адресного пространства программы: адрес загрузки Kernel32.dll, адрес функции LoadLibrary и т.д. (исходник ищи на нашем диске).
Запустив ее на выполнение, можно увидеть в консоли примерно следующее:
Kernel32 loaded at 77400000
Address of LoadLibrary = 77A04E7D
Если теперь перезагрузить компьютер и снова запустить программу на выполнение, результат будет отличаться от предыдущего:
Kernel32 loaded at 77B30000
Address of LoadLibrary = 773A0E7D
Кстати, для того чтобы воспользоваться технологией ASLR, при компиляции проекта в Visual Studio необходимо выставить опцию компоновщика /dynamicbase.
Запрет на выполнение
Следующий инструмент защиты программного кода отличается своей бескомпромиссностью. Это технология NX (No eXecute). Для тех, кто не в теме, поясню. Согласно этой концепции, которая реализована, кстати, не только компанией Microsoft, но и другими авторитетными конторами и известна под разными именами, программный код условно делится на две части. Первой может быть передано управление, но она не может быть перезаписана произвольными данными. Вторая может перезаписываться, но запуск на выполнение здесь уже запрещен. То есть по отношению к участку программного кода одновременно не могут быть реализованы обе привилегии - на запись и на выполнение. Как говорится, одно из двух. И никаких компромиссов.
Нельзя сказать, что в Microsoft в этом плане изобрели что-то принципиально новое. Стек с защитой от выполнения реализован в солярке от Sun Microsystems на несколько лет раньше, чем в Windows. Ну а самыми первыми такой подход к организации структуры программного кода опробовали разработчики OpenBSD. Другими словами, NX есть не что иное, как широко известная технология DEP (Data Execution Prevention). Реализация же NX-защиты сводится к присвоению особых меток сегментам памяти, предназначенным исключительно для хранения данных. Обнаружив попытку выполнения кода, записанного в такой сегмент, система устроит хакеру большой облом.
Для того чтобы реализовать в программном коде поддержку NX, в свойствах компоновщика выставляй ключ /NXCOMPAT. Кстати, софт, компоновка которого осуществлялась с упомянутым выше ключом, может воспользоваться преимуществами технологии NX или другой аналогичной технологии не только в среде Windows Vista, но и в Windows XP со вторым сервис-паком. Именно в Windows XP, а не в WV, как пишут многие, Microsoft впервые реализовала NX.
Давай испытаем NX. Как происходит внедрение шелл-кода, тебе уже хорошо известно (ну не зря же ты читаешь наш журнал). Вот одна из наиболее распространенных схем: запускаем уязвимую программу (или дожидаемся ее запуска), находим адрес, по которому располагается вершина стека, определяем его размер. После этого перезаписываем содержимое стека шелл-кодом и передаем ему управление (подробности ищи на диске).
Стек под угрозой
const unsigned char scode[] = ... //зло ^_^
typedef void (*RunShell)(void);
int main (int argc, char* argv[]){
char StackBuf[256];
RunShell shell = (RunShell)(void*)StackBuf;
strcpy_s (StackBuf, sizeof(StackBuf), (const char *)scode);
(*shell)();
return 0;
}
Если попытаться запустить приведенный пример (естественно, соответствующим образом его доработав, реализовав боевые функции), выбрав в качестве жертвы программу, собранную без поддержки технологии NX, то шелл-код будет успешно скопирован в адресное пространство стека со всеми вытекающими отсюда последствиями. В случае сборки программы с ключом /NXCONPAT такой финт ушами не останется незамеченным, и система незамедлительно сообщит о нем пользователю.
Сообщение об ошибке содержит в себе адрес, по которому возникло исключение. В моем случае это 0x0012fe98. Что это за адрес? Это адрес, по которому расположился массив StackBuf. То есть причиной исключения стала попытка интерпретировать секцию с данными как набор инструкций.
Я уже упоминал, что подобная технология разрабатывается не только Microsoft, но и другими компаниями, в том числе и производителями железа. Достаточно часто встречаются технологии, аналогичные NX, реализованные на аппаратном уровне. Так что можешь еще раз изучить возможности BIOS своей тачки и поискать в ней соответствующий пунктик. Кстати, в Windows Vista можно посмотреть, защищен тот или иной компонент системы (либо стороннее приложение) технологией NX, с помощью диспетчера задач, в котором теперь есть колонка Data Execution Prevention.
Флаг GS
Это еще одна опция, которая, будучи использованной при сборке программы, повышает ее надежность в среде Windows Vista. Фишка здесь в следующем. При использовании опции /GS в момент записи содержимого регистра EBP в стек между локальной переменной, записанной в стеке, и ее адресом не существует прямой и однозначной связи. Вместо этого соответствие устанавливается через специальный посредник - сookie.
Благодаря этому становится невозможным прямое обращение к стеку и изменение содержащихся в нем значений переменных.
Защита стека с помощью cookie
void VulnerableFunc( const char* input, char* out ) { // Готовим адресное пространство для локальных переменных 00401000 sub esp,104h // Копируем секретный cookie в регистр eax 00401006 mov eax,dword ptr [_security_cookie (403000h)] // Ксорим указатель вершины стека с помощью cookie 0040100B xor eax,esp // Записываем результат в буфер 0040100D mov dword ptr [esp+100h],eax char* pTmp; char buf[256]; strcpy( buf, "Prefix:" ); 00401014 mov ecx,dword ptr [string "Prefix:" (4020DCh)] // Прячем аргументы функции за cookie 0040101A mov eax,dword ptr [esp+108h] 00401021 mov edx,dword ptr [esp+10Ch]
В качестве дополнительного бонуса от использования опции /GS мы получаем еще один уровень обнаружения переполнения стека. По утверждению представителей Microsoft, все исходники Windows Vista собраны с включенной опцией /GS. Так что делай выводы и пользуйся на здоровье.
Защита кучи
Еще относительно недавно атаки на переполнение кучи были экзотикой. Сегодня это одна из распространенных тенденций, и тому есть ряд вполне объяснимых причин. Все они сводятся к тому, что на протяжении многих лет основной мишенью хакерских атак являлся стек. Пристальное внимание к стеку со стороны хакеров вынудило разработчиков бросить все свои силы на защиту этого элемента программной архитектуры. В то время как защита стека оттачивалась в бою и становилась все изощреннее (но так и не стала совершенной), безопасности кучи не уделялось практически никакого внимания. В результате, когда стали известны первые случаи успешных атак на переполнение кучи, многие оказались не готовы к такому повороту событий - эффективных методов защиты просто не было.
Одна из разновидностей атаки на переполнение кучи заключается в заполнении ее большим объемом данных, после которого должна последовать не менее большая серия NOP’ов, что в конечном счете приведет к ошибке переполнения и передаче управления на шелл-код. Описанная технология достаточно стара и впервые серьезно посадила на измену пользователей по всему миру в далеком 2001 году в виде сетевого червя Code Red. С тех пор атаки на переполнение кучи стали более изощренными.
IT-сообщество осознало необходимость защиты кучи, и не только Microsoft, но и многие другие компании активизировали свою деятельность в этом направлении.
В довистовую эпоху (а не закрепить ли мне за собой авторские права на этот термин?) для защиты кучи приходилось сбрасывать в null все неиспользуемые указатели.
Что же нам предлагает Vista? Вот неполный список улучшений, призванных защитить кучу от переполнения:
* проверка валидности ссылок, связывающих с предыдущим и последующим элементом кучи; * случайное размещение блока метаданных (генерируется случайное число и ксорится с первоначальным адресом); * проверка целостности данных; * случайное размещение начального адреса кучи (работает только при использовании ASLR); * случайная адресация элементов, хранящихся в куче.
В том случае если проверка валидности ссылок, связывающих между собой элементы кучи, закончится неудачей, приложение будет аварийно завершено с целью предотвращения передачи управления на нелегитимный участок кода. В Windows XP при возникновении проблем со ссылками они просто игнорировались и выполнение программы не прерывалось. Это позволяло без особых усилий спровоцировать разрушение практически любого процесса.
Пример кода, подверженного атаке на переполнение кучи
char* pBuf = (char*)malloc(128); char* pBuf2 = (char*)malloc(128); char* pBuf3 = (char*)malloc(128); memset(pBuf, 'A', 128*3); printf("Freeing pBuf3\n"); free(pBuf3); printf("Freeing pBuf2\n"); free(pBuf2); printf("Freeing pBuf\n"); free(pBuf);
В Windows Vista, даже при отключенной опции terminate on corruption, обеспечивающей защиту кучи, при первой же попытке вызвать функцию free() из нашего примера выполнение программы будет прервано. На других системах, включая Windows XP и Windows Server 2003, проблема останется незамеченной. Однако радости в том, что процесс в лучших самурайских традициях скорее сделает себе харакири, чем даст себя опозорить грязному эксплойту, мало. Поэтому для того чтобы, обнаружив проблемы с защитой кучи, приложение не падало замертво, а отслеживало подобные инциденты и реагировало на них менее кровавым способом, верным решением будет использование опции terminate on corruption.
Для превращения программы из паникера-камикадзе в доблестного самурая необходимо добавить в функцию Main() или WinMain() несколько несложных строк:
bool EnableTerminateOnCorrupt()
{
if( HeapSetInformation( GetProcessHeap(),
HeapEnableTerminationOnCorruption, NULL, 0 ) )
{
printf( "Terminate on corruption enabled\n" );
return true;
}
printf( "Terminate on corruption not enabled - err = %d\n",
GetLastError() );
return false;
}
Естественно, при создании финального релиза отладочные функции printf() нужно будет заменить вызовом обработчика ошибок.
Дополнительно к этому ты можешь использовать кучу с низким уровнем фрагментации в противовес стандартной куче, которая подвержена значительной фрагментации. Реализовать подобное не просто, а очень просто:
bool EnableLowFragHeap() { ULONG ulHeapInfo = 2; if( HeapSetInformation( GetProcessHeap(), HeapCompatibilityInformation, &ulHeapInfo, sizeof( ULONG ) ) ) { printf( "Low fragmentation heap enabled\n" ); return true; } printf( "Low fragmentation heap not enabled - err = %d\n", GetLastError() ); return false; } SafeSEH
И под занавес еще одна полезная опция. Наверняка, тебе уже приходилось иметь дело с такой фишкой, как Structured Execution Handler (SEH). SafeSEH, поддержка которой включена в Windows Vista, предоставляет собой еще более надежный механизм мониторинга динамических исключений. Эта опция компоновщика позволяет в момент вызова функции запоминать адрес, по которому этот вызов был осуществлен. После этого периодически осуществляется сравнение фактического адреса с тем, что запомнила система. И если обнаруживается несовпадение, значит что-то нарушило штатный ход выполнения программного модуля, и процесс тихо, без пыли и шума отправляется к праотцам.
Однако возможности этой технологии, как и других упомянутых в статье, не безграничны. В частности, она не защитит твой код в том случае, если в результате атаки на переполнение буфера будет модифицирована структура EXCEPTION_REGISTRATION, предназначенная для хранения адреса, по которому возникло исключение.
Проще простого
Как видишь, вопреки сомнениям скептиков (весьма, кстати, обоснованных), Vista значительно продвинулась в плане защиты от атак на переполнение. Более того, все эти возможности доступны и тебе. Все, что от тебя требуется, - не полениться выставить соответствующие опции компоновщика. Конечно, можно попенять на некоторое снижение производительности, но... Будь откровенен сам с собой и признайся, что гораздо более серьезные накладные расходы тянет за собой код, написанный твоими же руками. Ведь времени на оптимизацию всегда не хватает, так же как и на обстоятельное тестирование. Так что, может быть, лучше, потеряв в производительности за счет повышения безопасности приложения, поискать эту производительность в другом месте?