ИмпортСкомпилированной программе неизвестно расположение в памяти DLL-ок, от которых она зависит, поэтому в программе, там, где стоят вызовы внешних функций, импортируемых из DLL, сделаны косвенные переходы по содержимому ячеек памяти, которые будут заполнены загрузчиком. Перед загрузкой в память в секции импорта РЕ-файла, содержится информация, необходимая для того, чтобы загрузчик мог определить адреса импортируемых функций и пристыковать эти адреса к отображению исполняемого файла. После загрузки в память секция импорта содержит указатели на функции, импортируемые из DLL. Секция IAT (Import Address Table ― таблица адресов импорта) состоит из массива 20-байтовых элементов типа IMAGE_IMPORT_DESCRIPTOR. Каждый элемент IMAGE_IMPORT_DESCRIPTOR соответствует одной из DLL, с которой неявно связан данный РЕ-файл. Одной DLL может соответствовать несколько элементов IMAGE_IMPORT_DESCRIPTOR. Так как количество элементов в массиве нигде явно не указано, поэтому последний элемент массива IMAGE_IMPORT_DESCRIPTOR должен имеет поля, содержащие NULL. Для размещения массива IMAGE_IMPORT_DESCRIPTOR, описывающего N библиотек, нужно (N+1)×20 байт памяти. За импорт в РЕ-файле отвечают элементы DATA DIRECTORY IMAGE_DIRECTORY_ENTRY_IMPORT1Указатель на таблицу импортируемых функций, используемую для связи файла с внешними DLL, и активируемую системным загрузчиком, когда все остальные механизмы импорта недоступны. Используется RVA- и VA-адресацияIMAGE_DIRECTORY_ENTRY_BOUND_IMPORT11Указатель на таблицу диапазонного импорта, имеющей приоритет над IMAGE_DIRECTORY_ENTRY_IMPORT и обрабатываемой загрузчиком в первую очередь. Используется RVA- и RRAW-адресацияIMAGE_DIRECTORY_ENTRY_IAT12указатель на IAT. В DATA DIRECTORY указатель на IAT может быть обнулен. Используется RVA- и VA-адресацияIMAGE_DIRECTORY_ENTRY_DELAY_IMPORT13указатель на таблицу отложенного импорта. Используется RVA/VA-адресацияRVA (Relative Virtual Address ― относительный виртуальный адрес). Многие поля в РЕ-файлах заполнены RVA. RVA ― это смещение данного элемента по отношению к адресу, с которого начинается отображение РЕ-файла в памяти. Допустим, загрузчик Windows отобразил РЕ-файл в виртуальное адресное пространство, начиная с адреса 400000h (базовый адрес РЕ-файла) и, допустим, некая таблица в отображении РЕ-файла начинается с адреса 401464h (виртуальный адрес таблицы (VA - Virtual Address)), тогда RVA данной таблицы равно 1464h: 401464h(виртуальный адрес(VA)) - 400000h(базовый адрес) = 1464h(RVA)Чтобы перевести RVA в виртуальный адрес, прибавьте RVA к базовому адресу. Классический импорт по именам Классический импорт по ординалам Отложенный импорт Импорт с привязкой к DLL (старый стиль) Импорт с привязкой (новый стиль) Классический импорт по именамТеорияСекция импорта содержит названия dll-файлов, имена функции, имена глобальных переменных и имена ресурсов импортируемых из dll-файлов, в виде ASCIIZ-строк. Ключевым элементом этой секции является Import Directory Table (массив импорта) представляющий из себя массив 20-ти байтовых элементов Import Directory Entry (входы в таблицу импорта) типа IMAGE_IMPORT_DESCRIPTOR. Самый последний элемент Import Directory Entry заполнен нулями и сигнализирует о конце массива Import Directory Table. Одному элементу Import Directory Entry соответствует одна DLL. Но одна DLL может соответствовать нескольким элементам Import Directory Entry. структура IMAGE_IMPORT_DESCRIPTOR Код (ASM): IMAGE_IMPORT_DESCRIPTOR STRUCT OriginalFirstThunk DD ? ;В этом поле содержится смещение (RVA) массива ;двойных слов. Каждое из этих двойных слов - объединение типа IMAGE_THUNK_DATA. ;Каждое двойное слово IMAGE_THUNK_DATA соответствует одной функции, импортируемой ;данным файлом. Массив _IMAGE_THUNK_DATA32 называется Import Lookup Table ;Каждый элемент Import Lookup Table содержит RVA на структуру _IMAGE_IMPORT_BY_NAME. ;Этот массив в процессе загрузки программы не изменяется и может совсем не использоваться TimeDateStamp DD 0 ForwarderChaine DD 0 ModName DD ? ;RVA строки символов ASCII, оканчивающейся ;нулем и содержащей имя импортируемой DLL (например, KERNEL32.DLL или USER32.DLL) FirstThunk DD ? ;RVA массива _IMAGE_THUNK_DATA32, называемого ;Import Address Table. До загрузки программы идентичен Import Lookup Table, после ;загрузки элементы массива содержат проекцию адресов функций IMAGE_IMPORT_DESCRIPTOR ends Import Lookup Table и Import Address Table ― массивы 8-ми байтовых элементов типа IMAGE_THUNK_DATA. Первые 4 байта элемента IMAGE_THUNK_DATA содержат RVA структуры _IMAGE_IMPORT_BY_NAME, вторые 4 байта равны нулю. структура IMAGE_IMPORT_BY_NAME Код (ASM): IMAGE_IMPORT_BY_NAME STRUCT Hint DW ? ;индекс строки с именем импортируемой функции в DLL ;если индексируемое имя не совпадает с Name_ - выполняется поиск строки по всей ;Export Name Pointer Table Name_ DB ? DUP(?),0;название импортированной функции, ASCIIZ-строка Pad DB ($ and 1) DUP(0);длинна строки выравнивается до четной границы ;еще одним 0 IMAGE_IMPORT_BY_NAME ends структура _IMAGE_IMPORT_BY_NAME состоит из 2-байтового числа (Hint), за которым следует имя функции. Так как количество элементов типа IMAGE_THUNK_DATA находящихся в одной DLL нигде явно не указывается, поэтому последний элемент IMAGE_THUNK_DATA является нулевым. Import Directory Table, Import Address Table, Hint/Name Table, DLL Name Table Import Table и Orignal First Thunk Import Table после переписывания PE-загрузчиком Загрузчик заполняет соответствующую таблице функций таблицу адресов после загрузки в адресное пространство исполняемого файла всех необходимых библиотек. Происходит вызов LoadLibrary для каждой требуемой библиотеки, а затем вызов GetProcAddress для каждой импортируемой функции. Самый простой и часто использующийся, но самый медленный способ импортирования функций. Практика Минимальный компилируемый пример, содержащий суть проблемы состоит только из вызова функции MessageBoxA. Минимизация проблемы позволяет исключить все ТО лишнее, эффект ЧЕГО в противном случае пришлось бы учитывать. Код (ASM): ; GUI # include win64a.inc .code MsgBoxText db "Win64 Assembly is Great!",0 MsgCaption db "import by name",0 WinMain proc push rbp mov edx,offset MsgBoxText lea r8,[rdx+sizeof MsgBoxText] invoke MessageBox,NULL,,,MB_OK pop rbp ret WinMain endp end собирается при помощи следующего bat-файла Код (Text): ::стираем с экрана cls ::путь к masm64 на диске set masm64_path=\masm59\ ::передаем bat-файлу название обрабатываемого asm-файла set filename=%~n1 ::удаляем ненужные файлы if exist %filename%.exe del %filename%.exe if exist %filename%.obj del %filename%.obj if exist errors.txt del errors.txt ::компиляция программы %masm64_path%bin\ml64 /Cp /c /I"%masm64_path%Include" %filename%.asm >> errors.txt ::если компиляция прошла неудачно, тогда в файле errors.txt содержатся найденные ошибки ::выход из программы if errorlevel 1 exit ::линковка программы %masm64_path%bin\link /SUBSYSTEM:WINDOWS /LIBPATH:"%masm64_path%Lib" ^ /LARGEADDRESSAWARE:NO /BASE:0x400000 /STUB:%masm64_path%bin\stubby.exe ^ /SECTION:.text,W /ALIGN:16 /entry:WinMain /MERGE:.rdata=.text ^ /fixed /nocoffgrpinfo %filename%.obj >> errors.txt ::если линковка прошла неудачно, тогда в файле errors.txt содержатся найденные ошибки ::выход из программы if errorlevel 1 exit ::удаляем программный мусор del %filename%.obj del errors.txt компилируем, получаем приложение в 624 байта, запускаем Загружаем получившийся exe-файл в hiew32.exe, F4 (mode), Select mode→Decode, F8(Header), F10(dir) видим расположение таблиц отвечающих за импорт Esc, F8, F7(Import) видно: hint импортируемой функции (482), ее название (MessageBoxA) имя dll откуда функция была импортирована (user32.dll) нажали на Enter и перешли к тому месту в программе, где происходит вызов MessageBoxA F3(Edit) видно, что для вызова функции MessageBox происходит обращение к ячейке памяти с адресом 1C0h
01C0:48 02 00 0000 00 00 0000 00 00 0000 00 00 00H☻.....0210: 38 02 00 0000 00 00 0000 00 00 0056 02 00 008☻ ______ V☻0220: C0 01 00 0000 00 00 0000 00 00 0000 00 00 00└☺0230: 00 00 00 0000 00 00 0048 02 00 0000 00 00 00______ H☻0240: 00 00 00 0000 00 00 00E2 01 4D 6573 73 61 67______ т☺Messag0250: 65 42 6F 7841 00 75 7365 72 33 322E 64 6C 6CeBoxA user32.dll0260: 00 00 00 0000 00 00 0000 00 00 0000 00 00 00 Объединяем наши теоретические знания с тем, что нам показывает hiew32 First Trunck01C0:dq 248h ;RVA на имя функции01C8:dq 0 ;NULL конец таблицы Import Address TableImport Directory Entry 10210:dd 238h ;RVA Ordinal First Trunck = 0238hdd 0 ;TimeDateStump = 0dd 0 ;ForwarderChain = 0dd 256h ;RVA DLL Name = 0256hdd 1C0h ;RVA First Trunck = 01C0hImport Directory Entry 20224:dd 0,0,0,0,0;все поля NULLOrdinal First Trunck0238:dq 248h ;RVA на имя функции0240:dq 0 ;NULL конец таблицы Import Lookup Table0248:db 0E2h,1,"MessageBoxA",0Import DLL Name0256:db "user32.dll",16 dup(0) Мэтт Питрек в "Форматы РЕ и COFF объектных файлов" пишет "Для пользователей Borland есть некоторая дополнительная тонкость в этом описании. В РЕ-файле, созданном TLINK32, отсутствует один из массивов. В таких файлах содержимое поля Characteristics в IMAGE_IMPORT_DESCRIPTOR (то есть ссылка на массив Name/hint) равно нулю (очевидно, загрузчики Win32 не нуждаются в этом массиве). Таким образом, во всех РЕ-файлах вообще обязан быть только массив, на который указывает поле FirstThunk (массив адресов импорта)." Попробуем и мы обнулить адрес таблицы Import Lookup Table.0210: 0,0,0,256h,1C0h Программа нормально запускается Если указатель нулевой ― обнуляем содержимое таблицы Import Lookup Table 01C0: dq 248h,0...0210:dd 0,0,0,256h,1C0h0224:dd 0,0,0,0,00238:dq 0,00248:db 0E2,1,"MessageBoxA",00256:db "user32.dll",16 dup(0) Программа нормально запускается сдвигаем на освободившееся место название функции и DLL и исправляем адреса ссылок на имя функции и имя DLL 01C0: dq 238h,0...0210: dd 0,0,0,246h,1C0h0224: dd 0,0,0,0,00238: db 0E2h,1,"MessageBoxA",00246:db "user32.dll",16 dup(0) Программа нормально запускается TLINK32 фирмы Borland обнуляет значение hint и ничего, файлы работают обнуляем hint ― программа нормально запускается удаляем 16 завершающих нулей ― программа нормально запускается удаляем ".dll" ― программа нормально запускается Наша модифицированная программа всё больше становится похожей на танк, которому оторвало башню, но он, несмотря ни на что, движется вперед 01C0: dq 236h,0...0210: dd 0,0,0,244h,1C0h0224: dd 0,0,0,0,00238: db "MessageBoxA",00244: db "user32" сдвигаем на 2 освободившихся байта названия функции и DLL ― программа нормально запускается Везде пишут Самый последний вход в таблицу импорта заполнен нулями и сигнализирует о конце массива. Но действительно ли должны быть заполнены нулями ВСЕ 20 байт? После недолгих экспериментов становится понятно, что нулевыми достаточно оставить только первый и последний 4-х байтовый элемент 01C0: dq 236h,0 ...0210: dd 0,0,0,244h,1C0h0224: dd 0,01234567h,89ABCDEFh,0FEDCBA98h,00238: db "MessageBoxA",00244: db "user32" программа нормально запускается. 12 свободных байт соответствуют числу символов в имени функции "MessageBoxA" и терминирующему строку нулю 01C0:dq 226h, 0...0210:dd 0,0,0,238h, 1C0h0224:dd 00228:db "MessageBoxA", 00234:dd 00238:db "user32"получаем приложение в 574 байта обратите внимание таблица Import Address Table заканчивается на 3 нулевых элемента, а Import Directory Entry 1 также начинается на 3 нулевых элемента, нельзя ли их совместить? Правим адрес начала кода Entry point [0B8]=1F8h → 1E8h адрес импорта [120h]=210 → 204h адрес таблицы импорта [178h]=1C0h → 200h сдвигаем код и данные на 16 байт адрес функции call [1C0h] → call [200h] адрес начала строки заголовка mov edx,4001D0h → mov edx,4001C0h 0200: dd 21Ah,0,0,00210: dd 22Ch,200h,0021C: db "MessageBoxA",00228: dd 0022C: db "user32" получаем приложение в 562 байта Классический импорт по ординаламТеорияПри программировании в 16-разрядных Windows, каждой импортируемой через DLL функции присваивался номер (ординал). Если программист не указывал для импортируемой функции номер, то этот номер назначал компоновщик. При программировании и в 16-разрядных, и в 32-разрядных Windows, автоматически присваиваемые ординалы могли различаться в разных сборках DLL. В 16-битных Windows импорт по имени не поощрялся (по соображениям производительности), и в результате каждой экспортируемой функции явно присваивался номер, что и было предпочтительным способом связывания функций. В Windows 32-64 именованный импорт является нормой, а явное присваивание номеров функций ушло в прошлое. Так как номера в таблице экспорта не были фиксированы.
Многие библиотеки DLL Windows экспортируют порядковые номера для поддержки устаревшего кода. В 16-разрядном коде Windows часто использовались порядковые номера, так как это позволяло уменьшить размер DLL. Сейчас MicroSoft не рекомендует экспортировать функции по порядковым номерам, если это не требуется для поддержки устаревшего кода. Файл LIB будет содержать сопоставление между порядковым номером, именем и адресом размещения в памяти функции, что позволяет использовать имя функции, как обычно в проектах, использующих DLL. Одному элементу таблицы соответствует одна DLL. В первом ее 4-х байтовом элементе указан ординал этой функции, во втором 4-х байтовом элементе будет любое отрицательное число (старший бит равен 1). Список заканчивается 8-ми байтовым нулевым элементом. Практика ищем ординал, который указан в user32.dll для функции MessageBoxA Код (Text): %masm64_path%\bin\dumpbin.exe /EXPORTS %windir%\system32\user32.dll /OUT:user32.txt в файле user32.txt находим строку Код (Text): ordinal hint RVA name .... 2043 212 000712B8 MessageBoxA создаем файл MyFunc.def со следующим содержимым Код (Text): LIBRARY "user32.dll" EXPORTS MyMessageBox @2043 NONAME Выдуманное вами имя функции @ordinal позволяет указать, что номер, а не имя функции попадет в таблицу экспорта библиотеки DLL. Дополнительное ключевое слово NONAME позволяет экспортировать только порядковый номер и сократить размер таблицы экспорта в DLL. На основе файла MyFunc.def создаем MyFunc.lib при помощи lib.exe Код (Text): %masm64_path%\bin\lib /DEF:MyFunc.def /OUT:MyFunc.lib /MACHINE:X64 или, если нет файла lib.exe, используем link.exe с ключом -lib Код (Text): %masm64_path%\bin\link -lib /DEF:MyFunc.def /OUT:MyFunc.lib /MACHINE:X64 создаем файл MyFunc.inc со следующим содержимым Код (Text): extern __imp_MyMessageBox:qword MyMessageBox TEXTEQU <__imp_MyMessageBox> добавляем в asm-файл строки includelib MyFunc.lib и include MyFunc.inc Код (ASM): include win64a.inc includelib MyFunc.lib include MyFunc.inc .code MsgBoxText db "Win64 Assembly is Great!",0 MsgCaption db "import by ordinal",0 WinMain proc push rbp mov edx,offset MsgBoxText lea r8,[rdx+sizeof MsgBoxText] invoke MyMessageBox,NULL,,,MB_OK pop rbp ret WinMain endp end компилируем, получаем приложение в 592 байта, запускаем hiew32 F4 (Mode), Select mode → Decode, F8 (Header), F7 (Import) Enter F3(Edit) F9(Update), F8(Header), F10(Dir) 004001B0:FB 07 00 0000 00 00 8000 00 00 0000 00 00 00√●____А.......00400200:30 02 00 0000 00 00 0000 00 00 00____0☻.00400210:40 02 00 00B0 01 00 0000 00 00 0000 00 00 00 @☻.00400220:00 00 00 0000 00 00 0000 00 00 0000 00 00 00.00400230: FB 07 00 0000 00 00 8000 00 00 0000 00 00 00√●____А.00400240: 75 73 65 7233 32 2E 646C 6C 00 0000 00 00 00 user32.dll_____First Trunck01B0: dd 7FBh,80000000h;ординал функции01C8: dd 0,0 ;NULL конец таблицы Import Address Table Import Directory Entry 10204: dd 230h ;RVA Ordinal First Trunck = 0230h dd 0 ;TimeDateStump = 0 dd 0 ;ForwarderChain = 0 dd 240h ;RVA DLL Name = 0240h dd 1B0h ;RVA First Trunck = 01B0h Import Directory Entry 20224: dd 0,0,0,0,0;все поля NULL Ordinal First Trunck0230: dd 7FBh,80000000h;ординал функции0238: dd 0,0 ;NULL конец таблицы Import Lookup Table Import DLL Name0240: db "user32.dll",6 dup(0) используем одну таблицу вместо двух, удаляем ".dll" и терминирующие нули из названия библиотеки
.004001B0: FB 07 00 0000 00 00 8000 00 00 0000 00 00 00√●_____А.....00400200:B0 01 00 0000 00 00 0000 00 00 00_____░☺.00400210:40 02 00 00B0 01 00 0000 00 00 0075 73 65 72@☻__░☺____user.00400220: 33 32 32 компилируем, получаем приложение в 546 байта, запускаем. Отложенный импортТеорияИсполняемые файлы, которые используют отложенный импорт, неявно ссылаются на DLL, указанную после опции /DELAYLOAD. Основная идея заключается в перенаправлении элементов таблицы импорта на специальный обработчик, динамически загружающий соответствующие DLL по мере необходимости вызова функций из этих DLL. Этот обработчик подставляет адреса этих функций в таблицу импорта. Очевидным преимуществом использования отложенного импорта является сокращение времени загрузки и инициализации DLL. Предположим, ваша программа вызывает достаточно редко какие-то функции из какой-то DLL. Если вы используете отложенный импорт для такой DLL ― вы сократите время работы загрузчика. Если функции из такой DLL по каким-то условиям за время работы программы не будут вызываться совсем, то загрузка такой DLL так и не произойдет. При использовании отложенного импорта компоновщик фактически генерирует небольшие кодовые заглушки и вставляет их вместе с кодом, генерируемым компилятором. Когда вы вызываете функцию, исполняемый файл неявно ссылается на DLL. Создает данные, описывающие имя функции и DLL, а затем делает переход к небольшой заглушке. При первом вызове заглушки он использует данные для вызова LoadLibrary и GetProcAddress. Адрес, возвращаемый GetProcAddress, сохраняется в заглушке, поэтому следующие вызовы идут напрямую к функции минуя заглушку. В коде заглушки можно также найти GetLastError, RaiseException, FreeLibrary и LocalAlloc функции. При использовании отложенного импорта компоновщик создает псевдо таблицу адресов импорта (IAT) и псевдотаблицу имен импорта (INT) для каждой DLL, которая использует отложенный импорт. Для каждой из библиотек DLL есть одна IAT и INT. Каждая запись IAT и INT представляет одну импортированную функцию в этой DLL. Структуры данных IAT и INT такиже какие используются при обычном импорте. Операционная система не знает о том, что у исполняемого файла есть дополнительные таблицы псевдоимпорта. Выбор хранить таблицы отложенного импорта в том же формате, что и обычный импорт, исключительно удобен для компоновщика и функции __delayLoadHelper. Кроме функций LoadLibrary и GetProcAddress, остальная часть кода манипулирует псевдотаблицами IAT и INT. Цель ― оптимизировать последующие вызовы функций. При первом вызове функции, запись в IAT для функции указывает на созданный компоновщиком код. После успешного завершения заглушки запись IAT указывает непосредственно на целевую функцию (Рисунок 1). Другой способ представить это выглядит так ― внутри вашего исполняемого файла загруженная при помощи отложенного импорта DLL имеет псевдотаблицу IAT с записями, в которых записаны адреса функций загруженные в память. Использование таблицы адресов псевдоимпорта Параметр dliNotify является одним из перечислений dliXXX, указанных в DELAYIMP.H. Параметр pdli является указателем на структуру DelayLoadInfo, которая также объявлена в файле DELAYIMP.H. Структура DelayLoadInfo содержит все, что вам нужно знать об этой конкретной функции, включая ее имя и имя DLL из которой она вызывается. Структура DelayLoadInfo строится «на лету» в функции __delayLoadHelper. Когда вы используете отложенный импорт для данной DLL, компоновщик создает два разных типа заглушек: Первым тип это заглушка «per-API». Компоновщик создает по одной такой заглушке для каждой функции, вызываемой из DLL-файла. Компоновщик присваивает имя в форме __imp_load_XXX, где XXX является именем функции. Например, заглушка для вызова MessageBoxA выглядит так: Код (ASM): __imp__load_MessageBoxA: 400297: mov [rsp+8],rcx 40029C: mov [rsp+10],rdx 4002A1: mov [rsp+18],r8 4002A6: mov [rsp+20],r9 4002AB: sub rsp,68 4002AF: movdqa [rsp+20],xmm0 4002B5: movdqa [rsp+30],xmm1 4002BB: movdqa [rsp+40],xmm2 4002C1: movdqa [rsp+50],xmm3 4002C7: mov rdx,rax; rdx=offset MessageBoxA ; jmp __tailMerge_USER32; так как у нас только одна функция поэтому jmp отсутствует __tailMerge_USER32: 4002CA: lea rcx,[40064C];offset DELAY_IMPORT_DESCRIPTOR_USER32 4002D1: call __delayLoadHelper ;call 40031C 4002D6: movdqa xmm0,[rsp+20] 4002DC: movdqa xmm1,[rsp+30] 4002E2: movdqa xmm2,[rsp+40] 4002E8: movdqa xmm3,[rsp+50] 4002EE: mov rcx,[rsp+70] 4002F3: mov rdx [rsp+78] 4002F8: mov r8,[rsp+80] 400300: mov r9,[rsp+88] 400308: add rsp,68 40030A: jmp rax Первые восемь команд Код (ASM): sub rsp,68 mov [rsp+70],rcx mov [rsp+78],rdx mov [rsp+80],r8 mov [rsp+88],r9 movdqa [rsp+20],xmm0 movdqa [rsp+30],xmm1 movdqa [rsp+40],xmm2 movdqa [rsp+50],xmm3 сохраняют значения изменяемых в функции регистров rcx, rdx, r8, r9 и xmm0-xmm3 в стеке. Команда mov rdx,rax передает через второй параметр в функцию адрес псевдо-IAT-записи для функции MessageBoxA. Код __delayLoadHelper, исправляет псевдо-IAT-запись перед возвратом. Когда импорт с задержкой вызывается в первый раз, запись псевдо-IAT указывает на эту заглушку для каждой функции. В первый раз управление передается на эту заглушку, а не на реальную функцию MessageBoxA. Последующие вызовы функции MessageBoxA будут передаваться функции MessageBoxA. Первый параметр указывает на второй тип сгенерированного линкером заглушки («per-DLL»). Таким образом, независимо от того, для скольких функций вы создаете отложенный импорт из USER32.DLL, всегда будет только одна заглушка USER32.DLL. Компоновщик называет эти заглушки __tailMerge_XXX, где XXX ― это имя DLL. Например, заглушка для USER32 выглядит так: Код (ASM): __tailMerge_USER32: lea rcx,__DELAY_IMPORT_DESCRIPTOR_USER32 CALL ___delayLoadHelper movdqa xmm0,[rsp+20] movdqa xmm1,[rsp+30] movdqa xmm2,[rsp+40] movdqa xmm3,[rsp+50] mov rcx,[rsp+70] mov rdx [rsp+78] mov r8,[rsp+80] mov r9,[rsp+88] add rsp,68 jmp rax Первая команда заглушки для каждой DLL помещает адрес структуры данных, которую компоновщик включил в другом месте исполняемого файла. Эта структура имеет тип ImgDelayDescr, определенный в DELAYIMP.H. Структура ImgDelayDescr содержит указатели на имя DLL, указатели на псевдо-IAT и INT библиотеки DLL и другие элементы, необходимые функции __delayLoadHelper. Это структура данных, на которую указывает слот IMAGE_DIRECTORY_ENTRY_DELAY_ IMPORT в DataDirectory исполняемого файла. Все значения указателей в ImgDelayDescr являются виртуальными адресами (то есть нормальными линейными адресами, которые могут использоваться в качестве указателей). Следующая инструкция заглушки для DLL делает фактический вызов __delayLoadHelper. Функция __delayLoadHelper возвращает адрес функции в регистре RAX. Следующие инструкции восстанавливают содержимое регистров rcx, rdx, r8, r9 и xmm0-xmm3, которые были помещены в стек заглушкой per-API. Последния команда делает косвенный переход на адрес, который находится в регистре RAX (то есть, значение, возвращаемое функцией __delayLoadHelper). Так как __delayLoadHelper исправил псевдо-IAT-запись, заготовка для каждой импортируемой функции выполняется только один раз. Последующие вызовы проходят псевдо-IAT непосредственно к функции. Аналогично, для каждого DLL-заглушки выполняется только один раз для каждой функции, загруженного через отложенный импорт.
структура ImgDelayDescr Код (ASM): ImgDelayDescr struct grAttrs dd ?;задает тип адресации, применяющийся в служебных ;структурах отложенного импорта (0 – VA, 1 – RVA) szName dd ?;содержит RVA/VA-указатель на ASCIIZ-строку с именем загружаемой DLL phmod dd ?;в это поле загрузчик DelayHelper помещает дескриптор динамически загружаемой DLL. pIAT dd ?;содержит указатель на таблицу адресов отложенного импорта, организованную точно ;так же, как и обычная IAT, с той разницей, что все элементы таблицы отложенного импорта ведут к delayloadhelper'у ;специальному динамическому загрузчику, который вызывает LoadLibrary (если библиотека еще не была загружена), ;а затем вызывает GetProcAddress и замещает текущий элемент таблицы отложенного импорта эффективным адресом ;импортируемой функции, поэтому все последующие вызовы данной функции будут осуществляется в обход delayloadhelper'а. pINT dd ?;содержит RVA-указатель на таблицу имен, во всем повторяющую ;стандартную таблицу имен pBoundIAT dd ?;полю хранящее RVA-указатель на таблицу диапазонного импорта. Если таблица диапазонного ;импорта не пуста и указанная временная метка совпадает с временной меткой соответствующей DLL, системный загрузчик ;проецирует ее на адресное пространство данного процесса и механизм отложенного импорта дезактивируется. pUnloadIAT dd ?;При выгрузке DLL из памяти, DLL может восстановить таблицу отложенного импорта ;в исходное состояние, обратившись к ее оригинальной копии, RVA-указатель на которую хранится в поле pUnloadIAT. ;Если копии нет, тогда указатель на нее будет обращен в ноль dwTimeStamp dd ?;0 если нет привязки, ;O.W. date/time stamp of DLL bound to Old BIND ImgDelayDescr ends Внутри Delay Load Directory Table есть стандартная таблица импорта, может выполняться привязка, то есть возникать Bound Delay Import Table, есть Unload Delay Import Table и т.п.Практика Код (ASM): ; GUI # include win64a.inc .code MsgBoxText db "Win64 Assembly is Great!",0 MsgCaption db "delay import",0 WinMain proc push rbp mov edx,offset MsgBoxText lea r8,[rdx+sizeof MsgBoxText] invoke MessageBox,NULL,,,MB_OK pop rbp ret WinMain endp end собирается при помощи следующего bat-файла Код (Text): cls set masm64_path=\masm59\ set filename=%~n1 %masm64_path%bin\ml64 /Cp /c /I"%masm64_path%Include" %filename%.asm >> errors.txt %masm64_path%bin\link /SUBSYSTEM:WINDOWS /LIBPATH:"%masm64_path%Lib" ^ /LARGEADDRESSAWARE:NO /BASE:0x400000 /STUB:%masm64_path%bin\stubby.exe ^ /SECTION:.text,W /ALIGN:16 /entry:WinMain /MERGE:.rdata=.text ^ /fixed /DELAYLOAD:user32.dll %filename%.obj >> errors.txt if errorlevel 1 exit del %filename%.obj del errors.txt компилируем, получаем приложение в 1952 байта, запускаем без ключа /DELAYLOAD:user32.dll было бы приложение в 624 байта, то есть добавилась заглушка в 1952-624=1328 байт. Загружаем получившийся exe-файл в hiew32.exe, F4 (mode), Select mode→Decode, F8(Header), F10(dir) видим расположение таблиц отвечающих за импорт Видно, что используется Import, Exeption, Import Table, Delay Import к импортируемой функции MessageBoxA из user32.dll добавились функции RaiseExeption, LocalAlloc, LoadLibraryA, GetProcAddress, GetLastError и FreeLibrary из kernel32.dll F3 (Edit) видно, что адрес функции «MessageBoxA» находится в ячейке с RVA 0750h [0750h]=00400316h, по адресу 00400316h в регистр rax передается адрес строки с названием функции «MessageBoxA» и происходит переход на адрес 40029Dh, где произойдет: вызов кода заглушки «per-API» после сохранения содержимого регистров rcx, rdx, r8, r9, xmm0-xmm3 передачи в rcx адреса DELAY_IMPORT_DESCRIPTOR_USER32, а в rdx адреса имени «MessageBoxA» вызов функции delayLoadHelper (call 400324h) Import RVA=678h Size 28h обратите внимание, что здесь только функции из kernel32.dll First Trunck0210: dq 726h ;RVA "RaiseException"0218: dq 718h ;RVA "LocalAlloc"0220: dq 708h ;RVA "LoadLibraryA"0228: dq 6F6h ;RVA "GetProcAddress"0230: dq 6E6h ;RVA "GetLastError"0238: dq 6D8h ;RVA "FreeLibrary"0240: dq 0 ;NULL конец таблицы Import Address Table Import Directory Entry 10678: dd 6A0h ;RVA Ordinal First Trunck = 06A0h dd 0 ;TimeDateStump = 0 dd 0 ;ForwarderChain = 0 dd 738h ;RVA DLL Name = 0738h dd 210h ;RVA First Trunck = 0210hImport Directory Entry 20224: dd 0,0,0,0,0;все поля NULLOrdinal First Trunck06A0: dq 726h ;RVA "RaiseException"06A8: dq 718h ;RVA "LocalAlloc"06B0: dq 708h ;RVA "LoadLibraryA"06B8: dq 6F6h ;RVA "GetProcAddress"06C0: dq 6E6h ;RVA "GetLastError"06C8: dq 6D8h ;RVA "FreeLibrary"06D0: dq 0 ;NULL конец таблицы Import Address Table Import DLL Name0738: db "kernel32.dll",12 dup(0) Exeption RVA=780h RVA=18 780: dd 29Dh784: dd 314h788: dd 5E8h78C: dd 324h790: dd 5E6h794: dd 5F0h798: dd 0,0 Delay Import RVA 608h Size 40h DELAY_IMPORT_DESCRIPTOR_USER32608: dd 1,250h,760h,750h,648h,668h,0,0628: dd 0, 0, 0, 0, 0, 0,0,0 Код (ASM): ImgDelayDescr struct grAttrs dd 1;тип адресации=RVA szName dd 250h;"user32.dll" pointer to dll name phmod dd 760h; address of module handle pIAT dd 750h;400316h address of the IAT pINT dd 648h;658h address of the INT pBoundIAT dd 668h;0 address of the optional bound IAT pUnloadIAT dd 0; address of optional copy of original IAT dwTimeStamp dd 0;0 if not bound, O.W. date/time stamp of DLL bound to Old BIND ImgDelayDescr ends
Импорт с привязкой к DLL (bound import) Биндинг Этот механизм заключается в записи в файле на диске по определенным смещениям адресов импортируемых функций, что уменьшает время загрузки файла, если временной штамп идентифицирующий файл и временной штамп загружаемой dll совпадают. При наличии такого импорта, проверка на совпадение временного штампа осуществляется для каждой dll, и в зависимости от ее результатов применяется этот или другой механизм импорта. Запись адресов в файл осуществляется утилитой bind. С этим механизмом связана проблема форвардинга ― передачи экспорта, когда экспортирующая функцию библиотека не содержит самой функции, а отсылает к другой библиотеке, содержащей эту функцию, может быть под другим именем. Это делается для обеспечения совместимости приложений с различными версиями ОC Windows. Существует старый и новый стиль биндинга, отличающиеся тем, как они решают проблему форвардинга. И старый и новый стиль использует ту же директорию, что и стандартный механизм импорта. Но старый хранит в ней и информацию о форвардных функциях, а новый использует для этого директорию импорта с привязкой, адресуемую через элемент IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT таблицы директорий. Bound-импорт Bound или привязанный импорт используется при новом стиле биндинга, а также и как самостоятельный механизм. Он сходен с предыдущим механизмом, но также позволяет приводить адреса форвардных функций к адресам, расположенных в библиотеках, непосредственно эти функции экспортирующих. При данной схеме работы в поля TimeDateStamp=-1 и ForwardChain=-1 и информация о связывании хранится в ячейке DataDirectory с индексом IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (=11). То есть это своего рода сообщение загрузчику о том, что нужно использовать bound import. Так же для «цепочки bound импорта» фигурируют свои структуры. Алгоритм работы заключается в следующем ― в виртуальную память приложения выгружается необходимая библиотека и все необходимые адреса «биндятся» еще на этапе компиляции. Используется в Windows для стандартных исполняемых файлов (Calculator, Minesweeper, NotePad и так далее), так как Windows меняет стандартные библиотеки достаточно редко. Из недостатков ― при перекомпиляции dll, нужно будет перекомпилировать само приложение, так как адреса функций изменены и по старым адресам ничего уже нет. Теория Связанный импорт сводится к проецированию необходимых библиотек на адресное пространство процесса, с жесткой прошивкой экспортируемых адресов на стадии компиляции приложения. Импорт с привязкой (Bound import) ― это вид импорта, при котором предполагается, что для определенной версии модуля, содержащего импортируемую функцию, адрес функции известен к моменту компиляции. Это относится, главным образом, к системным библиотекам, которые загружаются в память всегда с одного и того же адреса для данной версии DLL. В таблицу импорта файла, импортирующего такую функцию, можно заранее, еще до загрузки модуля, записать предполагаемый адрес функции. Для обеспечения контроля версии модуля, к которому привязан данный, при осуществлении привязки создается дополнительная таблица информации о привязанных модулях, на которую ссылается поле ForwarderChain. В поле TimeDateStamp помещается отметка времени модуля, содержащего функцию (значение поля TimeDateStamp из PE-заголовка этого модуля). Существует «новый стиль» привязки ― TimeDateStamp = -1, а таблица информации о привязках доступна через элемент IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT каталога DataDirectory. При настройке таблицы импорта файла, импортирующего функции из некоторой DLL с привязкой, загрузчик первым делом сравниваетTimeDateStamp для каждого элемента таблицы импорта с временной отметкой (TimeDateStamp) DLL. Если они равны, то соответствующая этому элементу подтаблица вообще не нуждается в настройке, так как содержит верные адреса. В противном же случае адреса считаются устаревшими, и загрузчик перезаписывает основную подтаблицу (на которую указывает FirstThunk данного элемента), исходя из данных оригинальной подтаблицы (на которую ссылается OriginalFirstThunk). Со связанным импортом связана всего одна структура (элемент IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT DATA_DIRECTORYуказывает на массив таких структр, завершающийся нулевым элементом): Код (ASM): IMAGE_BOUND_IMPORT_DESCRIPTOR struct TimeDateStamp dd ?;временную метку DLL для bound импорта, ;и такой импорт будет осуществлен тогда, когда временные метки структуры и dll ;совпали, либо когда TimeDateStamp = 0 OffsetModuleName dw ?;указатель на имя dll, отсчитываемый ;от начала массива структур [B]IMAGE_BOUND_IMPORT_DESCRIPTOR[/B] NumberOfModuleForwarderRefs dw ?;указывает на количество форвардов, ;назначение этого поля не ясно (Array of zero or more IMAGE_BOUND_FORWARDER_REF follows IMAGE_BOUND_IMPORT_DESCRIPTOR ends Если временная отметка DLL соответствует временной отметке, прописанной в PE- заголовке, загрузчик проецирует адреса вызываемых функций на адресное пространство DLL. Практика Код (ASM): ; GUI # include win64a.inc .code MsgBoxText db "Win64 Assembly is Great!",0 MsgCaption db "bind import",0 WinMain proc push rbp mov edx,offset MsgBoxText lea r8,[rdx+sizeof MsgBoxText] invoke MessageBox,NULL,,,MB_OK pop rbp ret WinMain endp end собирается при помощи следующего bat-файла Код (Text): cls set masm64_path=\masm59\ set filename=%~n1 %masm64_path%bin\ml64 /Cp /c /I"%masm64_path%Include" %filename%.asm >> errors.txt %masm64_path%bin\link /SUBSYSTEM:WINDOWS /LIBPATH:"%masm64_path%Lib" ^ /LARGEADDRESSAWARE:NO /BASE:0x400000 /STUB:%masm64_path%bin\stubby.exe ^ /SECTION:.text,W /ALIGN:16 /entry:WinMain /MERGE:.rdata=.text ^ /fixed %filename%.obj >> errors.txt if errorlevel 1 exit %masm64_path%bin\bind -o -u %filename%.exe del %filename%.obj del errors.txt компилируем, получаем приложение в 624 байта напомню строку, которую мы выдернули при помощи утилиты dupmbin из user32.dll ordinalhintRVAname 2043 212 000712B8 MessageBoxA78C20000h+712B8h=78C912B8h First Trunck1C0: dd 78C912B8h,0 ;адрес в памяти по которому расположена функция MessageBoxA1C8: dd 0,0 ;NULL конец таблицы Import Address Table Import Directory Entry 120C: dd 238h ;RVA Ordinal First Trunck = 0238h210: dd 58E501ECh ;TimeDateStump = 5 апреля 2017 22:40:44 1491403244214: dd 0FFFFFFFFh ;ForwarderChain = -1218: dd 256h ;RVA DLL Name = 0256h21C: dd 1C0h ;RVA First Trunck = 01C0h Import Directory Entry 2220: dd 0,0,0,0,0;все поля NULL Ordinal First Trunck238: dd 248h,0 ;RVA имя функции240: dd 0,0 ;NULL конец таблицы Import Lookup Table248: db 0E2h,1,"MessageBoxA",0 Import DLL Name256: db "user32.dll",16 dup(0)
Код (Text): cls set masm64_path=\masm59\ set filename=%~n1 %masm64_path%bin\ml64 /Cp /c /I"%masm64_path%Include" %filename%.asm >> errors.txt %masm64_path%bin\link /SUBSYSTEM:WINDOWS /LIBPATH:"%masm64_path%Lib" ^ /LARGEADDRESSAWARE:NO /BASE:0x400000 /STUB:%masm64_path%bin\stubby.exe ^ /SECTION:.text,W /ALIGN:16 /entry:WinMain /MERGE:.rdata=.text ^ /fixed %filename%.obj >> errors.txt if errorlevel 1 exit %masm64_path%bin\bind -u %filename%.exe del %filename%.obj del errors.txt Код (Text): usage: BIND [switches] image-names... [-o] disable new import descriptors First Trunck1C0: dq 78C912B8h ;адрес в памяти по которому расположена функция MessageBoxA1C8: dq 0 ;NULL конец таблицы Import Address TableImport Directory Entry 120C: dd 238h ;RVA Ordinal First Trunck = 0238h210: dd 0FFFFFFFFh ;TimeDateStump = -1 214: dd 0FFFFFFFFh ;ForwarderChain = -1218: dd 256h ;RVA DLL Name = 0256h21C: dd 1C0h ;RVA First Trunck = 01C0hImport Directory Entry 2220: dd 0,0,0,0,0 ;все поля NULLOrdinal First Trunck238: dq 248h ;RVA имя функции240: dq 0 ;NULL конец таблицы Import Lookup Table248: db 0E2h,1,"MessageBoxA",0Import DLL Name256: db "user32.dll",16 dup(0) Использованная литература Мэтт Питрек "Форматы PE и COFF объектных файлов" Mett Pietrek "Under The Hood" Microsoft system journal 1998 Dec Volodya, NEOx "Об упаковщиках в последний раз: Часть первая - теоретическая" Kris Kaspersky "Путь воина – внедрение в pe/coff-файлы" dx "PE-формат. Часть 3 — Импорт"