Как отключить dll от процесса c

Обновлено: 07.07.2024

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

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

На рис 20-1 показано, как приложение явно загружает DLL и связывается с ней

Явная загрузка DLL

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

HINSTANCE LoadLibraryEx( PCTSTR pszDLLPathName, HANDLE hFile, DWORD dwFlags);

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

общает адрес виртуальной памяти, но которому спроецирован образ файла. Если спроецировать DLL на адресное пространство процесса не удалось, функции возвращают NULL Дополнительную информацию об ошибке можно получить вызовом GetLastError

Очевидно, Вы обратили внимание на два дополнительных параметра функции LoadLibraryEx, hFile и dwFlags Первый зарезервирован для использования в будущих версиях и должен быть NULL Bo втором можно передать либо 0, либо комбинацию флагов DONT_RESOLVE_DLL_REFERENCES, LOAD_LIBRARY_AS_DATAFILE и LOAD_WITH_ ALTERED_SEARCH_PATH, о которых мы сейчас и поговорим.

1 ) Заголовочный файл с экспортируемыми

6) Заголовочный файл с импортируемыми

прототипами структурами и идентификаторами

прототипами, структурами и идентификаторами 7)

(символьными именами) 2) Исходные файлы

Исходные файлы С/С++ в которых нет ссылок на

С/С++ в которых реализованы экспортируемые

импортируемые функции и переменные 8)

функции и определены переменные 3)

Компилятор создает OBJ файл из каждого

Компилятор создает OBJ-файл из каждого

исходного файла С/С++ 9) Компоновщик собирает

исходного файла С/С++ 4) Компоновщик

ЕХЕ-модуль из OBJ-модулей (LIB файл DLL не

собирает DLL из OBJ модулей 5) Если DLL

нужен, так как нет прямых ссылок на

экспортирует хотя бы одну переменную или

экспортируемые идентификаторы, раздел импорта в

функцию компоновщик создает и LIB-файл

Рис. 20-1. Так DLL создается и явно связывается с приложением

Этот флаг укапывает системе спроецировать DLL на адресное пространство вызывающего процесса. Проецируя DLL, система обычно вызывает из нее специальную функцию DllMain (о ней — чуть позже) и с ее помощью инициализирует библиотеку. Так вот, данный флаг заставляет систему проецировать DLL, не обращаясь к DllMain.

Кроме того, DLL может импортировать функции из других DLL При загрузке библиотеки система проверяет, использует ли она другие DLL; если да, то загружает и их При установке флага DONT_RESOLVE_DLL_REFERENCES дополнительные DLL

автоматически не загружаются.

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

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

Этот флаг изменяет алгоритм, используемый LoadLibraryEx при поиске DLL-файла. Обычно поиск осуществляется так, как я рассказывал в главе 19 Однако, если данный флаг установлен, функция ищет файл, просматривая каталоги в таком порядке

1. Каталог, заданный в napaмeтре pszDLLPathName.

2. Текущий каталог процесса.

3. Системный каталог Windows.

4. Основной каталог Windows.

5. Каталоги, перечисленные в переменной окружения PATH

Явная выгрузка DLL

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

BOOL FreeLibrary(HINSTANCE hinstDll);

Вы должны передать в FreeLibrary значение типа HINSTANCE, которое идентифицирует выгружаемую DLL. Это значение Вы получаете после вызова LoadLibrary(Ex).

DLL можно выгрузить и с помощью другой функции:

VOID FreeLibraryAndExitThread( HlNSTANCE hinstDll, DWORD dwExitCode);

Она реализована в Kernel32.dll так:

VOID FreeLibraryAndExitThread(HINSTANCE hinstDll, DWORD dwExitCode)

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

вызывая сначала FreeLibrary, а потом ExttThread.

Если поток станет сам вызывать FreeLibrary и ExitThread, возникнет очень серьезная проблема: FreeI.ibrary тут же отключит DLL от адресного пространства процесса. После возврата из FreeLibrary код, содержащий вызов ExttThread, окажется недоступен, и поток попытается выполнить не известно что. Это приведет к нарушению доступа и завершению всего процесса!

