Новые книги

Книга автора бестселлера `Факс-модем: от покупки и подключения до выхода в Интернет` — Дмитрия Кирсанова — первый полный курс веб-дизайна на русском языке, написанный профессиональным дизайнером. От теоретических основ визуального дизайна до интернетовских технологий и приемов практической работы над сайтом — все это есть в книге, написанной понятно, подробно и увлекательно. Издание будет полезно не только начинающим создателям сайтов, но и дизайнерам, работающим в более традиционных областях, специалистам по рекламе и маркетингу, художникам, программистам, — и, конечно же, всем творческим и любознательным людям.

Глава 22. Внедрение DLL и перехват API-вызовов

ГЛАВА 22 Внедрение DLL и перехват API-вызовов

О среде Windows каждый процесс получает свое адресное пространство. Указатели, используемые Вами для ссылки на определенные участки памяти, — это адреса в адресном пространстве Вашего процесса, и в нем нельзя создать указатель, ссылающийся на память, принадлежащую другому процессу Так, если в Вашей программе есть «жучок», из-за которого происходит запись по случайному адресу, он не разрушит содержимое памяти, отведенной другим процессам.

WINDOWS 98
В Windows 98 процессы фактически совместно используют 2 Гб адресного пространства (от 0x80000000 до 0xFFFFFFFF). На этот регион отображаются только системные компоненты и файлы, проецируемые в память (подробнее на эту тему см. главы 1 3, 14 и 17)

Раздельные адресные пространства очень выгодны и разработчикам, и пользователям. Первым важно, что Windows перехватывает обращения к памяти по случайным адресам, вторым — что операционная система более устойчива и сбой одного приложения не приведет к краху другого или самой системы. Но, конечно, за надежность приходится платить: написать программу, способную взаимодействовать с другими программами или манипулировать другими процессами, теперь гораздо сложнее.

Вот ситуации, в которых требуется прорыв за границы процессов и доступ к адресному пространству другого процесса:

  • создание подкласса окна, порожденного другим процессом;
  • получение информации для отладки (например, чтобы определить, какие DLL используются другим процессом);
  • установка ловушек (hooks) в других процессах.

В этой главе я расскажу о нескольких механизмах, позволяющих внедрить (inject) какую-либо DLL в адресное пространство другого процесса. Ваш код, попав в чужое адресное пространство, можетустроить в нем настоящий хаос, поэтомухорошенько взвесьте, так ли Вам необходимо это внедрение.

Пример внедрения DLL

Допустим, Вы хотите создать подкласс от экземпляра окна, порожденного другим процессом. Это, как Вы помните, позволит изменять поведение окна Все, что от Вас для этого требуется, — вызвать функцию SetWindowLongPtr, чтобы заменить адрес оконной процедуры в блоке памяти, принадлежащем окну, новым — указывающим на Вашу функцию WndProc. В документации Platform SDK утверждастся, что приложение

не может создать подкласс окна другого процесса Это не совсем верно Проблема создания подкласса окна из другого процесса па самом деле сводится к преодолению границ адресного пространства

Вызывая SetWindowLongPtr для создания подкласса окна (как показано ниже), Вы говорите системе, что все сообщения окну, на которое указывает hwnd, следует направлять не обычной оконной процедуре, а функции MySubclassProc

SetWindowLongPtr(hwnd, GWLP_WNDPROC, MySubclassProc);

Иными словами, когда системе надо передать сообщение процедуре WndProc указанного окна, она находит ее адрес и вызывает напрямую В нашем примере система видит, что с окном сопоставлен адрес функции MySubclassProc, и поэтому вызывает именно ее, а нс исходную оконную процедуру

Проблема с созданием подкласса окна, принадлежащего другому процессу, состоит в том, что процедура подкласса находится в чужом адресном пространстве Упрощенная схема приема сообщений оконной процедурой представлена па рис 22.1 Процесс А создает окно На адресное пространство этого процесса проецируется файл User32.dlI Эта проекция User32.dll О1вечает за прием и диспетчеризацию сообщений (синхронных и асинхронных), направляемых любому из окон, созданных потоками процесса А Обнаружив кякое-то сообщение, она определяет адрес процедуры WndProc окна и вызывает ее, передавая описатель окна, сообщение и параметры wParam и lParam Когда WndProc обработает сообщение, Uscr32 dll вернется в начало цикла и будет ждать следующее оконное сообщение

rihter22-1.jpg

Рис. 22-1. Поток процесса В пытается создать подкласс окна, сформированного потоком процесса А

Теперь допустим, что процесс В хочет создать подкласс окна, порожденного одним из потоков процесса А Сначала код процесса В должен определить описатель этого окна, что можно сделать самыми рязными способами В примере на рис 22-1 поток процесса В просто вызывает FindWindow, затем — SetWtndowLongPtr, пытаясь изменить адрес процедуры WndProc окна Обратитевниманис пытаясь Этот вызов не дает ничего, кроме NULL Функция SetWindowLongPtr просто проверяет, не хочет

ли процесс изменить адрес WndProc окна, созданного другим процессом, и,если да, игнорирует вызов

А если бы функция SetWindowLongPtr могла изменить адрес WndProc? Система тогда связала бы адрес процедуры MySubclassProc с указанным окном. Затем при посылке сообщения этому окну код User32 в процессе А извлек бы данное сообщение, получил адрес MySubclassProc и попытался бы вызвать процедуру по этому адресу. Но это привело бы к крупным неприятностям, так как MySubclassProc находится в адресном пространстве процесса В, а активен — процесс А. Очевидно, если бы User32 обратился по данному адресу, то на самом деле он обратился бы к какому-то участку памяти в адресном пространстве процесса А, что, естественно, привело бы к нарушению доступа к памяти

Чтобы избежать этого, было бы неплохо сообщить системе, что MySubclassProc находится в адресном пространстве процесса В, и тогда она переключила бы контекст перед вызовом процедуры подкласса Увы, по ряду причин такая функциональность в системе не реализована.

  • Подклассы окон, созданных потоками других процессов, порождаются весьма редко. Большинство приложений делает это лишь применительно к собственным окнам, и архитектура памяти в Windows этому не препятствует.
  • Переключение активных процессов отнимает слишком много процессорного времени.
  • Код MySubclassProc должен был бы выполняться потоком процесса В, но каким именно - новым или одним из существующих?
  • Как User32.dll узнает, с каким процессом связан адрес оконной процедуры?

Поскольку удачных решений этих проблем нет, Microsoft предпочла запретить функции SeiWindowLongPtr замену процедуры окна, созданного другим процессом.

Тем не менее порождение подкласса окна, созданного чужим процессом, возможно: нужно просто пойти другим путем. Ведь на самом деле проблема не столько в создании подкласса, сколько в закрытости адресного пространства процесса. Если бы Вы могли как-то поместить код своей оконной процедуры в адресное пространство процесса А, это позволило бы вызвать SetWindowLongPtr и передать ей адрес MySubclassProс, в процессе А. Я называю такой прием внедрением (injecting) DLL в адресное пространство процесса. Мне известно несколько способов подобного внедрения Рассмотрим их по порядку, начиняя с простейшего.

Внедрение DLL c использованием реестра

Если Вы уже работали с Windows, то знаете, что такое реестр. В нем хранится конфигурация всей системы, и, модифицируя в реестре тс или иные параметры, можно изменить поведение системы. Я намерен поговорить о параметре реестра:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows_NT\CurrentVersion\Windows\AppImt_DLLs

WINDOWS 98
Windows 98 игнорирует этот параметр реестра, поэтому для нее такой способ внедрения DLL не сработает.

Список параметров в разделе реестра, где находится AppInit_DLLs, можно просмотреть с помощью программы Registry Editor (Редактор реестра). Значением параметра AppInit_DLLs может быть как имя одной DLL (c указанием пути доступа), так и имена

