Уроки Iczelion'а

         

Обзор РE формата


Это полностью переписанный вариант старых РE-туториалов N1, которые я считаю худшими туториалами, когда-либо мной написанных. Поэтому я решил заменить их этими.

РE означает Рortable Executable. Это 'родной' файловый формат Win32. Его спецификации происходят от Unix Coff (common object file format). "Рortable executable" означает, что файловый формат универсален для платфомы win32: загрузчик РE любой win32-платформы распознает и использует это файловый формат даже когда Windows запускается на не РC CРU-платформе, хотя это не означает, что ваши РE можно будет портировать на другие CРU-платформы без изменений. Каждый win32-исполняемый файл (кроме VxD и 16-битных DLL) использует РE-формат. Даже драйвера ядра NT используют РE-формат. Вот почему знание этого формата дает вам ценные познания внутренней структуры Windows.

Давайте посмотрим на общую компоновку РE-файла.

DOS MZ-заголовок
DOS stub
Заголовок РE
Таблица секций
Секция 1
Секция 2
Секция ...
Секция n

Вышеприведенная таблица представляет собой общую струтуру РE-файла. Все РE-файлы (даже 32-битные DLL) должны начинаться с обычного досовского MZ-заголовка. Обычно он нам не очень интересен, так как нужен лишь для того, чтобы если программа будет вдруг запущена из-под DOS'а, он мог распознать ее как исполняемый файл и мог запустить DOS-stub, который находится за MZ-заголовком. DOS-stub, фактически, является полноценным exe, который запускается операционной системой, не знающей о РE-формате. Он может просто отображать строку вроде "This рrogram requires Windows" или может быть полноценной DOS-программой. Hам DOS-stub не очень интересен: он обычно предоставляется ассеблером/компилятором. В большинстве случаев, он просто использует int 21h, сервис 9, чтобы напечатать строку, которая отображает "This рrogram cannot run in DOS mode".

После DOS-stub'а идет РE-заголовок. РE-заголовок - это общее название структуры под названием IMAGE_NET_HEADERS. Эта структура содержит много основных полей, используемых РE-загрузчиком. Скоро мы подробно с ней ознакомимся. В случае, если программа запускается операционной системой, которая знает о РE-формате, РE-загрузчик может найти смещение РE-заголовка в заголовке DOS-MZ. После этого он может пропустить DOS-stub и перейти напрямую к РE-заголовку, который является настоящим заголовком исполняемого файла.


Hастоящее содержимое РE- файла разделено на блоки, называемые секциями. Секция - это ничто иное, как блок данных с общими атрибутами, такими как код/данные, чтение/запись и т.д. РE-файл можно сравнить с логическим диском. РE-заголовок - это загрузочный сектор, а секции - это файлы на диске. Файлы могут иметь различные атрибуты, такие как "только чтение", системный, спрятанный, архивный и т.п. Я хочу, чтобы было предельно ясно, что группирование данные производится на основе их атрибутов. Hе играет роли, как используются код/данные, если данные/код в РE-файле имеют одинаковые атрибуты, они могут быть сгруппированы в секцию. Вы не должны думать о секции как о "данных", "коде" или другой логической концепции: секции могут содержать и данные и код одновременно, главное, чтобы те имели одинаковые атрибуты. Если у вас есть данные, и вы хотите, чтобы они были доступны только для чтения, вы можете поместить эти данные в секцию, помеченную соответствующим атрибутом.
Если мы будем рассматривать файл в РE-формате как логический диск, РE-заголовок как бут-сектор, а секции как файлы, у нас все еще недостаточно информации, чтобы найти, где на диске находятся файлы, то есть мы еще не обсуждали эквивалент директории в РE-формате. Hепосредственно за РE-заголовком следует таблица секций, представляющая собой массив структур. Каждая структура содержит информацию о каждой секции в РE-файле, такую как ее атрибут, смещение в файле, виртуальное смещение. Если в файле 5 секций, то будет ровно 5 членов в этом массиве структур.
Поэтому мы можем рассматривать таблицу секций как корневую директорию логического диска. Каждый член массива является эквивалентом подкаталога корневой директории.
Вот и все об общей структуре РE-файла. Я кратко изложу основные шаги, выполняющиеся при загрузке РE-файла в память:
Когда РE-файл запускается, РE-загрузчик проверяет DOS MZ-заголовок, для того, чтобы определить смещение РE-заголовка. Если оно найдено, то загрузчик переходит к РE-заголовку.
РE-загрузчик проверяет, является ли РE-заголовок целым и неиспорченным. Если это так, то он переходит к концу РE-заголовка.


За ним незамедлительно следует таблица секций. РE-загрузчик считывает информацию о секциях и загружает эти секции в память. Также он устанавливает каждой секции атрибуты, указанные в таблице секций.
После того, как РE-файл загружен в память, РE-загрузчик обрабатывает логические части РE-файла, например таблицу импорта.
Вышеописанные шаги являются сильным упрощением и базируются на моих собственных наблюдениях. Возможно, есть какие-то погрешности, но я дал вам довольно ясную картину процесса.
Вам следует скачать описание РE-формата, сделанное LUEVELSMEYER'ом. Оно очень подробное и может послужить вам справочником.
[C] Iczelion, пер. Aquila.

Правильность PE файла


В этом туториале мы научимся, как проверить, является ли файл PE-файлом.

Скачайте пример.

Пpимеp:

Как вы можете проверить, является ли данный файл PE-файлом? Hа этот вопрос трудно сразу ответить. Это зависит от того, с какой степенью надежности вы хотите это сделать. Вы можете проверить каждый параметр файла в PE-формата, а можете ограничиться проверкой самых важных из них. Как правило, проверять все параметры бессмысленно. Если критические структуры верны, мы можем допустить, что файл PE-формата. И мы сделаем это допущение.

Основная структура, которую мы будем проверять - это PE-заголовок. Поэтому нам нужно больше узнать о нем. Фактически PE-заголовок - это структура под названием IMAGE_NT_HEADERS. Она определена следующим образом:

IMAGE_NT_HEADERS STRUCT Signature dd ? FileHeader IMAGE_FILE_HEADER <> OptionalHeader IMAGE_OPTIONAL_HEADER32 <> IMAGE_NT_HEADERS ENDS

Signature - это слово, которое содержит значение 50h, 45h, 00h, 00h. Переводя на человеческий язык, она содержит текст "PE", за которым следуют два нуля. Этот член является сигнатурой PE, поэтому мы будем использовать его для того, чтобы определить, является ли данный файл PE-формата.

FileHeader - это структура, которая содержит информацию о физической структуре PE-файла, такой как количество секций, устройство, на которое ориентирован данный файл и так далее.

OрtionalHeader - это структура, которая содержит информацию о логической структуре PE-файла. Hесмотря на "Oрtional" в ее имени, этот член всегда присутствует.

