Как известно, VB6 обладает одним очень большим недостатком - он не позволяет создавать обыкновенные DLL (т.е. такие DLL, функции из которых мы можем импортировать например при помощи Declare Sub/Function).
Меня эта несправедливость долгое время не устраивала, и я решил исправить её.
FireNativeDLL 1.0 - это средство, которое добавит в VB возможность создавать обычные DLL (т.н. в народе NativeDLL).
Как этим пользоваться?
- Установка FireNative DLL
Для начала необходимо [скачать] дистрибутив FireNativeDLL.← ссылка не работает, но ищите дальше в топике, кто-то выложил какую-то версию FNDLL.
Дистрибутив представляет собой WinRarSFX-архив. После запуска, указываем путь, по которому у вас установлен VB6 (обычно это: C:\Program Files\Microsoft Visual Studio\VB98\ ) и нажимаем "Install".
Установка FireNativeDLL завершена. - Создание нового проекта
Запускаете Visual Basic 6. После установки FireNativeDLL у вас должен появиться шаблон проекта "Standard DLL". Выбираете этот шаблон и нажимаете "Открыть": - Разработка DLL
Далее, откроется проект, состоящий из обычного модуля и модуля класса. Вы можете работать с проектом как с обычной ActiveX-библиотекой, т.е. добавлять формы, модули, классы, ресурсы и т.д. в свой проект.
Помните, что наличие класса CStdDLLInfo обязательно в проекте.
Опишите функционал библиотеки, создайте весь необходимый вам код. - Экспорт функций DLL
Пришло время определить, какие из функций вашего проекта должны быть доступны извне. Список этих функций необходимо определить в классе CStdDLLInfo. Открываете код класса CStdDLLInfo, читаете сопроводительную инструкцию. Далее, для каждой экспортируемой функции создаёте запись вида:- Код: Выделить всё
.Export "ВнешнееИмяФункции", AddressOf ВашаФункция
внутри With Informer ... End With блока.
ВнешнееИмяФункции - имя, под которым функция будет доступна извне. Оно может отличаться от настоящего имени функции в вашем коде.
Помните, что экспортировать можно только функции, определённые в модулях. - Точка входа DLL
Точка входа DLL - процедура, имеющая три параметра (ByVal hInstance As Long, ByVal lReason As Long, ByVal lReserved As Long)которую Windows вызывает в 4 случаях:- Загрузка DLL в процесс
Когда DLL подгружается в процесс, Windows вызывает точку входа, передавая в качестве аргумента lReason значение константы DLL_PROCESS_ATTACH.
При использовании:- Explicit-импорта (с помощью TLB) это происходит при запуске приложения.
- Implicit-импорта (с помощью Declare Sub/Function) это происходит при самом первом вызове какой-либо функции из библиотеки.
- При импорте с помощью LoadLibrary/GetProcAddress это происходит во время вызова LoadLibrary()
Важно отметить, что при вызове с DLL_PROCESS_ATTACH точка входа должна возвратить 1, обозначая тем самым, что внутренняя инициализация библиотеки прошла успешно.
Если же ваша точка входа возвращает 0, при использовании:- Explicit-импорта (с помощью TLB) ваше приложение не запустится. Система выдаст сообщение об ошибке.
- Implicit-импорта (с помощью Declare Sub/Function) VB выдаст ошибку File not found: 'имя_библиотеки.dll' при самом первом вызове функции из библиотеки.
- При использовании LoadLibrary, LoadLibrary не загрузит библиотеку и возвратит 0.
Точку входа в этом случае логичнее использовать для инициализации каких-то внутренних данных/объектов вашей библиотеки (если вы в этом конечно нуждаетесь). - Выгрузка DLL из процесса
Когда DLL выгружается из процесса, Windows вызывает точку входа, передавая в качестве аргумента lReason значение константы DLL_PROCESS_DETACH.
Как при использовании Implicit, так и при использовании Explicit -импорта, точка входа в этом случае будет вызвана при завершении вашего приложения.
Фактически же, точка входа в этом случае вызывается при вызове функции FreeLibrary().
Система абсолютно безразлична к тому, что возвращает точка входа в этом случае.
Логичнее всего использовать точку входа в этом случае для выгрузки и освобождения памяти/объектов, занятой/созданных ею в процессе работы (если конечно такая необходимость у вас есть). - Создание нового потока внутри процесса
При создании потока (нити выполнения) в контексте процесса (с помощью функций CreateThreed, CreateRemoteThreed и т.д) Windows вызовет точку входа каждой библиотеки, загруженной на тот момент в процесс, передавая в качестве аргумента lReason значение константы DLL_THREAD_ATTACH.
Вы можете использовать эту возможность для инициализации каких-либо данных, специфичных для каждого потока. - Выход из потока
При выходе из (разрушении) потока, Windows вызовет точку входа каждой библиотеки, загруженной в процесс на момент разрушения потока. При вызове в качестве аргумента lReason будет передано значение константы DLL_THREAD_DETACH.
Вызов точки входа будет произведён в контексте того потока, из которого происходит выход.
Если вам не нужны возможности точки входа, вы можете её не определять. Для этого, уберите (удалите или закомментируйте) процедуру DllEntryPoint в модуле modLibrary, а также строчку .SetEntryPoint AddressOf DllEntryPoint в классе CStdDLLInfo. - Загрузка DLL в процесс
- Компилирование библиотеки
Откомпилируйте свою библиотеку обычным способом.
Если вы все сделали правильно, у вас сразу же получится готовая к использованию библиотека.
Практика
Давайте попробуем с помощью FireNativeDLL написать пробную DLL.
- Создаём новый проект Standard DLL.
- Убираем свою точку входа (см. выше как это сделать)
- Помещаем в модуль следующий код:
- Код: Выделить всё
Public Function GetUserAge() As Integer
GetUserAge = CInt(InputBox("Сколько вам лет?", "Привет!"))
End SubFunction
Public Sub ShowScreenSize
MsgBox "Размер вашего экрана: " + Cstr(Screen.Width / Screen.TwipsPerPixelX ) + "x" + cstr(Screen.Height / Screen.TwipsperPixelY ) + " пикселей", vbInformation, "Сведения об экране"
End Sub
- Теперь сделаем так, чтобы функции GetUserAge и ShowScreenSize стали экспортируемыми. Для этого откроем класс CStdDLLInfo и добавим туда по строчке для каждой функции. Таким образом, код класса будет выглядеть примерно так:
- Код: Выделить всё
Public Sub GetDllInfo(ByVal Informer As Object)
With Informer
' Формат записи удивительно прост
' Пример:
' .Export "SomeFunction", AddressOf MySomeFunction
' - "SomeFunction" - "экспортное" имя функции
' - MySomeFunction - имя функции в модуле
' Внимание:
' Все экспортируемые функции должны быть непременно
' в модуле (в модулях, - не обязательно все в одном)
' В остальном никаких ограничений нет
'===================================
.Export "GetAge", AddressOf GetUserAge
.Export "ShowScreenResolution", AddressOf ShowScreenSize
'====================================
' Если вы хотите определить свою собственную точку входа,
' сделайте это вызовом SetEntryPoint:
''''''' .SetEntryPoint AddressOf DllEntryPoint
End With
End Sub
- Компилируем библиотеку. Назовём её testlib.dll
- Запустим Dependency Walker ( Dependency Walker - стандартная утилита, позволяющая смотреть кто что экспортирует, и кто у кого чего импортирует (иными словами - иерархию зависимости) входящая в поставку Visual Studio 6. Поэтому она у вас должна быть. Должна, но не обязана - вы могли отключить установку Tools при инсталляции VS. В этом случае рекомендую вам ею обзавестись.), и убедимся, что наши функции экспортируются:
- А теперь проверим непосредственно вызов функций из DLL.
Допустим, нашу библиотеку мы положим в корень диска C, и путь к ней теперь таков: c:\testlib.dll- Код: Выделить всё
Private Declare Function GetAge Lib "c:\testlib.dll" () As Integer
Private Declare Sub ShowScreenResolution Lib "c:\testlib.dll" ()
Private Sub Form_Click()
MsgBox "Ваш возраст: " + CStr(GetAge())
ShowScreenResolution
End Sub
Запустите, щелкните по форме и убедитесь, что всё работает!- Попробуем вызвать ShowScreenResolution через rundll32:
Пуск->Выполнить->"rundll32 c:\testlib.dll ShowScreenResolution"->[ОК] - С++ (создайте проект Win32 Console Application):
- Код: Выделить всё
#include "stdafx.h"
#include <windows>
int main(int argc, char* argv[])
{
HMODULE MyLib;
FARPROC pFunction;
MyLib = LoadLibrary("c:\\testlib.dll");
if(MyLib)
{
pFunction = GetProcAddress(MyLib, "ShowScreenResolution");
pFunction();
FreeLibrary(MyLib);
}
}
Внимание!
Следует помнить о том, что:
- VB ожидает на входе получить строку в юникоде. Это значит, что возможно вам придётся делать A- и W- варианты функции (т.е. MyFunctionA, MyFunctionW ), в которых одним из аргументов является строка, либо один из этих вариантов.
A-вариант функции должен перед использованием строки сделать с ней sMyStringArgument = StrConv(sMyStringArgument, vbUnicode). (Но учтите, что нам приходит немного не так строка, что ожидает VB, -- спасибо keks-n за объективное замечание)
Декларировать такую функцию придётся как обычно -
Declare Function ...... (ByVal sMyStringArgument As String) ....
W-вариант же, наоборот, ничего не должен делать со строкой.
Однако декларировать такую функцию придётся таким образом:
Declare Function ... (ByVal sMyStringArgument As Long) ......
А при вызове, передавать StrPtr(sSomeString). - VB не предполагает, что функции, вызываемые с помощью Declare, могут выбрасывать VB-подобные ошибки.
Поэтому, если внутри функции в вашей библиотеке произойдёт ошибка, которая при этом вами не обрабатывается (внутри функции нет On Error ... -структур), выполнение улетит чёрти-куда.
Это означает, что вам всегда нужно предусматривать всевозможные ошибки, при проектировнии DLL-библиотеки.
Перспективы развития
У меня в планах сделать так, чтобы информация об экспортируемых функциях помещалась ещё и в TLB. Это значит, что во-первых, нам станет доступен Explicit-импорт функций.
Т.е. нам не нужно будет знать, как декларировать функции, нам вообще не нужно будет их декларировать - мы подключаем библиотеку в Project-References и сразу же можем использовать все интересующие нас функции.
Т.е. открыв Object Browser, мы сможем найти там все экспортируемые функции библиотеки.
Explicit-импорт хорош тем, что он происходит значительно быстрее (где-то в 3-4 раза быстрее), чем импорт через Declare.
Во-вторых, это значит, что мы сможем нормально обрабатывать ошибки, возникающие внутри библиотеки.
Ну и в-третьих, это значит, что нам абсолютно не придётся возиться с проблемой конвертирования строк Unicode->ANSI->Unicode, которая преследует нас при использовании Declare.
Рулеззз
_____________________________
В принципе, для того, чтобы использовать FireNativeDLL прочитанной информации будет целиком и полностью достаточно. Прочтение следующего далее текста является совершенно не обязательным, поэтому его можно пропустить.
Тем не менее, самые любопытные могут продолжить. Новичков сразу предупрежу, что вы встретите для себя множество непонятных вещей, которые, возможно, вас отпугнут.
Опять же, говорю, понимание этой части не требуется для использования FireNativeDLL, поэтому, пусть вас не пугает то, что вы что-то не поймёте (или вообще ничего не поймёте ).
_____________________________
Как это работает?
Итак, приступим.
Для начала вспомним проект NativeDLL от tyomitch-а и GSerg-а. Там DLL создавалась EXE-шником, который компилировался VB. При этом, компоновщику (далее: линкеру) подсовывался ключ /FIXED:NO. Иными словами, мы заставляли компоновщика создавать секцию .reloc .
Я же выбрал несколько иной путь. Пойдём от того, что VB уже умеет создавать ActiveX DLL. А ActiveX DLL являются частным случаем NativeDLL, поэтому при создании ActiveX DLL мы уже имеем секцию релоков и главное - инициализацию рантайма .
Значит, наша задача сводится только к тому, чтобы сформировать в DLL-файле таблицу экспорта. А это относительно несложно.
Возникает вопрос: как получить список функций, которые создатель DLL хочет экспортировать? Список функций мы можем засунуть много куда (например - в ресурсы), но дело всё в том, что нам нужно знать не только имена функций, но ещё и их адреса в библиотеки.
Поэтому единственный вараинт, который мне пришёл в голову - сделать класс (СStdDLLInfo), который будет опрашиваться и который возвратит нам список имён/адресов для всех экспортируемых функций.
Итак, начнём делать функцию ProcessDLLFile(Byval sFileName as string), которая будет ActiveXDLL превращать в NativeDLL.
Для начала нам нужно каким-то образом создать экземпляр класса CStdDLLInfo. CoCreateInstance скажите вы? Ан нет - CoCreateInstance требуется, чтобы класс был зарегистрирован в реестре, а в условиях нашей задачи это не допустимо. Вы только представьте себе - нам нужно создать простенькую DLL-ку с двумя функциями (которую вообще нет нужды регистрировать где-либо), и нам придётся её регистрировать как ActiveX-библиотеку. Поэтому, пойдём другим путём - экземпляр класса можно создать с помощью IClassFactory::CreateInstance. Для вызова IClassFactory::CreateInstance нам нужно знать CLSID класса, который мы хотим создать. Где взять этот CLSID? Для каждой конкретной откомпилированной библиотеки он будет свой, поэтому зашить его в код (в отличие от IID-ов интерфейсов IUnknown и IClassFactory) мы не можем. Но мы знаем, что CLSID-ы хранятся в TLB, которую VB прячет в ресурсы каждой производимой им ActiveX библиотеки, а значит и в ресурсах нашей библы TLB есть.
Итак, нам нужно достать из ресурсов библиотеки TLB, найти в ней нужный класс, получить его CLSID, а затем создать экземпляр.
Загрузить TLB из ресурсов нам поможет функция LoadTypeLibEx. Постфикс Ex отличает её от LoadTypeLib тем, что у неё есть аргумент regkind, позволяющий нам загрузить TLB, не регистрируя её в реестре, а это очень важно для нас.
Передаём этой функции путь к библиотеке, значение константы REGKIND_NONE (означающее, что TLB не нужно регистрировать), и указатель на переменную-приёмник, в которую функция запишет указатель на некоторый объект.
Поподробнее об некотором объекте. Так вот, некоторый объект, это объект, с которым можно работать через интерфейс ITypeLib и получать информацию о типах, описанных в TLB. И тут мы встречаем новую проблему. Работать с объектами в VB мы можем только в двух случаях:
1) У нас есть описание интерфейса, который поддерживает этот объект, через который мы с ним будем работать.
2) Объект поддерживает интерфейс IDispath. Тогда мы можем вызывать методы "через точечку" даже не зная, есть они у объекта или нет.
К сожалению, у нас нет TLB с описанием интерфейса ITypeLib (его можно сделать при желании или взять у Edanmo, но это не так интересно как то, что я опишу далее), и наш некоторый объект не поддерживает IDispath.
Углубимся чуть-чуть в основы COM. Интерфейс - это, по сути, всего лишь массив Long-ов, в котором содержатся адреса методов. У каждого метода есть свой ID. Каждому методу соответствует свой DWORD (т.е. Long) в VTbl (в соответствии с ID-ом метода), который содержит адрес метода в памяти. Т.е. чтобы вызвать какой-то метод, надо узнать его адрес (из VTbl), вызвать его, передав в качестве первого аргумента указатель на объект, а в качестве остальных аргументов - те аргументы, которые должен принимать метод (если метод не имеет параметров, ничего больше и не передаётся).
Значит, чтобы вызвать какой-либо метод какого-либо объекта, нужно знать:
- Указатель на объект
- ID метода
- Кол-во аргументов, которое ожидает метод
Указатель на объект у нас есть, ну а порядковый номер метода CreateInstance мы узнать всегда можем.
Итак, сделаем функцию, которая будет вызывать метод объекта по его ID.
Функция должна:
- Получить адрес VTbl
- Прибавить к нему (ID-1)*4 -- получив адрес ячейки, в которой находится адрес метода
- Прочитать эту ячейку - получить указатель на метод.
- Вызвать функцию по указателю, передав требуемые аргументы.
Хех И снова проблема: в VB нет встроенного ассемблера, поэтому мы никак не можем вызвать функцию по указателю .
Тем не менее, сдаваться мы не намерены, и поэтому будем искать выход из ситуации - способ вызвать функцию по указателю. Некоторые для этих целей используют CallWindowProc, но я решил пойти другим путём.
Так вот, встроенного ассемблера у нас в VB нет, но есть "невстроенный", например FASM :) Ничего не мешает нам написать нужный код на ассемблере, а потом тупо записать его поверх VB-шного кода.
Мы напишем на ассемблере код, а затем запишем его поверх функции ASMCall. Главное в этом деле - сделать так, чтобы функция ASMCall была больше, чем код, который мы напишем на асме.
После того, как мы запишем поверх родного кода ASMCall свой код, вызов ASMCall будет приводить к тому, что будет выполняться не VB-шный код, а тот код, который мы написали на ассемблере
Для более-менее универсального STDCALL-вызова нам необходимо знать три вещи: адрес, кол-во аргументов, и сами аргументы.
Пусть наша функция ASMCall будет принимать три аргумента: указатель на функцию, указатель на массив аргументов и кол-во аргументов:
- Код: Выделить всё
Public Function ASMCall(ByVal lpFunction As Long, ByVal lpArguments As Long, ByVal lArgCount As Long) As Long
' Тут будет мусор, который растянет функцию до нужных размеров.
' До каких - можно будет сказать только после написания кода на асме.
End Function
Теперь, обратимся собственно к ассемблерному коду. Я родил следующее (синтаксис FASM-а):
- Код: Выделить всё
pop edx
pop eax
pop esi
pop ecx
or ecx, ecx
jz Calling
CopyDword:
push dword[esi+ecx*4-4]
loop CopyDword
Calling:
push edx
jmp eax
(кратко поясню, что делает этот код: в EDX мы помещаем адрес возврата, в EAX - адрес функции, в ESI - указатель на начало массива аргументов, в ECX - кол-во аргументов; сраниваем значение ECX (т.е. кол-во аргументов) с нулём, если оно нулевое - переходим к коду, который идёт после метки Calling. Если же значение ECX ненулевое - кладём в стек число, которое лежит по адресу ESI + ECX * 4 - 4. Уменьшаем ECX, и делаем так, пока ECX не станет равен 0.
Когда ECX станет равным нулю - все аргументы будут уложены в стек в обратном порядке (как требует соглашение STDCALL). Теперь кладём в стек адрес возврата и переходим к выполнению функции.
Некоторых смущает jmp eax вместо call eax. Так вот - JMP мы делаем из соображений экономии. Возврат после jmp произойдёт сразу туда куда надо, тогда как после call выполнение возвратится к инструкции, которая была бы после call. Т.е. после call должен был бы идти ещё и retn 0.)
Отлично! Код, делающий вызов по указателю у нас есть. Теперь осталось записать его вместо оригинального кода функции ASMCall.
Для этого мы воспользуемся функцией CopyMemory.
Методом проб и ошибок, было установлено, что код
- Код: Выделить всё
Dim c As Long
c = &H12345678
c = VarPtr(c)
ASMCall = VarPtr(c)
достаточно велик, для того чтобы вместить в себя наш асм-код.
Итак, этот бредовый код мы помещаем в функцию ASMCall. Перед использованием функции ASMCall нам надо записать в неё нужный код. Для этого сделаем процедуру PrepareASMFunctions.
-- Что должна сделать PrepareASMFunctions?
-- Да сколько можно говорить - записать нужный код поверх ASMCall - скажите вы.
И будете неправы .
Дело в том, что память в Windows (вообще, не в Windows, а в IA-32) устроена так, что всё адресное пространство делится на страницы. Страница является ключевым понятием в работе виртуальной памяти: память отводится страницами, память характеризуется страницами, и т.д.
Так вот, у каждой страницы памяти есть атрибуты - что можно делать с этой страницей, а что нельзя. На аппаратном уровне есть всего два атрибута - можно ли читать/выполнять и можно ли писать. Но Windows отдаляет нас от аппаратного уровня, и заставляет нас поверить, что на самом деле атрибута 3: R / W / X - т.е. read / write / execute.
У страниц памяти, где располагается код, стоят атрибуты r-x. Т.е. записывать туда мы ничего не можем. Поэтому нам надо изменить атрибуты защиты памяти, записать, а потом вернуть прежние атрибуты.
Смена атрибутов производится функцией VirtualProtect.
Итак, немного протрудившись, имеем следующий код:
- Код: Выделить всё
Dim lPageAccess As Long
Dim bASMCall(0 To 16) As Byte
bASMCall(0) = &H5A ' pop edx ' Кладём в EDX адрес возврата
bASMCall(1) = &H58 ' pop eax ' Кладём в EAX адрес процедуры
bASMCall(2) = &H5E ' pop esi ' Кладём в ESI указатель на начало массива аргументов
bASMCall(3) = &H59 ' pop ecx ' Кладём в ECX кол-во аргументов
bASMCall(4) = &H9 ' or ecx, ecx ' Если аргументов нет, пропускаем след. шаг
bASMCall(5) = &HC9
bASMCall(6) = &H74 ' jz вызов ' Делаем сразу же вызов.
bASMCall(7) = &H6 '
bASMCall(8) = &HFF ' push dword[esi+ecx*4-4] ' Кладём в стек нужный аргумент
bASMCall(9) = &H74
bASMCall(10) = &H8E
bASMCall(11) = &HFC
bASMCall(12) = &HE2 ' loop на пуш ' Далаем это, пока аргументы не кончатся (ECX не станет = 0)
bASMCall(13) = &HFA
bASMCall(14) = &H52 ' push edx ' Возвращаем "на место" адрес возврата
bASMCall(15) = &HFF ' jmp eax ' И переходим на процедуру. (Здесь мог быть call/retn 0 - но это больше, и дольше )
bASMCall(16) = &HE0
' Записываем новый код в функцию ASMCall:
VirtualProtect AddressOf ASMCall, 17, PAGE_READWRITE, lPageAccess
CopyMemory AddressOf ASMCall, VarPtr(bASMCall(0)), 17
' Делаем страницы памяти вновь выполняемыми
VirtualProtect AddressOf ASMCall, 17, lPageAccess, lPageAccess
Думаете, всё так хорошо, как кажется? Нет же!
Этот способ не будет работать в среде разработки - только лишь в скомпилированном проекте будет работать он.
--Что же делать? Неужели, не получится?
-- Ещё как получится. Мы ведь не намерены сдаваться?
Нам на помощь придёт условная компиляция. Мы тот же самый код поместим в библиотеку caller.dll .
А дальше, в зависимости от того, в IDE мы сейчас или нет будем использовать разный код - либо функцию из caller.dll, либо свою, т.е:
- Код: Выделить всё
#If IN_IDE Then
Public Declare Function ASMCall Lib "caller.dll" Alias "StdCall" (ByVal lpFunction As Long, ByVal lpArguments As Long, ByVal lArgCount As Long) As Long
#End If
...
#If Not IN_IDE Then
' Нам нужна эта функция только если мы не в VBIDE.
' Т.е. только если мы - скомпилированный проект.
' В противном случае используется функция из caller.dll
Public Function ASMCall(ByVal lpFunction As Long, ByVal lpArguments As Long, ByVal lArgCount As Long) As Long
' Не смотря на бредовость нижеследующего кода, удалять его
' КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО. На самом деле этот код здесь
' только для того, чтобы раздуть ASMCall до нужных размеров.
' (Нужных - достаточных для записи туда своего кода)
Dim c As Long
c = &H12345678
c = VarPtr(c)
ASMCall = VarPtr(c)
End Function
#End If
Public Sub PrepareASMFunctions()
#If Not IN_IDE Then
' Дело в том, что при запуске проекта из под VBIDE, проект компилируется
' в псевдокод. Это значит, что трюк с перезаписью кода функции не сработает
' Поэтому мы пошли на такую хитрость (да какая там хитрость?), как
' использование разных ASMCall-ов с помощью условной компиляции.
' Этот же код я поместил в библиотеку caller.dll, и вынес его в экспорты
' как функцию StdCall (потому что данный код делает вызов по соглашению StdCall)
' Так вот, если вы запускаете из под VBIDE - константа IN_IDE включена
' и вызывается функция из библиотеки.
' Если же вы хотите скомпилировать проект, убираете константу IN_IDE,
' компилируете, получаете вызов через реврайтинг родной функции.
' Таким образом, библиотека caller.dll НЕ НУЖНА для скомпилированного
' проекта.
Dim lPageAccess As Long
Dim bASMCall(0 To 16) As Byte
bASMCall(0) = &H5A ' pop edx ' Кладём в EDX адрес возврата
bASMCall(1) = &H58 ' pop eax ' Кладём в EAX адрес процедуры
bASMCall(2) = &H5E ' pop esi ' Кладём в ESI указатель на начало массива аргументов
bASMCall(3) = &H59 ' pop ecx ' Кладём в ECX кол-во аргументов
bASMCall(4) = &H9 ' or ecx, ecx ' Если аргументов нет, пропускаем след. шаг
bASMCall(5) = &HC9
bASMCall(6) = &H74 ' jz вызов ' Делаем сразу же вызов.
bASMCall(7) = &H6 '
bASMCall(8) = &HFF ' push dword[esi+ecx*4-4] ' Кладём в стек нужный аргумент
bASMCall(9) = &H74
bASMCall(10) = &H8E
bASMCall(11) = &HFC
bASMCall(12) = &HE2 ' loop на пуш ' Далаем это, пока аргументы не кончатся (ECX не станет = 0)
bASMCall(13) = &HFA
bASMCall(14) = &H52 ' push edx ' Возвращаем "на место" адрес возврата
bASMCall(15) = &HFF ' jmp eax ' И переходим на процедуру. (Здесь мог быть call/retn 0 - но это больше, и дольше )
bASMCall(16) = &HE0
' Записываем новый код в функцию ASMCall:
VirtualProtect AddressOf ASMCall, 17, PAGE_READWRITE, lPageAccess
CopyMemory AddressOf ASMCall, VarPtr(bASMCall(0)), 17
' Делаем страницы памяти вновь выполняемыми
VirtualProtect AddressOf ASMCall, 17, lPageAccess, lPageAccess
#End If
End Sub
Вот! Вызывать по указателю мы научились. Теперь надо научится вызывать методы объектов, используя VTbl.
Я уже описывал, что для этого нам нужно сделать, поэтому просто выложу код:
- Код: Выделить всё
Public Function CallVirtualMethod(ByVal lpObject As Long, ByVal lMethodID As Long, ParamArray Arguments()) As Long
' Вызов виртуального метода в плане техническом не отличается
' от вызова обычной процедуры.
' В плане же логическом - отличие заключается в том, что при вызове
' первым аргументом передаётся указатель на объект.
' Адреса виртуальных методов объекта содержатся в VTable.
' Нам нужно найти VTable объекта, взять оттуда адрес, вызвать
' процедуру по этому адресу, передав указатель на объект.
' Ничего сложного :)
Dim lpVTable As Long ' Указатель на VTable
Dim lpMethod As Long ' Адрес кода метода
Dim lArguments() As Long ' Массив аргументов
Dim lArgCount As Long ' Кол-во аргументов
Dim i As Long
GetMem4 lpObject, VarPtr(lpVTable) ' Получаем адрес VTable
GetMem4 lpVTable + (lMethodID - 1) * 4, VarPtr(lpMethod)
lArgCount = UBound(Arguments) - LBound(Arguments) + 1
ReDim lArguments(0 To lArgCount) ' +1, потому что нам нужно передать указатель на объект помимо всего
For i = 1 To lArgCount
lArguments(i) = CLng(Arguments(LBound(Arguments) + i - 1))
Next i
lArguments(LBound(lArguments)) = lpObject ' И кладём туда указатель на объект
' Теперь осталось лишь вызвать интересующий нас метод
CallVirtualMethod = ASMCall(lpMethod, VarPtr(lArguments(0)), lArgCount + 1)
End Function
Итак. Мы дошли до самого интересного. Опрашивая некоторый объект, мы доходим до IClassFactory::CreateInstance.
Вызывая IClassFactory::CreateInstance, получаем ссылку на объект класса CStdDLLInfo.
Вызываем у него метод GetDllInfo и он заполняет нужными данными наши структуры, а также сообщает нам о желании автора определить свою точку входа.
Список экспортируемых функций и их RVA мы имеем, остаётся создать свою таблицу экспорта и сохранить изменения в файле.
Делаем (я не буду описывать процесс создания таблицы экспорта - иначе спать я сегодня не лягу - кому интересно, посмотрите как это сделано в исходнике ) это.
Пытаемся вызвать функцию из библиотеки и ... о чудо! - функция вызывается
А теперь пытаемся вызвать такую функцию, которая требует инициализации рантайма (например такую, как ShowScreenResolution), и получаем облом.
Я некоторое время не мог понять, почему оно так. Но не буду мучить читателя, а сразу скажу - дело в том, что родная точка входа библиотеки инициализирует рантайм лишь частично. А полная инициализация рантайма происходит лишь при создании IClassFactory.
Для нас это не очень приятная новость, потому что это означает то, что нам придётся внедрять в DLL код, который будет делать и первичную и вторичную инициализацию рантайма. О как
Сложно? Но мы не сдаёмся, помните?
Итак, открываем ActiveX библиотеку, сделанную при помощи VB в каком-нибудь отладчике.
Я буду использовать свой любимый отладчик - OllyDbg
Когда мы откроем библиотеку и посмотрим на точку входа библиотеки, мы увидим следующее (на самом деле - следующее мы не увидим. А конкретно - мы не увидим многочисленных стрелочек, которые я сам пририсовал и кусочек таблицы релоков. Я сейчас поясню, что и зачем):
Напомню - наша задача сейчас - внедрить в библиотеку код инициализации рантайма.
Наш код должен:
- Вызвать msvbvm60!UserDLLMain
- Создать экземпляр IClassFactory, если только Reason=DLL_PROCESS_ATTACH.
- Уничтожить его (если создали)
- Вызвать пользовательскую точку входа
Ясно, что этот код будет слишком большим, и записать его поверх того, что изображено на картинке ну никак не получится.
Поэтому, код мы поместим в свободное место в секции кода. Но разместить код там мало, надо ещё как-то передать на него управление.
Я не просто так поместил на картинку кусочек таблицы релокации. Дело в том, что два push-а, которые помещают в стек какие то VA (а не RVA) могут делать это лишь в том случае, если эти VA всегда будут действительными. А они будут всегда действительными только в том случае, если при загрузке по нестандартной базе их кто-то подправит. А подправить их смогут лишь релоки.
Смотрим в релоки, и правда - dword-ы по смещениям &h11001426 и &h1100142b релочятся. Это с одной стороны хорошо, с другой плохо. Плохо потому, что если мы запишем туда какой-либо код, во время релокации он непременно испортится (если конечно не получится так, что в этих местах не окажутся какие-либо выгодные нам числа).
Поэтому, единственное, что мы можем безболезненно править - это число, которое я на рисунке выделил зелёным (pop-ы мы не учитываем).
Сейчас, это инструкция перехода туда, куда указывает синяя стрелка. Мы же сделаем так, что это будет инструкция перехода туда, куда указывает красная стрелка (она пока никуда не указывает - потому что мы ещё ничего не внедряли).
Определились, в "зелёный" DWORD мы запишем rel32-операнд перехода, так, чтобы он был на наш код (далее "хукер").
Что же должен делать хукер? Я уже описывал это, поэтому перейдём сразу к коду (синтаксис FASM-а):
- Код: Выделить всё
; Стандартная фишка - обращение к стеку через ebp.
; в EBP сохраняем ESP, и освобождаем себе мозги от лишних дум
push ebp
mov ebp, esp
add ebp, 4
; Создаём фальшивый стековый фрейм. В принципе, могли бы и
; не создавать, а отмотать esp вперёд после вызова. Было бы
; быстрее. Ладно, потом исправлю :)
push dword[ebp+20d]
push dword[ebp+16d]
push dword[ebp+12d]
push dword[ebp+08d]
push dword[ebp+04d]
call 0x00351030 ; Число взято с потолка. Потом запишем сюда настоящее число
; Сраниванием Reason c DLL_PROCESS_ATTACH.
cmp dword[ebp+16d], 1
jne SkipRTInitializing ; Если нет, пропускаем вторичную инициализацию
mov ecx, [esp+16d] ; Кладём в ECX hInstance = ImageBase
lea edx, [ecx+0x00351030] ; Помещаем в EDX VA DllGetClassObject.
lea eax, [ecx+0x00351030] ; Приёмник
push eax
push eax
lea eax, [ecx+0x00351030] ; IID
push eax
lea eax, [ecx+0x00351030] ; classid
push eax
call edx ; Вызывем DllGetClassObject.
pop edx ; Кладём в EDX указатель на ссылку на объект.
mov edx, [edx] ; Получаем ссылку на объект
mov ecx, [edx] ; Получаем указатель на VTbl
add ecx, 8 ; Прибавляем 8, получаем указатель на Release.
push edx ; Помещаем указатель на объект в стек
call dword[ecx] ' и вызываем Release.
SkipRTInitializing:
pop ebp ; Возвращаем старое значение ebp
pop eax ; Помещаем в стек адрес возврата
add esp, 8 ; "Убираем" из стека два ненужных аргумента
push eax ; Кладём адрес возврата на место
db 0xe9, 0,0,0,0 ; FASM был настолько интеллектуален, что
; не дал мне возможности сделать call rel32
; оптимизируя его до call rel8
; Я его перехитрил, тем не менее :)
retn 12d ; Родной returner (если NEP не определена)
В скомпилированном виде этот код занимает чуть меньше 100 байт. Внедряем его в конец в свободное место в секции кода, после чего вместо левых 00351030 записываем RVA нужных нам данных, которые мы поместили в секцию данных (т.н. "кусочек данных", см. исходники DLL-модификатора).
Собственно говоря, на этом наше нелёгкое дело заканчивается. Функция ProcessDLLFile готова.
Возникает другой вопрос. А когда мы должны вызывать её, и вообще, как отловить момент компиляции файла?
Отловить момент компиляции относительно легко - для этого переименую link.exe в old_link.exe, а сами встанем на место link.exe .
Теперь нас будут вызывать в процессе компиляции, передавая нам множество интересных данных, но нас из них интересует лишь ключ /OUT:"какой_то_путь".
Задача нашего link.exe - перенаправить вызов на оригинальный линкер (old_link.exe), дождаться его завершения, а потом, скормить файл функции ProcessDLLFile.
Есть тут, правда, два не очень приятных момента:
1) Функция LoadTypeLibEx отказывается работать внутри link.exe
2) Вызывать ProcessDLLFile лучше всего асинхронно, чтобы не подтормаживать среду.
По этим двум причинам я вынес ProcessDLLFile в отдельное приложение, которое запускает псведо-линкером и выполняется асинхронно.
That's all