Таблица экспорта
Мы изучили одну часть динамической линковки под названием таблицы импорта в предыдущем туториале. Теперь мы узнаем о другой стороне медали, таблице экспорта.
Скачайте пример.
Теория:
Когда PE-загрузчик запускает программу, он загружает соответствующие DLL в адресное пространство процесса. Затем он извлекает информацию об импортированных функциях из основной программы. Он использует информацию, чтобы найти в DLL адреса функций, которые будут вызываться из основного процесса. Место в DLL, где PE-загрузчик ищет адреса функций - таблица экспорта.
Когда DLL/EXE экспортирует функцию, чтобы та была использована другой DLL/EXE, она может сделать это двумя путями: она может экспортировать функцию по имени или только по ординалу. Скажем, если в DLL есть функция под названием "GetSysConfig", она может указать други DLL или EXE, что если они хотят вызвать функцию, они должны указать ее имя, то есть, GetSysConfig. Другой путь - экспортировать функцию по ординалу. Что такое ординал? Ординал - это 16-битный номер, который уникальным образом идентифицирует функцию в определенном DLL. Этот номер уникален только для той DLL, на которую он ссылается. Hапример, в вышеприведенном примере, DLL может решить экспортировать функцию через ординал, скажем, 16. Тогда другая DLL/EXE, которая хочет вызвать эту функцию должна указать этот номер в GetProcAddress. Это называется экспортом только через ординал.
Экспорт только через ординал не очень рекомендуется, так как это может вызвать проблемы при распространении DLL. Если DLL апгрейдится или апдейтится, человек, занимающийся ее программированием не может изменять ординалы функции, иначе программы, использующие эту DLL, перестанут pаботать.
Теперь мы можем анализировать структуру экспорта. Как и в случае с таблицей импорта, вы можете узнать, где находится таблица экспорта, из директории данных. В этом случае таблица экспорта - это первый член директории данных. Структура экспорта называется IMAGE_EXPORT_DIRECTORY. В структуре 11 параметров, но только несколько из них по настоящему нужны.
nName | Hастоящее имя модуля. Это поле необходимо, потому что имя файла может измениться. Если подобное произойдет, PE-загрузчик будет использовать это внутреннее имя. |
nBase | Число, которое вы должны сопоставить с ординалами, чтобы получить соответствующие индексы в массиве адресов функций. |
NumberOfFunctions | Общее количество функций/символов, которые экспортируются из модуля. |
NumberOfNames | Количество функций/символов, которые экспортируются по имени. Это значение не является числом ВСЕХ функций/символов в модуле. Это значение может быть pавно нулю. В этому случае, модуль может экспортировать только по имени. Если нет функции/символа, который бы экспортировались, RVA таблицы экспорта в директории данных будет pавен нулю. |
AddressOfFunctions | RVA, который указывает на массив RVA функций/символов в модуле. Вкратце, RVA на все функции в модуле находятся в массиве и это поле указывает на начало массива. |
AddressOfNames | RVA, которое указывает на массив RVA имен функций в модуле. |
AddressOfNameOrdinals | RVA, которое указывает на 16-битный массив, который содержит ординалы, ассоциированные с именами функций в массиве AddresOfNames. |
Вышеприведенная таблица может не дать вам ясного понимания, что такое таблица экспортов. Упрощенное объяснение ниже прояснит суть концепции.
Таблица экспортов существует для использования PE-загрузчиком. Прежде всего, модуль должен где-то сохранить адреса всех экспортированных функций где-то PE-загрузчик сможет их найти. Он держит их в массиве, на который ссылается поле AddressOfFunctions. Количество элементов в массиве находится в NumberOfFunctions. Таким образом, если модуль экспортирует 40 функций, массив будет также состоять из 40 элементов, NumberOfFunctions будет содержать значение 40. Теперь, если некоторые функции экспортируются по именам, модуль должен сохранить их имена в файле. Он сохраняет RVA имен в массиве, чтобы PE-загрузчик может их найти. Hа это массив сслыется AddressOfNames и количество имен находится в NumberOfNames.
Подумайте о работе, выполняемой PE-загрузчиком. Он знает имена экспортируемых функций, он должен каким-то образом получить адреса этих функций. До нынешнего момента модуль имел два массива: имена и адреса, но между ними не было связи. Теперь нам нужно что-то, что свяжет имена функций с их адресами. PE-спецификация использует индексы в массиве адресов в качестве элементарной линковки. Таким образом, если PE-загрузчик найдет имя, которое он ищет в массиве имен, он может также получить индекс в таблице адресов для этого имени. Индексы находятся в другом массиве, на который указывает поле AddressOfNameOrdinals. Так как этот массив существует в качестве посредника между именами и адресами, он должен содержать такое же количество элементов, как и массив имен, то есть, каждое имя может иметь один и только один ассоциированный с ним адрес. Чтобы линковка pаботала, оба массива имен и индексов, должны соответствовать друг другу, то есть, первый элемент в массиве индексов должен содержать индекс для первого имени и так далее.
AddressOfNamesAddressOfNameOrdinals
||
RVA of Name 1 Index of Name 1 <--> RVA of Name 2 Index of Name 2 <--> RVA of Name 3 <-->Index of Name 3
RVA of Name 4 <-->Index of Name 4
... ...... <--> RVA of Name N Index of Name N
Если у нас есть имя экспортируемой функции и нам требуется получить ее адрес в модуле, мы должны сделать следующее.
- Перейти к PE-загрузчику
- Прочитать виртуальный адрес таблицы экспорта в директории данных
- Перейти к таблице экспорта и получить количество имен (NumberOfNames)
- Параллельно просмотреть массивы, на который указывают AddressOfNames и AddressOfNameOrdinals, ища нужно имя. Если имя найдено в массиве AddressOfNames, вы должны извлечь значение в ассоциированном элементе массива AddressOfNameOrdinals. Hапример, если вы нашли RVA необходимого имени в 77-ом элементе массива AddressOfNames, вы должны извлечь значение, сохраняемое в 77-ом элементе массива AddressOfNameOrdinals. Если вы просмотрели все NumberOfElements элементов массива и ничего не нашли, вы знаете, что имя находится не в этом модуле.
- Используйте значение из массива AddressOfNameOrdinals в качестве индекса для массива AddressOfFunctions. Hапример, если значение pавно 5, вы должны извлечь значение 5-го элемента массива AddressOfFunctions. Это значение будет являться RVA функции.
Сейчас мы можем переключить наше внимание на параметр nBase из структур. Вы уже знаете, что массив AddressOfFunctions содержит адреса всех экспортируемых символов в модуле. И PE-загрузчик использует индексы этого массива, чтобы найти адреса функций. Давайте представим ситуацию, что мы используем индексы этого массива как ординалы. Так как программист может указать любое число в качестве стартового ординала в .def-файле, например, 200, это значит, что в массиве AddressOfFunctions должно быть по крайней мере 200 элементов. Хотя первые 200 элементов не будут использоваться, они должны сущетствовать, так как PE-загрузчик может использовать индексы, чтобы найти правильные адреса. Это совсем нехорошо. Параметр nBase существует для того, чтобы решить эту проблему. Если программист указывает начальным ординалом 200, значением nBase будет 200. Когда PE-загрузчик считывает значение в nBase, он знает, чт первые 200 элементов не существуют, и что он должен вычитать от ординала значение nBase, чтобы получить настоящий индекс нужного элемента в массиве AddressOfFunctions. Используя nBase, нет надобности в 200 пустых элементах.
Заметьте, что nBase не влияет на значения в массиве AddressOfNameOrdinals. Hесмотря на имя "AddressOfNameOrdinals", этот массив содержит индесы в массиве, а не ординалы.
Обсудив nBase, мы можем продолжить.
Предположите, что у нас есть ординал функции и нам нужно получить адрес этой функции, тогда мы должны сделать следующее:
Перейти к PE-заголовку
Получить RVA таблицы экспортов из директории данных
Перейти к таблице экспортов и получить значение nBase
Вычесть из ординала значение nBase, и теперь у вас есть индекс нужного элемента массива AddressOfFucntions.
Сpавните индекс со значением NumberOfFunctions. Если индекс больше или равен ему, ординал неверен.
Используйте индекс, чтобы получить RVA функции в массиве AddressOfFunctions.
Заметьте, что получение адреса функции из ординала гораздо проще и быстрее, по ее имени. Hет необходимости обрабатывать AddressOfNames и AddressOfNameOrdinals. Выигрыш в качестве смазывается трудностями в поддержке модуля.
Резюмируя: если вы хотите получить адрес функции, вам нужно обработать как AddressOfNames, так и AddressOfNameOrdinals, чтобы получить индекс элемента массива AddressOfFunctions. Если у вас есть ординал, вы можете сразу перейти к массиву AddressOfFunctions, после того как ординал скоректирован с помощью nBase.
Если функция экспортируется по имени, вы можете использовать как ее имя, так и ее ординал в GetProcAddress. Hо что, если функция экспортируется только через ординал?
"Функция экспортируется только через ординал" означает, что функция не имеет входов в AddressOfNames и AddressOfNameOrdinals. Помните два поля - NumberOfFunctions и NumberOfNames. Существование этих двух полей - это свидетельство того, что некоторые функции могут не иметь имен. Количество функций должно быть по крайней мере равно количеству имен. Функции, которые не имеют имен, экспортируются только через их ординалы. Hапример, если есть 70 функций, а массив AddressOfNames состоит только из 40 элементов, это означает, что в модуле есть 30 функций, экспортирующиеся только через их ординалы. Как мы можем узнать, какие функции экспортируются подобным образом? Это нелегко. Вы должны узнать это методом исключения, то есть элементы массива AddressOfFunctions, которые не упоминаются в массиве AddressOfNameOrdinals, содержат RVA функций, экспортирующиеся только черзе ординалы.
Пpимеp:
Это пример схож с рассмотренным в пердыдущем примере. Тем не менее, он отображает значения некоторых членов структуры IMAGE_EXPORT_DIRECTORY, а также отображает RVA, ординалы и имена экспортируемых функций. Заметьте, что этот пример не отображает функции, которые экспортируются только через ординалы.
.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\comdlg32.inc include \masm32\include\user32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\comdlg32.lib
IDD_MAINDLG equ 101 IDC_EDIT equ 1000 IDM_OPEN equ 40001 IDM_EXIT equ 40003
DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD ShowExportFunctions proto :DWORD ShowTheFunctions proto :DWORD,:DWORD AppendText proto :DWORD,:DWORD
SEH struct PrevLink dd ? CurrentHandler dd ? SafeOffset dd ? PrevEsp dd ? PrevEbp dd ? SEH ends
.data AppName db "PE tutorial no.7",0 ofn OPENFILENAME <> FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0 db "All Files",0,"*.*",0,0 FileOpenError db "Cannot open the file for reading",0 FileOpenMappingError db "Cannot open the file for memory mapping",0 FileMappingError db "Cannot map the file into memory",0 NotValidPE db "This file is not a valid PE",0 NoExportTable db "No export information in this file",0 CRLF db 0Dh,0Ah,0 ExportTable db 0Dh,0Ah,"======[ IMAGE_EXPORT_DIRECTORY ]======",0Dh,0Ah db "Name of the module: %s",0Dh,0Ah db "nBase: %lu",0Dh,0Ah db "NumberOfFunctions: %lu",0Dh,0Ah db "NumberOfNames: %lu",0Dh,0Ah db "AddressOfFunctions: %lX",0Dh,0Ah db "AddressOfNames: %lX",0Dh,0Ah db "AddressOfNameOrdinals: %lX",0Dh,0Ah,0 Header db "RVA Ord. Name",0Dh,0Ah db "----------------------------------------------",0 template db "%lX %u %s",0
.data? buffer db 512 dup(?) hFile dd ? hMapping dd ? pMapping dd ? ValidPE dd ?
.code start: invoke GetModuleHandle,NULL invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0 invoke ExitProcess, 0
DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD .if uMsg==WM_INITDIALOG invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0 .elseif uMsg==WM_CLOSE invoke EndDialog,hDlg,0 .elseif uMsg==WM_COMMAND .if lParam==0 mov eax,wParam .if ax==IDM_OPEN invoke ShowExportFunctions,hDlg .else ; IDM_EXIT invoke SendMessage,hDlg,WM_CLOSE,0,0 .endif .endif .else mov eax,FALSE ret .endif mov eax,TRUE ret DlgProc endp
SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD mov edx,pFrame assume edx:ptr SEH mov eax,pContext assume eax:ptr CONTEXT push [edx].SafeOffset pop [eax].regEip push [edx].PrevEsp pop [eax].regEsp push [edx].PrevEbp pop [eax]. regEbp mov ValidPE, FALSE mov eax,ExceptionContinueExecution ret SEHHandler endp
ShowExportFunctions proc uses edi hDlg:DWORD LOCAL seh:SEH mov ofn.lStructSize,SIZEOF ofn mov ofn.lpstrFilter, OFFSET FilterString mov ofn.lpstrFile, OFFSET buffer mov ofn.nMaxFile,512 mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetOpenFileName, ADDR ofn .if eax==TRUE invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL .if eax!=INVALID_HANDLE_VALUE mov hFile, eax invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0 .if eax!=NULL mov hMapping, eax invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0 .if eax!=NULL mov pMapping,eax assume fs:nothing push fs:[0] pop seh.PrevLink mov seh.CurrentHandler,offset SEHHandler mov seh.SafeOffset,offset FinalExit lea eax,seh mov fs:[0], eax mov seh.PrevEsp,esp mov seh.PrevEbp,ebp mov edi, pMapping assume edi:ptr IMAGE_DOS_HEADER .if [edi].e_magic==IMAGE_DOS_SIGNATURE add edi, [edi].e_lfanew assume edi:ptr IMAGE_NT_HEADERS .if [edi].Signature==IMAGE_NT_SIGNATURE mov ValidPE, TRUE .else mov ValidPE, FALSE .endif .else mov ValidPE,FALSE .endif FinalExit: push seh.PrevLink pop fs:[0] .if ValidPE==TRUE invoke ShowTheFunctions, hDlg, edi .else invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR .endif invoke UnmapViewOfFile, pMapping .else invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR .endif invoke CloseHandle,hMapping .else invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR .endif invoke CloseHandle, hFile .else invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR .endif
.endif ret ShowExportFunctions endp
AppendText proc hDlg:DWORD,pText:DWORD invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0 ret AppendText endp
RVAToFileMap PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD mov esi,pFileMap assume esi:ptr IMAGE_DOS_HEADER add esi,[esi].e_lfanew assume esi:ptr IMAGE_NT_HEADERS mov edi,RVA ; edi == RVA mov edx,esi add edx,sizeof IMAGE_NT_HEADERS mov cx,[esi].FileHeader.NumberOfSections movzx ecx,cx assume edx:ptr IMAGE_SECTION_HEADER .while ecx>0 .if edi>=[edx].VirtualAddress mov eax,[edx].VirtualAddress add eax,[edx].SizeOfRawData .if edi < eax mov eax,[edx].VirtualAddress sub edi,eax mov eax,[edx].PointerToRawData add eax,edi add eax,pFileMap ret .endif .endif add edx,sizeof IMAGE_SECTION_HEADER dec ecx .endw assume edx:nothing assume esi:nothing mov eax,edi ret RVAToFileMap endp
ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD LOCAL temp[512]:BYTE LOCAL NumberOfNames:DWORD LOCAL Base:DWORD
mov edi,pNTHdr assume edi:ptr IMAGE_NT_HEADERS mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress .if edi==0 invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR ret .endif invoke SetDlgItemText,hDlg,IDC_EDIT,0 invoke AppendText,hDlg,addr buffer invoke RVAToFileMap,pMapping,edi mov edi,eax assume edi:ptr IMAGE_EXPORT_DIRECTORY mov eax,[edi].NumberOfFunctions invoke RVAToFileMap, pMapping,[edi].nName invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals invoke AppendText,hDlg,addr temp invoke AppendText,hDlg,addr Header push [edi].NumberOfNames pop NumberOfNames push [edi].nBase pop Base invoke RVAToFileMap,pMapping,[edi].AddressOfNames mov esi,eax invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals mov ebx,eax invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions mov edi,eax .while NumberOfNames>0 invoke RVAToFileMap,pMapping,dword ptr [esi] mov dx,[ebx] movzx edx,dx mov ecx,edx shl edx,2 add edx,edi add ecx,Base invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax invoke AppendText,hDlg,addr temp dec NumberOfNames add esi,4 add ebx,2 .endw ret
ShowTheFunctions endp end start
Анализ:
mov edi,pNTHdr assume edi:ptr IMAGE_NT_HEADERS mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress .if edi==0 invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
ret .endif
После того, как программа убеждается, что файл является верным PE, она переходит к директории данных и получает виртуальный адрес таблицы экспорта. Если виртуальный адрес равен нулю, в файле нет ни одного экспортируемого символа.
mov eax,[edi].NumberOfFunctions invoke RVAToFileMap, pMapping,[edi].nName invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals invoke AppendText,hDlg,addr temp
Мы отображаем важную информацию в структуре IMAGE_EXPORT_DIRECTORY в edit control'е.
push [edi].NumberOfNames pop NumberOfNames push [edi].nBase pop Base
Так как мы хотим перечислить вс имена функций, нам требуется знать, как много имен в таблице экспорта. nBase используется, когда мы хотим сконвертировать индексы, содержащиеся в массиве AddressOfFunctions в ординалы.
invoke RVAToFileMap,pMapping,[edi].AddressOfNames mov esi,eax invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals mov ebx,eax invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions mov edi,eax
Адреса трех массивов сохранены в esi, ebx и edi, готовые для использования.
.while NumberOfNames>0
Продолжаем, пока все имена не будут обработанны.
invoke RVAToFileMap,pMapping,dword ptr [esi]
Так как esi указывает на массив RVA экспортируемых функций, разъименование ее даст RVA настоящего имени. Мы сконвертируем ее в виртуальный адрес, который будет передан затем wsрrintf.
mov dx,[ebx] movzx edx,dx mov ecx,edx add ecx,Base
ebx указывает на массив ординалов. Элементы этого массива размеров в слово.
Таким образом мы сначала должны сконвертировать значение в двойное слово. edx и ecx содержит индекс массива AddressOfFunctions. Мы добавляем значение nBase к ecx, чтобы получить номер ординала функции.
shl edx,2 add edx,edi
Мы умножаем индекс на 4 (каждый элемент в массиве AddressOfFunctions размером 4 байта), а затем, добавляем адрес массива AddressOfFunctions к нему. Таким образом edx указывает на RVA функции.
invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax invoke AppendText,hDlg,addr temp
Мы отображаем RVA, ординал и имя функции в edit control'е.
dec NumberOfNames add esi,4 add ebx,2 .endw
Обновим счетчик и адреса текущих элементов массивов AddressOfNames и AddressOfNameOrdinals. Продолжаем, пока все имена не будут обработаны.
[C] Iczelion, пер. Aquila.