    ПРОРЫВ ЗА ПРЕДЕЛЫ ПРОЦЕССА 

    глава 18 

    В среде Win32 каждый процесс получает свое 4-гигабайтное адресное 
пространство в диапазоне 32-битных адресов от 0х00000000 до OxFFFFFFFF. 
Указатели, используемые Вами для ссылки на определенный участок памяти, — это 
адреса в адресном пространстве Вашего процесса, и в нем нельзя создать 
указатель, ссылающийся на память, принадлежащую другому процессу. Так, если в 
Вашей программе есть «жучок», из-за которого происходит запись по случайному 
адресу, он не разрушит содержимое памяти, отведенной другим процессам. 
    Одна из самых крупных проблем 16-разрядной Windows в том, что все процессы 
выполняются в общем адресном пространстве. Если какой-то из них записывает 
что-то в память, нельзя исключить, что эта память уже принадлежит другому 
процессу или (еще хуже!) операционной системе. Но в Win32, где применяются 
раздельные адресные пространства, процессу крайне сложно повлиять на 
выполнение другого. 

    \wiNDows7 
    В Windows 95 процессы фактически совместно используют 2 Гб адресного 
пространства (от 0х80000000 до OxFFFFFFFF). На эту область отображаются только 
системные компоненты и файлы, проецируемые в память (подробнее см. главы 5 и 8). 

    Раздельные адресные пространства в Win32 очень выгодны и разработчикам, и 
конечным пользователям. Первым важно, что среда Win32 перехватит обращения к 
памяти по случайным адресам, вторым — что операционная система более устойчива 
и одно приложение не приведет к краху другого или самой системы. Однако многие 
16-разрядные программы для Windows построены на том, что все процессы делят 
одно адресное пространство. Перенос таких программ в Win32 — задача не из простых. 
    Но иногда и в Win32 возникают ситуации, когда нужен доступ к адресному 
пространству другого процесса. Вот несколько примеров: 
    • порождение подкласса окна, созданного другим процессом; 
    • отладка (скажем, определение того, какие DLL используются другим процессом); 
    • установка ловушек (hooking) в других процессах. 
    В этой главе я расскажу о трех механизмах, с помощью которых Win32-npouecc 
может «прорваться» за свои границы. Все три механизма основаны на концепции 
внедрения (injecting) какой-либо DLL в адресное пространство другого процесса. 

    600 ______________________________Windows для профессионалов 


    Зачем нужен прорыв за границы процессов 

    Допустим, Вы хотите создать подкласс от экземпляра окна, порожденного 
другим процессом. Это, как Вы помните, позволит изменять поведение окна. В 
16-разрядной Windows Вы для этого просто обращались к функции SetWindowLong и 
в блоке памяти, принадлежащем окну, заменяли адрес его оконной процедуры новым 
— указывающим на Вашу WndProc. В документации на Win32 (там, где описывается 
функция SetWindowLong) утверждается, что приложение не может породить подкласс 
окна, созданного другим процессом. Это не совсем верно. Проблема создания 
подкласса окна из другого процесса на самом деле сводится к преодолению границ 
адресного пространства. Как в 16-разрядной Windows, так и в Win32 вызов: 

    SetWindowLong(hwnd. GWL_WNDPROC, MySubclassProc); 

    говорит системе, что все сообщения окну, на которое указывает hwnd, 
следует направлять не обычной оконной процедуре, а функции MySubclassProc. 
Иными словами, когда системе надо передать сообщение указанной процедуре окна 
WndProc, она отыскивает ее адрес и напрямую вызывает WndProc. В данном примере 
система обнаружит, что с окном сопоставлен адрес функции MySubclassProc, и 
поэтому вызовет именно ее, а не исходную оконную процедуру. 
    В Win32 проблема с порождением подклассов окон, принадлежащих другим 
процессам, состоит в том, что процедура подкласса находится в ином адресном 
пространстве. Упрощенная схема приема сообщений оконной процедурой 
представлена на рис. 18-1. Предполагается, что процесс А создал окно. Файл 
USER32.DLL спроецирован на адресное пространство процесса А. Эта проекция 
USER32.DLL отвечает за прием и диспетчеризацию сообщений (синхронных и 
асинхронных), направляемых любому из окон, созданных потоками процесса А. 
Обнаружив какое-то сообщение, она определяет адрес процедуры WndProc окна и 
вызывает ее, передавая описатель окна, сообщение и параметры wParam и IParam. 
Когда WndProc обработает сообщение, USER32 вернется в начало цикла и будет 
ждать следующего оконного сообщения. 
    Теперь предположим, что процесс В хочет породить подкласс окна, созданного 
одним из потоков процесса А. Сначала код процесса В должен определить 
описатель этого окна, что можно сделать самыми разными способами. В примере на 
рис. 18-1 поток процесса В просто вызывает FindWindow, затем — SetWindowLong, 
пытаясь изменить адрес оконной процедуры WndProc. Обратите внимание: пытаясь. 
В Win32 этот вызов ничего не даст, кроме NULL. Код SetWindowLong просто 
проверяет, не пытается ли процесс изменить адрес WndProc окна, созданного 
другим процессом, и, если да, игнорирует вызов. 
    А если бы Win32-peaлизaция функции SetWindowLong могла модифицировать 
оконную процедуру WndProc7 Система тогда связала бы адрес процедуры 
MySubclassProc с указанным окном. Затем при посылке сообщения этому окну код 
USER32 в процессе А извлек бы данное сообщение, получил адрес MySubclassProc и 
попытался бы вызвать процедуру по этому адресу. Но это привело бы к крупным 
неприятностям, так как MySubclassProc находится в адресном пространстве 
процесса В, а активен — процесс А. Очевидно, если USER32 обратится по данному 
адресу, то на самом деле он обратится к какому-то участку памяти в адресном 
пространстве процесса А, что, естественно, закончится нарушением доступа к памяти. 
    Чтобы избежать этого, было бы неплохо сообщить системе, что MySubclassProc 
находится в адресном пространстве процесса В, и тогда она переключила бы 
контекст перед вызовом процедуры подкласса. Увы, по ряду причин такая 
функциональность в системе не реализована: 
    • Подклассы окон, созданных потоками других процессов, порождаются весьма 
редко. Большинство приложений делает это лишь применительно к собственным 
окнам, и архитектура памяти в Win32 этому не препятствует. 
    • Переключение активных процессов отнимает слишком много процессорного времени. 

    _____________________Глава 18. Прорыв за пределы процесса 601 


    Процесс А                           Процесс В 
    _________________________Д         _______________________| 
    ЕХЕ-файл                             ЕХЕ-файл                         : 
    LRESULT WndProc (HWND hwnd, UNIT uMsg. .,.) {             void SomeFunc 
(void) {                              .; 
    ;                                                          HWNO hwnd ^ 
FindWindow("Class-A". NULL):           :, 
    SetWindowLong(hwnd, GWL_WNDPROC,                   .; 
    MySubClassProc); 
    } 
    LHESULT MySubClassProc (HWND hwnd, UNIT uMsg, ...) { 
    )__________________________________ 
    ___________________________________I     ___________________________________I 
    Файл USER32.DLL                      Файл USER32.DLL 
    LONG DIspatchMessage (CONST MSG •msg) !                   : 
    LONG IResult: 
    WNDPROC IpfriWndProc = (WNDPROC) 
    GetWindowLongfmsg, hwnd, GWL..WNDPROC); 
    IResult = lpfnWndProc(nisg, hwnd, msg. 
    message, wParam, IParam); 
    return(lResult); 
    )_______________________________                          ___   ___        
   __________ 
    —————————————————————————————————I    —————————————————————————————————I 

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

    • Код MySubclassProc должен был бы выполняться потоком процесса В, но 
каким именно — новым или одним из существующих? 
    • Как USER32 узнает, с каким процессом связан адрес оконной процедуры? 
    Поскольку удачных решений этих проблем нет, Microsoft предпочла запретить 
функции SetWindowLong изменять процедуру окна, созданного другим процессом. 
    Тем не менее порождение подкласса окна, созданного чужим процессом, 
возможно: нужно просто пойти другим путем. Ведь на самом деле проблема не 
столько в создании подкласса, сколько в закрытости адресного пространства 
процесса. Если бы Вы могли както поместить код своей оконной процедуры в 
адресное пространство процесса А, это позволило бы вызвать SetWindowLong и 
передать ей адрес MySubclassProc в процессе А. Я называю такой прием 
внедрением DLL в адресное пространство процесса. Мне известно три способа 
подобного внедрения. Рассмотрим их по порядку, начиная с простейшего. 

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

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

    HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs 

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

    602 ______________________________Windows для профессионалов 

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

    Registry Edit View Help 
    :                  _J MCIE:<ipnsions3 NamB__^_^„__._J Date____ 
    '                  _1 MC.132                     .^(Default)      '    
""(value not set) 
    _J Midimap                   '«yApplnitJXLs        "C:\MYLIB.DLL" 
    „I ModuleCompatibility         .^DeviceNotSelected... "15" 
    ± •_J Network                   «^Spooler            "yes" 
    ; .       EH_] NetrorkCards              "ilswapdisk 
    !        '. C3 OpenGLDrivers             .a3TransmissionRetry... "90" 
    « _J Perilib 
    •_J Ports 
    •          :        a—I ProfileList 
    •!_] related.desc 
    +   I Time Zones 
    + _} Type 1 Installer 
    ;                    l'"i Userinstallable.drivers 
    ^^EuSftBBftI 
    " j Winlogon 
    <:; .^J WOW 
    Ll Nikita 
    I        № CJ ODBC 
    LJ Program Groups 
    :   CJ Secure 
    •+' _1 Windows 31 Migration Status      —I 
    + _J SYSTEM                         _»j 
    ll_____________ ...L'll.-l .._._,„ .... ............I          „.„.„ .2J 
    MyC:omputeAHKEY_LOC^L_MACHlNE\SOF^ARE\Microsof^windowsNT\CurrentVersion\^^^ 

    При перезагрузке компьютера \Уш32-подсистема Windows NT сохраняет значение 
этого параметра. Далее, когда модуль USER32.DLL спроецирован на адресное 
пространство процесса, USER32 получает сохраненное значение параметра от 
Win32-noncHCTeMbi и вызывает LoadLibrary для всех DLL, указанных в параметре 
AppInit_DLLs. В момент загрузки DLL-библиотеки инициализируются вызовами их 
функций DllMain с параметром fwdReason, равным DLL_PROCESS_ATTACH. При этом 
USER32 не проверяет, успешно ли загружены и инициализированы эти DLL. 
    Это простейший способ внедрения DLL. Все, что от Вас требуется, — добавить 
значение в уже существующий параметр реестра. Однако здесь есть ряд недостатков. 
    Во-первых, так как Win32-пoдcиcтeмa считывает значение параметра при 
загрузке, после его изменения придется перезагружать компьютер. Выход и 
повторный вход в систему не сработает. 
    Во-вторых, Ваша DLL будет спроецирована на адресные пространства лишь тех 
процессов, на которые отображен и USER32. А последнее делается только в 
GUI-приложениях, т. е. данный способ не подходит для программ консольного 
типа, — например, компиляторов или компоновщиков. 
    В-третьих, Ваша DLL будет спроецирована на все приложения с графическим 
интерфейсом. Но Вам-то почти наверняка надо внедрить DLL только в один или 
несколько определенных процессов. Чем больше процессов попадет «под тень» 
такой DLL, тем выше вероятность аварии. Ведь теперь Ваш код выполняется 
потоками этих процессов, и если он зациклится или некорректно обратится к 
памяти, Вы повлияете на поведение и устойчивость соответствующих процессов. 
Поэтому лучше внедрять свою DLL в как можно меньшее число процессов. 

    _____________________Глава 18. Прорыв за пределы процесса 603 

    И, наконец, Ваша DLL проецируется на GUI-приложение в течение всей его 
жизни. Тут есть некоторое сходство с предыдущей проблемой. Желательно не 
только внедрять DLL в минимальное число процессов, но и проецировать ее на эти 
процессы как можно меньше времени. Допустим, Вы хотите создать подкласс 
главного окна WordPad в тот момент, когда пользователь запускает Ваше 
приложение. Естественно, пока пользователь не откроет его, внедрять DLL в 
адресное пространство WordPad не требуется. Когда пользователь закроет Ваше 
приложение, целесообразно отменить переопределение оконной процедуры WordPad. 
И в этом случае DLL тоже незачем «держать» в адресном пространстве WordPad. 
Так что самое лучшее — внедрять DLL только на то время, в течение которого она 
действительно нужна конкретной программе. 

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

    Внедрение DLL в адресное пространство процесса возможно и с применением 
ловушек. Чтобы они работали в Win32 так же, как в 16-разрядной Windows, 
Microsoft пришлось создать механизм, позволяющий внедрять DLL в адресное 
пространство другого процесса. Рассмотрим его на примере. 
    Процесс А (вроде утилиты Spy++) устанавливает ловушку WH_GETMESSAGE и 
наблюдает за сообщениями, обрабатываемыми окнами в системе. Ловушка 
устанавливается вызовом SetWindowsHookEx: 

    ННООК hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hinstDll. NULL); 

    Параметр WH_GETMESSAGE определяет тип ловушки, a GetMsgProc — адрес 
функции (в адресном пространстве Вашего процесса), которую система должна 
вызывать всякий раз, когда окно собирается обработать сообщение. Параметр 
hinstDll идентифицирует DLL, содержащую функцию GetMsgProc. В Win32 значение 
hinstDll для данной DLL фактически задает 32-битный адрес в виртуальной 
памяти, по которому DLL спроецирована на адресное пространство процесса. И, 
наконец, последний параметр, NULL, указывает поток, для которого предназначена 
ловушка. Поток может вызвать SetWindowsHookEx и передать ей идентификатор 
другого потока в системе. Передавая NULL, мы сообщаем системе, что ставим 
ловушку для всех существующих в ней потоков. 
    Теперь посмотрим, как все это действует: 
    1. Поток процесса В собирается направить сообщение какому-либо окну. 
    2. Система проверяет, не установлена ли для данного потока ловушка WH_GETMESSAGE. 
    3. Затем выясняет, спроецирована ли DLL, содержащая функцию GetMsgProc, на 
адресное пространство процесса В. 
    4. Если указанная DLL еще не спроецирована, система отображает ее на 
адресное пространство процесса В и увеличивает счетчик блокировок (lock count) 
проекции DLL в процессе В на 1. 
    5. Система проверяет, не совпадают ли значения hinstDll, относящиеся к 
процессам А и В. Если hinstDll в обоих процессах одинаковы, то и адрес 
GetMsgProc в этих процессах тоже одинаков. Тогда система может просто вызвать 
GetMsgProc в адресном пространстве процесса А. 
    6. Если же hinstDll различны, система определяет адрес функции GetMsgProc 
в адресном пространстве процесса В по формуле: 
    GetMsgProc В = histDll В + (GetMsgProc А - hinstDll А) 

    604 ______________________________Windows для профессионалов 

    Вычитая hinstDU А из GetMsgProc А, Вы получаете смещение (в байтах) адреса 
функции GetMsgProc. Добавляя это смещение к hinstDll В, Вы получаете адрес 
GetMsgProc, соответствующий проекции DLL в адресном пространстве процесса В. 
    7. Счетчик блокировок проекции DLL в процессе В увеличивается на 1. 
    8. Вызывается GetMsgProc в адресном пространстве процесса В. 
    9. После возврата из GetMsgProc счетчик блокировок проекции DLL в адресном 
пространстве процесса В уменьшается на 1. 
    Вот так и реализуется механизм ловушек в среде Win32. Кстати, когда 