С другой стороны, если поток обратится к FreeLibraryAndExitThread, она вызовет FreeLibrary, и та сразу же отключит DLL, Но следующая исполняемая инструкция находится в KerneI32.dlI, а нс в только что отключенной DLL. Значит, поток сможет продолжить выполнение и вызвать ExitThread, которая корректно завершит его, не возвращая управления.

Впрочем, FreeLibraryAndExitThread может и не понадобиться. Мне она пригодилась лишь раз, когда я занимался весьма нетипичной задачей. Да и код я писал под Windows NT 3-1, где этой функции не было. Наверное, поэтому я так обрадовался, обнаружив ее в более новых версиях Windows.

На самом деле LoadLibrary и LoadLibraryEx лишь увеличивают счетчик числа пользователей указанной библиотеки, a FreeLibrary и FreeLibraryAndExitThread его уменьшают Так, при первом вызове LoadLibrary дум загрузки DLL система проецирует образ DLL-файла иа адресное пространство вызывающего процесса и присваивает единицу счетчику числа пользователей этой DLL Если поток того же процесса вызывает LoadLibrary для той же DLL еще раз, DLL больше не проецируется; система просто увеличивает счетчик числа ее пользователей — вот и все.

Чтобы выгрузить DLL из адресного пространства процесса, FreeLibrary придется теперь вызывать дважды: первый вызов уменьшит счетчик до 1, второй — до 0. Обнаружив, что счетчик числа пользователей DLL обнулен, система отключит ее. После этого попытка вызова какой-либо функции из данной DLL приведет к нарушению доступа, так как код по указанному адресу уже не отображается на адресное пространство процесса.

Система поддерживает в каждом процессе свой счетчик DLL, т. e. если поток процесса А вызывает приведенную ниже функцию, а затем тот же вызов делает поток в процессе В, то

Точка входа DLL по умолчанию — _DllMainCRTStartup

В Windows все библиотеки DLL могут содержать необязательную функцию точки входа, обычно называемую DllMain , которая вызывается как для инициализации, так и для завершения. Это дает возможность выделять или освобождать дополнительные ресурсы по мере необходимости. Windows вызывает функцию точки входа в четырех ситуациях: присоединение процесса, отсоединение процесса, присоединение потока и отсоединение потока. Когда библиотека DLL загружается в адресное пространство процесса либо при загрузке ее использующего приложения, либо при запросе приложением библиотеки DLL во время выполнения, операционная система создает отдельную копию данных библиотеки DLL. Это называется присоединением процесса. Присоединение потока происходит, когда процесс, в который загружена библиотека DLL, создает новый поток. Отсоединение потока происходит при завершении потока. Отсоединение процесса происходит, когда библиотека DLL больше не нужна и освобождается приложением. Операционная система выполняет отдельный вызов точки входа библиотеки DLL для каждого из этих событий, передавая аргумент reason для каждого типа события. Например, операционная система отправляет DLL_PROCESS_ATTACH в качестве аргумента reason для обозначения присоединения процесса.

При создании библиотек DLL в Visual Studio точка входа по умолчанию _DllMainCRTStartup , предоставляемая VCRuntime, связывается автоматически. Не нужно указывать функцию точки входа для библиотеки DLL с помощью параметра компоновщика /ENTRY (символ точки входа).

Несмотря на то, что с помощью параметра /ENTRY компоновщика можно указать другую функцию точки входа для библиотеки DLL, его использовать не рекомендуется, так как функция точка входа должна будет в том же порядке дублировать все действия, выполняемые _DllMainCRTStartup . VCRuntime предоставляет функции, позволяющие дублировать свое поведение. Например, можно вызвать __security_init_cookie сразу же при присоединении процесса для поддержки параметра проверки буфера /GS (проверка безопасности буфера). Можно вызвать функцию _CRT_INIT , передавая те же параметры, что и функция точки входа, для выполнения остальных функций инициализации или завершения DLL.

Инициализация библиотеки DLL

Библиотека DLL может содержать код инициализации, который должен выполняться при загрузке библиотеки DLL. Для выполнения ваших собственных функций инициализации и завершения библиотеки DLL _DllMainCRTStartup вызывает функцию с именем DllMain , которую вы сами можете указать. У DllMain должна быть сигнатура, необходимая для точки входа библиотеки DLL. Функция точки входа по умолчанию _DllMainCRTStartup вызывает DllMain с использованием тех же параметров, передаваемых Windows. По умолчанию, если вы не предоставите функцию DllMain , ее предоставит Visual Studio и свяжет таким образом, чтобы функция _DllMainCRTStartup всегда могла что-то вызывать. Это означает, что если вам не нужно инициализировать библиотеку DLL, никаких особых действий при создании библиотеки DLL выполнять не требуется.

