Dll в машинных кодах

Дата публикации 18 май 2004

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):
  1.  
  2. n data.bin
  3. r cx
  4. 200
  5. f 0 l 200 0
  6. e 0 "Dll"
  7. e 10 "Экспортированная функция"
  8. m 0 l 200 100
  9. w
  10. q

Надеюсь, понятно, что в файле записаны команды для debug. На этот раз мы решили несколько автоматизировать процесс J.

Итак, строка с заголовком располагается у нас в начале секции данных по смещению 2000h, а «любимый» адрес в памяти будет соответственно 10002000h. Для строки с сообщением цифры будут 2010h и 10002010h соответственно.

А теперь - код! Придется снова импортировать MessageBoxA из User32.dll; на этот раз IAT расположится в собственной секции - .rdata - как обычно, в самом начале, т.е. по смещению 3000h; а «любимый» адрес будет 10003000h. Других импортов нет. Заполняем файл code.txt:

Код (Text):
  1.  
  2. n code.bin
  3. r cx
  4. 200
  5. f 0 l 200 0
  6. a 0
  7. ; параметры MessageBox’a
  8. db 6a 0
  9. db 68  0 20 0 10
  10. db 68 10 20 0 10
  11. db 6a 0
  12. ; вызов MessageBox
  13. db ff 15 0 30 0 10
  14. ; возврат
  15. db c3
  16.  
  17. m 0 l 200 100
  18. w
  19. q

Пустую строку нельзя убирать! А то debug будет ругаться, а вы получите фуфло вместо классного бинарного блока J.

Однако, как мы - не прошло и пяти минут, а уже полфайла составили! На носу новый материал, однако. Посмотрим на код и выделим «топкие» места:

Код (Text):
  1.  
  2. 1000: 6a 00 68 <strong>00</strong> | <strong>20 00 10</strong> 68 | <strong>10 20 00 10</strong> | 6a 00 ff 15
  3. 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):
  1.  
  2. n reloc.bin
  3. r cx
  4. 200
  5. f 0 l 200 0
  6. a 0
  7. ; RVA страницы
  8. dw 1000 0
  9. ; размер блока
  10. dw 10 0
  11. ; настройки
  12. dw 3003
  13. dw 3008
  14. dw 3010
  15.  
  16. m 0 l 200 100
  17. w
  18. 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):
  1.  
  2. n rdata.bin
  3. r cx
  4. 200
  5. f 0 l 200 0
  6. a 0
  7. ; Импорт
  8. ; IAT
  9. dw 3020 0 0 0
  10. ; Таблица поиска
  11. dw 3020 0 0 0
  12. ; Имя импортируемого модуля
  13. db &quot;User32.dll&quot; 0
  14.  
  15. a 20
  16. ; Импортируемая функция с hint’ом
  17. db 0 0 &quot;MessageBoxA&quot; 0
  18.  
  19. a 30
  20. ; Таблица импорта:
  21. ; смещение таблицы поиска
  22. dw 3008 0
  23. ; 2 пустых поля
  24. dw 0 0 0 0
  25. ; смещение имени dll
  26. dw 3010 0
  27. ; смещение IAT
  28. dw 3000 0
  29.  
  30. a 60
  31. ; Экспорт
  32. ; Таблица экспорта:
  33. ; 3 пустых поля
  34. dw 0 0 0 0 0 0
  35. ; смещение имени dll
  36. dw 3094 0
  37. ; база ординалов
  38. dw 1 0
  39. ; число адресов
  40. dw 1 0
  41. ; число имен
  42. dw 1 0
  43. ; смещение адреса
  44. dw 3088 0
  45. ; смещение указателя имени
  46. dw 3090 0
  47. ; смещение ординала
  48. dw 308C 0
  49. ; (Таблица) адресов
  50. dw 1000 0
  51. ; (Таблица) ординалов
  52. dw 0 0
  53. ; (Таблица) указателей имен
  54. dw 30a0 0
  55. ; Имя dll
  56. db &quot;Dll.dll&quot; 0
  57.  
  58. a a0
  59. ; Экспортируемая функция
  60. db &quot;Function1&quot; 0
  61.  
  62. m 0 l 200 100
  63. w
  64. q

Помните, пустые строки нельзя трогать! Теперь надо лишь слегка подправить PE-заголовок - файл header.txt:

