fighting 0-day: способы борьбы с неизвестными атаками

введение

Рынок информационной безопасности во всем мире очень быстро и успешно развивается в нескольких различных направлениях. Как правило, это либо разработка специального ПО для защиты (например, файрволлы, VPN-системы, антивирусы, SPAM-фильтры и пр.) либо предоставление специализированных услуг (вроде анализа рисков компании и разработки политики информационной безопасности). Но, к сожалению, вся индустрия очень часто забывает о такой вещи, как 0-day-уязвимости и приватные эксплоиты, которые постоянно появляются в соответствующих неофициальных кругах, а также покупаются и используются уже более заинтересованными лицами.

В первой части моего материла я хотел бы ввести вас в курс дела, описав основные фундаментальные атаки на компьютерные системы, а также способы обхода защит, созданных специалистами по безопасности.

линия фронта

Итак, давайте определимся, какие атаки представляют собой главную опасность для программного обеспечения (и разумеется для ОС, на которой установлено данное ПО) - это атаки переполнения буфера в стеке и куче (heap), ошибки форматной строки, целочисленные переполнения, а также другие уязвимости, которые мы опишем подробнее в следующих разделах. Это те вещи, которые грамотные разработчики постоянно ищут в своем коде, это то, что является причиной 99% всех Advisory (уведомлений, касающихся безопасности), это те патчи, которые приходится устанавливать системным администраторам чуть ли не каждый день.

Но предположим такой вариант, что хакер нашел уязвимость раньше разработчика (такое случается постоянно, хотя бы потому, что у разработчика полно других забот). Как правило, он разрабатывает эксплойт - специальный код, которых использует конкретную уязвимость. По-вашему, этот эксплоит он немедленно отправит в BugTraq, и разработчики, проанализировав код, выпустят соответствующую заплатку? Уверяю вас, времена full- disclosure (полного разглашения информации) уже давно прошли, как и времена различных security-team'ов, которые занимались подобной практикой. Те, кто остались, либо ушли на работу в коммерческие организации, либо в глубокий андеграунд. Как правило, первые не занимаются подобными вещами, а вот вторые не только продают подобный warez в чистом виде, но и активно его используют (к примеру для создания ботнетов, организации DDoS-атак, рассылки спама, а также для других более утонченных и комплексных проектов). И увы, тут не помогут ни новейшие файрволлы и антивирусы, ни системы анализа защищенности - от подобных атак не могут защитить стандартные средства, здесь нужен другой подход. Но прежде чем обсуждать возможные варианты защиты, давайте подробнее остановимся на этих атаках.

stack overflow

Это самый старый и известный метод, котором пользуются люди, которые ищут ошибки в ПО и пишут эксплоиты. Проблема состоит в функциях, которые не проверяют границ (например strcpy(), gets(), strcat(), scanf(), fscanf(), getwd() и пр, если применительно к C/C++). Из за этого мы можем выйти за границы массива (группа данных идентичного типа) и изменить важные значения, сохраненные в стеке, в том числе EIP - адрес возврата (это место, куда программа передает управление после выхода из функции) скажем на адрес нашего шеллкода (машинный код, который выполняет нужные нам действия, чаще всего это запуск интерпретатора командной строки). Итак, давайте на примере посмотрим, как данные некой функции располагаются в стеке, используя классический strcpy-баг:

stack.c:

char *vuln(char *msg) {
int int1;
char buf[80];
int int2;
strcpy(buf,msg);
return msg;
}

int main (int argv, char **argc[]) {
char *ptr;
ptr = vuln (argc[1]); }


После загрузки программы в виртуальную память, стек будет выглядеть так:

start()/main() аргументы main() 12 байтов
main() сохраненный eip 4 байта
main() сохраненный ebp 4 байта
main() ptr 4 байта
main()/vuln() аргументы vuln() 4 байта
vuln() сохраненный eip 4 байта
vuln() сохраненный ebp 4 байта
vuln() int1 4 байта
vuln() buf 80 байтов
vuln() int2 4 байта


Итак, мы контролируем msg, буффер buf фиксированного размера (80 байтов, но в реальности компилятор может выделить немного больше пространства). Функция strcpy не проверяет границ, поэтому мы можем переписать все за buf, включая сохраненный адрес возврата в стеке, как это видно на схеме. Проверим:

MiagBox:/home/stas/samag# gcc -o stack stack.c
stack.c: In function `main':
stack.c:10: warning: passing arg 1 of `vuln' from incompatible pointer type
stack.c:10:22: warning: no newline at end of file