Следующая сигнатура используется для DllMain :

Некоторые библиотеки создают для функции DllMain программу-оболочку. Например, в обычной библиотеке DLL MFC реализуйте функции-члены InitInstance и ExitInstance объекта CWinApp для выполнения операций инициализации и завершения, требуемых библиотекой DLL. Дополнительные сведения см. в разделе Инициализация обычных библиотек DLL MFC.

В отношении операций, выполняемых в точке входа DLL, действуют серьезные ограничения. См. общие рекомендации для конкретных API-интерфейсов Windows, вызывать которые в DllMain небезопасно. Если требуется сделать что-то помимо простейшей инициализации, сделайте это в функции инициализации для DLL. Можно потребовать, чтобы приложения вызывали функцию инициализации после выполнения DllMain и перед вызовом любых других функций в библиотеке DLL.

Инициализация обычных (не MFC) библиотек DLL

Чтобы выполнить собственную инициализацию в обычных (не MFC) библиотеках DLL, использующих предоставленную VCRuntime точку входа _DllMainCRTStartup , исходный код библиотеки DLL должен содержать функцию с именем DllMain . Следующий код представляет собой базовую схему, показывающую, как может выглядеть определение DllMain :

В более старой документации по Windows SDK говорится, что фактическое имя функции точки входа библиотеки DLL должно быть указано в командной строке компоновщика с помощью параметра /ENTRY. В Visual Studio использовать параметр /ENTRY не нужно, если функции точки входа задано имя DllMain . На самом деле, если вы используете параметр/ENTRY и задаете функции точки входа имя, отличное от DllMain , инициализация CRT будет проходить неправильно, пока функция точки входа не выполнит же вызовы инициализации, что и _DllMainCRTStartup .

Инициализация обычных библиотек DLL MFC

Так как обычные библиотеки DLL MFC содержат объект CWinApp , они должны выполнять задачи инициализации и завершения в том же расположении, что и приложение MFC: в функциях-членах InitInstance и ExitInstance класса, производного от CWinApp . Поскольку MFC предоставляет функцию DllMain , которая вызывается _DllMainCRTStartup для DLL_PROCESS_ATTACH и DLL_PROCESS_DETACH , создавать собственную функцию DllMain не требуется. Предоставляемая MFC функция DllMain вызывает InitInstance при загрузке библиотеки DLL и ExitInstance перед выгрузкой библиотеки DLL.

Обычная библиотека DLL MFC может отслеживать нескольких потоков, вызывая TlsAlloc и TlsGetValue в своей функции InitInstance . Эти функции позволяют библиотеке DLL отслеживать данные конкретного потока.

Если в обычной библиотеке DLL MFC, которая динамически связывается с MFC, используется поддержка MFC OLE, базы данных MFC (или DAO) или сокетов MFC, соответственно, отладочные библиотеки DLL расширения MFC MFCO версия D.dll, MFCD версия D.dll и MFCN версия D.dll (где версия — номер версии) связываются автоматически. Необходимо вызвать одну из следующих стандартных функций инициализации для каждой из этих библиотек DLL, используемых в CWinApp::InitInstance обычной библиотеки DLL MFC.

Тип поддержки MFC Вызываемая функция инициализации
MFC OLE (MFCO версия D.dll) AfxOleInitModule
База данных MFC (MFCO версия D.dll) AfxDbInitModule
Сокеты MFC (MFCO версии D.dll) AfxNetInitModule

Инициализация библиотек DLL расширения MFC

Так как в библиотеках DLL расширения MFC нет объекта, производного от CWinApp (в отличие от обычных библиотек DLL MFC), в функцию DllMain , создаваемую мастером DLL MFC, следует добавить собственный код инициализации и завершения.

Мастер предоставляет следующий код для библиотек DLL расширения MFC. В коде PROJNAME является заполнителем для имени проекта.

При создании CDynLinkLibrary объекта во время инициализации библиотека DLL расширения MFC может экспортировать объекты CRuntimeClass или ресурсы в клиентское приложение.