нескольких DLL, разделенных пробелами или запятыми Поскольку пробел используется здесь в качестве разделителя, в именах файлов не должно быть пробелов. Система считывает путь только первой DLL в списке — пути остальных DLL игнорируются, поэтому лучше разметать свои DLL в системном каталоге Windows, чтобы не указывать пути. Как видите, я указал в параметре AppInit_DLLs только одну DLL и задал путь к ней: C:\MyLib.dll.

rihter22-2.jpg

При следующей перезагрузке копьютера Windows сохранит значение этого параметра. Далее, когда User32.dll будет спроецирован па адресное пространство процесса, этот модуль получит уведомление DLL_PROCESS_ATTACH и после его обработки вызовет LoadLibrary для всех DLL, указанных в параметре AppImtDLLs. В момент загрузки каждая DLL инициализируется вызовом ее функции DllMain с параметром fwdReason, равным DLL_PROCESS_ATTACH. Поскольку внедряемая DLL загружается на такой ранней стадии создания процесса, будьте особенно осторожны при вызове функций. Проблем с вызовом функций Kernel32.dll не должно быть, но в случае других DLL они вполне вероятны — User32.dll не проверяет, успешно ли загружены и инициализированы эти DLL. Правда, в Windows 2000 модуль User32.dll ведет себя несколько иначе, но об этом — чуть позже

Это простейший способ внедрения DLL. Все, что от Вас требуется, — добавить значение в уже существующий параметр реестра. Однако он не лишен недостатков.

  • Так как система считывает значение параметра при инициализации, после его изменения придется перезагружать компьютер. Выход и повторный вход в систему не сработает — Вы должны перезагрузить компьютер. Впрочем, сказанное относится лишь к Windows NT версии 4.0 (или ниже). В Windows 2000 модуль User32.dll повторно считывает параметр реестра AppInit_DLLs при каждой загрузке в процесс, и перезапуска системы не требуется.
  • Ваша DLL проецируется на адресные пространства только тех процессов, на которые спроецирован и модуль LIser32 dll Его используют все GLI-приложения, но большинство программ консольного типа — нет. Поэтому такой метод не годится для внедрения DLL, например, в компилятор или компоновщик.
  • Ваша DLL проецируется на адресные пространства всех GUI-процессов. Но Вам-то почти наверняка надо внедрить DLL только в один или несколько определенных процессов. Чем больше процессов попадет "под тень" такой DLL, тем выше вероятность аварийной ситуации. Ведь теперь Ваш код выполняется потоками этих процессов, и, если он зациклится или некорректно обратится к памяти, Вы повлияете на поведение и устойчивость соответствующих процессов. Поэтому лучше внедрять свою DLL в как можно меньшее число процсссов
  • Ваша DLL проецируется на адресное пространство каждого GUI-процесса в течение всей его жизни, Тут есть некоторое сходство с предыдущей проблемой. Желательно не только внедрять DLL в минимальное число процессов, но и проецировать ее на эти процессы как можно меньшее время Допустим, Вы хотите создать подкласс главного окна WordPad в тот момент, когда пользователь запускает Ваше приложение Естественно, пока пользователь не откроет Ваше приложение, внедрять DLL в адресное пространство WordPad не требуется. Kогда пользователь закроет Ваше приложение, целесообразно отменить переопределение оконной процедуры WordPad. И в этом случае DLL юже незачем «держать» в адресном пространстве WordPad Так что лучшсс решение — внедрять DLL только па то время, в течение которого она действительно нужна конкретной программе

Внедрение DLL с помощью ловушек

Внедрение DLL в адресное пространство процесса возможно и с применением ловушек. Чтобы они работали так же, как и в 16-разрядной Windows, Microsoft пришлось создать механизм, позволяющий внедрять DLL в адресное пространство другого процесса Рассмотрим его на примере

Процесс А (вроде утилиты Spy++) устанавливает ловушку WH_GETMESSAGE и наблюдает за сообщениями, которые обрабатываются окнами в системе. Ловушка устанавливается вызовом SetWindowsHookEx

HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMbgProc, hiribtDll, 0);

Аргумент WH_GETMESSAGE определяет тип ловушки, а параметр GetMsgProc — адрес функции (в адресном пространстве Вашего процесса), которую система должна вызывать всякий раз, когда окно собирается обработать сообщение Параметр hinstDll идентифицирует DLL, содержащую функцию GetMsgProc В Windows значение htnstDll для DLL фактически задаст адрес в виртуальной памяти, по которому DLL спроецирована на адресное пространство процесса. И, наконец, последний аргумент, 0, указывает поток, для которого предназначена ловушка. Поток может вызвать SelWindowsHookEx и передать ей идентификатор другого потока в системе Передавая 0, мы сообщаем системе, что ставим ловушку для всех существующих в ней GUI-потоков

Теперь посмотрим, как все это действует:

1. Поток процесса В собирается направить сообщение какому-либо окну.

2. Система проверяет, не установлена ли для данного потока ловушка WH_GETMESSAGE.

3. Затем выясняет, спроецирована ли DLL, содержащая функцию GctMsgProc, на адресное пространство процесса В

4. Если указанная DLL еще не спроецирована, система отображает ее на адресное пространство процесса В и увеличивает счетчик блокировок (lock count) проекции DLL в процессе В на 1

5. Система проверяет, не совпадают ли значения hinstDll этой DLL, относящиеся к процессам А и В Если hinstD!l в обоих процессах одинаковы, то и адрес GetMsgProc в этих процессах тоже одинаков Тогда система может просто вызвать GetMsgProc в адресном пространстве процесса А. Если же hinstDll различны, система определяет адрес функции GetMsgProc в адресном пространстве процесса В по формуле:

GetMsgProc В = histDll В + (GetMsgProc А - hinstDll А)

Вычитая hinstDll из GetMsgProcA, Вы получаете смещение (в байтах) адреса функции GetMsgProc. Добавляя это смещение к hinstDll В, Вы получаете адрес GetMsgProc, соответствующий проекции DLL в адресном пространстве процесса В

6. Счетчик блокировок проекции DLL в процессе В увеличивается на 1.

7. Вызывается GetMsgProc в адресном пространстве процесса В.

8. После возврата из GetMsgProc счетчик блокировок проекции DLL в адресном пространстве процесса В уменьшается на 1.

Кстати, когда система внедряет или проецирует DLL, содержащую функцию фильтра ловушки, проецируется вся DLL, а не только эта функция. А значит, потокам, выполняемым в контексте процесса В, теперь доступны все функции такой DLL.

Итак, чтобы создать подкласс окна, сформированного потоком другого процесса, можно сначала установить ловушку WH_GETMESSAGE для этого потока, а затем — когда будет вызвана функция GetMsgProc - обратиться к SetWtndowLongPtr и создать подкласс Разумеется, процедура подкласса должна быть в той же DLL, что и GetMsgProc.

В отличие от внедрения DLL с помощью реестра этот способ позволяет в любой момент отключить DLL от адресного пространства процесса, для чего достаточно вызвать:

BOOL UnhookWindowsHookEx(HHOOK hHook);

Когда поток обращается к этой функции, система просматривает внутренний список процессов, в которые ей пришлось внедрить данную DLL, и уменьшает счетчик ее блокировок на 1 Как только этот счетчик обнуляется, DLL автоматически выгружается. Вспомните: система увеличивает его непосредственно перед вызовом GetMsgProc (см. выше п. 6). Это позволяет избежать нарушения доступа к памяти Если бы счетчик не увеличивался, то другой поток мог бы вызвать UnhookWindowsHookEx в тот момент, когда поток процесса В пытается выполнить код GetMsgProc,