MiagBox:/home/stas/samag# gdb ./stack
GNU gdb 6.3-debian
<skipped>
This GDB was configured as "i386-linux"...
Using host libthread_db library "/lib/tls/libthread_db.so.1".

(gdb) r `perl -e 'print "A"x256'`

Starting program: /home/stas/stack `perl -e 'print "A"x256'`
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()


Как видите, программа пыталась выполнить инструкцию по несуществующему адресу 0x41414141 (41 in hex = A), то есть мы полностью контролируем значение адреса возврата (регистра EIP) и можем заставить программу выполнить любые действия.

Конечно, существуют различные вариации на тему написания эксплойтов переполнения буффера, так как методы эксплойтинга stack-based-переполнений очень сильно эволюционировали (nops+sc, eggshell, Murat Balaban method, ret-to-func и тд.). Но для нас, как правило, это не играет большой роли.

heap overflow

Данные атаки не так хорошо изучены и описаны, по сравнению со stack overflow, но, тем не менее, широко распространены. Для хранения очень больших объемов данных часто используют кучу (heap) - специальную область памяти, которая, в отличие от стека, растет вверх. В heap-based переполнении мы можем переписать указатели (например, указатель на файл - подставить туда свое значение, допустим /etc/shadow и если процесс запущен под рутом, то мы увидим содержимое этого файла), указатели на функции (к примеру, заменить адрес функции на адрес нашего шеллкода) и пр. Проблема все та же - использование небезопасных функций, отсутствие проверки границ, исполняемый heap. Рассмотрим простой пример переполнения кучи (используется Doug Lea's Malloc):

heap.c:

#include
int main(int argc, char **argv) {
char *ptr,buff[10];
ptr=malloc(buff);
strcpy(buff,argv[1]);
free(ptr); }


Компилируем:

MiagBox:/home/stas/samag# gcc -o heap heap.c
heap.c: In function `main':
heap.c:5: warning: assignment makes pointer from integer without a cast
heap.c:7:13: warning: no newline at end of file


А теперь в дебаггер:

MiagBox:/home/stas/samag# gdb ./heap
GNU gdb 6.3-debian
<skipped>

(gdb) r `perl -e 'print "A"x256'`
Starting program: /home/stas/samag/heap `perl -e 'print "A"x256'`
Program received signal SIGSEGV, Segmentation fault.
0x40086ac9 in free () from /lib/tls/libc.so.6


У нас не получается стандартного переполнения, так как происходит вызов функции free(), которая пытается освободить память по адресу 0x41414141, естественно такого адреса просто не существует и программа успешно падает. Чтобы обмануть free(), нам надо просто подставить ей любой валидный адрес в пространстве кучи. Проверим:

(gdb) r `perl -e 'print "A"x28'``printf "\xff\x84\x04\x08"``perl -e 'print "A"x28'`
Starting program: /home/stas/samag/heap `perl -e 'print "A"x28'``
printf "\xff\x84\x04\x08"``perl -e 'print "A"x28'`
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()


Мы передали free() адрес в куче, который она успешно освободила (к слову сказать, это адрес ptr в куче - во избежание лишних проблем мы передали именно этот адрес), и после выхода из функции соответственно передаем управление адресу, который мы полностью контролируем.

Есть еще очень много различных способов, вариантов и возможностей использования heap-based переполнений. Эти ошибки довольно распространены и дополнительная (и наверное, главная) сложность состоит в том, что такие ошибки очень сложно отлавливать на уровне кода - то есть, потенциально, их очень много.

format strings

Ошибки данного класса появились из-за особенностей реализации функций семейства printf (и им подобных). Проблема в том, что если не указан спецификатор формата, пользователь может сам его указать, а значения система будет брать из стека (с вершины). Более того, есть спецификатор %n, который позволяет записать определенное значение по заданному адресу (что и позволяет нам выполнять произвольный код)! Рассмотрим небольшой пример уязвимой программы:

test.c:

#include
int main(int argc, char *argv[]) {
char stack[5]="info", fmtbug[10];
strncpy(fmtbug,argv[1],sizeof(fmtbug));
printf(fmtbug); /* Fomrat String Bug! */
printf("\n");
}


Компилруем:

MiagBox:/home/stas/samag# gcc -o test test.c
test.c:8:2: warning: no newline at end of file


Исполняем:

MiagBox:/home/stas/samag# ./test data

data


Итак, попробуем получить данные из стека:

MiagBox:/home/stas/samag# ./test "data %x %x"
data bffffbda aЪїJinfo


Как видите, мы добрались до массива stack.

При определенных условиях, мы можем изменить процесс выполнения программы, то есть заставить ее выполнить некий условный код по нашему адресу, используя все тот же спецификатор "%n", цель стандартная - перевести управление на наш вредоносный код. Про банальный DoS ala "%s" x 100 я упоминать не буду.

Однако успокаивает то, что подобные ошибки довольно легко отлавливать автоматизированными средствами, но это не касается функций, которые написали программисты специально для своего ПО.

integer overflow

Это самый новый способ из описываемых мною фундаментальных атак, который очень быстро набирает обороты. Переменные с типом int - это область в памяти (как правило, 32 или 64 бита), предназначенная для хранения некого целочисленного значения. Целочисленное переполнение - это выход за пределы допустимого значения, если в переполнении, к примеру, стека играет роль размер массива, то здесь имеет значение максимальное число, которое может храниться в переменной. В некоторых случаях мы можем влиять на процесс выполнения программы. Давайте рассмотрим пример:

int.c:

#include
#include
int main(int argc, char *argv[]) {
unsigned short a;
int b = atoi(argv[1]);
char c[100];
a=b;
/* protection */
if (a >= 100) {
printf("kind of protection.. Exiting\n");
return 0;
}

memcpy(c,argv[2],b);
printf("%s\n",c);
return 0;
}


На первый взгляд, программа должна работать нормально. Проверим:

MiagBox:/home/stas/samag# gcc -o int int.c
MiagBox:/home/stas/samag# ./int 3 testmessage
tes


Но что будет, если мы попытаемся переполнить i ?

MiagBox:/home/stas/samag# ./int 65536 testmessage

Segmentation fault


Дело в том, что размер аргумента копируется в b, а потом "переносится" в a, в котором не хватает места для числа (тип short). Из-за этого мы проходим проверку. После этого могут быть использованы стандартные методы (вроде переполнения стека). И хотя целочисленные переполнения в чистом виде почти не предоставляют угрозы, тем не менее, они иногда позволяют провести другие атаки вроде переполнения стека или использования ошибок форматной строки. Хотелось бы заметить, что сейчас появились способы детектирования целочисленного переполнения. Возможно, в скором времени подобные проверки будут встроены в компиляторы.

другие техники

Существуют также несколько других общеизвестных проблем и техник, и я вкратце опишу их.

Return-to-Func (Return-into-Libc) - как правило, используется для обхода систем с неисполняемым (non-executable) стеком. Суть работы довольно проста - поскольку мы не можем выполнить код в стеке, мы возвращаемся не в стек, а в область памяти, используемую общими библиотеками. К примеру, мы можем переписать EIP на адрес функции system(), передав дальше аргументы (/bin/sh, например). Поскольку никакого кода в самом стеке не будет выполняться, мы успешно получаем оболочку (если переписываем на system() и указателем на /bin/sh в качестве аргумента, разумеется). С этим, как правило, борются наличием null-байтов в адресах функций, а также рандомизацией их адресов (подобные способы реализованы в некоторых средствах защиты).

Frame pointer overflow - это атака, при которой мы можем переполнить наш буфер всего лишь на один байт. При определенных условиях мы можем выполнить произвольный код. Идея состоит в том, что мы в состоянии переписать 1 байт в EBP. После выхода из функции регистр EBP будет скопирован в ESP, то есть мы можем изменять регистр произвольным значением (не совсем произвольным, так как нам доступен для перезаписи только один байт). Итак, вот псевдокод, это конец функции main():

0x81253b1 : movl %ebp,%esp
0x81253b3 : popl %ebp
0x81253b4 : ret
0x81253b5 : nop
0x81253b6 : nop


То есть программа копирует EBP в ESP и выполняет инструкции по адресу, который находится в стеке (ret) - на что указывает ESP.

Adj overflow (adjacent memory overflow) - это последствия особенностей реализации некоторых функций. Многие функции находят конец строки по null байту (0x00), но что если строка у нас такого же размера, как и буфер?

NULL-байт просто не будет записан! Рассмотрим пример:

adj.c:

#include
int main (int argc[], char *argv[]) {
char buff1[263];
char buff2[1024];
char buff3[256];
strncpy(buff3, argv[1], sizeof(buff3));
strncpy(buff2, argv[2], sizeof(buff2));
sprintf(buff1, "%s", buff3);
}


Итак, вопрос: Может ли buff3 переполнить buff1? Может! Если buff3 не будет завершен NULL байтом, то sprintf будет продолжать копирование, пока не встретит этот null-byte, а вот и последствия:

MiagBox:/home/stas/samag# gcc -o adj adj.c
MiagBox:/home/stas/samag# gdb ./adj
<skipped>

(gdb) r `perl -e 'print "A"x240'` `perl -e 'print "B"x125'`

/* Немного меньше нашего буфера, "блокирует" strcpy.. */

Starting program: /home/stas/samag/adj `perl -e 'print "A"x240'` `perl -e 'print "B"x125'`

Program exited with code 0360.

(gdb) r `perl -e 'print "A"x250'` `perl -e 'print "B"x125'`

/* Немного больше нашего буфера, "блокирует" strcpy.. */

Starting program: /home/stas/samag/adj `perl -e 'print "A"x250'` `perl -e 'print "B"x125'`

Program exited with code 0372.

(gdb) r `perl -e 'print "A"x256'` `perl -e 'print "B"x125'`

/* oops.. это равно размеру нашего буффера */

Starting program: /home/stas/samag/adj `perl -e 'print "A"x256'` `perl -e 'print "B"x125'`

Program received signal SIGSEGV, Segmentation fault.

0x42424242 in ?? ()


Мы можем изменить процесс выполнения программы.. Как видите, при определенных условиях так можно эксплойтить функцию strncpy ("безопасная" замена strcpy, с проверкой границ) и не только ее. Однако стоит отметить, что данная атака требует множество условий.

shellcoding

Как я уже говорил, шеллкод - это машинный код, как правило локально вызывающий оболочку или привязывающий ее к сокету (используется для удаленных эксплойтов). Есть еще много различных типов шеллкодов, это reverse-conected шеллкоды (как правило, используются для обхода файрволлов и других фильтров сетевого трафика), полиморфные шеллкоды (используются для обхода систем обнаружения вторжений), architecture spanning шеллкоды (это машинный код, который будет успешно работать на различных архитектурах, например на SPARC и на Intel, это повышает универсальность шеллкода), alphanumeric- и unicode-шеллкоды (используются для обхода фильтров на различные символы в приложениях) и т. д., все это очень большая, интересная и сложная тема. Вот пример простого execve-шеллкода (под ОС Linux):

"\x31\xc0" // xor %eax,%eax
"\x50" // push %eax
"\x68\x2f\x2f\x73\x68" // push $0x68732f2f
"\x68\x2f\x62\x69\x6e" // push $0x6e69622f
"\x89\xe3" // mov %esp,%ebx
"\x8d\x54\x24\x08" // lea 0x8(%esp,1),%edx
"\x50" // push %eax
"\x53" // push %ebx
"\x8d\x0c\x24" // lea (%esp,1),%ecx
"\xb0\x0b" // mov $0xb,%al
"\xcd\x80" // int $0x80


Данный машинный код выполняет /bin/sh (linux x86). Тонкости работы кода нас не сильно интересуют, нам лишь важно знать, что шеллкод используется в 95% эксплойтов, и при удачной атаке на него передается управление, и он, разумеется, выполняется. Работа шеллкода составляет главную и ключевую роль практически в любом эксплойте.

Вот вы и познакомились с фундаментальными атаками, теперь давайте немного рассмотрим системы защиты, созданные специалистами по безопасности.

разработанные методы защиты

С момента появления и активного развития фундаментальных атак прошло очень много времени, разработчики защиты придумали немало средств, которые сильно осложнили жизнь атакующим. Но, к сожалению, практически во всех методы защиты были найдены уязвимости, в том числе архитектурные. Итак, сначала я решил разделить защитные механизмы на различные группы, вроде non-executable stack и canary protection, но потом решил отдельно описать некоторые защитные средства, поскольку многие из них комбинируют используемые техники.

StackGuard/StackShield

Суть работы этих программ довольно проста - StackGuard записывает canary (специальное псевдослучайное значение) между сохраненным адресом возврата и EBP. Если значение canary изменилось, то это событие квалифицируется как атака на переполнение буфера. StackShield действует иначе - он создает специальное пространство retarray для хранения копии адреса возврата. После выхода из функции этот адрес копируется в EIP. Каким бы безопасным это не казалось на первый взгляд, существует по крайней мере несколько способов обхода данных средств защиты. Во-первых, нам не обязательно переписывать canary, если мы можем воздействовать на указатель - переписать его на адрес возврата, а потом попросту заменить его на шеллкод.

Хотя, конечно, успех атаки зависит от кода функции, что естественно очень сильно снижает риски.. Но есть другие способы, например, немного изменить Global Offset Table (GOT).

Скажем, вот в таком коде:

printf ("a=%x > This is 1st strcpy\n",a);
strcpy(a,argv[1]);
printf ("a=%x > After 1st strcpy\n",a);
strncpy(a,argv[2],16);
printf("and after second strcpy..\n");


нам надо переписать GOT printf() на адрес system(), в результате программа выполнит

system("and after second strcpy..\n");

и все что нам теперь нужно - это создать скрипт "and" в нашей директории, который и запустит программа.

Или, например, если программа вызывает функцию exit() (разумеется, у нас по-прежнему должен иметься в наличии указатель, которым мы можем манипулировать), мы можем переписать адреса функций в структуре fnlist на адрес нашего шеллкода.

Это лишь часть найденных способов обхода подобных защит. Данные программы сейчас уже практически нигде не используются, но надо отдать им должное, так как многие современные системы защиты используют представленные авторами этих продуктов техники.

Windows 2003

В операционную систему Microsoft Windows 2003 встроена защита от переполнения стека.

Она заключается в следующем: за адресом возврата (который сохраняется в стеке, чтобы программа знала куда обращаться после выхода из функции) кладется специальное число (canary, cookie). Если мы попытаемся переписать адрес возврата, соответственно, изменим canary и выполнение процесса прекратится (вы не находите сходства со StackGuard?).

Но существует несколько способов обхода подобной защиты, нацеленных не на архитектурную составляющую защиты в целом и не на особенности реализации уязвимых функций в эксплуатируемом ПО, а именно на ее реализацию специалистами софтверного гиганта. Если cookie не совпадает с оригинальным значением, которое хранится в секции .data, то выполняется специальный security-handler, который тоже хранится в секции .data. Если никакой обработчик (handler) не задан, тогда UnhandledExceptionFilter устанавливается на 0x00000000 и выполняется функция UnhandledExceptionFilter – которая, в свою очередь, загружает в общую память библиотеку faultrep.dll и вызывает функцию ReportFault (это то самое всплывающее окно, которое просит вас отправить отчет о ошибке в Microsoft).

Стоит заметить, что в ОС Windows существуют обработчики исключений, которые помогают программистам делать приложения более стабильными. Если происходит какое-то непредвиденное действие, например, попытка записи в read-only память, для такого действия может быть написано исключение, которое в последствии может "реанимировать" процесс. Дело в том, что в каждом потоке процесса существует как минимум один обработчик. Информация о нем хранится в стеке, в структуре EXCEPTION_REGISTRATION, которая имеет два элемента - указатель на следующую структуру EXCEPTION_REGISTRATION и указатель на обработчик исключений, который мы не можем просто переписать - так как теперь все адреса обработчиков исключений зарегистрированы и хранятся в Load Configuration Directory. То есть, прежде чем выполнить обработчик по некому адресу, он сверяется со списком зарегистрированных обработчиков и не будет выполнен, если не будет там найден. Но стоит заметить, если адрес обработчика находится за пределами адресного пространства загруженного модуля, то это адрес будет выполнен! Также если адрес находится в области heap - он тоже будет выполнен, хотя, если адрес будет в области стека, по понятным причинам он выполнен не будет.

Итак, нам надо сгенерировать исключение, например, попытаться что-то записать за пределами стека. Это вызовет исключение. Теперь остается только переписать указатель на обработчик вне адресного пространства загруженного модуля. Он будет проверен по списку зарегистрированных исключений, но поскольку адрес находится вне адресного пространства, ему будет передано управление. Теперь атакующему остается лишь найти блок инструкций вне адресного пространства, который переведет управление на область в памяти, которую контролирует атакующий (то есть в область, в которую он может загрузить шеллкод). Как вариант можно еще (если это позволяет эксплуатируемый код) загрузить шеллкод в буфер, который находится в куче (heap) и переписать указатель обработчика на адрес буфера в пространстве кучи.

Windows XP Service Pack 2

С течением времени Microsoft решила усовершенствовать свою защиту, обратив внимание на область кучи (heap). Однако, к сожалению, надежды не увенчались успехом (увы, но такое часто случается). Существует как минимум несколько различных способов противодействия защите, встроенной в пакет обновлений Service Pack 2 операционной системы Windows XP.

Итак, чтобы обезопасить себя от переполнений, в систему менеджмента кучи было добавлено две проверки чанков (chunks) - во первых, это проверка cookie в заголовке чанка, а во вторых, довольно эффективная проверка указателей Flink и Blink (это указатели на следующий и предыдущий свободные блоки соответственно). На мой взгляд, наиболее предпочтительный метод обхода защиты заключается в следующем - многие API-функции хранят некоторые данные загруженных библиотек (DLL) в куче, то есть еще до старта main() в куче уже создано несколько чанков, как правило их структура выглядит так:


|-----------------|
| chunk header |
|-----------------|
| 0 X |
| A B |
| 0 0 |
| ? ? |
|-----------------|


где A и B - адреса следующей и предыдущей структур соответственно, и если мы сможем их заменить, то будем в состоянии переписать произвольные 4 байта в памяти (например, адрес возврата). Эта атака возможна потому, что не происходит никакой проверки указателей на структуры (A и B). Также существует другой метод, который предложил россиянин Александр Анисимов. Метод связан с атакой на ассоциативные массивы, так как в них тоже не проводится никаких специальных проверок, но такой метод довольно сложно применить на практике. Последствия - возможность переписать произвольные 1016 байтов в памяти.

готовые решения

Рынок информационной безопасности потихоньку стал немного присматриваться к защите от переполнения буфера, встраивая защитные механизмы в свои HIPS (Host Intrusion Prevention Systems), которые, помимо всех прочих возможностей (противодействие spyware/adware, встроенный файрволл, аудит различных событий и пр.), содержат так называемую технологию Stack Backtracing, которая позволяет проводить перехват и анализ функций на наличие шеллкода и соответственно запрещать вызовы при наличии такового (как ни странно, в этом содержится своя слабость - эти продукты не предотвращают атаки переполнения буфера в принципе, то есть не создают неисполняемые стек и кучу, а лишь пытаются перехватить шеллкод в вызываемых (читай - перехватываемых) API-функциях). Первая проблема состоит в том, что шеллкод может детектировать функции, которые перехватываются. Защитные механизмы в обычную преамбулу функции (push ebp; mov ebp, esp) добавляют call или jmp, и для шеллкода не составит труда обнаружить данную "сигнатуру". После успешного детектирования шеллкод может обойти систему защиты, например, вставить свою копию преамбулы, а потом перевести управление сразу после API-хука. Но самый универсальный способ, который действует как против перехвата функций на уровне пользователя (ring3), так и на уровне ядра (ring0) - это подделка stack-фреймов, то есть шеллкоду надо создать поддельный stack фрейм без ebp в стеке. А поскольку техника stack backtracing зависит от наличия ebp в стеке, чтобы найти следующий stack-фрейм, поддельный фрейм может остановить stack backtracing. Итак, нам всего лишь надо создать stack-frame с верным адресом возврата, который указывает на read-only область памяти. Кроме того, надо заметить, что перехват API-функций на уровне пользователя несет в себе кучу проблем. Во первых, необходимо перехватывать и анализировать все функции, которые может использовать атакующий (например, NAI Entercept 4.1 перехватывает LoadLibraryA, но ничего не знает о LoadLibraryW). Во вторых, некоторые продукты перехватывают функции только в kernel32.dll, забывая при этом о ntdll.dll, это недоработка все в том же NAI Entercept. Также существуют способы обхода многих других средств защиты, например Open Wall (патч Александра Песляка, более известного как Solar Designer), GrSecurity, обход DEP (как софтверного, так и хардварного) и т. д. Я думаю, достаточно. Главное, чтобы вы уловили суть и идеи атак и защитных механизмов, а также некоторые способы их обхода (некоторые продукты уже устранили описанные уязвимости, некоторые нет).

общие принципы защиты

Итак, после всего прочитанного вы твердо решили серьезно повысить безопасность своих систем. С чего начать? Я выделил несколько этапов активной защиты. Во первых, полезная нагрузка эксплойта (payload) пройдет множество систем по определенному маршруту. Поэтому очень важно, чтобы наш сервер "прикрывал" пограничный маршрутизатор и/или файрволл с установленной и правильно настроенной системой обнаружения/предотвращения атак (а еще лучше, если подобная система будет стоять на самом хосте-мишени, если это позволяет производительность машины).

Давайте подробнее рассмотрим методы защиты, которые применяются в современных системах обнаружения вторжений. Главный бастион - это сигнатуры (определенная последовательность байтов в пакете, которая соответствует некоторому действию, например, попытки эксплойта RADIUS ATTR_TYPE_STR). Многие IDS имеют базу данных сигнатур, которые включают в себя обнаружение совершенно различных действий - сканирования портов, попытки определения операционной системы (OS fingerprinting), попытки DDoS'а и пр. Но самое интересное для нас - это блокирование атак и обнаружение/перехват шеллкодов. Эксплойты (разумеется, уже существующие) перехватываются по определенным особенностям. Вот, например, пример сигнатуры для эксплойта под kadmind у IDS Snort:

alert tcp $EXTERNAL_NET any -> $HOME_NET 751 (msg:"EXPLOIT kadmind buffer overflow attempt"; flow:established,to_server; content:"|00 C0 05 08 00 C0 05 08 00 C0 05 08 00 C0 05 08|"; reference:bugtraq,5731; reference:bugtraq,6024; reference:cve,2002-1226; reference:cve,2002-1235; reference:url,www.kb.cert.org/vuls/id/875073; classtype: shellcode detect; sid:1895; rev:8

Это означает, что если TCP-пакет с любым адресом источника попытается достучаться до IP-адресов, которые содержатся в $HOME_NET по 751 порту, и будет содержать в себе определенный объектный код, который представлен в шестнадцатеричном виде, то система обнаружения вторжений квалифицирует данное событие как попытку эксплойта BOF в kadmind. Хотя, на мой взгляд, блокирование эксплойтов по сигнатурам приносит сомнительную пользу (лишняя трата времени, не находите? Хотя...) Более интересная часть - это обнаружение и перехват шеллкодов. Достигается это теми же сигнатурами, которые содержат в себе часто используемые шеллкодами байты и функции (последовательности байтов). К примеру, все в том же IDS Snort есть препроцессор fnord, который даже пытается определить полиморфные шеллкоды.

Самый примитивный метод - определять шеллкод по наличию NOP, то есть, разумеется, не просто 0x90 (это No Operation для x86), а аналогичные по функционалу операции вроде inc/dec, xchg %ebp,%ebp и пр, а также вариации nop-ов для различных микро-процессоров. Можно "вытягивать" системные вызовы из трафика, например setuid/setgid. Кроме того, есть много довольно интересных дополнительных техник, которые активно используются некоторыми системами обнаружения/предотвращения вторжений. Например, детектирование типичных приглашений интерпретаторов командой строки (/bin/sh и cmd.exe). Не стоит и говорить, что скажем перехват шеллкодов по различным "nop-like" сигнатурам очень серьезно влияет на производительность машины. Конечно, в любом случае надо пристально изучать используемую вами IDS систему, а лучше предварительно проконсультироваться с поставщиком (главное, чтобы консультацию проводили грамотные специалисты, а то в наше время могут преподнести "перехват" десятка не актуальных эксплойтов по сигнатурам как 100% защиту от вредоносного кода. Это печально, но вопрос здесь скорее к менеджерам компаний- распространителей IDS систем).

Второй (и бесспорно самый главный) бастион защиты - это используемая вами операционная система. Здесь надо рассматривать несколько возможностей атаки. Мы сразу обратим внимание как на локальные атаки, то есть когда у злоумышленника уже есть действующий аккаунт в системе, так и на удаленные атаки, учитывая, просто к примеру, что использовался какой-нибудь метод полиморфизма и шеллкод все таки прошел через IDS/IPS фильтр. Давайте по порядку остановимся на каждой атаке и составим необходимые общие рекомендации по защите. Эксплуатирование уязвимости типа переполнения буфера в стеке поможет предотвратить неисполняемый стек, который сделает невозможным выполнение шеллкода (да и любого другого кода) в стеке. Хотя, некоторым приложениям это необходимо, но системы защиты придумали различные ухищрения, чтобы бороться с этим ограничением. Но существует другая проблема - ret-to-func (ret-into-libc) эксплойты, которые с легкостью обходят подобную защиту, вызывая функции из общей памяти и подставляя соответствующие аргументы. Но их проблема в том, что они используют определенные "зашитые" адреса функций (вообще использование каких-то определенных значений понижает универсальность эксплойта, но во многих случаях это необходимо, точнее - неизбежно). Специалисты по безопасности придумали средство против данной атаки - рандомизация адресов функций, то есть при каждом новом запуске приложения адреса функций в памяти будут меняться, плюс они могут содержать часто недопустимые байты, например null-byte (такая техника используется, к примеру, в дистрибутиве Open Wall GNU/Linux).

С переполнениями в куче как правило борются, создавая неисполняемую кучу, что предотвращает выполнение любого кода в данной области памяти (non- executable heap также содержится в некоторых средствах защиты; по моим данным, PaX Team впервые разработала такой модуль), а также часто модифицируют процедуру менеджмента кучи, добавляя различные проверки чанков (на валидность cookie, например).

С атаками на ошибки форматной строки дела обстоят немного иначе. Как вариант можно перехватывать вызовы уязвимых функций к библиотекам (например, syslog(), printf(), snprintf() и пр., которые подвержены атакам форматной строки).

Как вы уже заметили, практически все эксплойты используют шеллкод (ну, конечно, существуют исключения, к примеру мы можем передать управление не нашему коду, а какой-нибудь другой функции, уже заложенной в программе, но это большая редкость). Шеллкод возможно перехватывать не только на уровне сети, но и на уровне хоста. Для этих целей существуют HIPS (Host Intrusion Prevention System) - специальное ПО, которое работает на уровне хоста и перехватывает функции, анализируя их на наличие шеллкода. Проще говоря, это уже рассмотренная нами технология stack backtracing. Конечно, существуют некоторые способы обхода данной технологии, но не надо забывать, что 99% эксплойтов используют стандартные шеллкоды и методы вторжения, которые никоим образом не предназначены для тюнингованных специализированной защитой систем (большинство людей, использующих эксплойт, часто понятия не имеют как он работает, не говоря уже о его модификации для обхода защитных средств).

Если рассматривать другие атаки, такие как целочисленное переполнение (integer overflow), adjancent memory overflow и frame pointer overwrite, то для защищаемых средств они не играют большой роли, поскольку подобные атаки позволяют лишь эксплуатировать вышеуказанные уязвимости в коде.

полезные рекомендации

По большому счету существует 2 метода нападения на систему - это либо автоматизированные средства, либо "прямые" действия самого атакующего. Но для нас это не играет большой роли, так как payload от этого не меняется. Но стоит заметить, что даже mass/auto rooting редко происходит без получения какой-либо информации о атакуемой системе, не говоря уже о "ручном" взломе. То есть атакующие пытаются определить хотя бы версию демона, так как нет никакого смысла атаковать Apache 1.3.26, если стоит IIS/6.0. Анализ баннера в большинстве случаев – самый актуальный способ анализа (возможен также анализ по fingerprints демонов/серверов для более точного определения версий и вообще принадлежности к конкретной линейке продуктов, но я думаю, что такая ситуация для автоматизированных средств в реальности маловероятна). Так что тут встает еще одна линия обороны - это скрытие информации от потенциального злоумышленника или его автоматизированного средства.

Security-сообщество пытается всех убедить что "Security through obscurity" не приносит реальной безопасности. Отчасти это так, но, к примеру, если мне удастся убедить атакующего, что на моей машине стоит, скажем, FreeBSD, а не Linux, то шеллкод, который будет использовать злоумышленник, не будет работать на моей системе, даже если и получит управление. На мой взгляд, надо, по возможности, прятать все, что только можно. Менять параметры, которые критичны для разлиных fingerprint-техник, менять системные баннеры и другие информационные заголовки, по которым можно определить версию демона/сервиса и тд. Одним словом вводить в заблуждение атакующего или его автоматизированные средства. Более того, не забудьте грамотно настроить свой файрволл, блокируйте неиспользуемые порты (это защитит вас от bind-шеллкодов), контролируйте установку внешних соединений (это должно защитить вас от reverse-connected шеллкодов, то есть когда ваша машина сама соединяется с компьютером взломщика). Не забываейте также о Chroot и разделении привилегий - это очень действенные меры по противодействию атакам (скажем, "порутанный" apache сбрасывает нам nobody shell, а если в /tmp запрещено выполнение программ, ситуация и вовсе незавидная для атакующего, даже без учета chroot). Если в chroot не будет /bin/sh - то это просто отлично (98% unix-like шеллкодов выполняют оболочку по этому адресу). Естественно, здесь я даже не устану упоминать о таких вещах, как выбор стойких паролей, правильная установка прав доступа, постоянная проверерка лог-файлов, контроль за действиями пользователей, безопасная настройка демонов и т. д, так как все это подробно описано во многих книгах и учебниках по информационной безопасности.

заключение

Любой рынок постоянно развивается, и безопасность здесь не является исключением. Но, к сожалению компании порой забывают о реальном обеспечении защиты, сводя все к покупке продуктов вроде файрволла или очередной VPN-системы, а также созданию политик безопасности и регламентов.. Только грамотная служба безопасности, либо действительно квалифицированные администраторы, которые разбираются в тонкостях безопасности вверенных им систем и сетевых устройств, могут обеспечить очень хорошую и реальную защиту предприятия от подобных атак! Но часто ни того, ни другого в организациях попросту нет, что, естественно, печально, и если нет возможности нанять квалифицированных специалистов по информационной безопасности, а обеспечивать реальную, а не "бумажную" защиту все же надо, я надеюсь, что моя статья помогла вам продвинуться в этом вопросе. И я также надеюсь, что этот материал заставит вас задуматься над проблемами, которые принес нам рынок 0-day, развивающийся молниеносными темпами и с каждым годом получая все больше и больше финансовых вложений от "заинтересованных лиц", которых, поверьте на слово, совсем не интересует обеспечение безопасности.



Лакехин Станислав, специалист компании Mobile Intelligence Agency, lakehin@miag.ru


Сетевые решения. Статья была опубликована в номере 06 за 2006 год в рубрике save ass…

©1999-2024 Сетевые решения