Если вы собираетесь использовать библиотеку DLL расширения MFC из одной или нескольких обычных библиотек DLL MFC, необходимо экспортировать функцию инициализации, которая создает объект CDynLinkLibrary . Эта функция должна вызываться из каждой обычной библиотеки DLL MFC, использующей библиотеку DLL расширения MFC. Подходящим местом для вызова этой функции инициализации является функция-член InitInstance объекта библиотеки DLL MFC, производного от CWinApp . Вызов следует выполнить до использования экспортированных классов или функций библиотеки DLL расширения MFC.

В функции DllMain , создаваемой мастером DLL MFC, вызов AfxInitExtensionModule записывает классы среды выполнения модуля (структуры CRuntimeClass ), а также фабрики объектов (объекты COleObjectFactory ) для использования при создании объекта CDynLinkLibrary . Следует проверить возвращаемое значение AfxInitExtensionModule . Если из AfxInitExtensionModule возвращается нулевое значение, из функции DllMain возвращается нуль.

Если библиотека DLL расширения MFC будет явно связана с исполняемым файлом (то есть исполняемый файл вызывает AfxLoadLibrary для связи с библиотекой DLL), необходимо добавить вызов AfxTermExtensionModule в DLL_PROCESS_DETACH . Эта функция позволяет MFC очищать библиотеку DLL расширения MFC при отсоединении каждого процесса от библиотеки DLL расширения MFC (что происходит при завершении процесса или при выгрузке библиотеки DLL в результате вызова AfxFreeLibrary ). Если библиотека DLL расширения MFC будет связана с приложением неявным образом, вызывать AfxTermExtensionModule не требуется.

Приложения, которые связываются с библиотеками DLL расширения MFC явным образом, должны вызывать AfxTermExtensionModule при освобождении библиотеки DLL. Если приложения используют несколько потоков, они также должны использовать AfxLoadLibrary и AfxFreeLibrary (вместо функций Win32 LoadLibrary и FreeLibrary ). Использование AfxLoadLibrary и AfxFreeLibrary гарантирует, что код запуска и завершения работы, выполняемый при загрузке и выгрузке библиотеки DLL расширения MFC, не нарушает глобальное состояние MFC.

Так как MFCx0.dll полностью инициализируется к моменту вызова DllMain , можно выделить память и вызвать функции MFC в DllMain (в отличие от 16-разрядной версии MFC).

Библиотеки DLL расширения могут поддерживать многопоточность, обрабатывая случаи DLL_THREAD_ATTACH и DLL_THREAD_DETACH в функции DllMain . Эти случаи передаются в DllMain при присоединении и отсоединении потоков от библиотеки DLL. Вызов TlsAlloc при присоединении библиотеки DLL позволяет ей поддерживать индексы локального хранилища потока (TLS) для каждого присоединенного потока.

Обратите внимание, что файл заголовка Afxdllx.h содержит специальные определения структур, используемых в библиотеках DLL расширения MFC, например определение для AFX_EXTENSION_MODULE и CDynLinkLibrary . Этот файл следует включить в библиотеку DLL расширения MFC.

Пример функции инициализации, обрабатывающей многопоточность, включен в раздел Использование локального хранилища потока в библиотеке динамической компоновки в Windows SDK. Обратите внимание, что пример содержит функцию точки входа с именем LibMain , но ей следует задать имя DllMain , чтобы она работала с библиотеками MFC и времени выполнения C.

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

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

На рис 20-1 показано, как приложение явно загружает DLL и связывается с ней

Явная загрузка DLL

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

HINSTANCE LoadLibraryEx( PCTSTR pszDLLPathName, HANDLE hFile, DWORD dwFlags);

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

общает адрес виртуальной памяти, но которому спроецирован образ файла. Если спроецировать DLL на адресное пространство процесса не удалось, функции возвращают NULL Дополнительную информацию об ошибке можно получить вызовом GetLastError

Очевидно, Вы обратили внимание на два дополнительных параметра функции LoadLibraryEx, hFile и dwFlags Первый зарезервирован для использования в будущих версиях и должен быть NULL Bo втором можно передать либо 0, либо комбинацию флагов DONT_RESOLVE_DLL_REFERENCES, LOAD_LIBRARY_AS_DATAFILE и LOAD_WITH_ ALTERED_SEARCH_PATH, о которых мы сейчас и поговорим.