Все это означает, что нельзя создать подкласс окна и тут же убрать ловушку — она должна действовать в течение всей жизни подкласса.

Утилита для сохранения позиций элементов на рабочем столе

Эта утилита, «22 DIPS.exe» (см. листинг на рис. 22-2), использует ловушки окон для внедрения DLL в адресное пространство Explorer exe. Файлы исходного кода и ресурсов этой программы и DLL находятся в каталогах 22-DIPS и 22-DIPSLib на компактдиске, прилагаемом к книге.

Компьютер я использую в основном для работы, и, на мой взгляд, самое оптимальное в этом случае разрешение экрана - 1152 x 8б4 Иногда я запускаю на своем компьютере кос-какие игры, но большинство из них рассчитано на разрешение 640 x 480 Когда у меня появляется настроение поиграть, приходится открывать апплет Display в Control PaneI и устанавливать разрешение 640 x 480, а закончив игру, вновь возвращаться в DispIay и восстанавливать разрешение 1152 x 864.

Возможность изменять экранное разрешение «на лету» — очень удобная функция Windows. Единственное, что мне не по душе, — при смснс экранного разрешения не сохраняются позиции ярлыков на рабочем столе. У мепя на рабочем столе масса ярлыков для быстрого доступа к часто используемым программам и файлам. Стоит мне

сменить разрешение, размеры рабочего стола изменяются, и ярлыки перестраиваются так, что ужс ничего не найдешь А когда я восстанавливаю прежнее разрешение, ярлыки так и остаются вперемешку. Чтобы навести порядок, приходится вручную перемещать каждый ярлык на свое место — очень интересное занятие!

В общем, мнс это так осточертело, что я придумал утилиту, сохраняющую позиции элементов на экране (Desktop Item Position Saver, DIPS). DIPS состоит из крошечного исполняемого файла и компактной DLL. После запуска исполняемого файла появляется следующее окно

rihter22-3.jpg

В этом окне поясняется, как работать с утилитой. Когда она запускается с ключом 5 в командной строке, в реестре создается подраздел

HKEY_CURRENT_USER\Software\Richter\Desktop Item Position Saver

куда добавляемся по одному параметру на каждый ярлык, расположенный на Вашем рабочем столе. Значение каждого параметра ~ позиция соответствующего ярлыка. Утилиту DIPS следует чапускать перед установкой более низкого экранного разрешения. Всласть наигравшись и восстановив нормальное разрешение, вновь запустите DIPS — на этот раз с ключом R. Тогда DlPS откроет соответствующий подраздел реестра и восстановит для каждого объекта рабочего стола его исходную позицию.

На первый взгляд утилита DIPS тривиальна и очень проста в реализации, Вроде бы только и надо, что получить описатель элемента управления ListView рабочего стола, заставить его (послав соответствующие сообщения) перечислить все ярлыки и определить их координаты, а потом сохранить полученные данные в реестре. Но попробуйте, и Вы убедитесь, что все не так просто. Проблема в том, что большинство оконных сообщений для стандартных элементов управления (например, LVM_GETITEM и LVM_GETITEMPOSITION) не может преодолеть границы процессов. Почему?

Сообщение LVM_GETITEM требует, чтобы Вы передали в параметре lParam адрес структуры LV_ITEM. Поскольку cc адрес имеет смысл лишь в адресном пространстве процесса — отправителя сообщения, процесс-приемник не может безопасно использовать его. Поэтому, чтобы DIPS работала так, как было обещано, в Explorer.exe надо внедрить код, посылающий сообщения LVM_GETITEM и LVM_GETITEMPOSITION элементу управления ListView рабочего стола.

NOTE
В отличие от новых стандартных элементов управления встроенные (кнопки, поля, метки, списки, комбинированные списки и т. д.) позволяют передавать оконные сообщения через границы процессов. Например, окну списка, созданному каким-нибудь потоком другого процесса, можно послать сообщение LB_GETTEXT, чей параметр lParam указывает на строковый буфер в адресном пространстве процесса-отправителя. Это срабатывает, потому что операционная система специально проверяет, не отправлено ли сообщение LB_GETTEXT,

и, если да, самя создает проецируемый в память файл и копирует строковые данные из адресного пространства одного процесса в адресное пространство другого

Почему Microsoft решила по-разному обрабатывать встроенные и новые элементы управления? Дело в том, что в 16-разрядной Windows, в которой все приложения выполняются в едином адресном пространстве, любая программа могла послать сообщение LB_GETTEXT окну, созданному другой программой. Чтобы упростить перенос таких приложений в Win32, Microsoft и пошла на эти ухищрения. А поскольку в 16-разрядной Windows нет новых элементов управления, то проблемы их переноса тоже нет, и Microsoft ничего подобного для них делать не стала.

Сразу после запуска DIPS получает описатель окна элемента управления LisrView рабочего стола:

// окно ListView рабочего стола - "внук" окна ProgMan

hwndLV = GetFirstChild(GetFirstChild(hindWindow(__TEXT("ProgHan"), NULL)));

Этот код сначала ищет окно класса ProgMan. Даже несмотря на то что никакой Program Manager не запускается, новая оболочка по-прежнему создает окно этого класса — для совместимости с приложениями, рассчитанными на старые версии Windows. У окна ProgMan единственное дочернее окно класса SHELLDLL_DefView, у которого тоже одно дочернее окно — класса SysListView32. Оно-то и служит элементом управления ListView рабочего стола. (Кстати, всю эту информацию я выудил благодаря Spy++.)

Получив описатель окна ListView, я определяю идентификатор создавшего его потока, для чего вызываю GetWindowThreadProcessId. Этот идентификатор я передаю функции SetDIPSHook, реализованной в DIPSLib.cpp. Последняя функция устанавливает ловушку WH_GETMESSAGE для данного потока и вызывает:

PostThreadMessage(dwThreadId, WM_NULL, 0, 0);

чтобы разбудить поток Windows Explorer. Поскольку для него установлена ловушка WH_GETMESSAGE, операционная система автоматически внедряет мою DIPSLib.dll в адресное пространство Explorer и вызывает мою функцию GetMsgProc. Та сначала проверяет, впервые ли она вызвана, и, если да, создает скрытое окно с заголовком "Richter DIPS". Возьмите на заметку что это окно создается потоком, принадлежащим Explorer. Пока окно создается, поток DIPS.exc возвращается из функции SetDIPSHook и вызывает:

GetMessage(&msg, NULL, 0, 0);

Этот вызов "усыпляет"- поток до появления в очереди какого-нибудь сообщения Хотя DIPS,exe сам не создает ни одного окна, у него всс же есть очередь сообщений, и они помещаются туда исключительно в результате вызовов PostThreadMessage. Взгляните на код GetMsgProc в DIPSLih.cpp: сразу после обращения к CreateDialog стоит вызов PostThreadMessage, который вновь пробуждает поток DIPS exe. Идентификатор потока сохраняется в разделяемой переменной внутри функции SetDIPSHook.

Очередь сообщений я использую для синхронизации потоков. В этом нет ничего противозаконного, и иногда гораздо проще синхронизировать потоки именно так, не прибегая к объектам ядра — мьютексам, семафорам, событиям и т. д. (В Windows очень богатый API; пользуйтесь этим.)

Когда поток DIPS.exe пробуждается, он узнает, что серверное диалоговос окно уже создано, и обращается к FindWmdow, чтобы получить его описатель. С этого момента для оргапичации взаимодействия между клиентом (утилитой DIPS) и сервером (скрытым диалоговым окном) можно использовать механизм оконных сообщений. Поскольку это диалоговое окно создано потоком, выполняемым в контексте процесса Explorer, нас мало что ограничивает в действиях с Explorer.

