Наши друзья
|
Пример атаки: взломанный компилятор C++ от компании Microsoft
Попробуем на примере прояснить нашу терминологию и связать ее с реальной жизнью. В этом разделе мы рассмотрим уже неоднократно упоминавшуюся (но очень важную) атаку на переполнение буфера. Безусловно, то, насколько опасной будет атака на переполнение буфера, зависит от конкретной ситуации. Случайное переполнение буфера, которое является простой ошибкой на техническом уровне, не представляет серьезного риска. Но в основном переполнения буфера очень опасны. Это настолько важное явление, что мы посвятили переполнению буфера целую главу (см. главу 7, "Переполнение буфера"). В данном случае мы используем реальный пример, чтобы показать, как шаблон атаки может превратиться в реальную программу атаки. При рассказе мы будем приводить фрагменты кода. Наши читатели могут стать на место злоумышленника, скопировать наш код, скомпилировать его и затем провести атаку на него, чтобы проанализировать результаты. Как вы увидите, это довольно забавный пример.
В феврале 2001 года компания Microsoft добавила свойство безопасности к своему компилятору для языка C++, последняя версия которого называлась и Visual C++.NET и Visual C++ version 7 5. Итак, чтобы использовать программу атаки в своих целях, нужно найти взломанную версию компилятора.
Новая функция безопасности компилятора была предназначена для автоматической защиты потенциально уязвимого исходного кода от некоторых типов атак на переполнение буфера. Защита достигается за счет нового свойства, которое позволяет разработчикам продолжать использовать потенциально уязвимые строковые функции, например strcpy () (источник многих проблем), и быть под "защитой" от манипуляции со стеком. Это новое свойство во многом основано на изобретении Криспина Кована (Crispin Cowan) под названием StackGuard и используется при создании стандартного машинного кода (а не кода на промежуточном языке .NET). Обратите внимание, что новая возможность предназначена для защиты любой программы, скомпилированной с помощь "защищенного" компилятора. Другими словами, использование новой возможности должно помочь разработчикам создавать более безопасное программное обеспечение. Однако, в своей взломанной форме, новая возможность Microsoft приводит к ложному чувству безопасности, поскольку ее легко преодолеть. По всей видимости, компания Microsoft, как и в прошлом, отдает предпочтение все же производительности, а не безопасности.
StackGuard — не самое лучшее средство для защиты от атак на переполнение буфера. На самом деле это средство было создано под сильным давлением со стороны разработчиков. Кован просто внес исправления в генератор кода дсс, чтобы не пришлось создавать новый генератор или полностью менять архитектур}' компилятора дсс.
Новая функция защиты от Microsoft обеспечивает возможность вызывать "защищенный обработчик ошибок" при возникновении потенциально опасной ситуации. Тот факт, что атака может быть выявлена на таком раннем этапе, демонстрирует мощь концепции шаблонов атак. Однако такой способ реализации защищенного обработчика ошибок, привел к тому, что сама функция безопасности Microsoft оказалась уязвимой для атак! Злоумышленник может провести специальную атаку на "защищенную" программу, которая непосредственно взломает защитный механизм. Безусловно, этот новый тип атаки определяет новый шаблон.
Существует несколько других, не основанных на StackGuard, методов, которыми могут воспользоваться создатели компиляторов для защиты от атак на переполнение буфера. Компания Microsoft предпочла слабое решение более сложному. Это просчет на уровне проекта, который приводит к возможности проведения большого количества атак на программный код, скомпилированный с помощью нового компилятора. Таким образом, 'компилятор Microsoft можно назвать "разносчиком уязвимых мест" в программах.
Вместо того чтобы надеяться на функцию запускаемого во время исполнения компилятора для защиты от атак на переполнение буфера, разработчики и архитекторы должны установить строгие правила создания программного обеспечения, предполагающие в том числе проверки исходного кода. Для обнаружения потенциальных проблем в исходном коде программ C++ (для защиты от которых и предназначена рассматриваемая нами функция Microsoft), могут и должны применяться средства статического анализа, например SourceScope от Citigal или программа с открытым исходным кодом ITS4. Гораздо лучше заранее полностью устранить эти проблемы в исходном коде, чем пытаться заблокировать их при попытке атак во время выполнения программы6.
Судя по высказываниям Билла Гейтса в январе 2002 года, компания Microsoft делает крутой поворот в сторону обеспечения безопасности. Однако у этой компании надолго хватит работы по усовершенствованиям, поскольку даже в функциях обеспечения безопасности присутствуют просчеты на уровне проекта.
Одним из положительных качеств StackGuard и родственного ему средства от Microsoft является эффективность механизмов проверки. Однако существует несколько способов для обхода этих механизмов. Атаки, которые провела Cigital для взлома механизмов защиты Microsoft, вовсе не являются новыми и вовсе не требуют каких-либо исключительных усилий. Если бы специалисты Microsoft ознакомились с литературой о недостатках StackGuard, то они смогли бы избежать возможности проведения таких атак.
Технические подробности атаки
Для создания приложений с функцией так называемой "проверки безопасности буфера" разработчики программ на Visual C+ + .Net (Visual C++ 7.0) могут воспользоваться параметром /GS компилятора. В 2001 году сотрудниками Microsoft было написано по меньшей мере две статьи о новой возможности, которые затем были удалены из Internet7. Основываясь на этой документации относительно параметра /GS и исследовав двоичные инструкции, генерируемые компилятором при использовании этого параметра, исследователи Cigital определили, что параметр /GS по сути является Win32-реализацией программы StackGuard. Этот вывод был проверен независимыми исследователями компании Immunix.
Благодаря переполнению буфера в стеке хакер получает массу возможностей для перехвата выполнения программы. При использовании широкоизвестного и популярного шаблона атаки в стеке перезаписывается адрес возврата функции данными, которые предоставляет хакер. Управление передается по адресу в эпилоге функции, в котором хакер размещает вредоносный код.
Разработчики StackGuard впервые предложили идею размещения сигнального слова (canary word) перед адресом возврата из функции в прологе функции. В эпилоге функции выполняется проверка содержимого стека на предмет того, не был ли изменен адрес возврата функции (по сигнальному слову). Позднее этот метод проверки был усовершенствован с помощью использования функции XOR для сигнального слова и адреса возврата, чтобы не дать хакеру возможности обойти значение сигнального слова и переписать адрес возврата из функции. StackGuard можно считать удачным решением для блокирования некоторых атак на переполнение буфера путем выявления этих атак во время выполнения программы. В подобном средстве под названием StackShield используется отдельный стек для хранения адресов возврата из функций.
Изменение адреса возврата из функции не является единственным способом для перехвата управления в программе. В статье на сайте журнала Phrack можно найти описание других возможных атак на переполнение буфера, которые позволяют преодолеть защиту, организованную с помощью средств, подобных StackGuard и StackShield8. Шаблон атаки можно охарактеризовать следующим образом. Если в стеке "после" уязвимого буфера существуют переменные, содержащие указатели на функции, то сначала хакер организовывает переполнение буфера, которое искажает данные указатели, и далее при вызове функций по этим указателям управление передается подготовленному коду, сохраненному по указанному адресу памяти. Затем предоставленное хакером значение может быть записано по этому адресу. Идеальной областью памяти для хакера будет указатель функции, которая вызывается позже в программе. В статье журнала Phrack обсуждается, как найти такой указатель функции в глобальной таблице смещений (Global Offset Table — GOT). Пример реальной атаки для обхода защиты StackGuard был опубликован по адресу http://www.securityfocus.com/archive/1/83769.
Обзор Microsoft-реализации средства StuckGuard
Много технических сведений по реализации параметра /GS в компиляторе от Microsoft можно найти в трех исходных файлах модуля CRT, а именно: seccinit. с, seccook.c и secfail.c. Другие детали можно узнать, исследовав инструкции, которые генерируются компилятором с установленным параметром /GS.
Единственное средство безопасности (сигнальное слово) инициализируется в вызове процедуры CRT INIT. Создан новый вызов библиотеки _set_security__error
handler, который может использоваться для установки определенного пользователем обработчика. Указатель функции к обработчику пользователя хранится в глобальной переменной userhandler. В эпилоге функции генерируемая компилятором инструкция переходит к функции security__checkcookie, определенной в
файле seccook.c. Если сигнальное слово было изменено, вызывается функция
security__error__handler, которая определена в файле secfail.c. Функция
security_error_handler сначала проверяет, был ли инсталлирован предоставленный пользователем обработчик. Если это так, то вызывается обработчик пользователя, а если нет, то по умолчанию выводится сообщение "Buffer Overrun Detected" ("Обнаружено переполнение буфера") и работа программы завершается.
В этой реализации функции защиты есть несколько проблемных решений. В Wn-dows не предусмотрено ничего подобного глобальной таблице смещений (GOT) с возможностью записи данных, поэтому даже учитывая описанное выше размещение данных в стеке, злоумышленнику непросто найти подходящий для использования указатель функции. Однако из-за доступности переменной user handler хакеру совсем не нужно выполнять сложный поиск подходящей цели!
Обход функции защиты для компилятора от Microsoft
Рассмотрим следующую небольшую программу.
#include <stdio.h> #include <string.h>
/*
где
request_data — входной параметр, в котором содержится предоставленная
пользователем зашифрованная строка, наподобие
"host=dot.net&id=user_i d&pw=user_password&cookie=da".
user_id - выходной параметр, который используется для копирования декодированного значения 'user_id'.
password- выходной параметр, который используется для копирования декодированного значения 'password'
*/
void decode(char *request_data, char *user_id, char Apassword){ char temp_request[64]; char *p_str;
strcpy(terrp_request, request_data) ; P_str = strtok(temp_request, "&") ;
while(p_str != NULL){
if (strncmp(p_str, "id=", 3) == 0){
strcpy(user_id, p_str + 3 );
Шаблоны атак
69
else if (strncmp(p_str, "pw=", 3) == 0){ strcpy(password, p_str + 3);
}
p_str = strtok(NULL, "&") ;
}
}
/*
Любая комбинация приведет к ошибке.
*/
int checkj)assword(char *id, char "password) { return -1;
}
/*
Мы используем параметр argv[l] для передачи строки запроса.
*/
int main(int argc, char ** argv) {
char user_id[32] ; char password[32] ;
user_id[0] = '\0 ' ; password[0] = '\0' ;
if ( argc < 2 ) {
printf("Usage: victim request.\n") ; return 0;
}
decode( argv[l] , user_id, password) ; if ( check_password(user_id, password) > 0 ){ // невыполняемый участок программы, printf("Welcome!\n"); }
* else{
printf("Invalid password, user:%s password:%s.\n", user_id, password) ;
}
return 0;
}
Функция decode содержит буфер, размер которого не проверяется, и параметры этой функции могут быть перезаписаны с помощью значения параметра temp_request.
Если программа компилируется с использованием параметра /GS, то невозможно организовать переполнение буфера и изменить ход исполнения программы с помощью затирания адреса возврата для функции decode. Однако для переполнения буфера можно использовать значение параметра user_id функции decode с целью заставить ее обращаться сначала к значению описанной выше переменной user_handler! Таким образом при вызове функции strcpy (user_id, p_str + 3 ) ; мы можем назначить желаемое значение для переменной user_handler. Например, мы можем заставить ее обращаться к области хранения в памяти функции printf ("Welcome ! \n") ;, таким образом, при обнаружении переполнения буфера оно может представляться установленным пользователем обработчиком ошибки и программа будет исполнять printf ('Welcome ! \n") ;. Наша строка для проведения атаки будет выглядеть примерно следующим образом.
id=[aflpec перехода]&pw=[любое]АААААА....ААА[адрес обработчика пользователя]
После компиляции такой "защищенной" выполняемой программы определение адреса области для хранения значения переменной user handler осуществляется достаточно просто при наличии минимальных знаний о восстановлении исходного кода. Результат состоит в том, что программа, которая должна быть защищена от атак, определенного типа оказывается уязвимой именно при проведении этих атак.
Решения
Существует несколько альтернативных методов защиты от атак на переполнение буфера. Лучшим решением является переход разработчиков на языки, обеспечивающие безопасность типов, например на Java или С#. Еще одним прекрасным решением являются компиляция программы с динамическими проверками строковых функций во время выполнения программы (хотя следует учесть снижение производительности). Эти решения не всегда удобны относительно задач конкретного проекта.
Кроме того, возможно изменение использующегося метода компиляции с применением параметра /GS. Основная цель предложенных далее исправлений заключается в том, чтобы добиться более высокого уровня целостности данных в стеке.
1. Гарантируйте целостность значений переменных, которые хранятся в стеке, с помощью более жестких проверок сигнального слова. Если переменная размещается после буфера в стеке, проверка должна выполняться до использования переменной. Частота таких проверок может устанавливаться с помощью использования зависимого от данных анализа.
2. Гарантируйте целостность значений переменных, которые хранятся в стеке, с помощью перестройки структуры стека. По возможности локальные "безбуферные" переменные должны размещаться перед переменными, которые используют буфер. Более того, поскольку параметры функций сохраняются в стеке «после буферов локальных функций (если они есть), то к параметрам функций должен применяться тот же подход. В прологе функции можно зарезервировать дополнительное пространство в стеке перед данными локальных буферов. Это позволяет скопировать значения всех параметров. При каждом использовании параметра в теле функции, его значение можно заменить значением созданной копии. Это решение было внедрено по крайней мере в одном исследовательском проекте IBM9.
3. Гарантируйте целостность значений глобальных переменных с помощью контролируемого механизма записи. Очень часто значения глобальных переменных искажаются в результате ошибок в программе и/или умышленного повреждения. Контролируемый механизм записи позволяет разместить набор таких переменных в область памяти, доступную только для чтения. При необходимости изменения значения переменной из этой области разрешение на доступ к этой области памяти должно быть изменено на "доступно для записи" ("writeable"). После внесения необходимых изменений относительно прав доступа, возвращается разрешение "только для чтения" (read-only). При таком механизме неожиданные попытки перезаписи значений защищенных переменных приводят к нарушениям прав доступа к памяти. Для тех переменных, значения которых изменяются один или два раза в ходе выполнения программы, издержки применения контролируемого механизма записи незначительны.
В следующих версиях этого компилятора от Microsoft эти идеи нашли применение (частично).
Обзор атаки
Теперь стала очевидной ирония этой атаки: компания Microsoft сама встроила "сеялку" уязвимых мест в свой компилятор добавив функцию, которая была предназначена для защиты от атак! При этом (что замечательно) шаблоном атаки для взлома этой возможности стал тот же шаблон, для защиты от которого предназначалась функция защиты. Суть проблемы заключается в том, что ранее безопасные строковые функции становятся уязвимыми при вызове новой функции. Как видим, это угрожает безопасности программного обеспечения, но защищает от взлома программ10.
Два года спустя после публичного обсуждения этого просчета, были созданы по крайней мере две программы атаки, использующие установку при компиляции параметра /GS для проведения двухэтапных атак. Как и предсказывалось, механизм защиты использовался в качестве плацдарма для проведения этих атак. |