1 ) Заголовочный файл с экспортируемыми

6) Заголовочный файл с импортируемыми

прототипами структурами и идентификаторами

прототипами, структурами и идентификаторами 7)

(символьными именами) 2) Исходные файлы

Исходные файлы С/С++ в которых нет ссылок на

С/С++ в которых реализованы экспортируемые

импортируемые функции и переменные 8)

функции и определены переменные 3)

Компилятор создает OBJ файл из каждого

Компилятор создает OBJ-файл из каждого

исходного файла С/С++ 9) Компоновщик собирает

исходного файла С/С++ 4) Компоновщик

ЕХЕ-модуль из OBJ-модулей (LIB файл DLL не

собирает DLL из OBJ модулей 5) Если DLL

нужен, так как нет прямых ссылок на

экспортирует хотя бы одну переменную или

экспортируемые идентификаторы, раздел импорта в

функцию компоновщик создает и LIB-файл

Рис. 20-1. Так DLL создается и явно связывается с приложением

Этот флаг укапывает системе спроецировать DLL на адресное пространство вызывающего процесса. Проецируя DLL, система обычно вызывает из нее специальную функцию DllMain (о ней — чуть позже) и с ее помощью инициализирует библиотеку. Так вот, данный флаг заставляет систему проецировать DLL, не обращаясь к DllMain.

Кроме того, DLL может импортировать функции из других DLL При загрузке библиотеки система проверяет, использует ли она другие DLL; если да, то загружает и их При установке флага DONT_RESOLVE_DLL_REFERENCES дополнительные DLL

автоматически не загружаются.

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

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

Этот флаг изменяет алгоритм, используемый LoadLibraryEx при поиске DLL-файла. Обычно поиск осуществляется так, как я рассказывал в главе 19 Однако, если данный флаг установлен, функция ищет файл, просматривая каталоги в таком порядке

1. Каталог, заданный в napaмeтре pszDLLPathName.

2. Текущий каталог процесса.

3. Системный каталог Windows.

4. Основной каталог Windows.

5. Каталоги, перечисленные в переменной окружения PATH

Явная выгрузка DLL

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

BOOL FreeLibrary(HINSTANCE hinstDll);

Вы должны передать в FreeLibrary значение типа HINSTANCE, которое идентифицирует выгружаемую DLL. Это значение Вы получаете после вызова LoadLibrary(Ex).

DLL можно выгрузить и с помощью другой функции:

VOID FreeLibraryAndExitThread( HlNSTANCE hinstDll, DWORD dwExitCode);

Она реализована в Kernel32.dll так:

VOID FreeLibraryAndExitThread(HINSTANCE hinstDll, DWORD dwExitCode)

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

вызывая сначала FreeLibrary, а потом ExttThread.

Если поток станет сам вызывать FreeLibrary и ExitThread, возникнет очень серьезная проблема: FreeI.ibrary тут же отключит DLL от адресного пространства процесса. После возврата из FreeLibrary код, содержащий вызов ExttThread, окажется недоступен, и поток попытается выполнить не известно что. Это приведет к нарушению доступа и завершению всего процесса!

С другой стороны, если поток обратится к FreeLibraryAndExitThread, она вызовет FreeLibrary, и та сразу же отключит DLL, Но следующая исполняемая инструкция находится в KerneI32.dlI, а нс в только что отключенной DLL. Значит, поток сможет продолжить выполнение и вызвать ExitThread, которая корректно завершит его, не возвращая управления.

Впрочем, FreeLibraryAndExitThread может и не понадобиться. Мне она пригодилась лишь раз, когда я занимался весьма нетипичной задачей. Да и код я писал под Windows NT 3-1, где этой функции не было. Наверное, поэтому я так обрадовался, обнаружив ее в более новых версиях Windows.

На самом деле LoadLibrary и LoadLibraryEx лишь увеличивают счетчик числа пользователей указанной библиотеки, a FreeLibrary и FreeLibraryAndExitThread его уменьшают Так, при первом вызове LoadLibrary дум загрузки DLL система проецирует образ DLL-файла иа адресное пространство вызывающего процесса и присваивает единицу счетчику числа пользователей этой DLL Если поток того же процесса вызывает LoadLibrary для той же DLL еще раз, DLL больше не проецируется; система просто увеличивает счетчик числа ее пользователей — вот и все.

