Коллектив авторов - Защита от хакеров корпоративных сетей
“Please check that it is in your path and readablen” );
exit(-1);
}
else
{
/*we loaded the dll correctly, time to scan it*/
printf(“Scanning %s for code useable with the %s
registern”,
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%Xtjmp %sn”,curpos,reg);
numaddr++;
}
/*check for call match*/
else if(!memcmp(curpos,callpat[regnum],2))
{
/* we have a call match */
printf(“0x%Xtcall %sn”,curpos,reg);
numaddr++;
}
/*check for push/ret match*/
else if(!memcmp(curpos,pushretpat[regnum],2))
{
/* we have a pushret match */
printf(“0x%Xtpush %s –“
“ retn”,curpos,reg);
numaddr++;
}
curpos++;
}
}
__except(1)
{
printf(“Finished Scanning %s for code
useable with”
“ the %s registern”,dll,reg);
printf(“ Found %d usable addressesn” ,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) в основном относится к переполнению буфера на локальной машине. Поскольку на многопользовательских машинах традиционно установлена операционная система Unix, то замечено, что при рассмотрении переполнения буфера термин «смещение» в системе Unix используется гораздо чаще, чем в какой-либо другой. Злонамеренный пользователь машины UNIX всегда располагает какими-то учетными записями пользователя и, как правило, обычно стремится приобрести права суперпользователя root. У пользователя Unix всегда имеется возможность откомпилировать любую программу, в том числе и программу переполнения буфера. На локальной машине программа переполнения буфера иногда вычисляет базовый адрес собственного стека, предполагая, что он совпадает с базовым адресом атакованной программы. В результате у злоумышленника появляется удобная для него возможность указать в команде явного перехода (direct jump) смещение относительно вычисленного базового адреса. Если все сделано правильно, то величина «базовый адрес+смещение» (base+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). О наиболее известных атаках на динамически распределяемую память будет рассказано позднее, а способ «принуждения динамически распределяемой памяти» отличается от них тем, что его целью является переполнение стека, а не кучи. В ряде случаев этот способ оказался полезным, в том числе и при переполнении буфера из-за расширения строк.
При исследовании доступных адресов памяти вида 0x00aa00bb, где aa и bb – управляемые символы, было обнаружено, что этот диапазон адресов информационный сервер Internet IIS отводит под динамически распределяемую память. Всякий раз при передаче запроса информационному серверу Internet он сохраняет в динамически распределяемой памяти данные сеанса. Было найдено, что в этом же диапазоне памяти сохранялись пользовательские переменные окружения протокола HTTP, но ими нельзя было воспользоваться. Способ распыления динамически распределяемой памяти заключается в создании в ней последовательности команд NOP и передачи туда управления. Это позволило переполнить стек, получить контроль над регистром EIP и с помощью команды перехода в область динамически распределяемой памяти выполнить размещенный в ней программный код.
Преимущества предложенного способа заключаются в использовании различных ухищрений обхода нулевых байт, вставленных в программный код полезной нагрузки в результате расширения байтов, а также в значительном увеличении размера кода полезной нагрузки, загружаемого в доступную память. Способ удобен еще и тем, что не требует задания смещения в команде передачи управления в область памяти с загруженной динамически подключаемой библиотекой. Достаточно только сослаться на область динамически распределяемой памяти.
Обратная сторона способа заключается в необходимости размещения в области динамически распределяемой памяти длинной последовательности команд NOP, позволяющей загрузить нужный код по желаемым адресам памяти.
Другой способ, основанный на спецификации %u (кодирование Unicode), был развит японским исследователем безопасности, известным под псевдонимом hsj. Он позволяет управлять всеми четырьмя байтами регистра EIP, что позволяет применить традиционные способы переполнения буфера. Перечисленные способы подтверждает существование нескольких решений одной проблемы. Кодирование Unicode специфично для информационного сервера Internet, поэтому способ японского исследователя применим только к нему, а у способа распыления динамически распределяемой памяти более широкая область применения. Им можно воспользоваться для переполнения буфера даже тогда, когда декодирование невозможно.