Чтобы сообщить своему диалоговому окну сохранить или восстановить позиции ярлыков на экране, достаточно послать сообщение:

// сообщаем окну DIPS, c каким окном ListView работать

// и что делать: сохранять или восстанавливать позиции ярлыков

SendMessage(hwndDIPS, WM_APP, (WPARAM) hwndLV, fSave);

Процедура диалогового окна проверяет сообщение WM_APP. Когда она принимает это сообщение, параметр wParam содержит описатель нужного элемента управления ListView, a lParam — булево значение, определяющее, сохранять текущие позиции ярлыков в реестре или восстанавливать.

Так как здесь используется SendMessage, а не PostMessage, управление не передается до завершения операции. Если хотите, определите дополнительные сообщения для процедуры диалогового окна — это расширит возможности программы в управлении Explorer. Закончив, я завершаю работу сервера, для чего посылаю ему сообщение WM_CLOSE, которое говорит диалоговому окну о необходимости самоуничтожения.

Наконец, перед своим завершением DIPS вновь вызывает SetDlPSHook, но на этот раз в качестве идентификатора потока передается 0 Получив нулевое значение, функция снимает ловушку WH_GETMESSAGE. А когда ловушка удаляется, операционная система автоматически выгружает DIPSLib.dll из адресного пространства процесса Explorer, и это означает, что теперь процедура диалогового окна болыпе не принадлежит данному адресному пространству Поэтому важно уничтожить диалоговое окно заранее — до снятия ловушки. Иначе очередное сообщение, направленное диалоговому окну, вызовет нарушение доступа. И тогда Explorer будет аварийно завершен операционной системой — с внедрением DLL шутки плохи!

Dips

DIPSLib

 

Внедрение DLL с помощью удаленных потоков

Третий способ внедрения DLL — самый гибкий. В нем используются многие особенности Windows: процессы, потоки, синхронизация потоков, управление виртуальной памятью, поддержка DLL и Unicode. (Если Вы плаваете в каких-то из этих тем, прочтите сначала соответствующие главы книги.) Большинство Windows-функций позволяет процессу управлять лишь самим собой, исключая тем самым риск повреждения одного процесса другим. Однако есть и такие функции, которые дают возможность управлять чужим процессом Изначально многие из них были рассчитзны на применение в отладчиках и других инструментальных средствах. Но ничто не мешает использовать их и в обычном приложении.

Внедрение DLL этим способом предполагает вызов функции LoadLibrary потоком целевого процесса для загрузки нужной DLL. Так как управление потоками чужого процесса сильно затруднено, Вы должны создать в нсм свой поток. К счастью, Windows-функция CreateRemoteThread делает эту задачу несложной:

HANDLE CreateRemoteThread( HANDLE hProcess, PSECURITY_ATTRIBUTES psa, DWORD dwStackSize, PTHREAD_START_ROUTTNE pfnStartAddr, PVOTD pvParam, DWOHD fdwCreate, PDWORD pdwThreadId);

Она идентична CreateThread, но имеетдополнительный параметр hProcess, идентифицирующий процесс, которому будет принадлежать новый поток. Параметр pfnStartAddr определяет адрес функции потока. Этот адрес, разумеется, относится к удаленному процессу — функция потока не может находиться в адресном пространстве Вашего процесса.

NOTE
В Windowb 2000 чаще используемая функция CreateThread, между прочим, реализована через вызов CreateRemoteThread

HANDLE CreateThread(PSECURITY_ATNRlBUTES psa, DWORD dwStackSize, PTHREAD_START_ROUriNE pfnStartAddr, PVOID pvParam, DWORD fdwCreate, PDWORD pdwThrcadID)
{

return (CreateRemoteThread(GetCurrentProcess(), psa, dwStackSize, pfnStartAddr, pvParam, fdwCreate, pdwThreadID));

}

WINDOWS 98
B Windows 98 функция CreateRemoteThread определена, но не реализована и
просто возвращает FALSE, последующий вызов GetLastError даеткод ERROR_CALL_NOT_IMPLEMENTED (Но функция CreateThread, которая создает поток в вызывающем процессе, реализована полностью.) Так что описываемый здесь метод внедрения DLL в Windows 98 не работает

О'кэй, теперь Вы знаете, как создать поток в другом процессе. Но как заставить этот поток загрузить пашу DLL? Ответ прост; нужно, чтобы он вызвал функцию LoadLibrary:

HINSTANCE LoadLibrary(PCTSTR pszlibFile);

Заглянув в заголовочный файл WinBase.h, Вы увидите, что для LoadLibrary там есть такие строки:

HINSTANCE WINAPI LoadLibraryA(LPCSTR pszLibFileName);
HINSTANCE WINAPI LuadLibiatyW(LPCWSTR pszLibFileName);

#ifdef UNICODE

#define LoadLibrary LoadLibraryW #else

#define LoadLibrary LoadLibraryA

#endif // !UNICODE

В действительности существует две функции LoadLibrary LoadLibraryA и LoadLibraryW. Они различаются только типом передаваемого параметра. Если имя фаЙла библиотеки хранится как ANSI-строка, вызывайте LoadLibraryA; если же имя файла представлено Unicode-строкой — LoadLibraryW. Самой функции LoadLibrary нет В большинстве программ макрос LoadLibrary раскрывается в loadLibraryA.

К счастью, прототипы LoadLibrary и функции потока идентичны. Вот как выглядит прототип функции потока:

DWORD WINAPI ThreadFunc(PVOID pvParam);

О'кэй, не идентичны, но очень похожи друг на друга Обе функции принимают единственный параметр и возвращают некое значение. Кроме того, обе используют одни и те же правила вызова — WINAPI. Это крайне удачное стечение обстоятельств, потому что нам как раз и нужно создать новый поток, адрес функции которого является адресом LoadLibraryA или LoadLibraryW По сути, требуется выполнить примерно такую строку кода

HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadlibraryA, "C.\\MyLibdll", 0, NULL);

Или, если Вы предпочитаете Unicode

HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryW, L"C \\MyLib.dll" , 0, NULL);

Новый поток в удаленном процессе немедленно вызывает LoadLibraryA (или LoadLibraryW), передавая ей адрес полного имени DLL Все просто. Однако Вас ждут две проблемы.

Первая в том, что нельзя вот так запросто, как я показал выше, передать LoadLibraryA или LoadLibraryW в четвертом параметре функции CreateRemoteThread Причина этого весьма неочевидна. При сборке программы в конечный двоичный файл помещается раздел импорта (описанный в главе 19). Этот раздел состоит из серии шлю-

зов к импортируемым функциям. Так что, когда Ваш код вызывает функцию вроде LoadLibraryA, в разделе импорта модуля генерируется вызов соответствующего шлюза. А уже от шлюза происходит переход к реальной функции.

Следовательно, прямая ссылка на LoadLibraryA в вызове CreateRemoteThread преобразуется в обращение к шлюзу LoadLibraryA в разделе импорта Вашего модуля. Передача адреса шлюза в качестве стартового адреса удаленного потока заставит этот поток выполнить неизвестно что. И скорее всего это окончится нарушением доступа. Чтобы напрямую вызывать LoadLibraryA, минуя шлюз, Вы должны выяснить ее точный адрес в памяти с помощью GetProcAddress.

Вызов CreateRemoteThread предполагает, что Kerne32.dll спроецирована в локальном процессе на ту же область памяти, что и в удаленном. Kernel32.dll используется всеми приложениями, и, как показывает опыт, система проецирует эту DLL в каждом процессе по одному и тому же адресу. Так что CreateRemoteThread надо вызвать так:

// получаем истинный адрес LoadLibraryA в Kernel32 dll PTHREAD_START_ROUTIHE pfnThreadRtn = (PTHREAD_START_ROUTINE)

GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");

HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, "C.\\MyLib.dll", 0, NULL);

Или, если Вы предпочитаете Unicode:

// получаем истинный адрес LoadLibraryA в Kernel32.dll PTHRFAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START ROUTINE)

GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");

HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, L"C:\\HyLib.dll", 0, NULL);

Отлично, одну проблему мы решили. Но я говорил, что их две. Вторая связана со строкой, в которой содержится полное имя файла DLL. Строка «C.\\MyLib.dll» находится в адресном пространстве вызывающего процесса. Ее адрес передается только что созданному потоку, который в свою очередь передает его в LoadLibraryA. Но, когда LoadLibraryA будет проводить разыменование (dereferencing) этого адреса, она не найдет по нему строку с полным именем файла DLL и скорее всего вызовет нарушение доступа в потоке удаленного процесса; пользователь увидит сообщение о необрабатываемом исключении, и удаленный процесс будет закрыт. Все верно: Вы благополучно угробили чужой процесс, сохранив свой в целости и сохранности

Эта проблема решается размещением строки с полным именем файла DLL в адресном пространстве удаленного процесса. Впоследствии, вызывая CreateRemoteThread, мы передадим ее адрес (в удаленном процессе). На этот случай в Windows предусмотрена функция VirtualAllocEx, которая позволяет процессу выделять память в чужом адресном пространстве:

PVOID VirtualAllocEx( HANDLE hProcess, PVOIO pvAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);

А освободить эту память можно с помощью функции VirtualFreeEx.

BOOL VirtualFreeEx( HANDLE hProcess, PVOID pvAddress, SIZE_T dwSize, DWORD dwFreeType);

Обе функции аналогичны своим версиям без суффикса Ex в конце (о них я рассказывал в главе 15). Единственная разница между ними в том, что эти две функции требуют передачи в первом параметре описателя удаленного процесса.

Выделив память, мы должны каким-то образом скопировать строку из локального адресного пространства в удаленное. Для этого в Windows есть две функции

BOOL ReadProcessMemory( HANDLE hProcess, PVOID pvAddressRemote, PVOID pvBufferLocal, DWORD dwSize, PDWORD pdwNumBytesRead);

BOOL WriteProcessMemory( HANDLE hProcess, PVOID pvAddressRemote, PVOTD pvBufferLocal, DWOHD dwSize, PDWORD pdwNumBytesWritten);

Параметр hProcess идентифицирует удаленный процесс, pvAddressRemote и pvBufferLocal определяют адреса в адресных пространствах удаленного и локального процесса, a dwSize — число передаваемых байтов. По адресу, на который указывает параметр pdwNumBytesRead или pdwNumBytesWritten, возвращается число фактически считанных или записанных байтов

Теперь, когда Вы понимаете, что я пьтаюсь сделать, давайте суммируем все сказанное и запишем это в виде последовательности операций, которые Вам надо будет выполнить

  1. Выделите блок памяти в адресном пространстве удаленного процесса через VirtualAllocEx.
  2. Вызвав WriteProcessMemory, скопируйте строку с полным именем файла DLL в блок памяти, выделенный в п 1
  3. Используя GetProcAddress, получите истинный адрес функции LoadLibraryA или LoadLibraryW внутри Kernel32.dll.
  4. Вызвав CreateRemoteThread, создайте поток в удаленном процессе, который вызовет соответствующую функцию LoadLibrary, передав ей адрес блока памяти, выделенного в п. 1.

На этом этапе DLL внедрена в удаленный процесс, а ее функция DllMam получила уведомление DLL_PROCESS_ATTACH и может приступить к выполнению нужного кода Когда DllMain вернет управление, удаленный поток выйдет из LoadLibrary и вернется в функцию BaseThreadStart (см. главу 6), которая в свою очередь вызовет ExitThread и завершит этот поток

Теперь в удаленном процессе имеется блок памяти, выделенный в п. 1, и DLL, все еще «сидящая» в его адресном пространстве. Для очистки после завершения удаленного потока потребуется несколько дополнительных операций.

  1. Вызовом VirtualFreeEx освободите блок памяти, выделенный в п. 1.
  2. С помощью GetProcAddress определите истинный адрес функции FreeLibrary внутри Kernel32.dll.
  3. Используя CreateRemoteThtead, создайте в удаленном процессе поток, который вызовет FreeLibrary с передачей HINSTANCE внедренной DLL.

Вот, собственно, и все. Единственный недостаток этого метода внедрения DLL (самого универсального из уже рассмотренных) — многие нужные функции в Windows 98 не поддерживаются. Так что данный метод применим только в Windows 2000.

Программа-пример lnjLib

Эта программа, «22 InjLib.exe» (см. листинг на рис. 22-3), внедряет DLL с помощью функции CreateRemoteThread Файлы исходного кода и ресурсов этой программы и DLL находятся в каталогах 22-InjLib и 22-ImgWalk на компакт-диске, прилагаемом к книге, После запуска InjLib на экране появляется диалоговое окно для ввода идентификатора выполняемого процесса, в который будет внедрена DLL. ,

rihter22-4.jpg

Вы можете выяснить этот идентификатор через Task Manager Получив его, программа попытается открыть описатель этого процесса, вызвав OpenProcess и запросив соответствующие права доступа.

hProcess = OpenProcess(
PROCESS_CREATE THREAD | // для CreateRemoteThread
PROCESS_VM_OPERATION | // для VirtualAllocEx/VirtualFreeEx
PROCESS_VM_WRITE, // для WriteProcessMemory
FALSE, dwProcessId);

Если OpenProcess вернет NULL, значит, программа выполняется в контексте защиты, в котором открытие описателя этого процесса не разрешено Некоторые процессы вроде WinLogon, SvcHost и Csrss выполняются по локальной системной учетной записи, которую зарегистрированный пользователь не имеет права изменять. Описатель такого процесса можно открыть, только если Вы получили полномочия на отладку этих процессов. Программа ProcessInfo из главы 4 демонстрирует, как это делается.

При успешном выполнении OpenProcess записывает в буфер полное имя внедряемой DLL. Далее программа вызывает lnject!ib и передает сй описатель удаленного процесса. И, наконец, после возврата из InjectLib программа выводит окно, где сообщает, успешно ли внедрена DLL, а потом закрывает описатель процесса.

Наверное, Вы заметили, что я специально проверяю, нс равен ли идентификатор процесса нулю. Если да, то вместо идентификатора удаленного процесса я передаю идентификатор процесса самой InjLib.exe, получаемый вызовом GetCurrenlProtessId. Тогда при вызове Injectlib библиотека внедряется в адресное пространство процесса InjLib. Я сделал это для упрощения отладки. Сами понимаете, при возникновении ошибки иногда трудно определить, в каком процессе она находится: локальном или удаленном. Поначалу я отлаживал код с помощью двух отладчиков: один наблюдал за InjLib, другой — за удаленным процессом Это оказалось стратттно неудобно Потом меня осенило, что InjLib способна внедрить DLL и в себя, т. e. в адресное пространство вызывающего процесса. И это сразу упростило отладку.

Просмотрев начало исходного кода модуля, Вы увидите, что InjectLib — на самом деле макрос, заменяемый на InjectLibA или InjectLibW в зависимости от того, как компилируется исходный код. В исходном коде достаточно комментариев, и я добавлю лишь одно. Функция lnjectLibA весьма компактна. Она просто преобразует полное имя DLL из ANSI в Unicode и вызывает lnjetlLibW, которая и делает всю работу. Тут я придерживаюсь того подхода, который я рекомендовал в главе 2.

lnjLib

 

Библиотека lmgWalk.dll