Чтобы выгрузить DLL из адресного пространства процесса, FreeLibrary придется теперь вызывать дважды: первый вызов уменьшит счетчик до 1, второй — до 0. Обнаружив, что счетчик числа пользователей DLL обнулен, система отключит ее. После этого попытка вызова какой-либо функции из данной DLL приведет к нарушению доступа, так как код по указанному адресу уже не отображается на адресное пространство процесса.

Система поддерживает в каждом процессе свой счетчик DLL, т. e. если поток процесса А вызывает приведенную ниже функцию, а затем тот же вызов делает поток в процессе В, то

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

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

"Dynamic Link Library" или DLL – это часть исполняемого РЕ-файла в виде внешнего модуля. Он оформлен как ларчик с N-нным количеством уникальных для наших программ функций, которых нет в составе системных Win32-API. Программный экзо-скелет динамических библиотек идентичен исполняемым файлам экзе, однако есть и некоторые нюансы:

Система предоставляет нам два способа подключения DLL к своим проектам – статический и динамический . В первом случае мы подключаем библиотеку и указываем импортируемые из неё функции на этапе компиляции РЕ-файла, и эти функции сразу загружаются в наше адресное пространство, вместе с приложением. Во-втором (динамическом) случае, можно загрузить функцию из DLL в произвольный момент времени, сыграв аккордом LoadLibrary() , GetProcAddress() и FreeLibrary() .

Скомпоновать библиотеку довольно просто – пишем обычный ЕХЕ, только в шапке указываем директиву "format PE DLL" . В результате, из выхлопной трубы fasm'a получим файл в формате *.dll. Однако при программировании пользовательских библиотек нужно учитывать ряд их особенностей, в частности релокацию образа в памяти.

Чтобы DLL не загрузилась поверх исполняемого приложения (конфликт базовых адресов), её ImageBase обязательно должна быть перемещаемой – достаточно добавить секцию .reloc в коде, об остальном компилятор позаботится сам. В этой секции будут собраны т.н. фиксапы (fixups) – адреса, к которым загрузчик должен будет внести поправки. Фиксапы применяются исключительно к инструкциям, которые обращаются по абсолютным адресам в памяти. Если адрес относительный (в пределах 127 байт), то он не требует модификации:

reloc_1.jpg

Такие отладчики как OllyDbg подчёркивают адреса, которые требуют коррекции после перемещения образа в памяти – на рис.выше их всего 4, и непосредственно опкод инструкции не учитывается (здесь push\call, хотя могут быть и условные\безусловные переходы). Размер самих фиксапов равен 12-бит (выделены красным), а это 2^12=4096 или одна страница виртуальной памяти. Соответственно фиксап не может адресовать блок памяти свыше 4 Кбайт. Другими словами, каждая страница (блок) имеет свой набор фиксов.


Точка входа в DLL-библиотеку

Теперь о насущном..
Подобно исполняемым экзе-приложениям, библиотеки тоже имеют свою точку входа – в доках MSDN эта функция известна как DllEntryPoint() (или DllMain в терминологии си). Здесь и кроется всё самое интересное, чему мы посвятим весь последующий разговор.

Любое обращение EXE-модуля к функциям из DLL происходит через системного посредника LdrLoadDLL() . В системном ансамбле, эта кошерная функция из Ntdll.dll играет огромную роль. Она не только загружает библиотеки в пространство юзера на этапе проецирования образа в память, но и обслуживает функции динамического вызова процедур типа LoadLibrary() , GetModuleHandle() и прочии, от которых мы ожидаем получить дескрипторы модулей. Вот её прототип:

Диалог этого загрузчика с вызываемой библиотекой происходит по схеме "запрос-ответ". Любая библиотека должна иметь упомянутую функцию взаимодействия с загрузчиком DllEntryPoint() , от которой в регистре EAX лоадер ждёт или TRUE (библиотека способна обработать запрос), или FALSE (что-то пошло не так). Не соблюдение этих правил приводит к краху приложения.