система внедряет или проецирует DLL, содержащую функцию — фильтр ловушек, 
проецируется вся DLL, а не только функция-фильтр. А значит, потокам, 
выполняемым в контексте процесса В, теперь доступны все функции этой DLL. 
    Таким образом, чтобы породить подкласс окна, созданного потоком другого 
процесса, можно сначала установить ловушку WH_GETMESSAGE для этого потока, а 
затем — когда будет вызвана функция GetMsgProc— обратиться к SetWindowLongti 
создать подкласс. Разумеется, оконная процедура должна быть в той же DLL, что 
и GetMsgProc. 
    В отличие от внедрения DLL с помощью реестра этот способ позволяет в любой 
момент отключить DLL от адресного пространства процесса, для чего достаточно вызвать: 

    BOOL UnhookWindowsHookEx(HHOOK hHook): 

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

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

    На компьютере я в основном работаю с деловыми приложениями, и, на мой 
взгляд, самое оптимальное для них разрешение — 1152 х 864. Но вот недавно я 
начал на своем компьютере играть, а большинство игр рассчитано на разрешение 
640 х 480. Когда у меня появляется настроение поиграть, приходится открывать 
Display (Экран) в Control Panel (Панель управления) и устанавливать разрешение 
640 х 480, а закончив игру, вновь возвращаться в Display и восстанавливать 
разрешение 1152 х 864. 
    Возможность изменять экранное разрешение «на лету» — очень удобное 
новшество Windows 95 и Windows NT 4. Единственное, что мне не по душе, — при 
смене экранного разрешения не сохраняются позиции ярлыков на рабочем столе 
(desktop). У меня на рабочем столе масса ярлыков для быстрого доступа к часто 
используемым программам и файлам. Стоит мне сменить разрешение, размеры 
рабочего стола изменяются, и ярлыки перестраиваются так, что уже ничего не 
найдешь. А когда я восстанавливаю прежнее разрешение, ярлыки так и остаются 
вперемешку. Чтобы навести порядок, приходится вручную перемещать каждый ярлык 
на свое место — очень интересное занятие! 
    В общем, мне это так осточертело, что я придумал утилиту, сохраняющую 
позиции элементов на экране (Desktop Item Position Saver, DIPS). DIPS состоит 
из крошечного исполняемого файла и компактной DLL. Запустив исполняемый файл, 
Вы увидите информационное окно: 

    _____________________Глава 18. Прорыв за пределы процесса 605 


    DIPS: Desktop Item Position Saver Utility 
    Copyright (c) 1 Э97 by Jeffrey RIchter.               | 
    This utility saves/restores the position of items on the desktop. 
    ! 1-1 save the desktop item positions: DIPS S 
    Го restore the desktop item positions: DIPS R 
    Save    I           Restore   |           Cancel 
    •—— ••J1       1             '                     • 

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

    HKEY_CURRENT_USER\Software\Richter\Desktop Item Position Saver 

    куда добавляется по одному параметру на каждый элемент, расположенный на 
Вашем рабочем столе. Значение каждого параметра — позиция соответствующего 
элемента. Утилиту DIPS следует запустить перед сменой экранного разрешения. 
Всласть наигравшись и восстановив нормальное разрешение, вновь запустите DIPS 
— на этот раз с ключом R. Тогда DIPS откроет соответствующий подраздел реестра 
и восстановит для каждого объекта рабочего стола его исходную позицию. 
    На первый взгляд утилита DIPS тривиальна и очень проста в реализации. 
Вроде только и надо, что получить описатель элемента управления «графический 
список» (ListView) рабочего стола, заставить его (послав соответствующие 
сообщения) перечислить все ярлыки и определить их координаты, а потом 
сохранить полученные данные в реестре. Но попробуйте, и Вы убедитесь, что все 
не так просто. Проблема в том, что большинство оконных сообщений для 
стандартных элементов управления (например, LVM_GETITEM и LVM_GETITEMPOSITION) 
не могут преодолеть границы процессов. Почему? 
    Сообщение LVM_GETITEM требует, чтобы Вы передали в параметре EPARAM адрес 
структуры EV_ITEM. Поскольку ее адрес имеет смысл лишь в адресном пространстве 
процесса — отправителя сообщения, процесс-приемник не может безопасно 
использовать его.' Поэтому, чтобы DIPS работала так, как было обещано, в 
EXPEORER.EXE надо внедрить код, посылающий сообщения EVM_GETITEM и 
LVM_GETITEMPOSITION элементу управления «графический список» рабочего стола. 
    Как я уже говорил, моя утилита состоит из двух компонентов: исполняемого 
файла DIPS.EXE, собранного из исходного файла DIPS.С, и библиотеки 
DIPSLIB.DLL, полученной из исходного файла DIPSLIB.C. В оба файла с исходным 
кодом включен заголовочный файл DIPSLIB.H (тексты всех файлов см. на рис. 18-2). 
    Сразу после запуска DIPS.EXE получает описатель элемента управления 
«графический список» рабочего стола: 

    // Окно Desktop ListView - "внук" окна ProgMan 
    hwndLV = GetFirstChild(GetFirstChild(FindWindow(__TEXT("ProgMan"), NULL))): 

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

    606 ______________________________Windows для профессионалов 

    Этот код сначала ищет окно класса ProgMan. Даже несмотря на то, что 
никакой Program Manager не запускается, новая оболочка по-прежнему создает 
окно этого класса — для совместимости с приложениями, рассчитанными на старые 
версии Windows. У окна ProgMan единственное дочернее окно класса 
SHELLDLL_DefView, у которого тоже одно дочернее окно — класса SysListView32. 
Оно-то и служит элементом управления «графический список» рабочего стола.2 
    Получив описатель окна графического списка, я определяю идентификатор 
создавшего его потока, для чего вызываю GetWindowThreadProcessId. Этот 
идентификатор я передаю в SetDIPSHook, реализованной в библиотеке DIPSLIB.C. 
Последняя функция устанавливает ловушку WH_GETMESSAGE для указанного потока и вызывает: 

    PostThreadMessage(dwThreadId, WM_NULL, 0. 0); 

    чтобы разбудить поток Explorer (Проводник). Для него установлена ловушка 
WH_GETMESSAGE, поэтому операционная система автоматически внедряет DIPSLIB.DLL 
в адресное пространство процесса Explorer и вызывает мою функцию GetMsgProc. 
Та сначала проверяет, впервые ли она вызвана, и, если да, создает скрытое окно 
с заголовком Richter DIPS. Возьмите на заметку, что это окно порождает поток, 
принадлежащий Explorer. Пока окно создается, поток DIPS.EXE возвращается из 
функции SetDIPSHook и вызывает: 

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

    Этот вызов «усыпляет» поток до появления в очереди какого-нибудь 
сообщения. Хотя DIPS.EXE сам не создает ни одного окна, у него все же есть 
очередь сообщений, и они помещаются туда исключительно в результате вызовов 
PostThreadMessage. Взгляните на код GetMsgProc в DIPSLIB.C: сразу после 
обращения к CreateDialog стоит вызов PostThreadMessage, который вновь 
пробуждает поток DIPS.EXE. Идентификатор потока сохраняется в общей переменной 
внутри функции SetDIPSHook. 
    Очередь сообщений я использую для синхронизации потоков. В этом нет ничего 
противозаконного, и иногда гораздо проще синхронизировать потоки именно так, 
не прибегая к объектам ядра — мьютексам, семафорам, событиям и т. д. (Win32 — 
очень богатый API; пользуйтесь этим.) 
    Когда поток DIPS.EXE пробуждается, он узнает, что серверное диалоговое 
окно уже открыто, и обращается к FindWindow, чтобы получить описатель окна. С 
этого момента для организации взаимодействия между клиентом (утилитой DIPS) и 
сервером (скрытым диалоговым окном) можно использовать механизм оконных 
сообщений. Поскольку это диалоговое окно создано потоком, выполняемым в 
контексте процесса Explorer, нас мало что ограничивает в наших действиях с Explorer. 
    Чтобы сообщить своему диалоговому окну, что нам надо сохранить или 
восстановить позиции ярлыков на экране, достаточно послать сообщение: 
    
    // сообщаем окну DIPS. с каким окном графического списка работать 
    // и что делать: сохранять или восстанавливать позиции ярлыков 
    SendMessage(hwndDIPS, WM_APP, (WPARAM) hwndLV, fSave): 
    
    Процедура диалогового окна проверяет сообщение WM_APP. Когда она принимает 
это сообщение, параметр WPARAM содержит описатель нужного элемента управления 
«графический список», a LPARAM представляет собой Булево значение, 
определяющее, сохранять текущие позиции ярлыков в реестре или восстанавливать. 
    Так как здесь используется SendMessage, а не PostMessage, управление не 
передается до завершения операции. Если хотите, определите дополнительные 
сообщения для процедуры диалогового окна — это расширит возможности программы 
в управлении Explorer. Закон2, Кстати, всю эту информацию я выудил благодаря 
Spy++ (поставляемой с Visual C++). 

    _____________________Глава 18. Прорыв за пределы процесса 607 

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

    DIPS.С 
    Модуль: DIPS,С 
    Автор: Copyright © 1996-1997, Джеффри Рихтер (Jeffrey Richter) 
    #include "..\CmnHdr.H"              /* см, приложение В */ 
    ttinclude <Windows.H> 
    ffinclude <WindowsX.H> 
    ftinclude <tchar.h> 
    ftinclude "Resource.h" 
    tt^clnrip •• \rnPSLih\DIPSLib.h" 
    /////////////////////////////////////////////7///7//////7//7//////////.' . 