Код (Text):
  1.  
  2. n Header.bin
  3. r cx
  4. 200
  5. f 0 l 200 0
  6. e 0 'MZ'
  7. e 3C 40
  8. e 40 'PE'
  9. e 44 4C 01
  10. a 46
  11. ; Число секций
  12. db 04
  13.  
  14. a 54
  15. ; Размер дополнительного заголовка
  16. db e0 00
  17. ; Тип файла: установить флаг dll и флаг, разрешающий
  18. ; загружать образ по базовому адресу, отличному от
  19. ; указанного в PE-заголовке
  20. db 0E 21
  21. ; &quot;Магическое&quot; значение
  22. db 0B 01
  23.  
  24. a 74
  25. ; Базовый адрес загрузки
  26. db 00 00 00 10
  27. ; Выравнивание секций
  28. db 00 10 00 00
  29. ; Выравнивание в файле
  30. db 00 02 00 00
  31. ; Старшая версия Windows
  32. db 04
  33.  
  34. a 88
  35. ; Старшая версия подсистемы
  36. db 04
  37.  
  38. a 90
  39. ; Размер загруженного файла в памяти
  40. db 00 50 00 00
  41. ; Размер всех заголовков в файле
  42. db 00 02
  43.  
  44. a 9C
  45. ; Подсистема
  46. db 02 00
  47.  
  48. a A0
  49. ; Зарезервированный размер стека
  50. db 00 00 10 00
  51. ; Выделенный размер стека
  52. db 00 10 00 00
  53. ; Зарезервированный размер кучи
  54. db 00 00 10 00
  55. ; Выделенный размер кучи
  56. db 00 10 00 00
  57.  
  58. a B4
  59. ; Число элементов каталога смещений
  60. db 10 00 00 00
  61. ;
  62. ; Каталога смещений:
  63. ; смещение таблицы экспорта
  64. dw 3060 0
  65. ; размер данных экспорта
  66. dw 4a 0
  67. ; смещение таблицы импорта
  68. dw 3030 0
  69. ; размер таблицы импорта
  70. dw 28 0
  71. ; пропускаем 24 байта (3 элемента)
  72. dw 0 0 0 0 0 0 0 0 0 0 0 0
  73. ; смещение таблицы настроек
  74. dw 4000 0
  75. ; размер таблицы настроек
  76. dw 10
  77.  
  78. a 138
  79. ; Начало таблицы секций
  80. ;
  81. ; имя первой секции
  82. db '.code' 0 0 0
  83. ; размер секции в памяти
  84. dw 200 0
  85. ; смещение секции относительно адреса загрузки
  86. dw 1000 0
  87. ; размер данных секции в файле
  88. dw 200 0
  89. ; смещение начала данных секции в файле
  90. dw 200 0
  91. ; Пропускаем 12 байтов
  92. dw 0 0 0 0 0 0
  93. ; атрибуты первой секции
  94. db 20 00 00 60
  95. ; вторая секция
  96. db '.data' 0 0 0
  97. dw 200 0
  98. dw 2000 0
  99. dw 200 0
  100. dw 400 0
  101. dw 0 0 0 0 0 0
  102. db 40 0 0 c0
  103. ; третья секция
  104. db '.rdata' 0 0
  105. dw 200 0
  106. dw 3000 0
  107. dw 200 0
  108. dw 600 0
  109. dw 0 0 0 0 0 0
  110. db 40 0 0 40
  111. ; четвертая секция
  112. db '.reloc' 0 0
  113. dw 200 0
  114. dw 4000 0
  115. dw 200 0
  116. dw 800 0
  117. dw 0 0 0 0 0 0
  118. db 40 0 0 42
  119.  
  120. m 0 l 200 100
  121. w
  122. q

Всё! Собираем все вместе в файле make.bat:

Код (Text):
  1.  
  2. @echo off
  3. debug &lt; header.txt &gt; report.lst
  4. debug &lt; code.txt &gt;&gt; report.lst
  5. debug &lt; data.txt &gt;&gt; report.lst
  6. debug &lt; rdata.txt &gt;&gt; report.lst
  7. debug &lt; reloc.txt &gt;&gt; report.lst
  8. 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):
  1.  
  2. .386
  3. .model flat,stdcall
  4. option casemap:none
  5.  
  6. include \masm32\include\windows.inc
  7. include \masm32\include\kernel32.inc
  8. include \masm32\include\user32.inc
  9. includelib \masm32\lib\kernel32.lib
  10. includelib \masm32\lib\user32.lib
  11.  
  12. .data
  13.  
  14. hM1 dword 0
  15. hM2 dword 0
  16. app db &quot;Test dll&quot;,0
  17. dll db &quot;dll.dll&quot;,0
  18. dll2    db &quot;dll2.dll&quot;,0
  19. fname   db &quot;Function1&quot;,0
  20. err1    db &quot;LoadLibrary (dll1) failed&quot;,0
  21. err1_1  db &quot;LoadLibrary (dll2) failed&quot;,0
  22. err2    db &quot;GetProcAddress (first) failed&quot;,0
  23. err2_1  db &quot;GetProcAddress (second) failed&quot;,0
  24.  
  25. .code
  26. start:
  27. invoke LoadLibrary,offset dll
  28. .if eax==0
  29.     invoke MessageBox,0,offset err1,offset app,MB_ICONERROR
  30.     ret
  31. .endif
  32. mov hM1,eax
  33. invoke LoadLibrary,offset dll2
  34. .if eax==0
  35.     invoke MessageBox,0,offset err1_1,offset app,MB_ICONERROR
  36.     ret
  37. .endif
  38. mov hM2,eax
  39. invoke GetProcAddress,hM1,offset fname
  40. .if eax==0
  41.     invoke MessageBox,0,offset err2,offset app,MB_ICONERROR
  42.     ret
  43. .endif
  44. call eax
  45. invoke GetProcAddress,hM2,offset fname
  46. .if eax==0
  47.     invoke MessageBox,0,offset err2_1,offset app,MB_ICONERROR
  48.     ret
  49. .endif
  50. call eax
  51. invoke FreeLibrary,hM1
  52. invoke FreeLibrary,hM2
  53. ret
  54. end start

Надо скопировать созданную нами dll.dll под новым именем dll2.dll в этот же каталог. Вся соль в том, что нам нужно проверить, что наши настройки были правильные, и система может с ними работать. А для этого требуются как минимум две dll, претендующие на одно и то же место в адресном пространстве. Самый ленивый способ, конечно, который только можно придумать - это просто использовать второй переименованный экземпляр. Но на радостях вы можете поэкспериментировать с текстами, хотя бы поменять выводимые MessageBox’ом сообщения и создать другую dll с другими именами модуля и экспортируемой функции.

До новых встреч! © Roustem


0 9.159
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532