LdrLoadDLL() может извещать библиотеку о четырёх событиях, которые происходят во-внешнем (по отношению к библиотеке) мире. Эта информация передаётся точке-входа в DLL в параметре fdwReason . Кроме события, загрузчик сразу передаёт либе её базу в памяти, и способ подключения к исполняемому файлу. Прототип описывается так:

Из всей этой братии, нам интересен лишь аргумент DLL_PROCESS_ATTACH = 1 , благодаря которому статически присобачив библиотеку к нашему процессу, мы можем например, предварительно расшифровать основной код программы, обнаружить отладчик в фоне и т.д. Дело в том, что загрузчик проецирует DLL в пространство процесса задолго до точки-входа в программу, с которой начинают анализ все отладчики, а значит Оля пропустит этот этап между ног. Здесь уместно вспомнить про функции TLS-Callback , ..но поскольку загрузчик парсит импорт из библиотек вторым (а TLS аж десятым), то выигрыш тут на лицо.


DLL – промышленная реализация

dllStack.jpg

Соответственно, мы можем снимать эти аргументы прямо со-стека, и сразу проверять их – код ниже придерживается именно такой политики:

Теперь у нас есть либа, и нужно написать родительское приложение, которое будет статически привязывать к себе эту библиотеку. Во-первых, обратим внимание на имя новоиспечённой DLL – здесь, в секции-экспорта я определил его как "about.dll", это важно! Теперь просто импортируем эту библиотеку по имени, и вызываем из неё функцию примерно так:

dll_example.jpg


Тёмная сторона луны

Пробежавшись по макушкам кода библиотек, посмотрим на них из другой проекции..
Алгоритм работы загрузчика образов LDR плохо освещён в документации и это не удивительно – весь ядерный код мастдая, коммерческая тайна (будь она не ладна). Как это принято у Microsoft, она советует нам ознакомиться с третьей поправкой, восьмого исправления, четвёртой редакции от 32 февраля где сказано, что "..в военное время не только прямой угол может достигть 100 градусов, но и функция инициализации DllEntryPoint() может использоваться не по назначению". Самое главное: кто, где и когда объявляет это положение неизвестно, а значит мы вольны назначать его сами.

Мощь (и беспомощность) точки-входа в библиотеку в том, что некоторая часть театра действий происходит под управлением системных механизмов, отследить которые из прикладного уровня довольно сложно. В документации на РЕ-файл можно найти формат каталога секций "Data-Directories" . В этом дире рассчитавшись на первый-второй выстроены в ряд все секции, которые обходит загрузчик образов LdrLoadDll() при инициализации приложения. Причём последовательность секций строго регламентируется. Вот как выглядит эта структура в представлении редактора PE-Explorer:

dataDir_12.jpg

Таким образом, импорт анализируется загрузчиком на самом начальном этапе, и большинство служебных структур прикладного уровня в этот момент даже не инициализированы ещё до конца – в частности, это относится к структуре PEB , не говоря уже о дочерней к ней структуре ТЕВ. Например, если мы внутри DllEntryPoint() захотим из РЕВ получить флаг-отладки нашего приложения "BeingDebugger" , то потерпим фиаско (проверено на практике). На скамейку запасных сразу отправляется и функция IsDebuggerPresent() , которая читает этот-же флаг из РЕВ. Значит нужно спускаться на уровень ниже, а для защитных механизмов это только гуд.

Если развивать мысль дальше, то наша библиотека не единственная у приложения. Кроме неё, в память каждого процесса система загружает и свои либы Ntdll.dll (собственно в ней и живёт загрузчик LDR), а так-же библиотеку kernel32.dll. С очерёдностью загрузки в память системных библиотек можно ознакомится в отладчике WinDbg, озадачив его командой !peb – поле InMemoryOrderModuleList как-раз отрапортует нам об этом:

pebLdr.jpg

Здесь видно, что первым десантируется в память мой исполняемый файл "DLL_attach.exe", следом за ним системные библиотеки, и только потом моя пользовательская либа "about.dll". Повторюсь, что система выстраивает структуру РЕВ только когда окончательно покончит с окружением процесса, скидывая в неё результаты проделанной работы. А лог на рисунке выше, WinDbg парсит уже из рабочего процесса, поэтому РЕВ как-бы готова к употреблению.


DllEntryPoint() на страже приложения