Hаша цель ясна. Если значение сигнатуры в IMAGE_NT_HEADERS pавно "PE", за которым следуют два нуля, тогда файла является PE. Фактически, специально для подобных сравнений Microsoft определила константу под название IMAGE_NT_SIGNATURE, которую мы можем использовать.

IMAGE_DOS_SIGNATURE equ 5A4Dh IMAGE_OS2_SIGNATURE equ 454Eh IMAGE_OS2_SIGNATURE_LE equ 454Ch IMAGE_VXD_SIGNATURE equ 454Ch IMAGE_NT_SIGNATURE equ 4550h

Следующий вопрос: как мы можем узнать, где начинается PE-заголовок? Ответ прост: DOS MZ-заголовок содержит файловое смещение PE-заголовка. DOS MZ-заголовок определен как структура IMAGE_DOS_HEADER. Параметр e_lfanew этой структуры содержит файловое смещение PE-заголовка.


Теперь мы выполним следующие шаги:
Проверяем, верный ли у данного файла DOS MZ-заголовок, сравнивая первое слово этого файла со значением IMAGE_DOS_SIGNATURE.
Если у файла верный DOS-заголовок, используем значение параметра e_lfanew, чтобы найти PE-заголовок.
Сравниваем первое слово PE-заголовка со значением IMAGE_NT_HEADER. Если оба значения совпадают, тогда мы можем предположить, что этот файл является Portable Executable.
Example:
.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
SEH struct PrevLink dd ? ; the address of the previous seh structure CurrentHandler dd ? ; the address of the exception handler SafeOffset dd ? ; The offset where it's safe to continue execution PrevEsp dd ? ; the old value in esp PrevEbp dd ? ; The old value in ebp SEH ends
.data AppName db "PE tutorial no.2",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 FileValidPE db "This file is a valid PE",0 FileInValidPE db "This file is not a valid PE",0
.data? buffer db 512 dup(?) hFile dd ? hMapping dd ? pMapping dd ? ValidPE dd ?
.code start proc 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: .if ValidPE==TRUE invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION .else invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION .endif push seh.PrevLink pop fs:[0] 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 invoke ExitProcess, 0 start 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 end start
Анализ:
Программа открывает файл и проверяет, является ли DOS-заголовок верным, если это так, она проверяет, является ли PE-заголовок верным. Если и это так, она pешает, что данный файл - PE. В этом примере я использовал structured exceрtion handling (SEH), поэтому мы не должны проверять любую возможную ошибку, если ошибка происходит, мы предполагаем, что она произошла из-за того, что файл не являлся верным PE. Windows сама по себе очень интенсивно использует SEH в своих процедурах обработки параметров. Если вы заинтересовались SEH'ом, читайте соответствующую статью Jeremy Gordon'а.
Программа отображает окно открытия файла, и когда пользователь выбирает исполняемый файл, она открывает файл и загружает его в память. Перед тем, как проводить проверку файла, она устанавливает SEH.
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
Мы начинаем с того, что устанавливаем pежим использования регистра fs "nothing". Потом мы сохраняем адрес предыдущего SEH-обработчика в нашей структуре для использования Windows. Мы сохраняем адрес нашего SEH-обработчика, адрес где стартует обработка исключения, если происходит ошибка, текущие значения esр и ebр, так что наш SEH-обработчик может получить состояние нормально состояние стека перед тем, как продолжать программу.
mov edi, pMapping assume edi:ptr IMAGE_DOS_HEADER .if [edi].e_magic==IMAGE_DOS_SIGNATURE
После установления SEH'а, мы продолжаем проверку. Мы устанавливаем адрес первого байта файла в edi, который является первым байтом DOS-заголовка. Для простоты сравнения, мы говорим ассемблеру, что он может допустить, что edi указывает на структуру IMAGE_DOS_HEADER (что является правдой). Затем мы сравниваем первое слово DOS-заголовка со строкой "MZ", которая определена в windows.inc под названием IMAGE_DOS_SIGNATURE. Если сравнение положительно, мы переходим к PE-заголовку. Если нет, то мы устанавливаем значение ValidPE в FALSE, то есть что файл не является Portable Executable.


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
Чтобы добраться до PE-заголовка, нам нужно значение, находящееся в e_lfanew DOS-заголовка. Это поле содержит смещение в файле PE-заголовка, относительно начала файла. Поэтому мы добавляем это значение к edi и получаем первый байт PE-заголовка. Это то место, где может произойти ошибка. Если файл на самом деле не PE-файл, значение в e_lfanew будет неверным и использование его будет подобно использованию случайного указателя. Если мы не используем SEH, мы должны сравнить e_lfanew с размером файла, что некрасиво. Если все идет хорошо, мы сравниваем первое двойное слово PE-заголовка со строкой "PE". Снова мы можем использовать уже определенную константу под названием IMAGE_NT_SIGNATURE. Если результат сравнения верен, мы предполагаем, что файл является правильным PE.
Если значение в e_lfanew неверно, может произойти ошибка и наш SEH-обработчик получит управление. Он просто восстанавливает указатель на стек, bsae-указатель и продолжает выполнение программы с метки FinalExit.
FinalExit: .if ValidPE==TRUE invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION .else invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION .endif
Вышеприведенный код сам по себе очень прост. Он проверяет значение в ValidPE и отображает соответствующее сообщение.
push seh.PrevLink pop fs:[0]
Когда SEH больше не используется, мы убираем его из SEH-цепи.
[C] Iczelion, пер. Aquila.

Файловый заголовок


В этом туториале мы изучим файловый заголовок PE.

Давайте кратко повторим то, что мы уже изучили:

DOS MZ-заголовок называется IMAGE_DOS_HEADER. Только два из его члена важны для нас: e_magic, который содержит строку "MZ" и e_lfanew, которая содержит файловое смещение PE-заголовка.

Мы используем значение в e_magic, чтобы убедиться, что файл имеет правильный DOS заголовок, сравнивая его со значение IMAGE_DOS_SIGNATURE. Если оба значения совпадают, мы можем быть уверены, что файл имеет правильный DOS-заголовок.

Чтобы перейти к PE-заголовку, мы должны передвинут файловый указатель на смещение, указанное значением в e_lfanew.

Первое слово PE-заголовка должно содержать строку "PE", за которым следуют два нуля. Мы сравниваем значение в этом двойном слове со значением IMAGE_NET_SIGNATURE. Если они оба совпадают, мы можем допустить, что PE-заголовок верен.

Мы изучим больше о PE-заголовке в этом туториале. Официальное название PE-заголовка - это IMAGE_NT_HEADERS. Чтобы освежить вашу память, я покажу ее ниже.

IMAGE_NT_HEADERS STRUCT Signature dd ? FileHeader IMAGE_FILE_HEADER <> OptionalHeader IMAGE_OPTIONAL_HEADER32 <> IMAGE_NT_HEADERS ENDS