•    • 
    BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    
    // связываем значок с диалоговым окном 
    chSETDLGICONS(hwnd, IDI_DIPS, IDI_DIPS); 
    return(TRUE): 
    1111111111111111111111111111111111111111111111111111111111111111111111111111111 
    
    void Dlg_OnGommand (HWND hwnd, int id, HWND hwndCtI, UINT codeNotify) { 
    switch (id) { 
    case IDC_SAVE: 
    case IDC_RESTORE: 
    case IDCANCEL: 
    EndDialog(hwnd, id); 
    break; 
    111 1-1 7. 41111111111111111111111111111111111111111111111111111111111111111111 

    см. след. стр. 

    Рис. 18-2. Листинг утилиты DIPS 

    608 ______________________________Windows для профессионалов 


    РИС. 18-2 (продолжение) 

    BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wPar-am, LPARAM 
IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDlalog); 
    chHANDLE_DLGMSG(hwnd. WM COMMAND. Dln_OnCorn"iand): 
    return(FALSE); 
    \ i 
    /'////У/7///7/'/77//////////////////У///////, '/'• ,. '• :: '• 
••''•',','/' 'i-• ': :'i • '1//'' • 
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCffidShdw) { 
    HWND hwndDIPS; 
    HWND hwndLV; 
    MSG msg; 
    TCHAR cWhatToDo: 
    ChWARNIFUNICODEUNDERWIN95(); 
    // преобразуем символ в командной строке в верхний регистр 
    CharUpperBuff(pszCmdLine, 1); 
    cWhatToDo = pszCmdLine[0]; 
    if ((cWhatToOo != „_TEXT('S')) && (cWhatToDo != __TEXT('R'))) { 
    // неверный аргумент в командной строке; сообщаем пользователю 
    cWhatToDo = 0; 
    \ 
    if (cWhatToDo == 0) { 
    // аргумента в командной строке нет: 
    // выводим диалоговое окно с инструкциями 
    switch (DialogBox(hinstExe, MAKEINTRESOURCE(IDD_DIPS), NULL, Dlg_Proc)) { 
    case IDC_SAVE: 
    cWhatToDo = __TEXT('S'); 
    break; 
    case IDC_RESTORE: 
    cWhatToDo = __TEXT('R'); 
    break; 
    } 
    if (cWhatToDo == 0) { 
    // пользователь не хочет ничего делать 
    ret'jrn(O): 

    см. след. стр. 

    _____________________ Глава 18. Прорыв за пределы процесса 609 


    РИС. 18-2 (продолжение) 

    II окно Desktop ListView - "внук" окна РгодМап 
    hwndLV = GetFirstChild( 
    GetFirstChild(FindWindow(__TEXT("progMan"), NULL))); 
    chASSERT(IsWindow(hwndLV)); 
    
    // Устанавливаем ловушку, внедряющую нашу DLL в адресное пространство 
    // процесса Explorer. После этого создаем скрытое немодальное диалоговое 
    // окно. Мы посылаем ему сообщения, которые говорят, что делать. 
    chVERIFY(SetDIPSHook( 
    GetWindowThreadProcessId(hwndLV, NULL))): 
    // ждем создания серверного окна DIPS 
    GetMessage(&msg, NULL, 0, 0); 
    // определяем описатель скрытого диалогового окна 
    hwndDIPS = FindWindow(NULL, __TEXT("Richter DIPS")): 
    // убедимся, действительно ли это окно создано 
    chASSERT(IsWindow(hwndDIPS)); 
    // сообщаем окну DIPS, с каким окном графического списка следует 
    // работать, а также что делать: сохранять или восстанавливать элементы 
    SendMessage(hwndDIPS, WM_APP, (WPARAM) hwndLV, (cWhatToDo == __TEXT('S'))); 
    
    // Сообщаем окну DIPS о необходимости самоуничтожения, Вместо 
    // PostMessage используем SendMessage, чтобы узнать об уничтожении 
    // окна до снятия ловушки. 
    SendMessage(hwndDIPS, WM_CLOSE, О, О); 
    
    // проверяем, действительно ли окно уничтожено 
    chASSERT(!IsWindow(hwndDIPS)): 
    // снятие ловушки приводит к удалению процедуры диалогового окна DIPS 
    // из адресного пространства процесса Explorer 
    SetDIPSHook(O); 
    return(O); 
    } 
    ///////////////////  конец файла ////////// ///////////I///////'/// 


    610 _____________________________Windows для профессионалов 


    РИС. 18^2 (продолжение) 

    DIPSLIB.C 
    /***»*,,,.,**,**,,*,*****..*.,...,,.*.,...,*...,..,*,..*,,**»».,**,,,..*»**,,»* 
    Модуль: DIPSLib.G 
    Автор: Copyright © 1996-1997, Джеффри Рихтер (Jeffrey Richter) 
    
    #include "..\CmnHdr.H"              /* см. приложение В */ 
    fflnclude <Wlndows.H> 
    «include <WindowsX.H> 
    #include <CommCtrl.H> 
    ftdefine DIPSLIBAPI __declspec(dllexport) 
    #include "DIPSLib.H" 
    «include "Resource,H" 
    ////////////// •• ••     - 11 •    '•   1/.1-111.1//..,,11,1.'.,1,1/,,//.//////,/////7//// 
    ftifdef _DEBUG 
    // эта функция вызывает отладчик 
    void ForceDebugBreak() { 
    __try { DebugBreakO: } 
    „except(UnhandledExcept ionFilteг( 
    GfitFxceptionInformationO)) { 
    ) 
    #else 
    ffdefine ForceDebugBreakO 
    #endlf 
    "7//////////////////    '.   '7 ..••'//•1/ •••;•/•, './/л 77/,.7, ,,•//,,/ 
    / / упреждающие ссылки 
    LRESULT WINAPI GetMsgProc (int nCode, WPARAM wParam, LPARAM IParam); 
    BOOL WINAPI DIPS_DlgProc(HWND hwnd. UINT uMsg, WPARAM wParam, LPARAM IParam); 
    
//////////////////////////////////////////7/////////////////////////////////7. • 
    
    // требуем от компилятора разместить переменную g_hhook в отдельном разделе 
    // данных Shared, а компоновщику сообщаем, что данные из этого раздела должны 
    // быть доступны всем экземплярам приложения 
    ftpragma data_seg("Shared") 
    ННООК g_hhook = NULL; 
    DWORD g_dwThreadIdDIPS = 0; 
    #pragma data_seg() 

    см. след. стр. 

    _____________________Глава 18. Прорыв за пределы процесса 611 


    РИС. 18-2 (продолжение) 

    / I сообщаем компоновщику, что у раздела Shared должны быть атрибуты 
    // для чтения, записи и "совместно используемый" 
    ffpragma coniment(linker, "/section:Shared, rws") 
    
    // не-разделяемые переменные 
    HINSTANCE g hinstDll = NULL: 
    ///////////////////////////////////////////////////////
    BOOL WINAPI DllMain (HINSTANCE hinstDll. DWORD fdwReason, LPVOID fImpLoad) { 
    switch (fdwReason) { 
    case DLL_PROCESS_ATTACH: 
    // подключаем DLL к адресному пространству текущего процесса 
    g_hinstDll = hinstDll; 
    break; 
    case DLL_THREAD_ATTACH: 
    // создаем поток в текущем процессе 
    break; 
    case DLL_THREAD_OETACH: 
    // поток завершен корректно 
    break: 
    case DLL_PROCESS_DETACH; 
    // вызывающий процесс отключает DLL от своего 
    // адресного пространства 
    break: 
    } 
    return(TRUE); 
    \ 
    i 
     //////////////////////////////////////////////
    ROOL WINAPI SetDIPSHook(DWORD dwThreadId) { 
    BOOL fOk = FALSE; 
    if (dwThreadId != 0) { 
    // проверяем, не установлена ли уже ловушка 
    chASSERT(g_hhook == NULL); 
    
    // сохраняем идентификатор потока в разделяемой переменной, 
    // чтобы после создания окна сервера наша функция GetMsgProc 
    // могла послать асинхронный ответ потоку 
    g_dwThreadIdDIPS = GetCurrentThreadId(); 

    см. след. стр. 

    612 ______________________________Windows для профессионалов 


    РИС. 18-2 (продолжение) 

    II устанавливаем ловушку для указанного потока 
    g„nhook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, 
    g_hinstDll, dwThreadId); 
    fOk = (g_nhook != NULL): 
    if (fOk) { 
    // ловушка установлена; отправляем сообщение в очередь 
    // потока, провоцируя вызов функции ловушки 
    fOk = PostThreadMessage(dwThreadId, WM_NULL, О, О): 
    } 
    } else { 
    // убедимся, что ловушка действительно установлена 
    chASSERT(g_hhook != NULL); 
    fOk = UnnookWlndowsHookEx(g_hhook); 
    g_hnook = NULL; 
    } 
    return(fOk); 
    \ 
    /////////////////////////////////////////////////////////////////////////////// 
    LRESULT WINAPI GetMsgProc (int nCode, WPARAM wParam, LPARAM IParam) { 
    static BOOL fFirstTime = TRUE;                       •'' 
    if (fFirstTime) { 
    // DLL только что внедрена 
    fFirstTime = FALSE; 
    // раскомментируйте следующую строку, чтобы вызвать отладчик 
    // для процесса, в который только что внедрена DLL 
    // ForceDebugBreakO: 
    // создаем окно сервера DIPS, обрабатывающее 
    // запросы пользователя 
    CreateDialog(g_hinstDll, MAKEINTRESOURCE(IDD_DIPS), NULL, DIPS_DlgProc): 
    // сообщаем утилите DIPS, что сервер стартовал 
    // и готов к обработке запросов 
    PostThreadMessage(g_dwThreadIdDIPS, WM_NULL, О, О); 
    } 
    return(CallNextHookEx(g_hhook, nCode, wParam, IParam)); 
    } 
    ////////////////////////////////////////////////////////

    см. глед. стр. 

    _____________________Глава 18. Прорыв за пределы процесса 613 


    РИС. 18-2 (продолжение) 

    void DIPS_OnClose(HWND hwnd) { 
    DestroyWindow(hwnd); 
    } 
    //////////////////////////////////
    static const TCHAR g_szReg3ubKey[] = 
    TEXT("Software\\Richter\\Desktop Item Position Saver");                    
    ///////////////////////////////////////////
    void SaveListViewItemPositions(HWND hwndLV) { 
    int nitem, nMaxItems = ListView_GetItemCount(hwndLV); 
    HKEY hkey; 
    DWORD dwDisposition; 
    // сохраняя новые позиции, удаляем из реестра информацию о старых 
    LONG I = RegDeleteKey(HKEY_CURRENT_USER, g_szRegSubKey); 
    
    // создаем нужный раздел реестра 
    1 = RegCreateKeyEx(RKEY_CURRENT_USER, g_szReg3ubKey, 
    О, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, 
    NULL, &hkey, &dwDisposition); 
    chASSERT(l == ERROR_SUCCESS); 
    for (nitem = 0; nitem < nMaxItems; nltem++) { 
    TCHAR szName[_MAX_PATH]; 
    POINT pt;                                                               : 
    // получаем имя и позицию элемента в графическом списке 
    ListView_GetItemText(hwndLV. nitem, 0. szName, chDIMOF(szName)): 
    ListView_GetItemPosition(hwndLV, nitem, &pt); 
    // сохраняем полученные данные в реестре 
    1 = RegSetValueEx(hkey, szName. 0, REG_BINARY, (PBYTE) &pt, sizeof(pt)); 
    chASSERT(l == ERROR_SUCCESS); 
    } 
    RegCloseKey(hkey); 
    } 
    1111111111111111/lllllllllllllllllllllllllllllllllllltllllllllllltlllllllllllll 
    void RestoreListViewItemPositions(HWND hwndLV) { 
    HKEY hkey: 
    LONG I = RegOpenKeyEx(HKEY_CURRENT_USER, g_szReg3ubKey, 
    0, KEY_QUERY_VALUE, &hkey); 
    if (1 == ERROR_SUCCESS) { 
    int nindex; 

    см. след. стр. 

    614 ______________________________Windows для профессионалов 


    РИС. 18-2 (продолжение) 
    
    II если в графическом списке установлен режим 
    // автоупорядочивания (AutoArrange), временно 
    // отключаем его 
    DWORD dwStyle = GetWindowStyle(nwndLV); 
    if (dwStyle & LVS_AUTOARRANGE) 
    SetWindowLong(hwndLV, GWL_STYLE, dwStyle & ~LVS_AUTOARRANGE); 
    I = NO.ERROR; 
    
    for (nindex =0; 1 '.= ERROR_NO_MORE_ITEMS; nlndex++) { 
    TCHAR szName[_MAX_PATH]; 
    POINT pt; 
    LV_FINDINFO Ivfi; 
    int cbValueName = chOIMOF(szName); 
    int cbData = sizeof(pt), nitem; 
    DWORD dwType; 
    // считываем данные из реестра 
    
    1 = RegEnumValue(hkey, nindex, szName, &cbValueName, 
    NULL, &dwType, (PBYTE) &pt, &cbData); 
    
    if (i == ERROR_NO_MORE_ITEMS) 
    continue; 
    
    if ((dwType == REG_BINARY) && (cbData == sizeof(pt))) { 
    // если данный параметр нам известен, пытаемся найти 
    // одноименный элемент графического списка 
    Ivfi.flags = LVFI_STRING; 
    Ivfi.psz = szName: 
    nitem = ListView_FindItem(hwndLV, -1, &lvfi); 
    if (nitem != -1) { 
    // элемент найден; изменяем его позицию 
    ListView_SetItemPosition(hwndLV, nitem, pt.x, pt.y); 
    } 
    } 
    i 
    
    II если раньше действовал режим автоупорядочивания, 
    // восстанавливаем его 
    SetWindowLong(hwndLV, GWL_STYLE, dwStyle): 
    RegCloseKey(hkey): 
    ////////////////////////////////
    BOOL WINAPI DIPS_DlgProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd, WM_CLOSE, DIPS_OnClose); 
    case WM_APP: 

    см. след. стр. 

    _____________________Глава 18. Прорыв за пределы процесса 615 


    РИС. 18-2 (продолжение) 

    II раскомментируйте следующую строку, чтобы вызвать отладчик 
    // для процесса, в который только что внедрена DLL 
    // ForceDebugBreakO; 
    if (IParam) 
    Save ListVlewItemPositions((HWND) wParam); 
    else 
    RestoreListViewItemPositlons((HWND) wParam); 
    break; 
    i i 
    return(FALSE); 
    1111111111111/11111111111111111111 конец файла ///////////////////////////////// 
    DIPSL1B.H 
    Модуль: DIPSLib.H 
    Автор: Copyright © 1996-1997, Джеффри Рихтер (Jeffrey Richter) 
    #if !defined(DIPSLIBAPI) 
    ffdefine DIPSLIBAPI __declspec(dllimport) 
    ftfindif 
    //////////////////////////////
    /I прототипы внешних функций 
    DIPSLIBAPI BOOL WINAPI SetDIPSHook(DWORD dwThreadId): 
    1111111111111/11111111111111111/11 конец файла ///////////////////////////////// 


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

    Этот способ внедрения DLL наиболее труден в реализации, зато самый гибкий. 
В нем используются многие особенности Win32: процессы, потоки, синхронизация 
потоков, структурная обработка исключений, управление виртуальной памятью и 
Unicode. (Если Вы «плаваете» в каких-то из этих тем, прочтите сначала 
соответствующие главы книги.) Внедрение DLL этим способом предполагает 
создание и выполнение потоков в адресном пространстве целевого процесса, а 
также доступ к физической памяти, переданной под стек нужного потока. Поэтому 
Вы должны четко понимать, как система создает потоки и как поток использует 
свой стек. Наверное, стоит освежить память, еще раз вернувшись к разделам 
«Функция CreateThread» (глава 4) и «Стек потока» (глава 7). 

    616 ______________________________Windows для профессионалов 


    Как загружается DLL 

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

    HINSTANCE LoadLibrary(LPCTSTR IpszLibFile); 
    Заглянув в заголовочный файл WINBASE.H, Вы увидите, что для LoadLibrary 
там есть такие строки: 
    HINSTANCE WINAPI LoadLibraryA(LPCSTR IpLibFileName); 
    HINSTANCE WINAPI LoadLibraryW(LPCWSTR IpLibFileName); 
    #ifdef UNICODE 
    ffdefine LoadLibrary LoadLibraryW 
    Seise 
    Sdefine LpadLibrary LoadLibraryA 
    Sendif // UNICODE 

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

    \Л/!п32-функции, влияющие на другие процессы 

    Дойдя до этого места, Вы уже должны хорошо разбираться в потоках и их 
стеках. Но прежде чем продолжить тему внедрения DLL в адресные пространства 
других процессов, хочу кратко рассмотреть \Уш32-функции, позволяющие одному 
процессу воздействовать на другой. Таких функций немного, поскольку они 
негативно влияют на надежность приложений. В основном они разработаны 
Microsoft специально для отладчиков, и большинству Win32приложений практически 
не нужны. В следующих двух таблицах собраны все \Уш32-функции, принимающие в 
качестве параметров описатели процессов и потоков. 

    \Л/т32-функции 
    для процессов           Описание 

    CreateProcess               Создает другой процесс. 
    FlushInstructionCache       Сбрасывает кэш команд другого процесса. 
    VirtualProtectEx           Изменяет атрибуты защиты страниц, переданных 
другому процессу. 
    VirtualQueryEx           Возвращает информацию о диапазоне страниц, 
принадлежащих другому процессу. 
    VirtualAllocEx             Новая функция Windows NT 4, резервирующая часть 
адресного пространства другого процесса и/или передающая ему память. 
    VirtualFreeEx             Новая функция Windows NT 4, осуществляющая 
возврат переданной другому процессу памяти и/или освобождающая часть адресного пространства. 
    GetProcessAffinityMask      Сообщает, на каких процессорах разрешено 
выполнение процесса. 
    GetProcess Times            Возвращает временные характеристики другого процесса. 

    см. след. стр. 

    _____________________Глава 18. Прорыв за пределы процесса 617 


    продолжение 

    У^п32-функции 
    для процессов           Описание 

    GetProcessWorkingSetSize    Возвращает минимальный и максимальный размер 
адресного пространства указанного процесса. 
    SetProcessWorkingSetSize    Устанавливает минимальный и максимальный 
размер адресного пространства указанного процесса. 
    TerminateProcess           Завершает другой процесс. 
    GetExitCodeProcess         Возвращает код завершения другого процесса. 
    CreateRemoteThread        Запускает поток в, другом процессе, 
    ReadProcessMemory        Считывает область памяти из адресного 
пространства другого процесса. 
    WriteProcessMemory        Записывает в память, принадлежащую адресному 
пространству другого процесса. 
    GetPriorityClass            Возвращает класс приоритета другого процесса. 
    SetPriorityClass             Устанавливает класс приоритета другого процесса. 
    WaitForInputIdle           Ждет момента, когда опустеет очередь ввода в 
потоке другого процесса. 

    Win32-4)ym<UMM 
    для потоков             Описание 

    SetThreadAffinityMask      Устанавливает, на каких процессорах разрешено 
выполнение потока. 
    GetThreadPriority          Возвращает приоритет другого потока. 
    SetThreadPriority           Устанавливает приоритет другого потока. 
    GetThreadTimes           Возвращает временные характеристики другого потока. 
    Terminate Thread           Завершает другой поток. 
    GetExitCode Thread         Возвращает код завершения другого потока. 
    GetThreadSelectorEntry      Возвращает элемент из таблицы дескрипторов 
другого потока (только для систем на базе х86). 
    GetThreadContext          Возвращает содержимое регистров процессора для потока. 
    SetThreadContext          Изменяет содержимое регистров процессора для потока. 
    ResumeThread             Увеличивает счетчик задержек (suspend count) 
другого процесса на 1. 
    SuspendThread            Уменьшает счетчик задержек другого процесса на 1. 
    Для внедрения DLL в адресное пространство другого процесса нам потребуется 
лишь девять из этих функций. Давайте кратко рассмотрим их. 
    Функция CreateRemoteThread 
    Эта функция позволяет создать поток, выполняемый в контексте другого процесса: 
    HANDLE CreateRemoteThread(HANDLE hProcess, LPSECURITY_ATTRIBUTES Ipsa, 
    DWORD cbStack, LPTHREAD_START_ROUTINE IpStartAddr, LPVOID IpvThreadParm, 
DWORD fdwCreate, LPDWORD IpIDThread); 
    Она идентична CreateThread, но имеет дополнительный параметр hProcess, 
идентифицирующий процесс, которому будет принадлежать новый поток. Параметр 
IpStartAddr определяет 

    618 ______________________________Windows для профессионалов 

    адрес функции потока. Этот адрес, разумеется, относится к удаленному 
процессу — функция потока не может находиться в адресном пространстве Вашего процесса. 

    VWINDOWS/ 
    В Windows NT чаще используемая функция CreateThread реализована через 
вызов CreateR emoteThread: 

    HANDLE CreateThread(LPSECURITY_ATTRIBUTES Ipsa, DWORD cbStack, 
    LPTHREAD_START_ROUTINE IpStartAddr, LPVOID IpvThreadParm, 
    DWORD fdwCreate, LPDWORD IpIDThread) { 
    return (CreateRemoteThr-ead(GetCurrentProcess(), Ipsa, cbStack, IpStartAddr, 
    IpvThreadParm, fdwCreate, IpIDThread)); 
    } 


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

    Функции GetJhreadContext и SetJhreadContext 

    В Win32 API лишь одна структура, зависимая от типа процессора, — CONTEXT. 
Приведенный ниже фрагмент кода показывает ее формат для процессоров х86. Эта 
структура разбита на 5 разделов. Раздел CONTEXT_CONTROL содержит управляющие 
регистры процессора: указатель команд, указатель стека, флаги и адрес возврата 
функции. (В отличие от х86, который при вызове функции помещает адрес возврата 
в стек, процессоры MIPS и Alpha сохраняют адрес возврата в одном из 
регистров.) Раздел CONTEXT_INTEGER соответствует целочисленным регистрам, 
CONTEXT_FLOATING_POINT — регистрам с плавающей точкой, CONTEXT_SEGMENTS — 
сегментным регистрам (только для х86), a CONTEXT_DEBUG_REGISTERS — регистрам, 
предназначенным для отладки (только для х86). typedef struct .CONTEXT { 
    / 
    // Флаги, управляющие содержимым записи CONTEXT. 
    // 
    
    // Если запись контекста используется как входной параметр, тогда раздел, 
    // управляемый флагом (когда он установлен), считается содержащим 
    // действительные значения. Если запись контекста используется для 
    // модификации контекста потока, то изменяются только те разделы, для 
    // которых флаг установлен. 
    // 
    
    // Если запись контекста используется как параметр IN OUT для захвата 
    // контекста потока, возвращаются только те разделы контекста, для которых 
    // установлены соответствующие флаги. Запись контекста никогда не 
    // используется как параметр OUT. 
    // 
    UWORO ContextFlag3: 

    ____________________Глава 18.  Прорыв за пределы процесса 619 
    // Этот раздел определяется/возвращается, когда в ContextFlags установлен 
    // флаг GONTEXT_DEBUG_REGISTERS. Заметьте, что CONTEXT_DEBUG_REGISTERS 
    // не включаются в CONTEXT_FULL. 
    // 
    DWORD DrO; 
    DWORD Dr1; 
    DWORD Dr2; 
    DWORD Dr3: 
    DWORD Dr6; 
    DWORD Dr7: 
    // 
    // Этот раздел определяется/возвращается, когда в ContextFlags 
    // установлен флаг CONTEXT_FLOATING_POINT, 
    // 
    FLOATING_SAVE_AREA FloatSave; 
    /7 
    // Этот раздел определяется/возвращается, когда в ContextFlags 
    // установлен флаг CONTEXT SEGMENTS. 
    // 
    DWORD SegGs; 
    DWORD SegFs; 
    DWORD SegEs: 
    DWORD SegDs: 
    // 
    // Этот раздел определяется/возвращается, когда в ContextFlags 
    // установлен флаг CONTEXT_INTEGER. 
    // 
    DWORD Edi; 
    DWORD Esl; 
    DWORD Ebx; 
    DWORD Edx; 
    DWORD Ecx; 
    DWORD Eax; 
    // 
    // Этот раздел определяется/возвращается, когда в ContextFlags 
    // установлен флаг CONTEXT_CONTROL. 
    // 
    DWORD Ebp; 
    DWORD Eip: 
    DWORD SegCs;     // следует очистить 
    DWORD EFlags;    // следует очистить 
    DWORD Esp; 
    DWORD SegSs; 
    } CONTEXT; 


    620 ______________________________Windows для профессионалов 

    Единственный элемент структуры CONTEXT, которому не соответствует 
какой-либо регистр процессора, — Context flags. Присутствуя во всех вариантах 
этой структуры, независимо от типа процессора, он подсказывает функции 
GetThreadContext, значения каких регистров Вы хотите узнать. Например, чтобы 
получить значения управляющих регистров для потока, напишите что-то вроде: 

    // создаем структуру CONTEXT 
    CONTEXT Context; 
    
    // сообщаем системе, что нас интересуют только управляющие регистры 
    Context.ContextFlags = CONTEXT_CONTROL; 
    
    // обращаемся к системе, чтобы получить значения регистров, 
    // связанных с потоком 
    GetThreadContext(hThread, &Context): 
    
    // действительные значения содержат элементы структуры CONTEXT, 
    // соответствующие управляющим регистрам, остальные значения 
    // не определены 
    
    Перед вызовом GetThreadContext надо инициализировать элемент ContextFlags. 
Чтобы получить значения как управляющих, так и целочисленных регистров, 
инициализируйте элемент ContextFlags так: 

    // сообщаем системе, что нас интересуют 
    // управляющие и целочисленные регистры 
    Context,ContextFlags = CONTEXT_CONTROL ; CONTEXT_INTEGER; 

    Есть еще один идентификатор, позволяющий узнать значения важных для потока 
регистров (т. е. используемых, по мнению Microsoft, чаще всего): 

    // сообщаем системе, что нас интересуют 
    // все значимые регистры 
    Context.ContextFlags = CONTEXT_FULL; 
    CONTEXT_FULL определен в файле WINNT.H, как показано в таблице: 
    Тип процессора    Определение CONTEXT_FULL 
    х&6            CONTEXT_CONTROL ! CONTEXTJNTEGER ! CONTEXT_SEGMENTS 
    MIPS           CONTEXT_CONTROL ! CONTEXT_FLOATING_POINT ! CONTEXTJNTEGER ! 
    CONTEXT_EXTENDED_INTEGER 
    Alpha           CONTEXT_CONTROL ! CONTEXT_FLOATING_POINT : CONTEXTJNTEGER 
    PowerPC         CONTEXT_CONTROL ! CONTEXT_FLOATING_POINT ! CONTEXTJNTEGER 

    После возврата из GetThreadContext Вы легко проверите значения любых 
регистров для потока, но помните, что такой код зависит от типа процессора. В 
следующей таблице перечислены элементы структуры CONTEXT, соответствующие 
указателям команд и стека для разных типов процессоров: 

    Тип процессора     Указатель команд      Указатель стека 

    х86                CONTEXT.Eip          CONTEXT.Esp 
    MIPS              CONTEXT.Fir          CONTEXT.IntSp 
    Alpha              CONTEXT.Fir          CONTEXT.IntSp 
    PowerPC           CONTEXT.Iar          CONTEXT.GprI 


    __________________Глава 18. Прорыв за пределы процесса 621 


    /вним*АниЕ\ 
    На процессорах PowerPC указатель стека (Gpri) относится к целочисленным, а 
не к управляющим регистрам. Поэтому для PowerPC в элемент ContextFlags 
структуры CONTEXT следует занести значение CONTEXTJNTEGER, а не CONTEXT_CONTROL. 
    Любой элемент структуры CONTEXT в принципе можно модифицировать. 
Разумеется, это не повлияет на содержимое регистров, связанных с потоком. Но 
последующий вызов SetThreadContext может изменить и сами регистры: 

    BOOL SetThreadContext(HANDLE hThread, CONST CONTEXT *lpContext); 

    Перед обращением к SetThreadContext инициализируйте элемент ContextFlags, 
как показано ниже: 

    CONTEXT Context; 
    
    // приостанавливаем поток 
    SuspendThread(hThread): 
    
    // получаем регистры для контекста потока 
    Context,ContextFlags = CONTEXT_CONTROL: 
    GetThreadContext(hThread, &Context); 
    // устанавливаем указатель команд по своему выбору; 
    
    // в нашем примере присваиваем значение 0х00010000 
    ftif defined(_ALPHA_) 
    Context.Fir = 0х00010000; 
    
    #elif defined(_MIPS_) 
    Context.Fir- = 0х00010000: 
    
    ffelif defined(_X86_) 
    Context.Eip = 0х00010000; 
    
    #elif defined(_PPC_) 
    Context.lar = 0х00010000; 
    Seise 
    #error Module contains CPU-specific code; modify and recompile. 
    #endif 
    
    // вносим изменения в регистры потока; ContextFlags 
    // можно не инициализировать, так как это уже сделано 
    Context.ControlFlags = CONTEXT_CONTROL; 
    SetThreadContext(hThread, &Context); 
    
    // возобновляем выполнение потока; оно начнется с адреса 0х00010000 
    ResumeThread(hThread); 


    Функции Virtua/QueryEx и VirtualProtectEx 

    Функция VirtualQueryEx возвращает информацию об адресном пространстве процесса: 

    DWORD VirtualQueryEx(HANDLE hProcess, LPCVOID IpvAddress, 
    PMEMORY_BASIC_INFORMATION pmbiBuffer, DWORD cbLength); 

    Она очень похожа на VirtualQuery (см. главу 7), но имеет дополнительный 
параметр hProcess, позволяющий указать процесс, информацию об адресном 
пространстве которого Вы хотите получить. 

    622 ______________________________Windows для профессионалов 

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

    DWORD VirtualProtectEx(HANDLE hProcess, LPVOID IpvAddress. 
    DWORD cbSize, DWORD fdwNewPr-otect, LPDWORD pfdwOldProtect); 

    Эта функция аналогична VirtualProtect (см. главу 7) и тоже отличается лишь 
наличием параметра hProcess. 

    Функции VirtualAHocEx и VirtualFreeEx 

    Они появились в Windows NT 4. VirtualAllocEx позволяет зарезервировать 
адресное пространство и/или передать память для другого процесса: 

    LPVOID VirtualAllocEx(HANDLE hProcess, LPVOID IpAddress. DWORD dwSize, 
    DWORD flAllocatiohType, DWORD flProtect); 

    a VirtualFreeEx — вернуть переданную другому процессу память и/или 
освободить зарезервированное адресное пространство: 

    BOOL VirtualFreeEx(HANDLE hProcess, LPVOID IpAddress. 
    DWORD dwSize, DWORD dwFreeType): 

    Обе функции аналогичны VirtualAlloc и VirtualFree (см. главу 7). 
Единственное отличие в том, что новые функции в качестве первого аргумента 
требуют описатель нужного процесса. 

    Функции ReadProcessMemory и Write ProcessMemory 

    Эти функции позволяют потоку копировать данные из адресного пространства 
своего процесса в адресное пространство другого процесса, и наоборот: 

    BOOL ReadProcessMemory(HANDLE hProcess, LPVOID IpBaseAddress, LPVOID IpBuffer, 
    DWORD cbRead, LPDWORD IpNumberOfBytesRead); 
    BOOL WriteProcessMemory(HANDLE hProcess, LPVOID IpBaseAddress, LPVOID IpBuffer, 
    DWORD cbWrlte, LPDWORD IpNuniberOfBytesWritten); 

    Параметр hProcess идентифицирует удаленный процесс, IpBaseAddress 
определяет базовый адрес памяти в адресном пространстве удаленного процесса, 
IpBuffer — базовый адрес в памяти локального процесса, cbRead и cbWrite— число 
передаваемых байтов, а по адресам, на которые указывают параметры 
IpNumberOfBytesRead и IpNumberOfBytes Written, функция возвращает число 
фактически переданных байтов. 

    Функция, внедряющая DLL в адресное пространство любого процесса 

    Теперь я готов (наконец-то!) обсудить третий способ внедрения DLL в другой 
процесс. Пока остается нерешенной одна проблема: вызов LoadLibrary заставляет 
систему проецировать указанную DLL на адресное пространство процесса, которому 
принадлежит вызывающий функцию поток. Но нам нужно совсем не то. Мы хотим, 
чтобы функцию LoadLibrary вызвал поток другого процесса. 
    Решение этой проблемы несомненно пошло мне впрок. Код пришлось 
переделывать не один раз, прежде чем я получил то, что представлено на рис. 
18-5. Результаты всех попыток я показывать не стану, но попробую объяснить, 
как я пришел к окончательной версии функции InjectLib. 

    _____________________ Глава 18. Прорыв за пределы процесса 623 

    Версия 0: почему не срабатывает тривиальный метод 
    Простейшее решение, казалось бы, лежит на поверхности: 

    HANDLE hProcessRemote', 
    DWORD dwThreadId: 
    HINSTANCE hinstKml = GetModuleHandle(__TEXT("Kernel32")); 
    CreateRemoteTriread(hProcessRernote, NULL, 0, 
    (LPTHREAD_START_ROUTINE) GetProcAddress( hinstKml, "LoadLibraryA"), 
    "C:\\MYLIB.DLL", 0, &dwThreadId): 

    Этот вызов CreateRemote Thread дает совсем не то, что Вы могли подумать, 
но сначала посмотрим, что же я пытался здесь сделать. А пытался я создать 
поток в удаленном процессе. Этот поток должен был начать с вызова 
LoadLibraryA. К счастью, этой функции нужен единственный 32-битный параметр — 
адрес ANSI-строки с именем загружаемой DLL. LoadLibraryA проецирует DLL на 
адресное пространство процесса, которому принадлежит поток, вызвавший эту 
функцию. После загрузки DLL она вернула бы управление функции StartOfThread, и 
поток был бы завершен. Конечно, MYLIB.DLL так и осталась бы в адресном 
пространстве удаленного процесса, так как счетчик числа ее блокировок никогда 
не уменьшится, — но об этом потом. 
    Понимаете ли Вы, почему приведенный выше код не работает? Все дело в том, 
что строка «C:\\MYLIB.DLL» находится в адресном пространстве вызывающего 
процесса. Вы передаете адрес этой строки (в локальном адресном пространстве) 
удаленному потоку. Когда он вызывает LoadLibraryA, та полагает, что ей передан 
адрес строки в адресном пространстве ее потока, и пытается считать неизвестно 
что. Обычно это приводит к нарушению защиты памяти в удаленном потоке; перед 
пользователем появится окно с сообщением о необработанном исключении, и 
удаленный процесс завершится. Да-да, именно удаленный — не Ваш. Неплохо, да? 
Вы только что «грохнули» чужой процесс, а Ваш продолжает работать как ни в чем 
не бывало! 
    Что тут действительно нужно, так это скопировать строку, идентифицирующую 
DLL, в адресное пространство удаленного процесса. Но куда? Ответ станет ясен 
чуть позже. 
    Не знаю, может, Вам показалось странным, почему я стремился получить адрес 
LoadLibraryA вызовом GetProcAddress вместо того, чтобы попросту написать: 

    CreateRenioteThread(hProcessRemote, NULL, 0, LoadLibraryA, 
"C:\\MYLIB.DLL", 0, &dwThreadId); 

    Причина весьма тонкая. Двоичный файл, получаемый в результате компиляции и 
компоновки Win32-nporpaMMbi, содержит таблицу переходов (jump table), которая 
состоит из серии шлюзов (thunks) к импортируемым функциям. Поэтому, когда Ваш 
код вызывает, скажем, LoadLibraryA, компоновщик на самом деле генерирует вызов 
шлюза в таблице переходов Вашего ЕХЕ- или DLL-файла. В свою очередь таблица 
формирует переход к собственно функции. Компоновщик делает это, чтобы 
сократить время загрузки ЕХЕ- или DLL-файла и сэкономить память. 
    Если поставить прямую ссылку на LoadLibraryA в вызове CreateRemoteThread, 
она будет преобразована компилятором в адрес шлюза LoadLibraryA в таблице 
переходов. А передача адреса шлюза как начального адреса удаленного потока 
приведет к выполнению этим потоком неизвестно чего, и в итоге скорее всего 
возникнет нарушение доступа. Вот поэтому — чтобы обратиться к LoadLibraryA в 
обход таблицы переходов — и надо узнать точный адрес этой функции через GetProcAddress. 
    Вызов CreateRemoteThread подразумевает, что модуль KERNEL32.DLL отображен 
на одни и те же адреса в адресных пространствах локального и удаленного 
процессов. Этот модуль нужен всем процессам, и, как показывает мой опыт, 
система проецирует его по 

    624 ________________________________ Windows для профессионалов 

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

    void __cdecl main(void) { 
    > 

    а затем загрузите PVIEW.EXE, Вы убедитесь, что даже такая «программка» 
требует отображения NTDLL.DLL и KERNEL32.DLL на адресное пространство своего процесса. 

    Версия 1: программирование в машинных кодах 

    Первая версия функции InjectLib, воплощенная мной в реальный код, работала 
примерно так. Сначала я создавал поток в удаленном процессе вызовом CreateRemoteThread: 

    hThread = CreateRemoteThread(hProcess, NULL, 1024, 0х00000000, NULL, 
CREATE_SUSPENDED, &dwThreadId); 

    Это довольно причудливый способ создания потока, поскольку адрес функции 
потока равен 0х00000000. Нарушение доступа гарантировано — как только поток 
начнет выполнение. Но я указал флаг CREATE_SUSPENDED, а это означает, что 
начальное значение счетчика задержек равно 1 и поэтому поток вообще не получит 
процессорного времени. 
    Далее мне нужно было узнать, где находится стек нового потока. Для этого я 
вызывал GetThreadContext и проверял адрес, хранящийся в регистре — указателе стека. 
    Зная этот адрес, я — с помощью WriteProcessMemory — копировал имя DLL в 
стек удаленного потока. Потом создавал буфер в адресном пространстве своего 
процесса и помещал в него машинный код (для процессоров Intel х86), который 
выполнял такие операции: 

    mov   еах, <адрес строки с полным именем DLL в стеке> 
    push  еах          ; заталкиваем адрес полного имени DLL в стек 
    call   LoadLibraryA ; вызываем функцию LoadLibrary 
    push  еах          ; сохраняем в стеке hinstDll нашей DLL (возвращенный в ЕАХ) 
    push  еах          ; еще раз сохраняем hinstDll 
    call   FreeLlbrary ; hinstDll для этого вызова находится в стеке 
    call   ExitThread   ; завершаем поток, используя hinstDll как код завершения 

    Все правильно, я сам брал машинные команды, соответствующие каждой 
инструкции языка ассемблера, и заполнял ими буфер. Затем снова вызвал 
WriteProcessMemory, чтобы записать этот код в стек удаленного потока 
(непосредственно за полным именем DLL). 
    Потом я изменил структуру CONTEXT для удаленного потока так, чтобы 
установить указатель стека на участок памяти перед моим машинным кодом, а 
указатель команд — на первый байт этого кода (см. рис. 18-3). Затем вызвал 
SetThreadContext, чтобы записать новые значения в регистры, связанные с 
удаленным потоком. 

    Контекст удаленного потока Стек удаленного потока                       
(регистры процессора) 
    Полное имя загружаемой DLL    | ^______ Указатель стека = адрес полного 
имени DLL 
    -^———— Указатель команд = адрес функции потока 
    Функция потока                                        и         пни 
(написанная в машинных кодах) 
    Неиспользованный остаток стека 

    Рис. 18-3. Регистры, связанные с удаленным потоком, указывают на 
содержимое стека другого удаленного потока 


    _________________ Глава 18. Прорыв за пределы процесса 625 

    Теперь мне оставалось вызвать ResumeThread, и удаленный поток начал бы 
выполнять мои машинные команды, т. е. загрузил бы DLL, выгрузил бы ее и 
завершил поток. 
    Кое-что в этом способе надо обсудить подробнее. Во-первых, мне пришлось 
самому вызывать ExitThread, так как я изменил указатель команд. Вспомните: для 
потока он инициализируется адресом функции StartOfThread (см. главу 4). 
Побочный эффект от изменения указателя команд состоит в том, что StartOfThread 
не выполняется. Обычно StartOfThread вызывает функцию потока, но так как я 
изменил указатель команд, StartOfThread мою функцию потока не вызывает. 
Следовательно, в конец кода нельзя поместить инструкцию RET (для Intel x86) — 
процессор не знал бы, куда передать управление. И, чтобы корректно завершить 
удаленный поток, приходится явно вызывать ExitThread. 
    Во-вторых, если библиотеку не удалось инициализировать должным образом, 
LoadLibrary A возвращает NULL. В этом месте мой код помещает NULL в стек и 
вызывает freeLibrary. Наверное, правильнее было бы сравнивать значение 
регистра ЕАХ с 0 и вызывать FreeLibrary, только если ЕАХ не равен 0. Я не 
включил в код такую проверку, потому что мне совсем не хотелось самому 
кодировать на машинном языке еще и эти инструкции. Мне казалось, FreeLibrary 
«поймет», что я передаю неверное значение hinstDll, и просто вернет FALSE, 
сообщая о неудачном вызове. 
    В-третьих, пропуск функции StartOfThread означает, что SEH-фрейм, 
создаваемый для потока по умолчанию, установлен некорректно. Это проблема, 
если код потока вызовет исключение. Допустим, мы отмахнулись от нее. В конце 
концов, что может случиться при вызове LoadLibraryA, FreeLibrary и 
ExitThreacP. Но вспомним главу 12: этот поток отвечает и за выполнение DllMain 
(из загружаемой нами библиотеки) со значениями fdwReason, равными 
DLL_PROCESS_ATTACH и DLL_PROCESS_DETACH. Если при этом появится необработанное 
исключение, система немедленно завершит процесс, ничего не сообщив 
пользователю. Конечно, я мог бы написать дополнительный машинный код и сам 
создать нужный SEH-фрейм, но это действительно очень трудно. К тому же SEH 
привязана к архитектуре конкретного процессора и крайне сложна. 
    И наконец, как видите, самая большая проблема — кропотливое кодирование на 
языке ассемблера. Мне как-то не понравилась идея работать на уровне машинных 
кодов, и я отказался от устранения потенциальных проблем. Кроме того, всю эту 
работу пришлось бы проделать заново для каждого типа процессора. И 
представьте, я уже провел ее для MIPS и взялся было за Alpha, когда один мой 
приятель предположил, что все-таки должен быть какой-то способ написать этот 
код как аппаратно-независимый. 
    Признаюсь, поначалу мы с ним были настроены весьма скептически, но потом 
стали обмениваться всякими бредовыми мыслями и в конце концов кое-что 
придумали. Я засел за код (сохранив, конечно, прежний вариант), и через 
несколько часов (что-то около двух ночи) мы все-таки получили нечто, что 
заработало на Intel и MIPS. Так появилась версия 2. 

    Версия 2: A/JocProcessMemory и Create Thread 

    Вот к чему мы пришли: 
    1. Выделяем участок памяти в адресном пространстве удаленного процесса. 
    2. Копируем код функции из адресного пространства нашего процесса в 
адресное пространство удаленного (подробности ниже). 
    3. Копируем в адресное пространство удаленного процесса структуру данных 
INJLIBINFO, содержащую полное имя DLL и другую важную информацию. 
    4. Вызываем CreateRemote Thread, передавая ей адрес скопированной функции 
в удаленном процессе как параметр IpStartAddr, а адрес структуры INJLIBINFO 
как параметр IpvThreadParm. 
    5. Ожидаем завершения удаленного потока. 
    6. Освобождаем память, выделенную на первом этапе. 

    626 ______________________________Windows для профессионалов 

    Теперь рассмотрим эти этапы подробнее. Начнем с того, как выделить и 
освободить память в адресном пространстве другого процесса. 
    Когда я впервые задумался над этой проблемой, то поначалу стал лихорадочно 
искать две \Ут32-функции: VirtualAllocEx и VirtualFreeEx. Я знал, что в Win32 
API есть VirtualQueryEx, позволяющая потоку одного процесса исследовать 
состояние памяти другого процесса, и VirtualProtectEx, дающая возможность 
потоку одного процесса изменять защиту страниц памяти другого процесса. Я был 
уверен, что есть и функции VirtualAllocEx и VirtualFreeEx. Но их нет! И это 
показалось мне чертовски странным. Но ведь должен же быть какой-то другой 
способ выделять память в чужом адресном пространстве! И тут я вспомнил, что 
при создании потока система выделяет ему память для стека, — ЭВРИКА! Если я 
создам удаленный поток, его стек будет создан в адресном пространстве 
удаленного процесса. 

    /анимдниЕ\ 
    В Windows NT 4 функции VirtualAllocEx и VirtualFreeEx все-таки появились, 
и это упрощает внедрение DLL. Но для тех, кто захочет применить этот способ g 
Windows NT 3.51 (и в более ранних версиях), я расскажу, что мне пришлось 
сделать для эмуляции VirtualAllocEx и VirtualFreeEx. Если Вы ориентируетесь 
только на Windows NT 4 (или более поздние версии), удалите мои функции 
AllocProcessMemory и FreeProcessMemory (см. ниже) и замените их вызовами 
VirtualAllocEx и VirtualFreeEx соответственно. 

    Когда это до меня дошло, реализация функций выделения и освобождения 
памяти в адресном пространстве удаленного процесса стала совершенно очевидной. 
Результат — файл PROCMEM.C (см. листинг на рис. 18-4), в котором содержатся 
две функции: AllocProcessMemory и FreeProcessMemory. 
    Рассмотрим сначала FreeProcessMemory. PVOID AllocProcessMemory(HANDLE 
hProcess, DWORD dwNumBytes); 
    Как и предполагает ее имя, эта функция выделяет память в адресном 
пространстве другого процесса. Параметр hProcess идентифицирует нужный 
процесс, a dwNumBytes определяет число выделяемых байтов. Функция возвращает 
адрес выделенной области памяти в адресном пространстве удаленного процесса 
или NULL — если память выделить не удалось. Вот как она работает. Сначала я 
вызываю CreateRemoteThread: 

    HINSTANCE hinstKrn! = GetModuleHandle(__TEXT("Kernel32")): 
    hThread = CreateRemoteThread(hProcess, NULL, dwNumBytes + sizeof(HANDLE), 
    (LPTHREAD_START_ROUTINE)GetProcAddress(hinstKrnl, "ExitThread"), 
    0, CREATE_SUSPENDED, &dwThreadId); 

    Вы знаете, что создание потока заставляет систему выделить память для его 
стека. Поэтому в третьем параметре функции CreateRemoteThread я указываю 
количество байтов, которое надо передать стеку потока (равное числу байтов, 
заданному в параметре функции AllocProcessMemory, плюс размер данных типа 
HANDLE). По причинам, которые Вы поймете позже, мне нужно место для хранения 
описателя потока; я сохранил его в начале выделенного блока памяти. 
    Я сообщаю системе, что поток должен начать выполнение с адреса, по 
которому расположена функция ExitThread. Начав выполнение, поток тут же 
вызовет ExitThread, передав ей 0. Это разумеется, приведет к немедленному 
завершению потока и освобождению памяти, занятой его стеком. Но эта область 
памяти мне пока еще нужна, поэтому я передаю в CreateRemoteThread флаг 
CREATE_SUSPENDED — он сообщает системе, что поток выполнять не надо; регистры 
и стек для потока инициализируются, но процессорное время не выделяется. 
Создав удаленный поток, система помещает его идентификатор в переменную 
dwThreadId и заставляет функцию вернуть описатель потока. 

    ____________________Глава 18. Прорыв за пределы процесса 627 

    Но зачем вообще вызывать ExitThreacP. Ведь создать удаленный поток можно проще: 

    hThread = CreateRemoteThread(hProcess, NULL, dwNumBytes + sizeof(HANDLE), 
    0х00000000. 0, CREATE_SUSPENDED, &dwThreadId); 

    Эта инструкция указывает, что удаленный поток должен начать выполнение с 
адреса 0х00000000, что безусловно вызовет нарушение доступа к памяти, как 
только поток возобновит свое выполнение. Но зачем мне возобновлять его 
выполнение — ведь освободить блок памяти можно и вызовом: 

    TerrriinateThread(hThread, О): 

    Да, TerminateThread заставляет систему завершить поток, но, увы, не 
освобождает память, занятую под стек потока (см. описание функции 
TerminateThread в главе 4). Поэтому, чтобы гарантировать освобождение памяти, 
выделенной в адресном пространстве удаленного процесса, удаленный поток — 
когда его выполнение возобновляется — должен вызвать ExitThread. 
    Теперь, создав удаленный поток, надо определить, где именно в адресном 
пространстве удаленного процесса расположен его стек. Это делается так: 

    CONTEXT Context; 
    Context.ContextFlags = CONTEXT_CONTROL : CONTEXT_INTEGER; 
    GetThreadContext(hThread, &Context): 
    // адрес вершины стека находится в регистре - указателе стека 

    GetThreadContext считывает процессорные регистры указанного потока, в 
данном случае удаленного. Структура CONTEXT содержит значение регистра — 
указателя стека, инициализируемого системой при создании потока, и хранит 
32-разрядный адрес вершины стека. Фактически этот адрес на 4 байта выше 
вершины стека. Когда любая функция помещает что-то в стек, процессор сначала 
уменьшает указатель стека на 4 байта и лишь потом записывает в стек новые 
данные. Вы должны вычесть 4 байта (размер 32-разрядной переменной) из адреса 
указателя стека и тогда получите адрес последнего 32-разрядного значения в стеке. 
    Имя регистра — указателя стека на разных процессорах разное, поэтому я 
написал макрос STACKPTR, который помогает абстрагироваться от имени этого 
регистра. В данной версии это единственная машинно-зависимая часть кода: 

    Sif defined(_X86_) 
    ftdefine STACKPTR(Context) ((Context).Esp) 
    #endif 
    (tif defined(_MIPS_) 
    ffdefine STACKPTR(Context) ((Context).IntSp) 
    ftendif 
    #if defined(_ALPHAJ 
    #define STACKPTR(Context) ((Context).IntSp) 
    Sendif 
    #if defined(_PPC_) 
    ffdefine STACKPTR(Context) ((Context). Gpr-l) 
    #endif 
    tfif !defined(STACKPTR) 
    #error Module contains CPU-specific code; modify and recompile. 
    Sendt f 


    628 ______________________________Windows для профессионалов 

    В самом конце я проверяю, определен ли STACKPTR. Если нет, я прекращаю 
компиляцию, используя директиву #error. Если в будущем Вы захотите 
компилировать этот код на процессоре с новой архитектурой, его придется 
модифицировать так, чтобы абстрагироваться от имени указателя стека и в этой архитектуре. 
    Получив адрес вершины стека, я вызываю VirtualQueryEx: 

    MEMORY_BASIC_INFORMATION mbi: 
    LPVOID pvMem; 
    VirtualQueryEx(hProcess. (PDWORD) STACKPTR(Context) - 1, &mbi, sizeof(mbi)); 
    pvMem = (PVOID) mbi.BaseAddress; 

    Как я уже говорил, эта функция, «заглянув» в адресное пространство другого 
процесса, заполняет структуру MEMORY_BASIC_INFORMATION соответствующими 
данными. Меня интересует элемент BaseAddress, содержащий нижний адрес памяти, 
переданной стеку, т. е. базовый адрес выделенного мной участка памяти. 
    В этот момент я получаю адрес памяти в удаленном процессе, и его надо 
вернуть вызывающей функции. Но, если впоследствии я захочу освободить этот 
блок памяти вызовом ResumeThread, то должен буду где-то сохранить описатель 
удаленного потока. Я мог бы заставить вызывающую функцию передавать 
AllocProcessMemory адрес переменной типа HANDLE, которая заполнялась бы перед 
возвратом из функции, но этот способ мне не понравился. Во-первых, вызывающей 
функции пришлось бы слишком много знать о деталях реализации 
AllocProcessMemory. Во-вторых, вызывающая функция отвечала бы за хранение 
описателя удаленного потока и передачу его корректного значения обратно мне — 
при вызове FreeProcessMemory. 
    Поэтому я решил сохранять описатель потока «на дне» блока памяти, 
выделяемого в адресном пространстве удаленного процесса. Я записываю туда 
описатель с помощью WriteProcessMemory. 

    f0k = WriteProcessMemory(hProcess. pvMem, ShThread, 
    sizeor(riThread), &dwNumBytesXrerred); 

    Далее увеличиваю указатель блока на размер описателя потока и возвращаю 
этот адрес вызывающей функции: 

    pvMem = (PVOID) ((PHANDLE) pvMem +1); 
    return(pvMem); 

    А освобождение памяти еще проще. Желая освободить память в удаленном 
адресном пространстве, локальный поток вызывает FreeProcessMemory. 

    BOOL FreeProcessMemory(HANDLE hProcess, PVOID pvMem); 

    с передачей описателя удаленного процесса и адреса памяти, возвращенного 
предыдущим вызовом AllocProcessMemory. Первое, что делает FreeProcessMemory, — 
получает описатель удаленного потока, который AllocProcessMemory записала в 
первые 4 байта выделенной памяти. Чтобы получить его обратно, нужно вычесть 
размер описателя потока из адреса памяти и вызвать ReadProcessMemory. 

    pvMem = (PVOID) ((PHANDLE) pvMem - 1); 
    f0k = ReadProcessMemory(hProcess, pvMem, &hTherad, 
    sizeof(hThread), &dwNumBytesXferred); 

    Теперь остается лишь возобновить поток, вызвав ResumeThread: 

    ResumeThread(ПТПread); 
    CloseHandle(hThread); 

    Начав выполнение, поток тут же вызовет ExitThread — он завершится, и 
система разрушит его стек. Кроме того, локальный поток должен вызвать 
CloseHandle, чтобы избежать случайного накопления описателей удаленных потоков 
в Вашем процессе при каждом вызове AllocProcessMemory. 

    _____________________Глава 18. Прорыв за пределы процесса 629 


    Функции модуля ProcMem 

    AllocProcessMemory и FreeProcessMemory выделяют и освобождают память в 
адресном пространстве другого процесса. Их код содержится в файле PROCMEM.С 
(см. листинг на рис. 18-4). Любой код, вызывающий эти функции, должен включать 
заголовочный файл PROCМЕМ.Н, также приведенный на рис. 18-4. Исходные тексты 
всех файлов находятся в каталоге TINJLIB на компакт-диске, прилагаемом к 
книге. Если Вы ориентируетесь только на Windows NT 4 (или более поздние ее 
версии), Вам не нужны эти файлы, и вместо содержащихся в них функций 
используйте VirtualAllocEx и Virtual?гееЕх. 

    PROCMEM.C 
    Модуль: РгосМест.С 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    ftinclude "..\CmnHdr.H"              /* см. приложение В */ 
    «include <windows.h> 
    ftinclude "ProcMem.H" 
    //////////////////////////////////////
    ftif defined(_X86_) 
    ftdefine STACKPTR(Context) ((Context).Esp) 
    ftendif 
    #lf defined(_MIPS_) 
    ffdefine STACKPTR(Context) ((Context),IntSp) 
    ftendif 
    ftif defined(_ALPHA_) 
    ffdefine STACKPTR(Context) ((Context).IntSp) 
    ftendif 
    ftif defined(_PPC_) 
    ftdefine STACKPTR(Context) ((Context).Gpr1) 
    ftendif 
    ftif !defined(STACKPTR) 
    (terror Module contains CPU-specific code; modify and recompile. 
    ftendif 
    ///////////////////////////////////
    PVOID AllocProcessMemory (HANDLE hProcess, DWORD dwNumBytes) { 
    CONTEXT Context; 
    DWORD dwThreadId, dwNuffiBytesXferred, dwError; 
    HANDLE hThread: 
    HINSTANCE hinstKrn! = GetModuleHandle(__TEXT("Kernel32")); 
    PVOID pvMem = NULL; 
    MEMORY_BASIC_INFORMATION mbi; 

    см. след. стр. 

    Рис. 18-4. Исходный текст функций из модуля ProcMem 

    630 ______________________________Windows для профессионалов 


    РИС. 18-4 (продолжение) 

    BOOL f0k = FALSE: // предполагаем ошибку 
    __try { 
    hThread = CreateRemoteThread( 
    hProcess, 
    NULL,            // атрибуты защиты по умолчанию 
    dwNumBytes + sizeof(HANDLE), 
    
    // объем памяти, выделяемой в удаленном процессе, 
    // плюс 4 байта на описатель потока 
    (LPTHREAD_START_ROUTINE) GetProcAddress(hinstKrnl, "ExitThread"), 
    // адрес функции, с которой поток начнет выполнение; 
    // передаем адрес ExitThread, чтобы разрушить стек потока 
    О,               // параметр, передаваемый функции потока, 
    // т. е. в ExitThread 
    CREATE_SUSPENDED,   // Флаги. Мы обязаны создать приостановленный 
    // поток, чтобы он не завершился, пока мы 
    // используем выделенную память. 
    &dwThreadId);       // идентификатор нового потока 
    if (hThread == NULL) { 
    dwError = GetLastError(); // для отладки 
    __leave: 
    } 
    Context.ContextFlags = CONTEXT_CONTROL : CONTEXT_INTEGER; 
    if (!GetThreadContext(hThread, &Context)) 
    __leave; 
    
    // определяем нижний адрес переданной памяти 
    if (sizeof(mbi) != VirtualQueryEx(hProcess, 
    (PDWORD) STACKPTR(Context) - 1. &mbi, sizeof(mbi))) 
    __leave: 
    // запоминаем описатель удаленного потока 
    // в первых байтах выделенной памяти 
    pvMem = (PVOID) mbi.BaseAddress; 
    
    f0k = WriteProcessMemory(hProcess, pvMem, ShThread, 
    sizeof(hThread), &dwNumBytesXferred); 
    if (!f0k) 
    „leave;                           ;.              ..   ...   .... 
    // устанавливаем указатель на адрес вслед за описателем потока 
    pvMem = (PVOID) ((PHANDLE) pvMem +1); 
    } 
    „finally { 
    if CfOk) { 

    см. след. стр. 

    ____________________Глава 18. Прорыв за пределы процесса 631 


    РИС. 18-4 (продолжение) 

    if (hThread) { 
    ResumeThread(hThread); 
    CloseHandle(hThread); 
    } 
    i-ivMpm = HIJI I ' 
    } 
    return(pvMern); 
    //////////// //////////// ////////////// ///////////////.' 
    BOOL FreeProcessMemory (HANDLE hProcess, PVOID pvMem) { 
    BOOL fOk; 
    HANDLE hThread; 
    DWORD dwNumBytesXferred; 
    // получаем описатель удаленного потока из блока памяти 
    pvMem = (PVOID) ((PHANDLE) pvMem - 1); 
    
    f0k = ReadProcessMemory(hProcess, pvMem, &hThread, 
    sizeof(hThread), &dwNumBytesXferred); 
    if (fOk) { 
    if (ResumeThread(hThread) == Oxffffffff) { 
    
    // выполнение потока возобновить не удалось 
    // возможно, из-за того, что наша программа 
    // затерла участок памяти, содержащий описатель 
    fOk = FALSE; 
    } 
    CloseHandle(hThread); 
    } 
    return(fOk); 
    i 
    //////////////////////////////////////////
    PROCMEM.H 
    Модуль: ProcMem.H 
    Автор: Copyright © 1995, Джеффри Рихтер (Jeffrey Richter) 
    PVOID AllocProcessMemory (HANDLE hProcess. DWORD dwNumBytes); 
    BOOL FreeProcessMemory (HANDLE hProcess, PVOID pvMeni); 
    IIIIIIH/III I/IIII//I// /II/II/II/I конец файла i /1 /111111 /11 i 11 /11 


    632 _____________________ _______ Windows для профессионалов 


    Функция InjectLib 

    Функция InjectLib (см. листинг на рис. 18-5) демонстрирует внедрение DLL в 
адресное пространство другого процесса. Файлы с исходным кодом для нее 
находятся в каталоге TINJLIB на компакт-диске, прилагаемом к книге. Чуть позже 
я расскажу, как определяется размер памяти, который надо выделить в адресном 
пространстве удаленного процесса. Выделив память, я «впрыскиваю» в этот блок 
памяти функцию ThreadFunc и структуру INJLIBINFO, после чего разрешаю 
выполнение функции. 
    Работая в удаленном процессе, ThreadFunc вызывает LoadLibrary для 
внедрения требуемой функции. Секрет создания машинно-независимой версии 
ThreadFunc в том, что ее надо написать на высокоуровневом языке (в данном 
случае на С), а генерацию машинного кода предоставить компилятору. Затем 
скопировать функцию из своего адресного пространства в адресное пространство 
удаленного процесса и там запустить ее на выполнение. Независимо от типа 
процессора компилятор сгенерирует для этой функции корректный машинный код. 
    Разрабатывая ThreadFunc, я должен был постоянно помнить, что после 
копирования в удаленное адресное пространство функция будет находиться по 
виртуальному адресу, который почти наверняка не совпадет с ее адресом в 
локальном адресном пространстве. Значит, надо написать функцию, не делающую 
внешних ссылок! Это очень трудно. 
    Если точнее, она не может содержать ссылок на глобальные или статические 
переменные, потому что такие ссылки представляют собой конкретные адреса в 
памяти, но в адресном пространстве удаленного процесса этих переменных нет. 
ThreadFunc не может также содержать прямых ссылок на другие функции. Иначе 
компилятор и компоновщик оптимизируют такие вызовы и преобразуют их в ссылки 
на шлюзы (в таблице переходов), которых нет в удаленном процессе. 
    Кроме того, общий размер используемых функцией локальных переменных не 
может превышать размера одной страницы. Всякий раз, обнаружив, что локальные 
переменные данной функции занимают более одной страницы, компилятор генерирует 
скрытый вызов функции (из стандартной библиотеки С), проверяющей переполнение 
стека. Такой вызов — прямая ссылка на другую функцию, которая приведет к 
исключению при выполнении нашей функции в удаленном процессе. 
    Несмотря на все эти ограничения, доступ функции к стеку по-прежнему 
возможен. Поэтому я поместил в стек указатель на структуру INJLIBINFO, 
содержащую всю необходимую для ThreadFunc информацию. В этой структуре 3 элемента: 

    typedef HINSTANCE (WINAPI *PROCLOADLIBRARY)(LPBYTE); 
    typedef BOOL (WINAPI •*PROCFREELIBRARY)(HINSTANCE) 
    typedef struct { 
    PROCLOADLIBRARY fnLoadLibr-ary; 
    PROCFREELIBRARY fnFreeLibrary; 
    BYTE pbLibFile[MAX_PATH * sizeof(WCHAR)]; 
    } INJLIBINFO, *PINJLIBINFO; 

    Первый элемент этой структуры, fnLoadLibrary, содержит абсолютный адрес 
либо LoadLibraryA, либо LoadLibraryW (в зависимости от того, какую строку 
содержит элемент pbLibFile: в ANSI или в Unicode). Второй — fnFreeLibrary— 
хранит абсолютный адрес функции FreeLibrary, а последний — строку (в ANSI или 
в Unicode), определяющую полный путь к загружаемой DLL. 
    При выполнении в удаленном потоке ThreadFunc передается адрес этой 
структуры (адрес скопирован в память, выделенную через AllocProcessMemory). 
ThreadFunc просто вызывает одну из функций LoadLibrary: 

    HINSTANCE hinstDH: 
    hinstDll = pInjLibInfo->fnLoadLibrary(pInjLibInfo->pbLibFile); 


    _____________________Глава 18. Прорыв за пределы процесса 633 

    Вспомните, возврат из этого вызова не происходит, пока DllMain из DLL не 
обработает уведомление DLL_PROCESS_ATTACH. После возврата HINSTANCE нашей DLL 
сохраняется в локальной переменной hinstDll. Затем — после успешной загрузки и 
инициализации библиотеки — ThreadFunc вызывает FreeLibrary. 

    if (hinstDll != NULL) { 
    pin]Liblnfo->fnFreeLibrary(hinstDll); 
    } 

    Возврат из FreeLibrary происходит, только когда DllMain обработает 
уведомление DLL_PROCESS_DETACH. 
    В конце концов ThreadFunc возвращает HINSTANCE загруженной DLL, а если 
загрузка DLL была неудачной — NULL. Возвращенное значение становится кодом 
завершения потока. Объект ядра, соответствующий удаленному потоку, продолжает 
существовать в системе, пока у нашего потока есть описатель удаленного. И 
чтобы получить код завершения потока, можно вызвать GetExitCodeThread. Таким 
образом, поток, выполняемый в локальном процессе, определит, успешно ли 
загружена DLL. 
    Обратите внимание на два преимущества ThreadFunc перед первой версией 
функции потока. Самое главное — нет надобности самому составлять код на 
машинном языке. А второе — ThreadFunc вызывается из функции StartOfThread. Это 
значит, что, закончив свою работу, ThreadFunc не нужно обращаться к ExitThread 
— она просто передает управление вызывающей функции. Кроме того, SEH-фрейм, 
создаваемый по умолчанию, настроен корректно и готов перехватывать 
необработанные исключения. 

    Функции Injectiib, InjectLibA, InjectLibWw InjectLibWorA 

    Теперь посмотрим на функцию, ответственную за внедрение ThreadFunc в 
адресное пространство удаленного процесса. Так как я хотел создать законченную 
библиотечную функцию, то предусмотрел в ней 2 точки входа: для ANSI и для 
Unicode. Таким образом, я написал 2 версии функции: одну для ANSI 
(InjectLibA), а другую для Unicode (InjectLibW); кроме того, я создал макрос 
InjectLib, который раскрывается в одну из этих функций в зависимости от того, 
определен ли UNICODE при компиляции. 

    BOOL InjectLibA (HANDLE hProcess, LPCSTR IpszLibFile); 
    BOOL InjectLibW (HANDLE hProcess, LPCWSTR IpszLibFile); 
    ftifdef UNICODE 
    (tdefine InjectLib InjectLibW 
    ffelse 
    Sdefine InjectLib InjectLibA 
    #endif // 'UNICODE 

    Оба прототипа функций и макрос определены в файле INJLIB.H (см. рис. 18-5). 
    Функции InjectLibA и InjectLibW— это заглушки, которые просто вызывают 
настоящую рабочую функцию InjectLibW'orA. Последняя является статической 
функцией, и ее нельзя вызвать извне модуля INJLIB.C (см. рис. 18-5). Вызывая 
InjectLibW'orA, заглушки передают ей: 
    • описатель процесса, в который следует внедрить DLL; 
    • адрес строки с полным именем DLL (строка может быть как в ANSI, так и в Unicode); 
    • Булево значение, определяющее тип строки IpszLibFile (TRUE соответствует 
Unicode, а FALSE — ANSI). 
    Значение, возвращаемое InjectLibW'orA, указывает, успешно ли загружена DLL 
в удаленный процесс. Функции InjectLibA и InjectLibW просто передают код 
возврата InjectLibWorA вызвавшей их программе. 

    634 ______________________________Windows для профессионалов 

    BOOL InjectLibA (HANDLE hProcess, LPCSTR IpszLibFile) { 
    return(InJectLibWor-A(hProcess, (LPBYTE) IpszLibFile, FALSE)); 
    } 
    BOOL InjectLibW (HANDLE hProcess, LPCWSTR IpszLibFile) { 
    return(InjectLibWorA(hProcess, (LPBYTE) IpszLibFile, TRUE)); 
    ( 

    Итак, каким же образом InjectLibWorA внедряет DLL в адресное пространство 
другого процесса? На первом этапе создается и инициализируется структура INJLIBINFO: 

    HINSTANCE hinstKrnI = GetModuleHandle(__TEXT("Kernel32")); 
    INJLIBINFO InjLibInfo; 
    InjLibInfo.fnLoadLibrary = (PROCLOADLIBRARY) GetProcAddress(hinstKrnl, 
    (fUnicode ? "LoadLibraryW" : "LoadLibraryA")); 
    InJLibInfo.fnFreeLibrary = (PROGFREELIBRARY) GetProcAddress(hinstKrnl, "FreeLibrary"); 
    InjLiblnfo,pbLibFile[0] = 0; // инициализируем позже 
    if (fUnicode) 
    wcscpy((LPWSTR) In]LibInfo.pbLibFile, (LPCWSTR) pbLibFile); 
    else 
    strcpy((LPSTR) InjLibInfo.pbLibFile, (LPCSTR) pbLibFile); 

    Эта структура копируется в блок памяти, выделенный в удаленном процессе, и 
инициализируется абсолютными адресами функций LoadLibraryA (или LoadLibraryW) 
и FreeLibrary. Затем полное имя DLL копируется из параметра pbLibFile в 
одноименный элемент структуры INJLIBINFO. 
    В коде подразумевается, что модуль KERNEL32.DLL отображен на адресное 
пространство как локального, так и удаленного процесса — причем по одному и 
тому же адресу. Как я уже говорил, тут нет никакого риска — я еще ни разу не 
видел, чтобы было иначе. 
    Теперь определим размер блока памяти, выделяемого в адресном пространстве 
удаленного процесса. Этот блок будет содержать копию ThreadFunc и структуры 
INJLIBINFO. Размер памяти, занимаемой кодом ThreadFunc, определяется 
вычитанием ее адреса в памяти из адреса расположенной за ней функции AfterThreadFunc. 

    const int cbCodeSize = ((LPBYTE) (DWORD) AfterThreadFunc - (LPBYTE) 
(DWORD) ThreadFunc); 

    Здесь имеется в виду, что компилятор и компоновщик разместят 
AfterThreadFunc в объектном коде сразу за ThreadFunc. Компиляторы для 
процессоров Intel, MIPS, Alpha и PowerPC, поставляемые с Visual C++, делают 
именно так. Но мне говорили, что один компилятор для Alpha (рассчитанный на 
операционные системы, отличные от Windows NT) помещает скомпилированные 
функции в объектный файл в обратном порядке. Вполне возможно, что на других 
компиляторах под Windows NT результат вычитания адреса ThreadFunc из адреса 
AfterThreadFunc будет иным, и тогда этот код «прикажет долго жить». 
    Впрочем, даже если и найдется компилятор под Windows NT, который обращает 
порядок размещения функций, все равно эту проблему легко скорректировать. Надо 
просто добавить функцию перед ThreadFunc и назвать ее BeforeThreadFunc. Затем 
сравнить адрес Before ThreadFunc с адресом AfterThreadFunc и вычесть адрес 
ThreadFunc из наибольшего значения. Конечно, нет гарантий, что в будущем 
какой-нибудь компилятор не станет размещать функции совершенно иначе, но, 
думаю, это маловероятно. 
    Итак, сейчас у нас есть размер кода ThreadFunc, но нам нужна и память, 
достаточная для размещения структуры INJLIBINFO: 

    const DWORD cbMemSize = CbCodeSize + sizeof(InjLibInfo) + 3: 


    _____________________Глава 18. Прорыв за пределы процесса 635 

    Вы заметили, что я добавил 3 к размеру памяти? Это из-за того, что все 
структуры и код должны быть выровнены по 32-битной границе. Например, если 
функция ThreadFunc требует 65 байтов, а я размещу структуру INJLIBINFO 
непосредственно за этим кодом, структура начнется с 65-го байта. Как только 
ThreadFunc попытается обратиться к этой структуре, процессор возбудит 
исключение из-за неправильного выравнивания данных. Для процессоров х86 тут 
нет ничего страшного, так как они автоматически выравнивают подобные данные, 
но на RISC-архитектурах это, действительно, проблема. Разместив INJLIBINFO 
перед кодом ThreadFunc, Вы ничего бы не добились. Поэтому при вычислении 
размера требуемого блока памяти, я просто предполагаю, что в худшем случае мне 
придется оставить 3-байтовый промежуток между кодом и структурой. 
    Память в адресном пространстве удаленного процесса выделяется вызовом моей 
функции AllocProcessMemory. 

    pdwCodeRemote = (PDWORD) AllocProcessMemory(hProcess, cbMemSize); 

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

    f0k = VirtualProtectEx(hProcess, pdwCodeRemote, cbMemSize, 
PAGE_EXECUTE_READWRITE, &dw01dProtect); 

    Эта проблема из числа тех, что долгое время остаются незамеченными. И 
действительно, я обнаружил ее, только когда писал эти строки — уже отработав 
код для платформ х86, MIPS и Alpha. Как это я ухитрился? Работая над кодом, 
надо ведь иногда тестировать его, тогда и заметишь, что удаленный поток 
возбуждает исключение, так? Так, да не так. Win32 поддерживает разные атрибуты 
защиты страниц, но процессор может и не поддерживать их. И в самом деле, 
процессоры х86, Alpha, MIPS и PowerPC игнорируют защиту страниц от исполнения. 
Они «считают»: раз страницу можно читать, значит, ее можно исполнять. Поэтому 
— и по эстетическим соображениям тоже — я решил добавить показанный выше вызов 
VirtualProtectEx. 
    Теперь самое время скопировать ThreadFunc и структуру INJLIBINFO в память, 
выделенную в адресном пространстве удаленного процесса: 

    f0k = WriteProcessMemory(hProcess, pdwCodeRemote, 
    (LPVOID) (DWORD) ThreadFunc, cbCodeSize, &dwNumBytesXferred); 
    
    // принудительно выравниваем структуру по 32-битной границе 
    PINJLIBINFO pInjLibInroRemote = (PINJLIBINFO) 
    (pdwCodeRemote + ((cbCodeSize + 4) & ~3)); 
    f0k = WriteProcessMemory(hProcess, pInjLibInfoRemote, &InjLibInfo, 
sizeof(InjLibInfo), &dwNumBytesXferred); 

    Итак, в удаленном процессе все готово. Следующий шаг — создание удаленного 
потока, который будет выполнять функцию ThreadFunc, используя данные из 
структуры INJLIBINFO. Поскольку он будет работать в асинхронном режиме по 
отношению к Вашему локальному потоку, последний следует перевести в состояние 
ожидания на то время, пока удаленный поток не закончит загрузку DLL и не завершится: 

    HANDLE hThread = CreateRemo1:eThread(hProcess, NULL, 0, 
    (LPTHREAD_START_ROUTINE) (DWORD) pdwCodeRemote, 
    pInjLibInfoRemote, 0, &dwThreadId); 
    WaitForSingleOb]ect(hThread, INFINITE): 


    636 ______________________________Windows для профессионалов 

    Когда удаленный поток завершится, надо будет узнать его код завершения. 
Этот код — HINSTANCE библиотеки, загруженной удаленно. Если HINSTANCE равен 
NULL, Вы узнаете, что инициализация DLL прошла неудачно; при этом 
InjectLibWorA возвращает FALSE (в ином случае — TRUE). Как только мы получили 
код завершения удаленного потока, его описатель нам больше не нужен, и память, 
выделенную в адресном пространстве удаленного процесса, можно освободить: 

    GetExltCodeThread(hThread, (PDWORD) &hinstDllRemote); 
    CloseHandle(hTriread); 
    FreeProcessMemory(hProcess, pdwCodeRemote); 
    return (hinstDIIRemote != NULL): 
    INJLIB.C 
    Модуль: InjLib.C 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    
    ((include "..\CmnHdr.H"               /* см. приложение В */ 
    ((include <windows,h> 
    ((include "ProcMeiri. H" 
    {(include "InjLib.H" 
    /////////////////////////////////////////////////////////////////////////////// 
    typedef HINSTANCE (WINAPI *PROCLOADLIBRARY)(LPBYTE): 
    typedef BOOL (WINAPI *PROCFREELIBRARY)(HINSTANCE); 
    typedef struct { 
    PROCLOA'DLIBRARY fnLoadLibrary: 
    PROCFREELIBRARY fnFreeLibrary: 
    BYTE pbLlbFile[MAX_PATH * sizeof(WCHAR)]; 
    } INJLIBINFO, *PINJLIBINFO; 
    /////////////////////////////////////////////////////////////////////////////// 
    
    / I вызовы подпрограммы контроля стека следует отключить 
    «pragma check_stack (off) 
    
    static DWORD WINAPI ThreadFunc (PINJLIBINFO pInjLibInfo) { 
    // размер локальных переменных, используемых этой функцией, 
    // должен быть меньше страницы 
    HINSTANCE hinstDll: 
    
    // вызываем LoadLibrary(A/W) для загрузки DLL 
    hinstDll = pInjLibInfo->fnLoadLibrary(pInjLibInfo->pbLibFile); 
    
    // Вызов LoadLibrary заставляет ситему спроецировать DLL на 
    // адресное пространство удаленного процесса и вызвать DllMain 
    // этой библиотеки со значением fdwReason, равным DLL_PROCESS_ATTACH, 

    см. след. стр. 

    Рис. 18-5. Реализация функции InjectLib 

    _____________________Глава 18. Прорыв за пределы процесса 637 


    РИС. 18-5 (продолжение) 

    II При обработке этого уведомления DLL может делать все что угодно. 
    // После возврата из DllMain система возвращает из вызова LoadLibrary 
    // HINSTANCE этой DLL. И здесь мы вызываем FreeLibrary, передавая 
    // ей этот HINSTANCE, чтобы выгрузить DLL. 
    
    // Если DLL не удалось загрузить или если функция DllMain 
    // (DLL_PROCESS_ATTACH) возвращает FALSE, hinstDll будет равен NULL. 
    
    // Если библиотека инициализирована успешно, выгружаем ее. 
    if (hinstDll != NULL) { 
    
    // вызов FreeLibrary заставит систему вызвать DllMain библиотеки 
    // с кодом DLL_PROCESS_DETACH; DLL сможет провести нужную очистку 
    pinjLlbInfo->fnFreeLlbгагу(hinstDll); 
    
    // код завершения потока - описатель DLL 
    return((DWORD) hinstDll): 
    \ i 
    ,11./7//////7//////////////////7//////1//7/7///7//////////////////'/////////'////1//1.••1 
    // эта функция отмечает адрес памяти непосредственно за ThreadFunc: 
    // ThreadFuncCodeSizeInBytes = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc 
    static void AfterThreadFunc (void) { 
    } 
    ffpragma check_stack 
    ////////////////////////////////////////////////,///7/,//,/////////,/////////////// 
    static BOOL InjectLlbWorA (HANDLE hProcess, 
    const BYTE * const pbLibFile, BOOL fUnicode) { 
    
    // HINSTANCE модуля Kernel32.DLL используется для получения адресов 
    // функций LoadLibraryA (или LoadLibraryW) и FreeLibrary 
    HINSTANCE hinstKrnI = GetModuleHandle(__TEXT("Kernel32")): 
    INJLIBINFO InjLibInfo; 
    
    // адрес в удаленном процессе, по которому копируется код 
    PDWORD pdwCodeRemote = NULL; 
    // вычисляем число байтов в коде функции ThreadFunc 
    (tifndef _PPC_ 
    const int cbCodeSize = ((LPBYTE) (DWORD) 
    AfterThreadFunc - (LPBYTE) (DWORD) ThreadFunc): 
    seise 
    // Внимание: процессор PowerPC не поддерживает непосредственные 
    // 32-битные значения (32-bit immediates). Поэтому компилятор VC++ 
    // для PowerPC реализует такие значения через смещения в таблице, 
    // в которой хранятся собственно значения. Ссылки на приведенные 
    OVf. OU'tl. CHJp. 

    638 _____________________________- Windows для профессионалов 


    РИС. 18-5 (продолжение) 

    // ниже функции представляют собой смещения в этой таблице. 
    // Поэтому, чтобы получить размер кода, надо вычитать содержимое, 
    // находящееся по этим смещениям. 
    const int cbCodeSize = ((LPBYTE) *(PDWORD) 
    AfterThreadFunc - (LPBYTE) *(PDWORD) ThreadFunc); 
    tfenni f 
    .// адрес в удаленном процессе, по которому копируется 
    // структура INJLIBINFO 
    PINJLIBINFO pInJLibInfoRemote = NULL; 
    
    // число байтов, копируемых в удаленный процесс 
    DWORD dwNumBytesXferred = 0: 
    
    // описатель и идентификатор потока, выполняющего 
    // удаленную копию ThreadFunc 
    DWORD dwThreadId = 0; 
    const DWORD cbMemSize = cbCodeSize + sizeof(InjLibInfo) + 3; 
    HANDLE hThread = NULL; 
    HINSTANCE hinstDnRemote - NULL' 
    BOOL fOk = FALSE; 
    DWORD dwOldProtect; 
    // инициализируем структуру INJLIBINFO, а затем копируем ее 
    // в память удаленного процесса 
    InjLibInfo,fnLoadLibrary = (PROCLOADLIBRARY) GetProcAddress(hinstKrnl, 
    (fUnicode ? "LoadLibraryW" : "LoadLibraryA")); 
    IniLiblnfo.fnFreeLibrary = (PROCFREELIBRARY) 
    GetProcAddress(hinstKrnl, "FreeLibrary"); 
    InjLibInfo.pbLibFile[0] = 0; // инициализируем позже 
    -_try { 
    
    // завершаем инициализацию структуры INJLIBINFO, 
    // скопировав полное имя загружаемой DLL 
    if (fUnicode) 
    wcscpy((LPWSTR) InjLibInfo.pbLibFile, (LPCWSTR) pbLibFile); 
    else 
    strcpy(CLPSTR) InjLibInfo.pbLibFile, (LPCSTR) pbLibFile): 
    
    // выделяем память в адресном пространстве удаленного процесса, 
    // достаточную для хранения кода ThreadFunc и структуры INJLIBINFO 
    pdwCodeRemote = (PDWORD) AllocProcessMemory(hProcess, cbMemSize); 
    if (pdwCodeRemote == NULL) __leave; 
    // изменяем атрибуты защиты страниц выделенной памяти 
    // на "исполнение, чтение, запись" 

    см. след. стр. 

    _____________________Глава 18. Прорыв за пределы процесса 639 


    РИС. IS^S (продолжение) 

    f0k = VirtualProtectEx(hProcess, pdwCodeRemote, 
    cbMemSize, PAGE_EXECUTE_REAOWRITE, &dw01dProtect); 
    if (!f0k) 
    „leave; 
    // записываем копию ThreadFunc в удаленный процесс 
    f0k = WriteProcessMeniory(hProcess, pdwCodeRemote, 
    (LPVOID)(DWORD) ThreadFunc, cbCodeSize, &dwNumBytesXferred); 
    if (!f0k) 
    __leave; 
    // записываем копию INJLIBINFO в память удаленного процесса 
    // (структура ДОЛЖНА быть выровнена по четной 32-битной границе) 
    pInjLibInfoRemote = (PINJLIBINFO) 
    ((PBYTE.) pdwCodeRemote + ((CbCodeSize + 4) & ~3)); 
    // помещаем InjLibInfo в блок памяти в удаленном потоке 
    f0k = WriteProcessMemory(hProcess, pInjLibInfoRemote, 
    &InjLibInfo, sizeof(InjLibInfo), &dwNumBytesXferred); 
    if (!f0k) 
    „leave; 
    hThread = CreateRernoteThread(hProcess, NULL, 0, 
    (LPTHREAD_START_ROUTINE)(DWORD) pdwCodeRemote, 
    pInjLibInfoRemote. 0,' &dwThreadId); 
    if (hThread == NULL) 
    __leave; 
    WaitPorSingleObject(hThread, INFINITE); 
    } // „try 
    __finally { 
    if (hThread != NULL) { 
    GetExitCodeThread(hThread. (PDWORD) &hinstDllRemote); 
    CloseHandle(hThread); 
    // начинаем выполнение удаленным потоком функции ThreadFunc 
    // с использованием измененного нами стека, который сейчас 
    // содержит инициализированную структуру INJLIBINFO 
    FreeProcessMeinory(hProcess, pdwCodeRemote); 
    } //„finally 
    // возвращаем TRUE, если загрузка DLL прошла успешно 
    return(hihst011Remote != NULL); 
    } 
    HHIIII IIIllllllllll lllll/lllllllll lllllllllllll/I IIIIIIIIIIIIII IIIIIIIIIIH/1 

    см. след. стр. 

    640 ______________________________Windows для профессионалов 


    РИС. 18-5 (продолжение) 

    BOOL WINAPI InjectLibA (HANDLE hProcess, LPCSTR IpszLibFile) { 
    return(InjectLibWorA(hProcess, (LPBYTE) IpszLibFile, FALSE)); 
    /////////////////////////////////////////////////////////////////////////////// 
    BOOL WINAPI InjectLibW (HANDLE hProcess, LPCWSTR IpszLibFile) { 
    return(InJectLibWorA(hProcess, (LPBYTE) IpszLibFile, TRUE)); 
    ////////////////////////////////// конец файла ///////////////////////////////// 
    INJLIB.H 
    /,.„.,.,..„„.*.**.„..„*...*.*.*„...*„„.,,.,.».„.*,..*,.*.«**.^»**<*..* 
    Модуль: InjLib.H 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    BOOL WINAPI InjectLibA (HANDLE hProcess, LPCSTR IpszLibFile); 
    BOOL WINAPI InjectLibW (HANDLE hProcess. LPCWSTR IpszLibFile); 
    ffifdef UNICODE 
    «define InjectLib InjectLibW 
    #else 
    Bdefine InjectLib InjectLibA 
    sendif // 'UNICODE 
    //7/////////////////////////////// конец файла I Ц / I I I I I H I i I I I 
I I I I I I I I I I I I I I I I i 


    Тестирование функции InjectUb 

    Написав функцию InjectLib, мне пришлось изобрести способ ее тестирования. 
В следующем разделе я расскажу, как мне это удалось. Протестировать нужно было 
2 компонента InjectLib. Поэтому я написал программу TInjLib (она вызывает 
InjectLib) и DLL для внедрения в удаленный процесс. Последний модуль считывает 
информацию, специфичную для процесса, в который он внедрен. Если информация, 
получаемая мной от DLL, выглядит правдоподобно, то, зная процесс, в которую 
внедрена DLL, я могу считать, что функция InjectLib работает корректно. 

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

    Программа TInjLib (TINJLIB.EXE) (см. листинг на рис. 18-6) демонстрирует, 
как вызывать функцию InjectLib. Файлы исходного кода и ресурсов для этой 
программы находятся в каталоге TINJLIB на компакт-диске, прилагаемом к книге. 
Программа принимает единственный параметр в командной строке — идентификатор 
какого-нибудь выполняемого в данный момент процесса. Это значение можно 
получить с помощью утилит PVIEW.EXE или 

    _____________________Глава 18. Прорыв за пределы процесса 641 

    PSTAT.EXE, поставляемых с Visual C++, или через Task Manager операционной 
системы Windows NT 4. Зная идентификатор, программа открывает описатель этого 
процесса, для чего вызывает OpenProcess и запрашивает нужные права доступа: 

    hProcess = OpenProcess( 
    PROCESS_CREATE_THREAD :       // для CreateRemoteThread 
    PROCESS_QUERY_INFORMATION :   // для VirtualQueryEx 
    PROCESS_VM_OPERATION ;        // для VirtualProtectEx, VirtualAllocEx 
    
    // и VirtualFreeEx 
    PROCESS_VM_READ :             // для ReadProcessMemory 
    PROCESS_VM_WRITE,             // для WnteProcessMemory 
    FALSE, dwProcessId): 

    Если OpenProcess вернет NULL, TInjLib не сможет открыть описатель 
процесса. Это может произойти в системе, работающей в режиме повышенной 
безопасности, или если программа пытается открыть описатель защищенного 
процесса. \Уш32-подсистема и некоторые процессы (например, WinLogon, ClipSrv и 
EventLog) защищены настолько, что программа не получит их описатели, если 
запросит права доступа, указанные выше. 
    При удачном обращении к OpenProcess программа TInjLib создает буфер, куда 
записывает полное имя внедряемой DLL, и вызывает InjectLib. После возврата из 
InjectLib программа выводит окно, в котором сообщает, внедрена ли DLL в 
удаленный процесс; потом закрывает описатель этого процесса — вот и все. 
    Просматривая код, Вы наверняка заметите, что я делаю специальную проверку, 
не равен ли нулю идентификатор процесса, переданный в командной строке. Если 
да, я использую идентификатор самой TINJLIB.EXE, вызывая GetCurrentProcessId. 
Тогда при вызове InjectLib программа внедряет DLL в свое адресное 
пространство. Я сделал это для упрощения отладки. Сами понимаете, при 
возникновении ошибки иногда трудно определить, в каком процессе она находится: 
локальном или удаленном. Поначалу я отлаживал код с помощью двух отладчиков: 
один наблюдал за TInjLib, другой — за удаленным процессом. Это оказалось 
страшно неудобно. Потом меня осенило, что TInjLib способна внедрить DLL и в 
себя, т. е. в адресное пространство вызывающего процесса. И это сразу 
упростило отладку. 

    TINJLIB.C 
    Модуль: TInjLib.С 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    «include "..\CmnHdr.H"              /* см. приложение В */ 
    «include <windows.h> 
    «include <windowsx.h> 
    «include <stdio.h> 
    «include <tchar.h> 
    «include "InjLib.H" 
    «include "Resource.h" 
    Illlllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllillll/l 

    см. след. стр. 

    Рис. 18-6. Листинг программы TInjLib 

    642 ___________________________.._Windows для профессионалов 


    РИС. 18-6 (продолжение) 

    BOOL Dlg_0nlnit0ialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    // связываем значок с диалоговым окном 
    chSETDLGICONS(hwnd, IOI_TINJLIB, IDI_TINJLIB); 
    returnfTPIJF): 
    ////// ///////////// ////////У////// ////////////// //////////////// ///////////// 
    void Dlg_OnCommand (HWND hwnd. int id, HWND hwndCtI, UINT codeNotify) { 
    BOOL fTranslated; 
    DWORD dwProcessId; 
    HANDLE hProcess; 
    switch (id) { 
    case IDC_INJECT: 
    dwProcessId = GetDlgItemInt(hwnd, IDC_PROCESSID, &fTranslated, FALSE); 
    if (dwProcessId == 0) { 
    
    // если идентификатор процесса 0, то все происходит 
    // в локальном процессе; это упрощает отладку 
    dwProcessId = GetCurrentProcessIdO; 
    ) 
    // получаем описатель целевого процесса 
    hProcess = OpenProcess( 
    PROCESS_CREATE_THREAD ;       // для CreateRemoteThread 
    PROCESS_QUERY_INFORMATION :   // для VirtualQueryEx 
    PROCESS_VM_OPERATION !        // для VirtualProtectEx 
    PROCESS_VM_READ ;             // для ReadProcessMemory 
    PROCESS_VM_WRITE,             // для WriteProcessMemory 
    FALSE, dwProcessId); 
    if (hProcess == NULL) { 
    chMB((GetLastError() == ERROR_ACCESS_DENIED) ? 
    __TEXT("Insufficient access to process") 
    : __TEXT("Invalid process Id")); 
    } else { 
    TCHAR szLibFile[MAX_PATH]: 
    GetModuleFileName(NULL, szLibFile, sizeof(szLibFile)); 
    _tcscpy(_tcsrchr(szLibFlle, 
    __TEXT('\\')) + 1, __TEXT("ImgWalk,DLL")); 
    chMB(InjectLlb(hProcess, szLibFile) ? 
    __TEXT("Remote DLL loaded") : 
    __TEXT("Remote DLL failed load")); 
    GloseHandle(hProcess); 
    } 
    break; 

    см. след. стр. 

    _____________________Глава 18. Прорыв за пределы процесса 643 


    РИС. 18-6 (продолжение) 

    case IDCANCEL: 
    EndDialogthwnd, id): 
    break; 
    v 
    ///////////////////////////////////////
    BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg. WPARAM wParam, LPARAM 
IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, DIg^OnInitDialog): 
    chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); 
    } 
    return(FALSE): 
    ////////////////////////////////////////////////
    int WINAPI _tWinMain (HINSTANGE hinstExe, 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) { 
    chWARNIFUNICODEUNDERWIN95(); 
    DialogBox(hinstExe, MAKEINTRESOURCE(IDD_TINJLIB), NULL, Dlg_Proc); 
    return(O); 
    ///////////////// конец файла ///////////////////////////////// 


    Динамически подключаемая библиотека IMGWALK.DLL 

    IMGWALK.DLL (см. листинг на рис. 18-7) — это динамически подключаемая 
библиотека, которая, будучи внедрена в адресное пространство процесса, выдает 
список всех DLL, используемых этим процессом. Файлы исходного кода и ресурсов 
для этой библиотеки находятся в каталоге IMGWALK на компакт-диске, прилагаемом 
к книге. Если, например, сначала запустить Notepad, а затем TInjLib, передав 
ей идентификатор процесса Notepad, то TInjLib внедрит IMGWALK.DLL в адресное 
пространство Notepad. Попав туда, ImgWalk определит, образы каких файлов (ЕХЕ 
и DLL) использует процесс Notepad, и покажет результаты в окне (см. 
иллюстрацию на следующей странице). 
    На первый взгляд кажется практически невозможным сделать то, что делает 
ImgWalk, не пользуясь недокументированными функциями. Но информацию об образе 
процесса можно получить путем его отладки. Правда, здесь есть проблемы, 
связанные с созданием отладчика, и самая большая в том, что отладчик, 
подключенный к отлаживаемой программе, не в состоянии отключиться от нее, пока 
та не завершится. Функция InjectLib свободна от этого недостатка — она 
подключает DLL к процессу, а затем отключает от него. 

    644 ______________________________Windows для профессионалов 


    01 B40000-H;\WINNT\system32\riotepad.exe 
    20000000-H:\MSDEV\BIN\SPY'HK40.DLL 
    76AAOOOO-H:\WINNT\Svstem32\INDICDLL.dll 
    779DOOOO-H:\WIMNT^system32\MSVCRT40.dll 
    779FOOOO-H:\WINNT\system32\MSVCRT.dll 
    77BFOOOO-H:\WINNT\systern32\COMCTL32.dll 
    77C40000-H:\WINNT\systern32',,SHELL32.dll 
    77DBOOOO-H:\WINNT\system32\comdlg32.dll 
    77DCOOOO-H:\WINNT\system32^DVAPI32.dll 
    77E20000-H:\WINNT\syslern32\RPCRT-1.dll 
    77E70000-H:\WINNT\syslem32\USER32.dll 
    77EDaOOO-H:\WIMMT^system32\GD132.dll 
    77FOOOOO-H:\WINNT\system32\l<ERNEL32.dll 
    77F60nOO-H:\WINNT\System32\ritdlLdll 
    780AOOOO-H;'\WINN'niSy8tem32\MSVCIRT.dll 
    T~o^j 

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

    while (Vir'tualQuery(lp, &mbi, sizeof(mbi)) == sizeof(mbi)) { 
    if (mbi.State == MEM_FREE) 
    mbi.AllocationBase = mbi.BaseAddress; 
    if ((mbi.AllocationBase == hinstDll) ',', 
    (mbi.AllocationBase != mbi.BaseAddress) !! 
    (mbi.AllocationBase == NULL)) { 
    // Имя модуля не включается в список, если 
    // истинно хотя бы одно из следующих условий: 
    // 1. Данный регион содержит нашу DLL. 
    // 2. Данный блок не ЯВЛЯЕТСЯ началом региона. 
    // 3, Адрес равен NULL. 
    nLen = 0; 
    } else { 
    nLen = GetModuleFileName((HINSTANCE) mbi.AllocationBase, 
    szModName, ARRAY_SIZE(szModName)); 
    if (nLen > 0) { 
    _stprintf(_tcschr(szBuf, 0), __TEXT("\n%08X-%s"), 
    mbi.AllocationBase, szModName); 
    Ip += mbi.RegionSize; 
    J 

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

    ____________________Глава 18. Прорыв за пределы процесса 645 

    загруженный модуль, и помещает в буфер szModName полное имя (вместе с 
путем) этого модуля. Затем я присоединяю HINSTANCE данного модуля (базовый 
адрес) и его полное имя к строке szBuf, которая в конечном счете и появится в 
окне. Когда цикл заканчивается, DLL открывает на экране окно со списком. 
    IMGWALK.C 
    Модуль: ImgWalk.C 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    ^„^^^^^^^,^^^^^„,.^^,^^^,.,.„.,«„.^„./ 
    ^include ",,\CmnHdr.H"              /* см. приложение В */ 
    ffinclude <windows.h> 
    einclude <stdio.h> 
    ftinclude <tchar,h> 
    ll/lllllll lll/lllllllll ll/llllllll/l/tll llllllHII/lllll/l lllllllllll lll/llllll 
    BOOL WINAPI DllMain (HINSTANCE hinstDll, DWORD fdwReason, 
    LPVOID IpvReserved) { 
    TCHAR szBuf[MAX_PATH * 30], szModNa(l)e[MAX_PATH]; 
    if (fdwReason == DLL_PROCESS_ATTACH) { 
    LPBYTE Ip = NULL; 
    MEMORY_BASIC_INFORMATION mbi; 
    int nLen; 
    szBuf[0] = 0; 
    szBuf[1] = 0; 
    while (VirtualQuery(lp, &mbi, sizeof(mbi)) == sizeof(mbi)) { 
    if (mbi.State == MEM_FREE) 
    mbi.AllocationBase = mbi.BaseAddress: 
    if ((mbi.AllocationBase == hinstDll) !! 
    (mbi.AllocationBase != mbi. BaseAddress) ',', 
    (mbi.AllocationBase == NULL)) { 
    // Имя модуля не включается в список, если 
    // истинно хотя бы одно из следующих условий: 
    // 1. Данный регион содержит нашу DLL. 
    // 2. Данный блок не ЯВЛЯЕТСЯ началом региона. 
    // 3. Адрес равен NULL, 
    nLen = 0; 
    } else { 
    nLen = GetModuleFileName( 
    (HINSTANCE) mbi.AllocationBase, 
    szModName, chDIMOF(szModName)); 

    см. след. стр. 

    Рис. 18-7. Листинг IMGWALK.DLL 

    646 ______________________________Windows для профессионалов 


    РИС. 18-7 (продолжение) 

    if (nLen > 0) { 
    _stpnntf(_tcschr(szBuf, 0), __TEXT( "\n%08X-%s"), 
    mbi.AllocationBase, szModName): 
    i 
    Ip += mbi.RegionSize; 
    } 
    chMB(&szBuf[1]); 
    } 
    return(TRUE); 
    i 
    ////////////////////////////////// конец файла ///////////////////////////////// 


    Заключение 

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

    Параметр реестра   Ловушки    Удаленные 

    Критерии                                   Applnit_DLLs                  потоки 

    Работает в Windows 95?                            Нет            Да          Нет 
    Работает в Windows NT?                           Да             Да          Да 
    Требует перезагрузки компьютера?                  Да             Нет         Нет 
    Требует, чтобы в целевой процесс                  Да             Да          Нет 
    проецировался USER32.DLL? 
    Требует, чтобы в целевой процесс                  Да             Да         Да 
    проецировался KERNEL32.DLL? 
    Возможна ли выгрузка внедренной DLL             Нет           Да         Да 
    из целевого процесса? 
    Внедряется ли DLL в каждый процесс.              Да            Нет        Нет 
    в который проецируется USER32.DLL? 
    Является ли исходный текст                       Да            Да         Да (на 99%) 
    машинно-независимым? 