Теперь будем мыслить так.. Если точка-входа в библиотеку с аргументом DLL_PROCESS_ATTACH отрабатывает на низком уровне, значит на её основе можно соорудить защитный механизм. Система вызывает DllEntryPoint() с аргументом ATTACH сразу после того-как DLL спроецирована на адресное пространство процесса – такая ситуация возможна всего один раз, и на протяжении всего "сеанса" больше не повторяется! В следующий раз, когда тред вызовет LoadLibrary() для уже спроецированной на память DLL, система просто увеличит счётчик обращения к ней и всё.

В предоставленном на суд примере, статически (явно) прилинкованная библиотека внутри DllEntryPoint() будет искать отладчик, и если обнаружит таковой, то вернёт ошибку. Как упоминалось выше, флаги-отладки из структуры РЕВ для этих целей уже не подходят, поэтому придётся искать обходные пути.

Одним из вариантов обнаружения факта отладки является проверка своего статуса в системе. Дело в том, что в дефолте, запущенный на исполнение процесс не имеет привилегии SeDebugPrivilege, зато ею обладает отладчик. Когда он загружает нас в своё тело, то автоматом передаёт и свою привилегию, чекнув которую мы можем определить этот факт. Есть куча способов узнать привилегию своего процесса, и мы воспользуемся самым простым – попытаемся открыть системный процесс csrss.exe.

CSRSS.EXE – это часть пользовательской подсистемы Win32, и при обычных обстоятельствах он не доступен прикладным задачам. Однако привилегия Debug снимает этот запрет, и мы можем открыть его функцией OpenProcess() со-всеми вытекающими последствиями. CSRSS (client\server run-time subsystem) отвечает за консоль, работу с потоками Thread, и за 16-битную среду MS-DOS (на х64 её кастрировали). Это процесс пользовательского режима, который перехватывает обращения к ядру и решает простые вопросы на уровне прикладных задач.

Проблема в том, что функции OpenProcess() требуется идентификатор PID открываемого процесса, т.е. нам нужно будет просканировать всю память и найти нужный процесс по его имени – тривиальная задача по обнаружению отладчика превращается в ад. В сети можно встретить разные варианты перечисления процессов – это CreateToolhelp32Snapshot() , обход в цикле через Process32First\Next() , EnumProcess() и тяжеловес NtQuerySystemInformation() .

Однако получить PID именно процесса CSRSS.EXE можно специально предназначенной для этого функцией из Ntdll.dll под названием CsrGetProcessId() – у неё нет аргументов и в EAX она сразу возвращает столь необходимый нам PID. С использованием этой функции, проверка на отладчик укладывается в пару строк ассемблерного кода. Мы поместим её внутрь DllEntryPoint() и будем проверять запрос на DLL_PROCESS_ATTACH .

В общем случае, программа будет следовать такому алго..
Мы пишем приложение, которое запрашивает пароль. Если юзер введёт валидный пасс, то управление примет зашифрованная функция, которую расшифрует декриптор из внешней библиотеки, с непримечальным именем "about.dll". Алгоритм декриптора – самый примитивный ксор 1-байтныйм ключом, однако тут есть подвох! Пароль на валидность мы вообще не будем проверять, а декриптор сняв с него хэш-сумму сразу расшифрует ей критический блок в основном приложении. Теперь уже взломщик не сможет просто обратить условие проверки, и ему придётся осуществлять только брут, перебором всех возможных ключей.

Если юзер подсунет левый пароль и его хэш не совпадёт с тем, которым мы зашифровали блок, то рано или поздно процессор нарвётся на исключение , поскольку пойдёт пахать зашифрованный код. Чтобы защитить честь его мундира, для таких случаев мы устанавливаем SEH-обработчик, который и будет отлавливать эти исключения. То-есть, если SEH примет управление, значит пасс невалидный и мы подкорректировав значение регистра EIP в контексте, выводим мессагу Wrong и на выход..

hash.jpg

После того-как получим хэш валидного пассворда, можно приступать к шифрованию всего блока этим ключом. Если на запрос юзер введёт валидный пасс, то функция декриптора в библиотеке рассчитает его хэш на автомате, и опять проксорит этим-же клюсом – в результате получим расшифрованный блок, и процессор не споткнётся уже об него. Ниже приведён готовый к употреблению код, который остаётся только скомпилировать, и в основном приложении зашифровать указанный блок:

Читайте также: