Гость
Четверг, 25.04.2024, 01:28

Легко и быстро!

Форма входа
Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0
Меню сайта
Наш опрос
Хотите ли вы, чтобы на сайте были размещены видео-уроки Photoshop?
Всего ответов: 15
Мини-чат
Календарь
«  Апрель 2024  »
ПнВтСрЧтПтСбВс
1234567
891011121314
15161718192021
22232425262728
2930

Пример 6: Создание DLL и их внедрение в программы

Для данного примера нам потребуется:

·        Компилятор Borland C++ Builder (в примере версия 6.0)

·        Утилита для работы с PE-файлами LordPE by Yoda

Для начала рассмотрим, что такое DLL, для чего они нужны и как их создавать. DLL (Dynamic Link Library - динамически компонуемая библиотека) - это участок кода, хранимый в файле с расширением .dll. Код может быть использован другими программами, но сама по себе библиотека программой не является. В общем, динамически компонуемые библиотеки представляют собой набор готовых скомпилированных функций. Но у этих библиотек есть свои особенности, например, при использовании функции, находящейся в одной DLL, разными программами в памяти будет постоянно находиться только одна библиотека, обеспечивая тем самым экономное расходование памяти. Загрузка библиотеки в память может быть статической и динамической.

При статической загрузке DLL автоматически загружается при запуске использующего ее приложения. Такая DLL содержит экспортируемые функции, описание которых находится в файле библиотеки импорта (import library file - .lib). Для использования статической загрузки необходимо на этапе компоновки к программе подключить .lib файл вашей DLL. В C++ Builder это сводится к включению в проект .lib файла через менеджер проектов.

При динамической загрузке вы можете загружать DLL при необходимости и выгружать ее когда она становится ненужной. Работать с такими библиотеками сложнее, чем со статическими. Однако для динамической загрузки требуется только сама DLL (не нужен ни .lib ни заголовочный файл, хотя его можно использовать для описания экспортируемых функций для предполагаемого пользователя).

Рассмотрим создание и использование DLL с динамической загрузкой. Для этого запускаем Builder, закрываем проект по умолчанию, нажимаем кнопочку New и выбираем DLL Wizard. Оставляем установки по умолчанию.

Перед нами открывается окно кода с огромным комментарием, который нам абсолютно не нужен )) Также по умолчанию каждая DLL-библиотека содержит функцию с именем DllEntryPoint вида:
BOOL WINAPI DllEntryPoint (HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad);

Создадим следом за этой функцией свою функцию ups(), которая будет выдавать на экран тестовое сообщение. Обратите внимание на ключевое слово extern "C" и модификатор __export для обозначения экспортируемой функции.

Для компиляции проекта необходимо воспользоваться командой Project->Build, потому как запуститься по нажатию Run библиотека не может (это же все-таки не программа). После компиляции мы получаем готовую библиотеку в виде .dll файла. Назовем ее ups.dll и приступим к написанию программы-загрузчика.

Теперь создаем обычный новый проект в Builder, выкладываем на форму кнопку и в обработчике пишем текст, показанный на рисунке. Разберемся, как это работает:

  • void (__stdcall *ups)(); - объявление указателя на функцию ups()
  • HINSTANCE ourdll = LoadLibrary("ups.dll"); - загрузка библиотеки в память
  • ups= (void(__stdcall *) ()) GetProcAddress(ourdll, "_ups"); -присвоение указателю адреса функции из DLL. При этом используется GetProcAddress для получения адреса функции
  • ups(); - рабочий вызов фунции (собственно то, для чего все это и делается)
  • FreeLibrary(ourdll); - выгрузка библиотеки из памяти.

Обратите внимание на то, что при загрузке можно указать точное местонахождение библиотеки (не обязательно в том же каталоге, где лежит приложение). Компилируем проект, не забываем положить ранее созданную библиотеку ups.dll в папку приложения. Запускаем и наблюдаем при нажатии кнопки тестовое сообщение, прописанное в библиотеке. Таким образом, мы научились как создавать собственные библиотеки, так и использовать их (или любые другие библиотеки) в своих программах.

А теперь вернемся немного назад, а именно к функции DllEntryPoint. Назначение её - сугубо информационное. Вызывая эту функцию, загрузчик сообщает библиотеке о том, что она будет подключена к какому-то процессу. Таким образом, загрузка любой программы включает в себя последовательный вызов функций DllEntryPoint всех используемых DLL. Аналогично, данный вызов осуществляется при отключении и выгрузке библиотеки.

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

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