ImgWalk.dll (см. листинг на рис. 22-4) — это DI,I., которая, будучи внедрена в адресное пространство процесса, выдает список всех DLL, используемых этим процессом. Файлы исходного кода и ресурсов этой DLL находятся в каталоге 22-ImgWalk на компакт-диске, прилагаемом к книге. Если, например, сначала запустить Notepad, a потом InjLib, передав ей идентификатор процесса Notepad, то InjLib внедрит ImgWalk.dll в адресное пространство Notepad. Попав туда, ImgWalk определит, образы каких файлов (EXE и DLL) используются процессом Notepad, и покажет результаты в следующем окне.

rihter22-5.jpg

Модуль ImgWalk сканирует адресное пространство процесса и ищет спроецированные файлы, вызывая в цикле функцию VirtualQuery, которая заполняет структуру MEMORY_BASIC_INFORMATION На каждой итерации цикла ImgWalk проверяет, нет ли строки с полным именем файла, которую можно было бы добавить в список, выводимый на экран

Char szBuf[MAX_PAIH * 100] = { 0 };

PBYTE pb = NULL;

MEMORY_BASIC_INFORMATION mbi;

while (VirtualQuery(pb, &mbi, sizeof(mbi}) == sizeof(mbi))
{

int nLen;

char szModName[MAX_PATH];

lf (mbl StatP == MEM_FRFF)

mbi.AllocationBase = mbi.BaseAddress;

if ((mbi.AllocationBase == hinstDll) ||
(mbi.Allocation8ase != mbi.BaseAddress} ||
(mbi.AllocationBase == NULL))
{

// Имя модуля не включается в список, если
// истинно хотя бы одно из следующих условий
// 1 Данный регион содержит нашу DLL
// 2 Данный блок НЕ является началом региона
// 3 Адрес равен NULL

nLen = 0;

}
else
{

nLen = GetModuleFileNameA((HINSTANCE) mbi.AllocationBase;

szModName, chDIMOF(szModName));

}

if (nLen > 0)
{

wspnntfA(strchr(szBuf, 0), "\n%08X-%s", mbi.AllocationBase, szModName);

}

pb += mbi.RegionSize;

}

chMB(&szBuf[1]);

Сначала я проверяю, не совпадает ли базовый адрес региона с базовым адресом внедренной DLL Если да, я обнуляю nLen, чтобы не показывать в окне имя внедренной DLL. Нет — пьтаюсь получить имя модуля, загруженного по базовому адресу данного региона, Если значение nLen больше 0, система распознает, что указанный адрес идентифицирует загруженный модуль, и помещает в буфер szModName полное имя (вместе с путем) этого модуля. Затем я присоединяю HINSTANCE данного модуля (базовый адрес) и его полное имя к строке szBuf, которая в конечном счете и появится в окне Когда цикл заканчивается, DLL открывает на экране окно со списком.

lmgWalk

Внедрение троянской DLL

Другой способ внедрения состоит в замене DLL, загружаемой процессом, на другую DLL. Например, зная, что процессу нужна Xyz.dll, Вы можете создать свою DLL и присвоить ей то же имя. Конечно, перед этим Вы должны переименовать исходную Xyz.dll.

В своей Xyz dll Вам придется экспортировать те же идентификаторы, что и в исходной Xyz.dll. Это несложно, если задействовать механизм персадресации функций (см. главу 20); однако его лучше не применять, иначе Вы окажетесь в зависимости от конкретной версии DLL Если Вы замените, скажем, системную DLL, a Microsoft потом добавит в нее новые функции, в Вашей версии той же DLL их не будет. А значит, не удастся загрузить приложения, использующие эти новые функции.

Если Вы хотите применить этот метод только для одного приложения, то можете присвоить своей DLL уникальное имя и записать его в раздел импорта исполняемого модуля приложения. Дело в том, что раздел импорта содержит имена всех DLL, нужных ЕХЕ-модулю. Вы можете "покопаться" в этом разделе и изменить его так, чтобы загрузчик операционной системы загружал Вашу DLL. Этот прием совсем неплох, но требует глубоких знаний о формате EXE- и DLL-файлов.

Внедрение DLL как отладчика

Отладчик может выполнять особые операции над отлаживаемым процессом. Когда отлаживаемый процесс загружен и его адресное пространство создано, но первичный поток сщс нс выполняется, система автоматически уведомляет o6 этом отладчик. В этот момент отладчик может внедрить в него нужный код (используя, например, WriteProcessMemory), а затем заставить его первичный поток выполнить внедренный код.

Этот метод требует манипуляций со структурой CONTEXT потока отлаживаемого процесса, а значит, Ваш код будет зависим оттипа процессора, и его придется модифицировать при переносе на другую процессорную платформу Кроме того, Вам почти наверняка придется вручную корректировать машинный код, который должен быть выполнен отлаживаемым процессом. Не забудьте и о жесткой связи между отладчиком и отлаживаемой программой как только отладчик закрывается, Windows немедленно закрывает и отлаживаемую программу. Избежать этого нельзя.

Внедрение кода в среде Windows 98 через проецируемый в память файл

Эта задача в Windows 98, по сути, тривиальна. В ней все 32-разрядные приложения делят верхние два гигабайта своих адресных пространств. Выделенный там блок памяти доступен любому приложению. С этой целью Вы должны использовать проецируемые в память файлы (см. главу 17). Сначала Вы создаете проекцию файла, а потом вызываете MapViewOfFile и делаете ее видимой. Далсс Вы записываете нужную информацию в эту область своего адресного пространства (она одинакова во всех адресных пространствах). Чтобы все это работало, Вам, вероятно, придется вручную писать машинные коды, а это затруднит перенос программы на другую процессорную платформу. Но вряд ли это должно Вас волновать — все равно Windows 98 работает только на процессорах типа x86.

Данный метод тоже довольно труден, потому что Вам нужно будет заставить потокдругого процесса выполнять код в проекции файла. Для этого понадобятся какието средства управления удаленным потоком. Здесь пригодилась бы функция CreateRemoteThread, но Windows 98 ее не поддерживает. Увы, готового решения этой проблемы у мсня нет.

Внедрение кода через функцию CreateProcess

Если Ваш процесс порождает дочерний, в который надо внедрить какой-то код, то задача значительно упрощается. Родительский процесс может создать новый процесс и сразу же приостановить его. Это позволит изменить состояние дочернего процесса до начала его выполнения. В то же время родительский процесс получает описатель первичного потока дочернего процесса. Зная его, Вы можете модифицировать код, который будет выполняться этим потоком. Тем самым Вы решите проблему, упомянутую в предыдущем разделе: в данном случае нетрудно установить регистр указателя команд, принадлежащий потоку, на код в проекции файла.

Вот один из способов контроля за тем, какой код выполняется первичным потоком дочернего процесса:

  1. Создайте дочерний процесс в приостановленном состоянии.
  2. Получите стартовый адрес его первичного потока, считав его из заголовка исполняемого модуля.
  3. Сохраните где-нибудь машинные команды, находящиеся по этому адресу памяти.
  4. Введите на их место свои команды. Этот код должен вызывать LoadLibrary для загрузки DLL.
  5. Разрешите выполнение первичного потока дочернего процесса.
  6. Восстановите ранее сохраненные команды по стартовому адресу первичного потока,
  7. Пусть процесс продолжает выполнение со стартового адреса так, будто ничего и не было.

Этапы 6 и 7 довольно трудны, но реализовать их можно — такое уже делалось Уэтого метода масса преимуществ. Во-первых, мы получаем адресное пространство до выполнения приложения. Во-вторых, данный метод применим как в Windows 98, так и в Windows 2000. В третьих, мы можем без проблем отлаживать приложение с внед-

ренной DLL, не пользуясь отладчиком. Наконец, он работает как в консольных, так и в GUI-приложениях.

