Dll в машинных кодах — Архив WASM.RU
В статье «Приложение Windows голыми руками» было показано, как с помощью debug «вручную» собрать простейшее Win32 exe-приложение с MessageBox’ом. На этот раз предлагается аналогичным образом создать простейшую dll; это тематическое продолжение прошлой статьи и в то же время необходимый фундамент для статей будущих - поскольку я собираюсь рассказывать в них о создании в машинных кодах компонентов COM, а для них обойтись без dll ну никак нельзя.
Я полагаю, что читатель внимательнейшим образом изучил прошлый материал и умеет теперь с лёту создавать PE-заголовки, таблицы импорта и секции кода и данных J. Предполагается также, что благодаря творчеству Свина и последовавшему за этим повальному поветрию увлечения мануалами от Интела никому не составит особого труда разобраться в hex’ах или хотя бы даже и в бинарных кодах. Поэтому все внимание сосредоточим на более полезных вещах - на том, что же отличает dll от обычных exe-файлов. А отличий главных два: появляются экспортируемые функции и, следовательно, таблица экспорта, а также, как ни прискорбно, придется нам разбираться с настройками (relocations), поскольку загрузка нашей dll по нашему любимому базовому адресу 10000000h вовсе не гарантируется L.
На этот раз для разнообразия сделаем в нашем PE-файле 4 секции - для кода, данных, импорта с экспортом и настроек. Проявим также, в отличие от прошлого раза, почтение и дадим им имена: .code, .data, .rdata и .reloc соответственно. Они расположатся в памяти по смещениям 1000h, 2000h, 3000h, 4000h, а в файле 200h, 400h, 600h и 800h соответственно. Само содержание будет тем же, т.е. экспортируем лишь одну функцию, вся работа которой будет заключаться в отображении MessageBox’а. В данных всего две строки; создаем файл data.txt:
Код (Text):
n data.bin r cx 200 f 0 l 200 0 e 0 "Dll" e 10 "Экспортированная функция" m 0 l 200 100 w qНадеюсь, понятно, что в файле записаны команды для debug. На этот раз мы решили несколько автоматизировать процесс J.
Итак, строка с заголовком располагается у нас в начале секции данных по смещению 2000h, а «любимый» адрес в памяти будет соответственно 10002000h. Для строки с сообщением цифры будут 2010h и 10002010h соответственно.
А теперь - код! Придется снова импортировать MessageBoxA из User32.dll; на этот раз IAT расположится в собственной секции - .rdata - как обычно, в самом начале, т.е. по смещению 3000h; а «любимый» адрес будет 10003000h. Других импортов нет. Заполняем файл code.txt:
Код (Text):
n code.bin r cx 200 f 0 l 200 0 a 0 ; параметры MessageBox’a db 6a 0 db 68 0 20 0 10 db 68 10 20 0 10 db 6a 0 ; вызов MessageBox db ff 15 0 30 0 10 ; возврат db c3 m 0 l 200 100 w qПустую строку нельзя убирать! А то debug будет ругаться, а вы получите фуфло вместо классного бинарного блока J.
Однако, как мы - не прошло и пяти минут, а уже полфайла составили! На носу новый материал, однако. Посмотрим на код и выделим «топкие» места:
Код (Text):
1000: 6a 00 68 <strong>00</strong> | <strong>20 00 10</strong> 68 | <strong>10 20 00 10</strong> | 6a 00 ff 15 1010: <strong>00 30 00 10</strong> | c3Я выделил жирным «любимые» адреса, попавшие в состав инструкций. Подлая система может закинуть нашу dll куда-нибудь совсем в другое место - вот тогда-то наши адреса и накроются, а код начнет запихивать в стек всякий хлам и вдобавок отправит все это вместо нашей импортированной функции по невесть какому адресу. Именно эти три 32-разрядных значения и должны быть настроены; а для этого их надо указать в таблице настроек.
Каждая настройка представлена всего лишь двумя байтами - 16-разрядным значением, причем 4 старших бита обозначают тип настройки. Для Win32 это практически всегда значение 3, означающее, что надо «поправить» 32-разрядный адрес по указанному смещению. А смещение указывают оставшиеся 12 битов. Но, как вы понимаете, этого хватает лишь на смещения в пределах одной страницы - 4 Кб. Так оно и есть - настройки группируются в блоки; для каждой настраиваемой страницы имеется свой блок, а в начале блока первые 4 байта содержат смещение данной страницы относительно базового адреса загрузки, а следующие 4 байта - размер блока (вместе с первыми 8 байтами). Остальное содержимое блока - набор настроек для данной страницы (см. рис.)
При загрузке dll система вычисляет так называемую дельту - разницу между базовым адресом загрузки, указанным в PE-заголовке, и адресом, по которому фактически загружена dll. Естественно, если dll загружена по своей «любимой» базе, дельта равна 0 и никаких настроек не требуется. Если же это не так, дельта добавляется к каждому 32-разрядному значению, для которого имеется настройка. Всего-то и делов.
В нашем случае должно быть 3 настройки; их смещения относительно начала страницы - 3, 8 и 10h. С учетом типа (3) получаем числа 3003h, 3008h и 3010h. Блок должен быть выровнен по 32-разрядной границе, поэтому в конец добавим наполнитель из «пустой» настройки (для нее есть даже свой тип - естественно, 0). В итоге получаем: RVA страницы - это смещение настраиваемой, т.е. кодовой страницы - 1000h; размер блока - 8 байт + 3 настройки по 2 байта + 1 «пустая» настройка (2 байта) - всего 10h.
Секция настроек готова! Набираем файл reloc.txt:
Код (Text):
n reloc.bin r cx 200 f 0 l 200 0 a 0 ; RVA страницы dw 1000 0 ; размер блока dw 10 0 ; настройки dw 3003 dw 3008 dw 3010 m 0 l 200 100 w qОстается экспорт - поскольку статью читают специалисты по импорту, о нем больше ни слова. Главная таблица, объединяющая все остальные - таблица экспорта - имеет следующий вид:
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of imageВзаимоотношения различных таблиц показаны на следующем рисунке:
Как видно по рисунку, данные для экспорта представляют собой преимущественно различного рода смещения и индексы. При экспорте по ординалам используются всего две таблицы: основная таблица экспорта и таблица экспортируемых адресов. Последняя представляет собой просто массив 32-разрядных смещений реализаций соответствующих функций относительно базового адреса загрузки. Т.е. у каждой экспортированной функции имеется свой индекс в этом массиве; добавив к этому индексу т.н. базу, получаем ординал этой функции.
Если функции экспортируются по именам, добавляются еще три таблицы: ординалов, указателей имен и самих имен. С таблицей имен все понятно: она содержит экспортируемые имена. Причем их может быть меньше, чем экспортируемых адресов. «Стыковку» же имен с адресами соответствующих функций осуществляют таблицы ординалов и указателей имен; фактически, это два тесно сопряженных друг с другом массива. Сопряжение осуществляется за счет того, что на одном и том же месте (с одинаковым индексом) в обоих массивах находятся данные для одной функции: в первом массиве - индекс для таблицы экспортируемых адресов, во втором - адрес строки с именем соответствующей функции. Причем функции в этих двух массивах расположены так, что их имена упорядочены по алфавиту (в порядке возрастания их индексов в этих массивах). Заметьте: упорядоченными должны быть не сами имена (их можно даже разбросать по всей секции); упорядоченными должны быть указатели на них в другой таблице. Следует также помнить, что индексы в таблице ординалов 16-разрядные, а указатели имен - 32-разрядные.
Пора взяться за последнюю секцию - ‘.rdata’. Сначала прикинем «макет» (см. рис.)
Теперь файл rdata.txt:
Код (Text):
n rdata.bin r cx 200 f 0 l 200 0 a 0 ; Импорт ; IAT dw 3020 0 0 0 ; Таблица поиска dw 3020 0 0 0 ; Имя импортируемого модуля db "User32.dll" 0 a 20 ; Импортируемая функция с hint’ом db 0 0 "MessageBoxA" 0 a 30 ; Таблица импорта: ; смещение таблицы поиска dw 3008 0 ; 2 пустых поля dw 0 0 0 0 ; смещение имени dll dw 3010 0 ; смещение IAT dw 3000 0 a 60 ; Экспорт ; Таблица экспорта: ; 3 пустых поля dw 0 0 0 0 0 0 ; смещение имени dll dw 3094 0 ; база ординалов dw 1 0 ; число адресов dw 1 0 ; число имен dw 1 0 ; смещение адреса dw 3088 0 ; смещение указателя имени dw 3090 0 ; смещение ординала dw 308C 0 ; (Таблица) адресов dw 1000 0 ; (Таблица) ординалов dw 0 0 ; (Таблица) указателей имен dw 30a0 0 ; Имя dll db "Dll.dll" 0 a a0 ; Экспортируемая функция db "Function1" 0 m 0 l 200 100 w qПомните, пустые строки нельзя трогать! Теперь надо лишь слегка подправить PE-заголовок - файл header.txt:
Код (Text):
n Header.bin r cx 200 f 0 l 200 0 e 0 'MZ' e 3C 40 e 40 'PE' e 44 4C 01 a 46 ; Число секций db 04 a 54 ; Размер дополнительного заголовка db e0 00 ; Тип файла: установить флаг dll и флаг, разрешающий ; загружать образ по базовому адресу, отличному от ; указанного в PE-заголовке db 0E 21 ; "Магическое" значение db 0B 01 a 74 ; Базовый адрес загрузки db 00 00 00 10 ; Выравнивание секций db 00 10 00 00 ; Выравнивание в файле db 00 02 00 00 ; Старшая версия Windows db 04 a 88 ; Старшая версия подсистемы db 04 a 90 ; Размер загруженного файла в памяти db 00 50 00 00 ; Размер всех заголовков в файле db 00 02 a 9C ; Подсистема db 02 00 a A0 ; Зарезервированный размер стека db 00 00 10 00 ; Выделенный размер стека db 00 10 00 00 ; Зарезервированный размер кучи db 00 00 10 00 ; Выделенный размер кучи db 00 10 00 00 a B4 ; Число элементов каталога смещений db 10 00 00 00 ; ; Каталога смещений: ; смещение таблицы экспорта dw 3060 0 ; размер данных экспорта dw 4a 0 ; смещение таблицы импорта dw 3030 0 ; размер таблицы импорта dw 28 0 ; пропускаем 24 байта (3 элемента) dw 0 0 0 0 0 0 0 0 0 0 0 0 ; смещение таблицы настроек dw 4000 0 ; размер таблицы настроек dw 10 a 138 ; Начало таблицы секций ; ; имя первой секции db '.code' 0 0 0 ; размер секции в памяти dw 200 0 ; смещение секции относительно адреса загрузки dw 1000 0 ; размер данных секции в файле dw 200 0 ; смещение начала данных секции в файле dw 200 0 ; Пропускаем 12 байтов dw 0 0 0 0 0 0 ; атрибуты первой секции db 20 00 00 60 ; вторая секция db '.data' 0 0 0 dw 200 0 dw 2000 0 dw 200 0 dw 400 0 dw 0 0 0 0 0 0 db 40 0 0 c0 ; третья секция db '.rdata' 0 0 dw 200 0 dw 3000 0 dw 200 0 dw 600 0 dw 0 0 0 0 0 0 db 40 0 0 40 ; четвертая секция db '.reloc' 0 0 dw 200 0 dw 4000 0 dw 200 0 dw 800 0 dw 0 0 0 0 0 0 db 40 0 0 42 m 0 l 200 100 w qВсё! Собираем все вместе в файле make.bat:
Код (Text):
@echo off debug < header.txt > report.lst debug < code.txt >> report.lst debug < data.txt >> report.lst debug < rdata.txt >> report.lst debug < reloc.txt >> report.lst copy /b header.bin+code.bin+data.bin+rdata.bin+reloc.bin dll.dllЗапускаем этот файл и - ура! - получаем нашу dll. Всенепременно надо заглянуть в заботливо созданный для вас файл отчета - report.lst, чтобы придирчиво поискать там ошибки, о которых сообщает debug. Ведь вы, конечно, прекрасно знаете: метод cut&paste не спасает от самых дебильных ошибок!
Да, это, конечно, хорошо; но ведь понадобится еще и тестовое exe-приложение, чтобы проверить работу нашей dll? Я нисколько не сомневаюсь, что ваш уровень теперь позволит с легкостью самостоятельно создать в debug эту тестовую программку J.
Ладно, ладно... Вижу вытянувшиеся физиономии некоторых. Вы славно потрудились сегодня (даже если просто прочли это до конца), и в качестве бонуса я решил предоставить «ленивое» тестовое приложение на MASM’е 32-ом. Вот оно:
Код (Text):
.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib .data hM1 dword 0 hM2 dword 0 app db "Test dll",0 dll db "dll.dll",0 dll2 db "dll2.dll",0 fname db "Function1",0 err1 db "LoadLibrary (dll1) failed",0 err1_1 db "LoadLibrary (dll2) failed",0 err2 db "GetProcAddress (first) failed",0 err2_1 db "GetProcAddress (second) failed",0 .code start: invoke LoadLibrary,offset dll .if eax==0 invoke MessageBox,0,offset err1,offset app,MB_ICONERROR ret .endif mov hM1,eax invoke LoadLibrary,offset dll2 .if eax==0 invoke MessageBox,0,offset err1_1,offset app,MB_ICONERROR ret .endif mov hM2,eax invoke GetProcAddress,hM1,offset fname .if eax==0 invoke MessageBox,0,offset err2,offset app,MB_ICONERROR ret .endif call eax invoke GetProcAddress,hM2,offset fname .if eax==0 invoke MessageBox,0,offset err2_1,offset app,MB_ICONERROR ret .endif call eax invoke FreeLibrary,hM1 invoke FreeLibrary,hM2 ret end startНадо скопировать созданную нами dll.dll под новым именем dll2.dll в этот же каталог. Вся соль в том, что нам нужно проверить, что наши настройки были правильные, и система может с ними работать. А для этого требуются как минимум две dll, претендующие на одно и то же место в адресном пространстве. Самый ленивый способ, конечно, который только можно придумать - это просто использовать второй переименованный экземпляр. Но на радостях вы можете поэкспериментировать с текстами, хотя бы поменять выводимые MessageBox’ом сообщения и создать другую dll с другими именами модуля и экспортируемой функции.
До новых встреч! © Roustem
Dll в машинных кодах
Дата публикации 18 май 2004