Теперь нам необходимо изменить .exe файл таким образом, чтобы в списке используемых программой DLL библиотек появилась наша библиотека. И тут нам снова пригодятся знания формата .exe файла Windows, который я затрагивал в предыдущем примере, тем более, что этот формат достаточно строго стандартизирован и подробно описан в документации. Поэтому, пропуская описание структуры PE-файла, рассмотрим структуру таблицы импорта.

Немного теории: таблица импорта находится в секции импорта и содержит всю информацию, необходимую загрузчику для подключения всех DLL и определения адресов вызываемых программой функций. Однозначно определить расположение таблицы импорта по имени секции не представляется возможным, т.к. в зависимости от компилятора данные имена значительно отличаются. Например, компилятор Borland именует секцию импорта как ".idata", компилятор Microsoft хранит таблицу импорта в секции с именем ".text".

Таблица импорта представляет собой массив записей определенного типа. Информация импорта начинается с Import Directory Table, которая описывает остальную информацию об импорте. Import Directory Table содержит адресную информацию используемую для разрешения ссылок на точки входа внутри образа библиотеки. Таблица импорта состоит из отдельных входов, как минимум по одному на каждую импортируемую библиотеку. Количество записей в массиве нигде в заголовках файла не хранится, а признаком конца массива является запись со всеми полями, установленными в NULL.
Каждой используемой программой DLL библиотеке соответствует одна запись в таблице импорта следующего вида:

Смещение

Размер

Название

Описание

00h

DWord

Import LookUp

Указатель на таблицу указателей (HintName Array), содержит ссылку на табличку RVA, содержащую информацию об импортируемых из данной библиотеки функциях

04h

DWord

Time/Date Stamp

Отметка о времени создания, часто содержит 0

08h

DWord

Forward Chain

Данное поле служит для реализации механизма "ссылочности" между DLL библиотеками Обычно равно 0FFFFFFFFh

0Ch

DWord

Name RVA

Это RVA указатель на нуль-терминированную ASCII строку, содержащую имя файла DLL библиотеки.

10h

DWord

Address Table RVA

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

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

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

Теперь начнем знакомиться с утилитой LordPE. Загружаем .exe файл скомпилированного проекта в программу (можно с помощью контекстного меню Проводника "Load into PE Editor") и наблюдаем картину, показанную на рисунке. Здесь мы можем лицезреть уже знакомые нам по предыдущему примеру поля PE-заголовка. При нажатии на кнопку "Sections" показывается таблица секций, присутствующих в файле, мы можем убедиться, что там присутствует секция ".idata", в которой и сохраняет компилятор от Borland таблицу импорта. Теперь начинаем самое интересное, а именно внесение необходимых нам изменений.

Нажимаем на кнопку "Directories", перед нами показывается информация о секциях, данные берутся из PE заголовка. Находим в списке таблицу импорта (Import Table), напротив нее есть 3 кнопочки. При нажатии на "L" появляется окно с просмотром структуры, там очень хорошо видны подключенные библиотеки и используемые импортируемые функции. При нажатии на "H" появляется Hex-редактор для ручного редактирования секции.

При нажатии на "…" появляется редактор таблицы импорта, в нем структурированным образом показаны все используемые DLL и импортируемые из них функции. При нажатии правой кнопки мыши можно в выпадающем меню выбрать полезные пункты типа "edit" – редактирование конкретной записи, но мы воспользуемся для своих целей пунктом "add import" – добавить импорт.

Прописываем нашу библиотеку ups.dll и импортируемую функцию _ups (c подчеркиванием в начале, как и в программе).

Нажимаем на "+", если все сделано правильно, т.е. существует в папке такая библиотека и у нее есть такая функция, то при нажатии на ОК мы можем убедиться в том, что наша библиотека с импортированной функцией присутствуют в секции импорта программы. Закрываем окно таблицы импорта, нажимаем везде "Save" и "OK", пока программа не закроется. Запускаем исправленный .exe файл и перед его запуском получаем тестовое сообщение от DLL. Наш код в DllEntryPoint сработал при запуске приложения!

 

Таким образом, мы получили ничем не примечательный файл, к которому подключена библиотека DLL, выполняющая свой код при запуске приложения. Такой метод применим к большинству обычных (не упакованных) EXE файлов.