Однако у него есть и недостатки. Внедрение DLL возможно, только если это делается из родительского процесса. И, конечно, этот метод создает зависимость программы от конкретного процессора; при eе переносе на другую процессорную платформу потребуются определенные изменения в коде.

Перехват API-вызовов: пример

Внедрение DLL в адресное пространство процесса — замечательный способ узнать, что происходит в этом процессе. Однако простое внедрение DLL не дает достаточной информации, Зачастую надо точно знать, как потоки определенного процесса вызывают различные функции, а иногда и изменять поведение той или иной Windowsфункции.

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

Для решения этой проблемы компания наняла меня, и я предложил поставить ловушку на функцию ExitProcess. Как Вам известно, вызов ExitProcess заставляет систему посылать библиотекам уведомление DLL_PROCESS_DETACH, Перехватывая вызов ExitProcess, мы гарантируем своевременное уведомление внедренной DLL о вызове этой функции. Причем уведомление приходит до того, как аналогичные уведомления посылаются другим DLL. В этот момент внедренная DLL узнает о завершении процесса и успевает провести корректную очистку. Далее вызывается функция ExitProcess, что приводит к рассылке уведомлений DLL_PROCESS_DETACH остальным DLL, и они корректно завершаются. Это же уведомление получает и внедренная DLL, но ничего особенного она не делает, так как уже выполнила свою задачу.

В этом примере внедрение DLL происходило как бы само по себе, приложение было рассчитано на загрузку именно этой DLL, Оказываясь в адресном пространстве процесса, DLL должна была просканировать ЕХЕ-модуль и все загружаемые DLL-модули, найти все обращения ExitProcess и заменить их вызовами функции, находящейся во внедренной DLL. (Эта задача не так сложна, как кажется.) Подставная функция (функция ловушки), закончив свою работу, вызывала настоящую функцию ExitProcess из Kernel32.dll.

Данный пример иллюстрирует типичное применение перехвата API-вызовов, который позволил решить насущную проблему при минимуме дополнительного кода.

Перехват API-вызовов подменой кода

Перехват API-вызовов далеко не новый метод — разработчики пользуются им уже многие годы. Когда сталкиваешься с проблемой, аналогичной той, о которой я только что рассказал, то первое, что приходит в голову, установить ловушку, подменив часть исходного кода. Вот как это делается.

  1. Найдите адрес функции, вызов которой Вы хотите перехватывать (например, ExitProcess в Kernel32.dll).
  2. Сохраните несколько первых байтов этой функции в другом участке памяти.
  3. На их место вставьте машинную команду JUMP для перехода по адресу подставной функции Естественно, сигнатура Вашей функции должна быть такой жс, как и исходной, т. e все параметры, возвращаемое значение и правила вызова должны совпадать.
  4. Теперь, когда поток вызовет перехватываемую функцию, команда JUMP перенаправит его к Вашей функции На этом зтапе Вы можете выполнить любой нужный код.
  5. Снимите ловушку, восстановив ранее сохраненные (в п. 2) байты.
  6. Если теперь вызвать перехватываемую функцию (таковой больше не являющуюся), она будет работать так, как работала до установки ловушки.
  7. После того как она вернет управление, Вы можете выполнить операции 2 и 3 и тем самым вновь поставить ловушку на эту функцию.

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

WINSOWS 98
В Windows 98 основные системные DLL (Kernel32, AdvAPI32, User32 и GDI32) защищены так, что приложение не может что-либо изменить на их страницах кода. Это ограничение можно обойти, только написав специальный драйвер виртуального устройства (VxD).

Перехват API-вызовов с использованием раздела импорта

Данный способ API-пeрeхвата рeшает обе упомянутые мной проблемы, Он прост и довольно надежен Но для его понимания нужно иметь представление о том, как осуществляется динамическое связывание. В частности, Вы должны разбираться в структуре раздела импорта модуля. В главе 19 я достаточно подробно объяснил, как создается этот раздел и что в нем находится. Читая последующий материал, Вы всегда можете вернуться к этой главе.

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

Поэтому, чтобы перехватить определенную функцию, надо лишь изменить ее адрес в разделе импорта. Все! И никакой зависимости от процессорной платформы. А поскольку Вы ничего не меняете в коде функции, то и о синхронизации потоков можно не беспокоиться.

Вот функция, которая делает эту сказку былью. Она ищет в разделе импорта модуля ссылку на идентификатор по определенному адресу и, найдя ее, подменяет адрес соответствующего идентификатора.

void ReplaceIATEntryInOneMod(PCSTR pszCalleeModName, PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller)
{

ULONG ulSize;

PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ImageDirectoryEntryToData(hmodCallor, TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);

if (pImportDesc == NULL)

return,; // в этом модуле нет раздела импорта

// находим дескриптор раздела импорм со ссылками
// на функции DLL (вызываемого модуля)
for (; pImportDesc->Name; pImportDesc++)
{

PSTR pszModName = (PSiR) ((PBYFE) hmodCaller + pImportDcsc->Name);

if (lstrcmpiA(pszModName, pszCalleeModName) == 0)

break;

}

if (pImportDesc->Name == 0)

// этот модуль не импортирует никаких функций из данной DLL return;

// получаем таблицу адресов импорта (IAT) для функций DLL PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA) ((PBYTE} hirodCaller + pImportDesc->FirstThunk);

// заменяем адреса исходных функций адресами своих функций
for (; pThunk->u1.Function; pThunk++)
{

// получаем адрес адреса функции
PROC* ppfn = (PROC*) &pThunk->u1.Function;

// та ли это функция, которая нас итересует?
BOOL fFound = (*ppfn == pfnCurrent);

// см. текст программы-примера, в котором
// содержится трюковый код для Windows 98

if (fFound)
{

// адреса сходятся, изменяем адрес в разделе импорта
WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew,
sizeof(pfnNew), NULL );

return; // получилось, выходим

}

}

// если мы попали сюда, значит, в разделе импорта
// нет ссылки на нужную функцию

}

Чтобы понять, как вызывать эту функцию, представьте, что у нас есть модуль с именем DataBase.exe. Он вызывает ExitProcess из Kernel32.dll, но мы хотим, чтобы он обращался к MyExitProcess в нашем модуле DBExtend.dll, Для этого надо вызвать ReplaceMTEntryInOneMod следующим образом.

PROC pfnOrig = GctProcAddress(GetModuleHandle("Kernel32"), "ExitProcess");
HMODULE hmodCaller = GetModuleHandle("DataBase.exe");

void RoplaceIATEntryInOrioMod(
"Kernel32.dll", // модуль, содержащий ANSI-функцию

pfnOrig, // адрес исходной функции в вызываемой DLL

MyExitProcess, // адрес заменяющей функции

hmodCaller); // описатель модули, из которого надо вызывать новую функцию

Первое, что делает ReplacelATEntryInOneMod, - находит в модуле hmodCalIer раздел импорта. Для этого она вызывает ImageDirectoryEntryToData и передает ей IMAGE_ DlRECTORY_ENTRY_IMPORT. Если последняя функция возвращает NULL, значит, в модуле DataBase.exe такого раздела нет, и на этом все заканчивается

Если же в DataBase.exe раздел импорта присутствует, то ImageDirectoryEntryToData возвращает его адрес как укачатель типа PIMAGE_IMPORT_DESCRIPTOR. Тогда мы должны искать в разделе импорта DLL, содержащую требуемую импортируемую функцию В данном примере мы ищем идентификаторы, импортируемые из Kernel32.dll (имя которой указывается в первом параметре ReplacelATEntryInOneMod), В цикле for сканируются имена DLL. Заметьте, что в разделах импорта все строки имеют формат ANSI (Unicode не применяется). Вот почему я вызываю функцию lstrcmpiA, а не макрос lstrcmpi.

Если программа не найдет никаких ссылок на идентификаторы в Kernel32 dll, то и в этом случае функция просто вернет управление и ничего делать не станет. А если такие ссылки есть, мы получим адрес массива структур IMAGE_THUNK_DATA, в котором содержится информация об импортируемых идентификаторах. Далее в списке из KerneI32.dll ведется поиск идентификатора с адресом, совпадающим с искомым. В данном случае мы ищем адрес, соответствующий адресу функции ExitProcess.

Если такого адреса нет, значит, данный модуль не импортирует нужный идентификатор, и ReplaceLWEntryInOneMod просто возвращает управление. Но если адрес обнаруживается, мы вызываем WriteProcessMemory, чтобы заменить его на адрес подставной функции Я применяю WriteProcessMemory, а не InterlockedExchangePomter, потому что она изменяет байты, не обращая внимания на тип защиты страницы памяти, в которой эти байты находятся Так, если страница имеет атрибут защиты PAGE_READONLY, вызов InterlockedExchangePointer приведет к нарушению доступа, а WriteProcessMemory сама модифицирует атрибуты защиты и без проблем выполнит свою задачу.

С этого момента любой поток, выполняющий код в модуле DataBase.exe, при обращении к ExitProcess будет вызывать нашу функцию. А из нее мы сможем легко получить адрес исходной функции ExitProcess в Kernel32.dll и при необходимости вызвать ее,

Обратите внимание, что ReplaceIATEntryInOneMod подменяет вызовы функций только в одном модуле. Если в его адресном пространстве присутствует другая DLL, использующая ExitProcess, она будет вызывать именно ExitProcess из Kernel32.dll.

Если Вы хотите перехватывать обращения к ExitProcess из всех модулей, Вам придстся вызывать ReplacelATEntryInOneMod для каждого модуля в адресном пространстве процесса. Я, кстати, написал еще одну функцию, ReplacelATEntryInAllMods. С помощыо Toolhelp-функций она перечисляет все модули, загруженные в адресное пространство процесса, и для каждого из них вызывает ReplatelATEritryInOneMod, передавая в качестве последнего параметра описатель соответствующего модуля.

Но и в этом случае могут быть проблемы Например, что получится, если после вызова ReplacelATEntrylnAlMods какой-нибудь поток вызовет LoadLibrary для загрузки

новой DLL? Если в только что загруженной DLL имеются вызовы ExitProcess, она будет обращаться не к Вашей функции, а к исходной. Для решения этой проблемы Вы должны перехватывать функции LoadLtbraryA, LoadLibraiyW, LoаdLibraryExA и LoadLibraryExW и вызывать Rер1асеlAТЕпtrу1пОпеMod для каждого загружаемого модуля.

И, наконец, есть еще одна проблема, связанная с GetProcAddress. Допустим, поток выполняет такой код

typedef int (WINAPI *PFNEXITPROCESS)(UINT uExitCode);
PFNEXITPROCESS pfnExitProcess = (PFNEXITPROCESS) GetProcAddress(
GetModuleHandle("Kernel32"), "ExitProcess");
pfnExitProcess(0);

Этот код сообщает системе, что надо получить истинный адрес ExitProcess в Kernel32.dll, а затем сделать вызов по этому адресу. Данный код будет выполнен в обход Вашей подставной функции. Проблема решается перехватом обращений к GetProcAddress При cc вызове Вы должны возвращать адрес своей функции

В следующем разделе я покажу, как на практике реализовать перехват API-вызовов и решить все проблемы, связанные с использованием LoadLibrary и GetProcAddress.

Программа-пример LastMsgBoxlnfo

Эта программа, «22 LabtMsgBoxInfo.exe» (см листинг на рис. 22-5), демонстрирует перехват API-вызовов. Она перехватывает все обращения к функции MessageBox из User32.dll. Для этого программа внедряет DLL с использованием ловушек. Файлы исходного кода и ресурсов этой программы и DLL находятся в каталогах 22-LastMsgBoxInfo и 22-LastMsgBoxIntoLib на компакт-диске, прилагаемом к книге*

После запуска LastMsgBoxInfo открывает диалоговое окно, показанное ниже.

rihter22-6.jpg

В этот момент программа находится в состоянии ожидания. Запустите какое-нибудь приложение и заставьте его открыть окно с тем или иным сообщением Тестируя свою программу, я запускал Notepad, набирал произвольный текст, а затем пытался закрыть его окно, не сохранив набранный текст Это заставляло Notepad выводить вот такое окно с предложением сохранить документ.

rihter22-7.jpg

DWORD dwOummy;

VirtualProtect(ppfn, Sizeof(ppfn), PAGE_EXECUTE_READWRITE, &dwDummy);

После отказа от сохранения документа диалоговое окно LastMsgBoxInfo приобретает следующий вид

rihter22-8.jpg

Как видите, LastMsgBoxlnfo позволяет наблюдать за вызовами функции MessageBox из других процессов

Код, отвечающий за вывод диалогового окна LastMsgBoxInfo и управление им весьма прост. Трудности начинаются при настройке перехвата API-вызовов. Чтобы упростить эту задачу, я создал С++-класс CAPIHook, определенный в заголовочном файле APTHook.h и реализованный в файле APIHook.cpp. Пользоваться им очень легко, так как в нем лишь несколько открытых функций-членов: конструктор, деструктор и метод, возвращающий адрес исходной функции, на которую Вы ставите ловушку

Для перехвата вызова какой-либо функции Вы просто создаете экземпляр этого класса:

CAPIHook g_MessageBoxA( "User32 dll", "MessageBoxA", (PROC) Hook_MessageBoxA, TRUE);

CAPIHook g_MessageBoxW("User32 dll", "MessageBoxW", (PROC) Hook_MessageBoxW, TRUE);

Мне приходится ставить ловушки на две функции; MessageBoxA и MessageBoxW. Обе эти функции находятся в User32.dll. Я хочу, чтобы при обращении к MessageBoxA вызывалась Hook_MessageBoxA, а при вызове MessageBoxW— Hook_MessageBoxW.

Конструктор класса CAPIHook просто запоминает, какую API-функцию нужно перехватывать, и вызывает ReplaceMTEntryInAllMods, которая, собственно, и выполняет эту задачу.

Следующая открытая функция-член — деструктор. Когда объект CAPlHook выходит за пределы области видимисти, деструктор вызывает ReplacelATEntryInAllMods для восстановления исходного адреса идентификатора во всех модулях, т. e. для снятия ловушки.

Третий открытый член класса возвращает адрес исходной функции Эта функция обычно вызывается из подставной функции для обращения к перехватываемой функции. Вот как выглядит код функции Hook_MessageBoxA:

int WTNAPI Hook_MessageBoxA(HWND hWnd, PCSTR pszText, PCSIR pszCaption, UINT uType)
{

int nResult = ((PFNMESSAGEBOXA)(PROC) q_MessageBoxA) (hWnd, pszText, pszCaption, uType);

SendLastMsgBoxInfo(FALSE, (PVOTD) pszCaption, (PVOID) pszText, nResult);

return(nResult);

}

Этот код ссылается на глобальный объекту MessageBoxA класса CAPIHook. Приведение его к типу PKOC заставляет функцию-член вернуть адрес исходной функции MessageBoxA в User32.dll.

Всю работу по установке и снятию ловушек этот С++-класс берет на себя. До конца просмотрев файл CAPIIIook.cpp, Вы заметите, что мой С++-класс автоматически

создает экземпляры объектов CAPIHook для перехвата вызовов LoadLibraryA, LoadLibraryW, LoadLibraryExA, LoadLibraryExW и GetProcAddress. Так что он сам справляется с проблемами, о которых я расскаяывал в предыдущем разделе.

LastMsgBoxlnfo

LastMsgBoxlnfoLib