Защита от хакеров корпоративных сетей
Шрифт:
– call EAX FF D0
– call EBX FF D3
– call ECX FF D1
– call EDX FF D2
– call ESI FF D6
– call EDI FF D7
– call ESP FF D4При просмотре памяти процесса из библиотеки KERNEL32. DLL были найдены следующие подходящие пары шестнадцатеричных байтов:
77F1A2F7 FF D0 call EAX
77F76231 FF D0 call EAX
7FFD29A7 FF D0 call EAX ; a whole block of this pattern exists
7FFD2DE3 FF E6 jmp ESI ; a whole block of this pattern exists
7FFD2E27 FF E0 jmp EAX ; a whole block of this pattern exists
77F3D793 FF D1 call ECX
77F7CEA7 FF D1 call ECX
77F94510 FF D1 call ECX
77F1B424 FF D3 call EBX
77F1B443 FF D3 call EBX
77F1B497 FF D3 call EBX
77F3D8F3 FF D3 call EBX
77F63D01 FF D3 call EBX
77F9B14F FF D4 call ESP
77F020B0 FF D6 call ESI
77F020D5 FF D6 call ESI
77F02102 FF D6 call ESI
77F27CAD FF D6 call ESI
77F27CC2 FF D6 call ESI
77F27CDB FF D6 call ESI
77F01089 FF D7 call EDI
77F01129 FF D7 call EDI
77F01135 FF D7 call EDIЭти пары шестнадцатеричных байтов могут быть использованы практически в любой программе. Но поскольку найденные пары шестнадцатеричных байтов – часть интерфейса ядра динамически подключаемой библиотеки DLL, то обычно они находятся по фиксированным адресам памяти,
– push EAX 50
– push EBX 53
– push ECX 51
– push EDX 52
– push EBP 55
– push ESI 56
– push EDI 57
– ret C3В динамически подключаемой библиотеке Kernel32.DLL содержатся следующие подходящие пары шестнадцатеричных байтов:
77F3FD18 push EDI
77F3FD19 ret
(?)
77F8E3A8 push ESP
77F8E3A9 retПрограмма поиска точек перехода Findjmp. На рисунке 8.23 представлена небольшая программа, которая сканирует двоичный код динамически подключаемой библиотеки. Входными параметрами программы являются имя динамически подключаемой библиотеки и название регистра из командной строки. Программа ищет характерные для поддерживаемых способов комбинации шестнадцатеричных цифр в размещенном в памяти двоичном коде заданной динамически подключаемой библиотеки. Она поддерживает способы передачи управления по только что записанному в стек адресу (push return), по содержимому регистра (call register) и явный переход по содержимому регистра (jump register).
Программа ищет в динамически подключаемой библиотеке команды перехода, которые можно использовать в своих целях. Вполне вероятно, что для экспериментов с переполнением буфера потребуется подходящее место для передачи управления специальному коду, адрес которого загружен в какой-либо регистр. Программа подскажет потенциальное место загрузки в регистр EIP адреса нужной программы.
Программа легко адаптируется для поиска других способов перехода или образцов программного кода в динамически подключаемой библиотеке DLL. В настоящее время программа поддерживает поиск следующих команд:
1) jmp reg;
2) call reg;
3) push reg / ret.
Всех их объединяет общий результат: в регистр EIP загружается содержимое регистра reg. Программа также распознает следующие регистры:
• EAX;
• EBX;
• ECX;
• EDX;
• ESI;
• EDI;
• ESP;
• EBP.
Программа компилируется как консольное приложение на любой платформе, поддерживающей интерфейс 32-разрядных Windows-приложений. Приложение может быть найдено в разделе сайта издательства www.syngress.com/solutions, посвященном книге.
Подпрограмма usage выводит в стандартное устройство вывода (консоль или принтер) краткую инструкцию по использованию программы.void usage
{
printf(“FindJmp usage\nfindjmp DLL reg\nEx: findjmp
KERNEL32.DLL ESP\n”);
exit (0);
}
/*The findjmp function is the workhorse. It loads the
requested dll, and searches for specific patterns for jmp
reg, push reg ret, and call reg.*/
void findjmp(char *dll,char *reg)
{
/* patterns for jmp ops */
BYTE jmppat[8][2]= {{0xFF,0xE0},{0xFF,0xE3},{0xFF,0xE1},
{0xFF,0xE2},{0xFF,0xE6},{0xFF,0xE7},
{0xFF,0xE4},{0xFF,0xE5}};
/* patterns for call ops */
BYTE callpat[8][2]= {{0xFF,0xD0},{0xFF,0xD3},{0xFF,0xD1},
{0xFF,0xD2},{0xFF,0xD6},{0xFF,0xD7},
{0xFF,0xD4},{0xFF,0xD5}};
/* patterns for pushret ops */
BYTE pushretpat[8][2]= {{0x50,0xC3},{0x53,0xC3},{0x51,0xC3},
{0x52,0xC3},{0x56,0xC3},{0x57,0xC3},
{0x54,0xC3},{0x55,0xC3}};
/*base pointer for the loaded DLL*/
HMODULE loadedDLL;
/*current position within the DLL */
BYTE *curpos;
/* decimal representation of passed register */
DWORD regnum=GetRegNum(reg);
/*accumulator for addresses*/
DWORD numaddr=0;
/*check if register is useable*/
if(regnum == -1)
{
/*it didn’t load, time to bail*/
printf(“There was a problem understanding the
register.\n”\
“Please check that it is a correct IA32 register
name\n”\
“Currently supported are:\n ”\
“EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP\n”\
);
exit(-1);
}
loadedDLL=LoadLibraryA(dll);
/* check if DLL loaded correctly*/
if(loadedDLL == NULL)
{
/*it didn’t load, time to bail*/
printf(“There was a problem Loading the requested
DLL.\n”\
“Please check that it is in your path and readable\n” );
exit(-1);
}
else
{
/*we loaded the dll correctly, time to scan it*/
printf(“Scanning %s for code useable with the %s
register\n”,
dll,reg);
/*set curpos at start of DLL*/
curpos=(BYTE*)loadedDLL;
__try
{
while(1)
{
/*check for jmp match*/
if(!memcmp(curpos,jmppat[regnum],2))
{
/* we have a jmp match */
printf(“0x%X\tjmp %s\n”,curpos,reg);
numaddr++;
}
/*check for call match*/
else if(!memcmp(curpos,callpat[regnum],2))
{
/* we have a call match */
printf(“0x%X\tcall %s\n”,curpos,reg);
numaddr++;
}
/*check for push/ret match*/
else if(!memcmp(curpos,pushretpat[regnum],2))
{
/* we have a pushret match */
printf(“0x%X\tpush %s –“\
“ ret\n”,curpos,reg);
numaddr++;
}
curpos++;
}
}
__except(1)
{
printf(“Finished Scanning %s for code
useable with”\
“ the %s register\n”,dll,reg);
printf(“ Found %d usable addresses\n” ,numaddr);
}
}
}
DWORD GetRegNum(char *reg)
{
DWORD ret=-1;
if(!stricmp(reg,“EAX”))
{
ret=0;
}
else if(!stricmp(reg,“EBX”))
{
ret=1;
}
else if(!stricmp(reg,“ECX”))
{
ret=2;
}
else if(!stricmp(reg,“EDX”))
{
ret=3;
}
else if(!stricmp(reg,“ESI”))
{
ret=4;
}
else if(!stricmp(reg,“EDI”))
{
ret=5;
}
else if(!stricmp(reg,“ESP”))
{
ret=6;
}
else if(!stricmp(reg,“EBP”))
{
ret=7;
}
/*return our decimal register number*/
return ret;
}Смещение. Термин смещение (offset) в
Последовательность команд NOP. В командах перехода следует точно указать адрес перехода. Для этого нужно решить практически неразрешимую задачу определения адреса программного кода полезной нагрузки в памяти. Сложность состоит в том, что программный код полезной нагрузки каждый раз загружается в разные места памяти. Для системы UNIX повторная компиляция одного и того же пакета программ в различных средах, различными компиляторами с отличающимися установками оптимизации является общепринятой практикой. Что работает у одной копии программного обеспечения, может не работать у другой. Для того чтобы преодолеть подобные затруднения, рекомендуется использовать последовательность команд NOP (No Operation). Идея проста. NOP – это команда, которая ничего не делает, но занимает место в памяти. Кстати, первоначально команда NOP была создана для отладки. Поскольку команда NOP занимает один байт памяти, то она нечувствительна к проблемам упорядочивания байтов и их выравнивания.
Трюк заключается в инициализации буфера командами NOP перед записью в него программного кода полезной нагрузки. Тогда при неточном определении адреса программного кода полезной нагрузки ничего страшного не произойдет, если найденный адрес будет указывать внутрь последовательности команд NOP. Адрес может указывать на любую область памяти в буфере, если буфер заполнен кодами команды NOP. В случае перехода на команду NOP выполнится она и все последующие, пока не дойдет очередь до первой команды программного кода полезной нагрузки. Чем больше заполненный командами NOP буфер, тем с меньшей точностью может быть определен адрес программного кода полезной нагрузки.
Программный код полезной нагрузки
Значение программного кода полезной нагрузки огромно. Однажды написав код полезной нагрузки, в дальнейшем можно наращивать его функциональные возможности хитроумными способами. Программный код полезной нагрузки может быть одним из наиболее полезных и созидательных компонент программы переполнения буфера.
Кодирование. С трудом верится в целесообразность чрезмерного усложнения своей работы. Большинство известных программ переполнения буфера состоят из блоков нечитаемого машинного кода. Вряд ли это кому-то понравится. Есть гораздо лучший способ кодирования полезной нагрузки: напишите код полезной нагрузки на языке C, C++ или встроенном ассемблере, а затем скопируйте откомпилированный код в программный код полезной нагрузки. Многие компиляторы запросто объединяют код на ассемблере и C в единую программу. Подобный способ написания созданных на разных языках программ называется способом комплексирования программ (fusion technique).
Комплексирование программ – сравнительно простой способ написания и компиляции программ на ассемблере с нетрадиционными ухищрениями. Некоторые из них обеспечивают внедрение в рабочие области памяти других процессов. Подобное Windows NT обеспечивает для аутентифицированных пользователей. Но при переполнении буфера того же может добиться и пользователь, не прошедший аутентификации. В любом случае программный код вставляется в пространство удаленного процесса.
Распыление динамически распределяемой памяти («куча»). (Динамически распределяемая память («куча») – область памяти, выделяемая программе для динамически размещаемых структур данных.) Во время исследования возможности использования уязвимости. IDA (Increment/Decrement Adress) информационного сервера Internet IIS (Internet Information Server) 4/5/6 столкнулись со странной ситуацией. Было обнаружено, что диапазон адресов, на которые мог ссылаться при переполнении буфера регистр EIP, сильно ограничен. Уязвимость. IDA была обусловлена переполнением буфера в результате расширения строки символов. Другими словами, бралась строка «AAAA» (в шестнадцатеричном представлении 0x41414141) и преобразовывалась к шестнадцатеричному значению 0x0041004100410041. Это было очень неприятно, так как в область памяти по адресу, начинающемуся с шестнадцатеричного значения 0x00, никакой код никогда не загружался. Поэтому традиционный способ передачи управления программному коду полезной нагрузки с помощью команд перехода по содержимому регистра (jmp ESP или jmp reg) оказался непригодным. Другим неприятным проявлением расширения строки символов было размещение нулевых байтов между байтами программного кода полезной нагрузки. Для преодоления этой проблемы был придуман новый способ, получивший название «принуждение динамически распределяемой памяти» (forcing the heap). Этот способ относится к классу нарушения «кучи» (heap violation). О наиболее известных атаках на динамически распределяемую память будет рассказано позднее, а способ «принуждения динамически распределяемой памяти» отличается от них тем, что его целью является переполнение стека, а не кучи. В ряде случаев этот способ оказался полезным, в том числе и при переполнении буфера из-за расширения строк.