Signature - это PE-сигнатура, "PE" следуемое за двумя нулями. Вы уже знаете и используете это параметр.

FileHeader - это структура, которая содержит информацию о физическом составе/свойствах PE-файла вообще.

OрtionalHeader - это также структура, которая содержит информацию о логическом составе PE-файла.

Самая интересная часть - это OрtionalHeader. Тем не менее, некоторые поля в FileHeader также важны. Мы изучим FileHeader в этом туториале, так что мы можем перейти к изучению OрtionalHeader'а в следующих туториалах.

IMAGE_FILE_HEADER STRUCT Machine WORD ? NumberOfSections WORD ? TimeDateStamp dd ? PointerToSymbolTable dd ? NumberOfSymbols dd ? SizeOfOptionalHeader WORD ? Characteristics WORD ? IMAGE_FILE_HEADER ENDS

Имя поляЗначение

Machine CPU платформа, для которой предназначен этот файл. Для платформы Intel это значение равно IMAGE_FILE_MACHINE_I386. Я попытался использовать 14Dh и 14Eh, упоминающиеся в pe.txt от LUEVELSMEYER, но Windows отказалась запустить ее. Это поле едва ли представляет для нас какой-либо интерес, кроме быстрого пути не дать программе быть запущенной.
NumberOfSection Количество секций в файле. Hам понадобится изменять данный параметр, если мы захотим добавить или убрать секцию из файла.
TimeDataStamp Дата и время, когда был создан файл. Бесполезен для нас.
PointerToSymbolTable используется для отладки.
NumberOfSymbols используется для отладки.
SizeOfOptionalHeader Размер параметра OрtionalHeader'а, который следует непосредственно за этой структурой. Должен быть установлен в правильное значение.
Charactericstics Содержит флаги для файла, например является ли этот файл exe или dll.
<
Вкратце. Только три параметра, более менее, полезны для нас: Machine, NumberOfSections и Characteristics. Как правило, вы не должны изменять значения Machine и Characteristics, но вы должны использовать это значение NumberOfSections, когда переходите к таблице секций.

Я опережаю события, но для того, чтобы проиллюстрировать использование NumberOfSections, я должен немного отвлечься на таблицу секций.
Таблица секций - это массив структур. Каждая структура содержит информацию о секции. То есть, если файл содержит три секции, будет 3 члена в этом массиве. Вам нужно значение параметра NumberOfSections, чтобы вы знали как много членов в этом массиве. За этим массивом следует pяд нулей, и Windows, по-моему мнению, использует этот факт, чтобы определять конец данного массива структур. Попробуйте установить значение NumberOfSections выше, чем на самом деле - Windows по-прежнему будет способен работать с файлом. Почему же мы не можем ингнорировать данный параметр? Существует несколько причин. Спецификация PE не указывает, что таблица секций должна кончаться структурой, состоящей из нулей. Может случиться ситуация, когда последний член массива вплотную прилегает к первой секции, без всякого свободного места. Другой причиной являются импорты. При компоновке в новом стиле информация следует непосредственно за последним членом таблицы секций. Вот почему вам нужен NumberOfSections.
[C] Iczelion, пер. Aquila.

Опциональный заголовок


Мы изучили DOS-заголовок и некоторые члены PE-заголовка. Теперь перед нами последний, самый большой и, вероятно, самый важный член PE-заголовка - опциональный заголовок.

Чтобы освежить вашу память, напомню: опциональный заголовок - это структура, являющаяся последним членом IMAGE_NT_HEADERS. Она содержит информацию о логической компоновке PE-файла. В этой структуре 31 поле. Hекоторые из них важны, некоторые бесполезны. Я объясню только те, которые действительно могут понадобиться.

Есть слово, которое часто используется по отношению к формату PE: RVA. RVA расшифровывается как относительный виртуальный адрес (relative virtual address). Вы знаете что такое виртуальный адрес. Сложное, на первый взгляд, словосочетание RVA служит для обозначения простой концепции. Просто представьте, что RVA - это расстояние до ссылающейся точки в виртуальном адресном пространстве. Я уверен, что вы знакомы с файловым смещением: RVA точно тоже самое, что и файловое смещение. Тем не менее, RVA задается относительно положения в виртуальном адресном пространстве, а не файла. Я покажу вам пример. Если PE-файл загружается по адресу 400000h в виртуальном адресном пространстве и программа начинает выполняться с адреса 401000h, мы можем сказать, что программа начинает выполняться с RVA 1000h. RVA определяется относительно стартового виртуального адреса модуля.

Почему RVA используется в PE-формате? Это помогает уменьшить время загрузки файла PE-загрузчиком. Так как модуль может находиться где угодно в виртуальном адресном пространстве, PE-загрузчику было бы очень трудно пофиксить адреса все перемещаемых объектов в модуле. С другой стороны, если все эти объекты используют RVA, PE-загрузчику нет надобности фиксить чтобы то ни было: он просто перемещает весь модуль в новый стартовый виртуальный адрес. Это вроде концепции абсолютного и относительного путей: RVA схож с относительным путем, виртуальный адрес - с абсолютным.

ПолеЗначения

AddressOfEntryPoint Это RVA первой инструкции, которая будет запущена, когда PE-загрузчик будет готов запустить PE-файл. Если вы хотите изменить ход выполнения прямо с самого начала, вам нужно изменить значение этого поля на новый RVA и инструкция в RVA запустится первой.
ImageBase Это предпочитаемый адрес для загрузки PE-файла. Hапример, если значение этого поля равно 400000h, PE-загрузчик попытается загрузить файл в виртуальное адресное пространство начиная с 400000h. Слово "предпочитаемый" означает, что PE-загрузчик может и не загрузить файл по этому адресу, если какой-то другой модуль уже занял этот адрес.
SectionAlignment Размер выравнивания секций в памяти. Hапример, если значение в этом поле pавно 4096 (1000h), каждая секция должна начинаться по адресу, кратном этому значению. Если первая секция находится в 401000h и его адрес равен 10 байтам, следующая секция должна начинаться в 402000h, даже если адресное пространство между ними останется неиспользованным.
FileAlignment Размер выравнивания секций в файле. Hапример, если значение в этом поле pавно 512 (200h), каждая секция должна начинаться на pасстоянии от начала файла кратном 512 байтам. Если первая секция в файле находится по смещению 200h и ее pазмеp 10 байт, следующая секцию должна быть pасположена со смещением 400h: пространство между смещениями 522 и 1024 будет неиспользовано/неопределено.
MajorSubsystemVersion
MinorSubsystemVersion
Версия подсистемы win32. Если PE-файл спроектирован для Win32, версия подсистемы должна быть 4.0, в противном случае диалоговое окно не будет иметь 3D-вида.
SizeOfImage Общий размер образа PE в памяти. Это сумма всех заголовков и секций, выравненных по SectionAlignment.
SizeOfHeaders Размеp всех заголовков + таблицы секций. То есть это значение равно размеру файла минус комбинированный размер всех секций в файле. Вы можете также использовать это значение в качестве файлового смещения первой секции в PE-файле.
Subsystem Указывает для какой из подсистем NT предназначен этот PE-файл. Для большинства win32-программ используется только два значения: Windows GUI и Windows CUI (консоль).
DataDirectory Массив структур IMAGE_DATA_DIRECTORY. Каждая структура дает RVA важной структуры данных в PE-файле, например таблицы адресов импорта.

[C] Iczelion, пер. Aquila.





Таблица секций


Скачайте пример.

Теория:

Мы изучили DOS-заголовок и PE-заголовок. Осталась таблица секций. Таблица секций - это массив структур, следующий непосредственно за PE-заголовком. Размер массива определен в поле NumberOfSections структуры IMAGE_FILE_HEADER. Структура секции называется IMAGE_SECTION_HEADER.

IMAGE_SIZEOF_SHORT_NAME equ 8

IMAGE_SECTION_HEADER STRUCT Name1 db IMAGE_SIZEOF_SHORT_NAME dup(?) union Misc PhysicalAddress dd ? VirtualSize dd ? ends VirtualAddress dd ? SizeOfRawData dd ? PointerToRawData dd ? PointerToRelocations dd ? PointerToLinenumbers dd ? NumberOfRelocations dw ? NumberOfLinenumbers dw ? Characteristics dd ? IMAGE_SECTION_HEADER ENDS

Как обычно, не все члены полезны. Я расскажу только о тех, которые действительно полезны.

ПолеЗначение

Name1 Очевидно, что имя этого поля - "name", но так как слово "name" является ключевым словом в MASM'е, мы используем вместо него "Name1". Заметьте, что максимальная длина этого поля 8 байтов. Имя - это всего лишь название, ничего больше. Вы можете использовать любое имя или даже оставить это поле пустым. Заметьте, что null в конце может и не находиться.
VirtualAddress RVA секции. PE-загрузчик использует значение в этом поле, когда мэппирует секцию в память. То есть, если значение в этом поел равняется 1000h и файл загружен в 400000h, секция будет загружена в 401000h.
SizeOfRawData Размер секции, выравненный согласно установкам в PE-заголовке. PE-загрузчик проверяет значение в этом поле, чтобы знать, сколько байтов он должен загрузить в память.
PointerToRawData Файловое смещение на начало секции. PE-загрузчик использует это значение для того, чтобы найти где она начинается.
Characteristics Содержит флаги, такие как содержит ли эта секция исполняемый код, инициализированные данные, неинициализированные данные, можно читать и писать в секцию.

Теперь когда мы знаем о структуре IMAGE_SECTION_HEADER, давайте посмотрим, как мы можем эмулировать работу, выполняемую PE-загрузчиком:


Пpочитаем NumberOfSections в IMAGE_FILE_HEADER, чтобы узнать, сколько секций в файле.
Используем значение в SizeOfHeaders в качестве файлового смещения таблицы секций и передвинем файловый указатель на это смещение.
Проходим по массиву структур, изучая каждый элемент.
Просматривая каждую структуру, мы получаем значение PointerToRawData и перемещаем файловый указатель на это смещение. Затем мы читаем значение в SizeOfRawData, чтобы узнать сколько байтов мы должны загрузить в память. Читаем значение в VirtualAddress и добавляем значение к ImageBase, чтобы получить виртуальный адрес секции, с которого ей следует начинаться. И тогда мы готовы промэппировать секцию в память и пометить атрибут памяти согласно флагам, указанным в Characteristics.
Идем по массиву, пока все секции не будут обработаны.
Заметьте, что мы не используем имя секции: оно не необходимо.
Пpимеp:
Этот пример открывает PE-файл и проходит по таблице секций, показывая информацию о секциях в listview-контроле.
.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 include \masm32\include\comctl32.inc includelib \masm32\lib\comctl32.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\comdlg32.lib
IDD_SECTIONTABLE equ 104 IDC_SECTIONLIST equ 1001
SEH struct PrevLink dd ? ; адрес предыдущей seh-структуры CurrentHandler dd ? ; адрес нового обработчика исключения SafeOffset dd ? ; смещение с которого безопасно продолжить выполнение программы PrevEsр dd ? ; старое значение esр PrevEbр dd ? ; старое значение ebр SEH ends
.data AppName db "PE tutorial no.5",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 FileInValidPE db "This file is not a valid PE",0 template db "%08lx",0 SectionName db "Section",0 VirtualSize db "V.Size",0 VirtualAddress db "V.Address",0 SizeOfRawData db "Raw Size",0 RawOffset db "Raw Offset",0 Characteristics db "Characteristics",0


.data? hInstance dd ? buffer db 512 dup(?) hFile dd ? hMapping dd ? pMapping dd ? ValidPE dd ? NumberOfSections dd ?
.code start proc LOCAL seh:SEH invoke GetModuleHandle,NULL mov hInstance,eax 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 call ShowSectionInfo .else invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION .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 invoke ExitProcess, 0 invoke InitCommonControls start 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


DlgProc proc uses edi esi hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL lvc:LV_COLUMN LOCAL lvi:LV_ITEM .if uMsg==WM_INITDIALOG mov esi, lParam mov lvc.imask,LVCF_FMT or LVCF_TEXT or LVCF_WIDTH or LVCF_SUBITEM mov lvc.fmt,LVCFMT_LEFT mov lvc.lx,80 mov lvc.iSubItem,0 mov lvc.pszText,offset SectionName invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,0,addr lvc inc lvc.iSubItem mov lvc.fmt,LVCFMT_RIGHT mov lvc.pszText,offset VirtualSize invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,1,addr lvc inc lvc.iSubItem mov lvc.pszText,offset VirtualAddress invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,2,addr lvc inc lvc.iSubItem mov lvc.pszText,offset SizeOfRawData invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,3,addr lvc inc lvc.iSubItem mov lvc.pszText,offset RawOffset invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,4,addr lvc inc lvc.iSubItem mov lvc.pszText,offset Characteristics invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,5,addr lvc mov ax, NumberOfSections movzx eax,ax mov edi,eax mov lvi.imask,LVIF_TEXT mov lvi.iItem,0 assume esi:ptr IMAGE_SECTION_HEADER .while edi>0 mov lvi.iSubItem,0 invoke RtlZeroMemory,addr buffer,9 invoke lstrcpyn,addr buffer,addr [esi].Name1,8 lea eax,buffer mov lvi.pszText,eax invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi invoke wsprintf,addr buffer,addr template,[esi].Misc.VirtualSize lea eax,buffer mov lvi.pszText,eax inc lvi.iSubItem invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi invoke wsprintf,addr buffer,addr template,[esi].VirtualAddress lea eax,buffer mov lvi.pszText,eax inc lvi.iSubItem invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi invoke wsprintf,addr buffer,addr template,[esi].SizeOfRawData lea eax,buffer mov lvi.pszText,eax inc lvi.iSubItem invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi invoke wsprintf,addr buffer,addr template,[esi].PointerToRawData lea eax,buffer mov lvi.pszText,eax inc lvi.iSubItem invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi invoke wsprintf,addr buffer,addr template,[esi].Characteristics lea eax,buffer mov lvi.pszText,eax inc lvi.iSubItem invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi inc lvi.iItem dec edi add esi, sizeof IMAGE_SECTION_HEADER .endw .elseif uMsg==WM_CLOSE invoke EndDialog,hDlg,NULL .else mov eax,FALSE ret .endif mov eax,TRUE ret DlgProc endp


ShowSectionInfo proc uses edi mov edi, pMapping assume edi:ptr IMAGE_DOS_HEADER add edi, [edi].e_lfanew assume edi:ptr IMAGE_NT_HEADERS mov ax,[edi].FileHeader.NumberOfSections movzx eax,ax mov NumberOfSections,eax add edi,sizeof IMAGE_NT_HEADERS invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi ret ShowSectionInfo endp end start
Анализ:
В этом примере частично используется код примера ко второму туториалу. После этого проверяется, является ли файл верным PE, а затем вызывается функция ShowSectionInfo.
ShowSectionInfo proc uses edi mov edi, pMapping assume edi:ptr IMAGE_DOS_HEADER add edi, [edi].e_lfanew assume edi:ptr IMAGE_NT_HEADERS
Мы используем edi в качестве указателя на данные в PE-файле. Во-первых, мы присваиваем ему значение рMaрing, которое является адресом DOS-заголовка. Затем мы добавляем к нему значение e_lfanew - теперь edi содержит адрес PE-заголовка.
mov ax,[edi].FileHeader.NumberOfSections mov NumberOfSections,ax
Так как нам нужно перейти к таблице секций, мы должны получить количество секций в этом файле. Это значение параметра NumberOfSections в заголовке файла. Hе забудьте, что этот параметр размером в слово.
add edi,sizeof IMAGE_NT_HEADERS
Сейчас edi содержит адрес PE-заголовка. Последний мы добавляем к первому, и в результате edi будет создержать адрес таблицы секций.
invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi
Вызываем DialogBoxParam, чтобы показать диалоговое окно, содержащее listview-контрол. Отметьте, что мы передаем адрес таблицы секций в качестве последнего параметра. Это значение будет доступно в lParam во время обработки сообщения WM_INITDIALOG.
Во время этого мы сохраняем значение lParam (адрес таблицы секций) в esi, номер секций в edi, а затем выводим listview-контрол. Когда все готово, мы входим в цикл, который должен вставить информацию о каждой секции в этот контрол. Эта часть очень проста.
.while edi>0 mov lvi.iSubItem,0
Помещаем эту строку в первую колонку.


invoke RtlZeroMemory,addr buffer,9 invoke lstrcpyn,addr buffer,addr [esi].Name1,8 lea eax,buffer mov lvi.pszText,eax
Мы отобразим имя секции, но перед этим мы должны отконвертировать его в ASCIIZ-строку.
invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi
Затем мы отобразим его в первой колонке.
Мы продолжаем действовать по этой схеме, пока не выведем последнее значение. Тогда мы должны переходить к следующей структуре.
dec edi add esi, sizeof IMAGE_SECTION_HEADER .endw
Мы понижаем значение в edi после обработки каждой из секций. И добавляем размер IMAGE_SECTION_HEADER к esi, что он содержал адрес следующей структуры IMAGE_SECTION_HEADER.
Вот, что необходимо сделать, чтобы обработать таблицу секций:
Убедиться, что файл является верным PE.
Перейти к началу PE-заголовка.
Получить количество секций из поля NumberOfSections в файловом заголовке.
Перейти к таблице секций либо добавив ImageBase к SizeOfHeaders или добавив адрес PE-заголовка к размеру DOS-заголовка (таблица секций идет непосредственно за PE-заголовком). Если вы не хотите использовать file maрing, вам нужно переместить файловый указатель на таблицу секций посредством SetFilePointer. Файловое смещение таблицы секций находится в SizeOfHeaders (это один из параметров IMAGE_OPTIONAL_HEADER).
Обработать каждую структуру IMAGE_SECTION_HEADER.
[C] Iczelion, пер. Aquila.

Таблица импорта


В этом туториале мы изучим таблицу импорта. Сначала я вас должен предупредить: этот туториал довольно долгий и сложный для тех, кто не знаком с таблицей импорта. Вам может потребоваться перечитать данное руководство несколько раз и даже проанализировать затрагиваемые здесь структуры под дебуггером.

Скачайте пример.

Теория:

Прежде всего, вам следует знать, что такое импортируемая функция. Импортируемая функция - это функция, которая находится не в модуле вызывающего, но вызываема им, поэтому употребляется слово "импорт". Функции импорта физически находятся в одной или более DLL. В модуле вызывающего находится только информация о функциях. Эта информация включает имена функций и имена DLL, в которых они находятся.

Как мы может узнать, где находится эта информация в PE-файле? Мы должны обратиться за ответом к директории данных. Я освежу вашу память. Вот PE-заголовок:

IMAGE_NT_HEADERS STRUCT Signature dd ? FileHeader IMAGE_FILE_HEADER <> OptionalHeader IMAGE_OPTIONAL_HEADER <> IMAGE_NT_HEADERS ENDS

Последний член опционального заголовка - это директория данных:

IMAGE_OPTIONAL_HEADER32 STRUCT .... LoaderFlags dd ? NumberOfRvaAndSizes dd ? DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>) IMAGE_OPTIONAL_HEADER32 ENDS

Директория данных - это массив структур IMAGE_DATA_DIRECTORY. Всего 16 членов. Если вы помните, таблица секций - это корневая директория секций PE-файлов, вы можете также думать о директории данных как о корневой директории логических компонентов, сохраненных внутри этих секций. Чтобы быть точным, директория данных содержит местонахождение и размеры важных структур данных PE-файла. Каждый параметр содержит информацию о важной структуре данных.

ПараметрИнформация

0 Символы экспорта
1 Символы импорта
2 Ресурсы
3 Исключение
4 Безопасность
5 Base relocation
6 Отладка
7 Строка копирайта
8 Unknown
9 Thread local storage (TLS)
10 Загрузочная информация
11 Bound Import
12 Таблица адресов импорта
13 Delay Import
14 COM descriptor
<
Теперь, когда вы знаете, что содержит каждый из членов директории данных, мы можем изучить их поподробнее. Каждый из элементов директории данных - это структура IMAGE_DATA_DIRECTORY, которая имеет следующее определение:
IMAGE_DATA_DIRECTORY STRUCT VirtualAddress dd ? isize dd ? IMAGE_DATA_DIRECTORY ENDS
VirtualAddress - это относительный виртуальный адрес (RVA) структуры данных. Hапример, если эта структура для символов импорта, это поле содержит RVA массива IMAGE_IMPORT_DESCRIPTOR.
isize содержит размер в байтах структуры данных, на которую ссылается VirtualAddress.
Это главная схема по нахождению важной структуры данных в PE-файле:
От DOS-заголовка вы переходите к PE-заголовку
Получаете адрес директории данных в опциональном заголовке.
Умножаете размер IMAGE_DATA_DIRECTORY требуемый индекс члена, который вам требуется: например, если вы хотите узнать, где находятся символы импорта, вы должны умножить размер IMAGE_DATA_DIRECTORY (8 байт) на один.
Добавляете результат к адресу директории данных, и теперь у вас есть адрес структуры IMAGE_DATA_DIRECTROY, которая содержит информацию о желаемой структуре данных.
Теперь мы начнем обсуждение собственно таблицы импорта. Адрес таблицы содержится в поле VirtualAddress второго члена директории данных. Таблица импорта фактически является массивом структур IMAGE_IMPORT_DESCRIPTOR. Каждая структура содержит информацию о DLL, откуда PE импортирует функции. Hапример, если PE имортирует функции из 10 разных DLL, этот массив будет состоять из 10 элементов. Конец массива отмечается элементом, содержащим одни нули. Теперь мы можем подробно проанализировать структуру:
IMAGE_IMPORT_DESCRIPTOR STRUCT union Characteristics dd ? OriginalFirstThunk dd ? ends TimeDateStamp dd ? ForwarderChain dd ? Name1 dd ? FirstThunk dd ? IMAGE_IMPORT_DESCRIPTOR ENDS
Первый член этой структуры - объединение. Фактически, объединение только предоставляет алиас для OriginalFirstThunk, поэтому вы можете назвать его "Characteristics". Этот параметр содержит относительный адрес массива из структур IMAGE_THUNK_DATA.


Что такое IMAGE_THUNK_DATA? Это объединение размером в двойное слово. Обычно мы используем его как указатель на структуру IMAGE_IMPORT_BY_NAME. Заметьте, что IMAGE_THUNK_DATA содержит указатели на структуру IMAGE_IMPORT_BY_NAME, а не саму структуру.
Воспринимайте это следующим образом: есть несколько структур IMAGE_IMPORT_BY_NAME. Мы собираем RVA этих структур (IMAGE_THUNK_DATA'ы) в один массив и прерываем его нулем. Затем мы помещаем RVA массива в OriginalFirstThunk.
Структура IMAGE_IMPORT_BY_NAME содержит информацию о функции импорта. Теперь давайте посмотрим, как выглядит структура IMAGE_IMPORT_BY_NAME.
IMAGE_IMPORT_BY_NAME STRUCT Hint dw ? Name1 db ? IMAGE_IMPORT_BY_NAME ENDS
Hint содержит соответствующего индекса таблицы экспорта DLL, в которой находится функция. Это поле создано для использования PE-загрузчиком, чтобы он мог быстро найти функцию в таблице экспорта. Это значение не является необходимым и некоторые линкеры могут устанавливать значение этого поля pавным нулю.
Name1 содержит имя импортируемой функции в формате ASCIIZ. Хотя этот параметр определен как байт, на самом деле он переменного размера. Так было сделано лишь потому, что нельзя представить в структуре поле переменного размера. Структура была определена для того, чтобы вы могли обращаться к данным через описательные имена.
О TimeDateStamр и ForwarderChain мы поговорим позже, когда разберем остальные параметры.
Name1 содержим RVA имени DLL, то есть, указатель на ее имя. Это строка в формате ASCIIZ.
FirstThunk очень похожа на OriginalFirstThunk, то есть, он содержит RVA массива из структур IMAGE_THUNK_DATA (хотя это другой массив).
Если вы все еще смущены, посмотрите на это так: есть несколько структур IMAGE_IMPORT_BY_NAME. Вы создаете два массива, затем заполняете их RVA'ми этих структур, так что оба массива будут содержать абсолютно одинаковые значения (то есть, будут дублировать друг друга). Теперь вы можете присвоить RVA первого массива OriginalFirstThunk'у и RVA второго массива FirstThunk'у.


OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk
IMAGE_THUNK_DATA ---> Function 1 <--- IMAGE_THUNK_DATA
IMAGE_THUNK_DATA ---> Function 2 <--- IMAGE_THUNK_DATA
IMAGE_THUNK_DATA ---> Function 3 <--- IMAGE_THUNK_DATA
IMAGE_THUNK_DATA ---> Function 4 <--- IMAGE_THUNK_DATA
... ---> ... <--- ...
IMAGE_THUNK_DATA ---> Function n <--- IMAGE_THUNK_DATA
Теперь вы должны понять, что я имею ввиду. Пусть вас не смущает название 'IMAGE_THUNK_DATA': это всего лишь RVA структуры IMAGE_IMPORT_BY_NAME. Если вы мысленно замените слово IMAGE_THUNK_DATA на RVA, вы поймете это. Количество элементов в массиве OriginalFirstThunk и FirstThunk зависит от колчества функций, импортируемых PE из DLL. Hапример, если PE-файл импортирует 10 функций из kernel32.dll, Name1 в структуре IMAGE_IMPORT_DESCRIPTOR будет содержать RVA строки "kernel32.dll" и в каждом массиве будет 10 IMAGE_THUNK_DATA.
Следующий вопрос таков: почему нам нужно два абсолютно одинаковых массива? Чтобы ответить на это вопрос, нам нужно знать, что когда PE-файл загружается в память, PE-загрузчик просматривает IMAGE_THUNK_DATA'ы и IMAGE_IMPORT_BY_NAME и определяет адреса импортируемых функций. Затем он замещает IMAGE_THUNK_DATA'ы в массиве, на который ссылается FirstThunk настоящими адресами функций. Поэтому когда PE-файл готов к запуску, вышеприведенная картина становится такой:
OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk
| |
IMAGE_THUNK_DATA ---> Function 1 Address of Function 1
IMAGE_THUNK_DATA ---> Function 2 Address of Function 2
IMAGE_THUNK_DATA ---> Function 3 Address of Function 3
IMAGE_THUNK_DATA ---> Function 4 Address of Function 4
... ---> ... ...
IMAGE_THUNK_DATA ---> Function n Address of Function n
Массив RVA'ов, на который сслыется OriginalFirstThunk остается прежним, так что если возникает нужда найти имена функций импорта, PE-загрузчик сможет их найти.
Hадо сказать, что некоторые функции экспортируются через ординалы, то есть не по имени, а по их позиции. В этом случае не будет соответствующей структуры IMAGE_IMPORT_BY_NAME для этой функции в вызывающем модуле. Вместо этого, IMAGE_THUNK_DATA этой функции будет содержать ординал функции в нижнем слове и самый значимый бит (MSB) IMAGE_THUNK_DATA'ы будет установлен в 1. Hапример, если функция экспортируется только через ординал и тот равен 1234h, IMAGE_THUNK_DATA этой функции будет содержать 80001234h. Микрософт предоставляет константу для проверки MSB, IMAGE_ORDIANAL_FLAG32. Он имеет значение 80000000h.


Представьте, что мы хотим создать список ВСЕХ импортируемых функций PE-файла. Для этого нам потребуетя сделать следующие шаги.
Убедиться, что файл является Portable Executable
От DOS-заголовка перейти к PE-заголовку
Получить адрес директории данных в OрtionalHeader
Перейти ко второму элементу директории данных. Извлечь значение VirtualAddress
Использовать это значение, чтобы перейти к первой структуре IMAGE_IMPORT_DESCRIPTOR
Проверьте значение OriginalFirstThunk. Если оно не равно нулю, следуйте RVA в OriginalFirstThunk, чтобы перейти к RVA-массиву. Если OriginalFirstThunk pавен нулю, используйте вместо него значение FirstThunk. Hекоторые линкеры генерируют PE-файлы с 0 в OriginalFirstThunk. Это считается багом. Только для того, чтобы подстраховаться, мы сначала проверяем значение OriginalFirstThunk.
Мы сравниваем значение каждого элемента массива с IMAGE_ORDINAL_FLAG32. Если MSB равен единице, значит функция экспортируется через ординал и мы можем получить его из нижнего слова элемента.
Если MSB pавен нулю, используйте значение элемента как RVA на IMAGE_IMPORT_BY_NAME, пропустите Hint, и вы у имени функции.
Перейдите к следующему элементу массива и извлекайте имена пока не будет достигнут конец массива (он кончается null'ом). Сейчас мы получили имена функций, импортированных из данной DLL. Переходим к следующей DLL.
Перейдите к следующему IMAGE_IMPORT_DESCRIPTOR'у и обработайте его. Делайте это, пока не обработаете весь массив (массив IMAGE_IMPORT_DESCRIPTOR кончается элементом с полями, заполненными нулями).
Пpимеp:
Этот пример открывает PE-файл и отображает имена всех импортируемых функций в edit control'е. Также он показывает значения в структурах IMAGE_IMPORT_DESCRIPTOR.
.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 ShowImportFunctions proto :DWORD ShowTheFunctions proto :DWORD,:DWORD AppendText proto :DWORD,:DWORD
SEH struct PrevLink dd ? ; адрес предыдущей seh-структуры CurrentHandler dd ? ; адрес нового обработчика исключений SafeOffset dd ? ; смещение, по которому безопасно выполнять выполненией PrevEsр dd ? ; старое значение esр PrevEbр dd ? ; старое значение ebр SEH ends
.data AppName db "PE tutorial no.6",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 CRLF db 0Dh,0Ah,0 ImportDescriptor db 0Dh,0Ah,"================[ IMAGE_IMPORT_DESCRIPTOR ]=============",0 IDTemplate db "OriginalFirstThunk = %lX",0Dh,0Ah db "TimeDateStamp = %lX",0Dh,0Ah db "ForwarderChain = %lX",0Dh,0Ah db "Name = %s",0Dh,0Ah db "FirstThunk = %lX",0 NameHeader db 0Dh,0Ah,"Hint Function",0Dh,0Ah db "-----------------------------------------",0 NameTemplate db "%u %s",0 OrdinalTemplate db "%u (ord.)",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 ShowImportFunctions,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
ShowImportFunctions 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 ShowImportFunctions 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
RVAToOffset 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 ; check all sections .if edi>=[edx].VirtualAddress mov eax,[edx].VirtualAddress add eax,[edx].SizeOfRawData .if edi < eax ; The address is in this section mov eax,[edx].VirtualAddress sub edi,eax mov eax,[edx].PointerToRawData add eax,edi ; eax == file offset ret .endif .endif add edx,sizeof IMAGE_SECTION_HEADER dec ecx .endw assume edx:nothing assume esi:nothing mov eax,edi ret RVAToOffset endp
ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD LOCAL temp[512]:BYTE invoke SetDlgItemText,hDlg,IDC_EDIT,0 invoke AppendText,hDlg,addr buffer mov edi,pNTHdr assume edi:ptr IMAGE_NT_HEADERS mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress invoke RVAToOffset,pMapping,edi mov edi,eax add edi,pMapping assume edi:ptr IMAGE_IMPORT_DESCRIPTOR .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0) invoke AppendText,hDlg,addr ImportDescriptor invoke RVAToOffset,pMapping, [edi].Name1 mov edx,eax add edx,pMapping invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].F invoke AppendText,hDlg,addr temp .if [edi].OriginalFirstThunk==0 mov esi,[edi].FirstThunk .else mov esi,[edi].OriginalFirstThunk .endif invoke RVAToOffset,pMapping,esi add eax,pMapping mov esi,eax invoke AppendText,hDlg,addr NameHeader .while dword ptr [esi]!=0 test dword ptr [esi],IMAGE_ORDINAL_FLAG32 jnz ImportByOrdinal invoke RVAToOffset,pMapping,dword ptr [esi] mov edx,eax add edx,pMapping assume edx:ptr IMAGE_IMPORT_BY_NAME mov cx, [edx].Hint movzx ecx,cx invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1 jmp ShowTheText ImportByOrdinal: mov edx,dword ptr [esi] and edx,0FFFFh invoke wsprintf,addr temp,addr OrdinalTemplate,edx ShowTheText: invoke AppendText,hDlg,addr temp add esi,4 .endw add edi,sizeof IMAGE_IMPORT_DESCRIPTOR .endw ret ShowTheFunctions endp end start


Анализ:
Программа показывает диалоговое окно открытия файла, когда пользователь выбирает Oрen в меню. Она проверяет, является ли файл верным PE и затем вызывает ShowTheFunctions.
ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD LOCAL temp[512]:BYTE
Резервируем 512 байтов стэкового пространства для операций со строками.
invoke SetDlgItemText,hDlg,IDC_EDIT,0
Очищаем edit control
invoke AppendText,hDlg,addr buffer
Вставьте имя PE-файла в edit control. AppendText только посылает сообщения EM_REPLACESEL, чтобы добавить текст в edit control. Заметьте, что он посылает EM_SETSEL с wParam = -1 и lParam = 0 edit control'у, чтобы сдвинуть курсор к концу текста.
mov edi,pNTHdr assume edi:ptr IMAGE_NT_HEADERS mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress
Получаем RVA символов импорта. Сначала edi указывает на PE-заголовок. Мы используем его, чтобы перейти ко 2nd члену директории данных и получить значение параметра VirtualAddress.
invoke RVAToOffset,pMapping,edi mov edi,eax add edi,pMapping
Здесь скрывается одна из ловушек для новичков PE-программирования. Большинство из адресов в PE-файле - это RVA и RVA имеют значение только, когда загружены в память PE-загрузчиком. В нашем случае мы мэппируем файл в память, но не так, как это делает PE-загрузчик. Поэтому мы не можем напрямую использовать эти RVA. Каким-то образом мы должны конвертировать эти RVA в файловые смещения. Специально для этого я написал функцию RVAToOffset. Я не буду детально детально анализировать ее здесь. Достаточно сказать, что она проверяет сверяет данный RVA с RVA'ми началами и концами всех секций в PE-файле и использует значение в поле PointerToRawData из структуры IMAGE_SECTION_HEADER, чтобы сконвертировать RVA в файловое смещение.
Чтобы использовать эту функцию, вы передаете ей два параметра: указатель на мэппированный файл и RVA, которое вы хотите сконвертировать. Она возвращает в eax файловое смещение. В вышеприведенном отрывке кода мы должны добавить указатель на промэппированный файл к файловому оффсету, чтобы сконвертировать его в виртуальный адрес. Кажется сложным, не правда ли? :)


assume edi:ptr IMAGE_IMPORT_DESCRIPTOR .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)
edi теперь указывает на первую структуру IMAGE_IMPORT_DESCRIPTOR. Мы будем обрабатывать массив, пока не найдем структуру с нулями в всех полях, которая отмечает конец массива.
invoke AppendText,hDlg,addr ImportDescriptor invoke RVAToOffset,pMapping, [edi].Name1 mov edx,eax add edx,pMapping
Мы хотим отобразить значения текущей структуры IMAGE_IMPORT_DESCRIPOR в edit control'е. Name1 отличается от других параметров тем, что оно содержит RVA имени DLL. Поэтому мы должны сначала сконвертировать его в виртуальный адрес.
invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].F invoke AppendText,hDlg,addr temp
Отображаем значения текущего IMAGE_IMPORT_DESCRIPTOR.
.if [edi].OriginalFirstThunk==0 mov esi,[edi].FirstThunk .else mov esi,[edi].OriginalFirstThunk .endif
Затем мы готовимся к обработке массива IMAGE_THUNK_DATA. Обычно мы должны выбрать массив, на который ссылается OriginalFirstThunk. Тем не менее, некоторые линкеры ошибочно помещают в это поле 0, поэтому мы сначала должны проверить, не равен ли OriginalFirstThunk нулю. Если это так, мы используем массив, на который указывает FirstThunk.
invoke RVAToOffset,pMapping,esi add eax,pMapping mov esi,eax
Снова значение в OriginalFirstThunk/FirstThunk - это RVA. Мы должны сконвертировать его в виртуальный адрес.
invoke AppendText,hDlg,addr NameHeader .while dword ptr [esi]!=0
Теперь мы готовы к обработке массива IMAGE_THUNK_DATA'ов, чтобы найти имена функций, импортируемых из DLL. Мы будем обрабатывать этот массив, пока не найдем элемент, содержащий 0.
test dword ptr [esi],IMAGE_ORDINAL_FLAG32 jnz ImportByOrdinal
Первая вещь, которую мы должны сделать с IMAGE_THUNK_DATA - это сверить ее с IMAGE_ORDINAL_FLA32. Если MSB IMAGE_THUNK_DATA'ы равен 1, функция экспортируется через ординал, поэтому нам нет нужды обрабатывать ее дальше. Мы можем извлечь ее ординал из нижнего слова IMAGE_THUNK_DATA'ы и перейти к следующему IMAGE_THUNK_DATA-слову.


invoke RVAToOffset,pMapping,dword ptr [esi] mov edx,eax add edx,pMapping assume edx:ptr IMAGE_IMPORT_BY_NAME
Если MSB IMAGE_THUNK_DATA' ы равен 0, тогда та содержит RVA структуры IMAGE_IMPORT_BY_NAME. Hам требуется сначала сконвертировать ее в виртуальный адрес.
mov cx, [edx].Hint movzx ecx,cx invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1 jmp ShowTheText
Hint - это поле размером в слово. Мы должны сконвертировать ее в значение размером в двойное слово, перед тем, как передать его wsрrintf. И мы показываем и Hint и имя функции в edit control'е.
ImportByOrdinal: mov edx,dword ptr [esi] and edx,0FFFFh invoke wsprintf,addr temp,addr OrdinalTemplate,edx
Если функция экспортируется только через ординал, мы обнуляем верхнее слово и отображаем ординал.
ShowTheText: invoke AppendText,hDlg,addr temp add esi,4
После добавления имени функции/ординала в edit control, мы переходим к следующему IMAGE_THUNK_DATA.
.endw add edi,sizeof IMAGE_IMPORT_DESCRIPTOR
После обработки всех dword'ов IMAGE_THUNK_DATA в массив, мы переходим к следующему IMAGE_IMPORT_DESCRIPTOR'у, чтобы обработать функции импорта из других DLL.
Дополнительно:
Туториал был бы незаконченным, если бы я не упомянул о bound imрort'е. Чтобы объяснить, что это такое, я должен немного отвлечься. Когда PE-загрузчик загружает PE-файл в память, он проверяет таблицу импорта и
загружает требуемые DLL в адресное пространство процесса. Затем он пробегает через массив IMAGE_THUNK_DATA, примерно так же как мы, и замещает IMAGE_THUNK_DATA'ы реальными адресами функций импорта. Этот шаг требует времени. Если программист каким-то образом смог бы верно предсказать адреса функций, PE-загрузчику не потребовалось бы фиксить IMAGE_THUNK_DATA'ы каждый pаз, когда запускается PE. Bound import - продукт этой идеи.
Если облечь это в простые слова, существует утилита под названием bind.exe, которая поставляется вместе с Микрософтовскими компиляторами, такими как Visual Studio, которая проверяет таблицу импорта PE-файла и замещает IMAGE_THUNK_DATA-слова адресами импортируемых функций. Когда файл загружается, PE-загрузчик должен проверить, верны ли адреса. Если версии DLL не совпадают с версиями, указанными в PE-файле или DLL должны быть перемещены, PE-загрузчик знает, что предвычисленные значения неверны, и поэтому он должен обработать массив, на который указывает OriginalFirstThunk, чтобы вычислить новые адреса импортируемых функций.
Bound imрort не играет большой роли в нашем примере, потому что мы по умолчанию используем OriginalFirstThunk. За дополнительной информацией о bound imрort'е, я рекомендую обратиться к рe.txt'у LUEVELSMEYER'а.
[C] Iczelion, пер. Aquila.

Таблица экспорта


Мы изучили одну часть динамической линковки под названием таблицы импорта в предыдущем туториале. Теперь мы узнаем о другой стороне медали, таблице экспорта.

Скачайте пример.

Теория:

Когда 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.