Импорт - это важнейший элемент любого исполняемого файла. Невозможно найти exe- или dll-файл без каких-либо таблиц, имеющих отношение к импорту, разве что у упакованных файлов и у вирусов. Импорт позволяет указать, какие внешние функции и из каких модулей требуются исполняемому файлу для нормальной работы. Например, если мы пишем простейший "Hello, world!", который выводит приветствие в MessageBox'е и завершает выполнение, то ему потребуются как минимум две функции: MessageBoxA (или MessageBoxW) из user32.dll и ExitProcess из kernel32.dll.
PE-формат имеет несколько способов импорта функций. Самый простой и часто использующийся, но самый медленный - это импорт через таблицу импортируемых функций, когда загрузчик заполняет соответствующую таблице функций таблицу их адресов после загрузки в адресное пространство исполняемого файла всех необходимых библиотек. По сути, это вызов LoadLibrary для каждой требуемой библиотеки и затем вызовы GetProcAddress для каждой импортируемой функции.
Второй механизм - это bound import (привязанный импорт), который сводится к тому, что на адресное пространство исполняемого файла проецируются библиотеки, а в таблице импорта уже зашиты адреса функций. Это быстрый механизм, но любое изменение динамических библиотек повлечет за собой обязательную перекомпиляцию (перепривязку) исполняемого файла, чтобы все зашитые адреса функций пересчитались. Тем не менее, этот механизм используется в Windows для всех стандартных исполняемых файлов (Калькулятор, Сапер, Блокнот и т.д.), так как стандартные библиотеки Windows меняются крайне редко. Привязанный импорт комбинируется с обычным импортом.
Третий, самый неочевидный механизм, это delay import (отложенный импорт), основная идея которого состоит в вызове некоторого обработчика, который должен получить адрес требуемой функции и записать его в таблицу адресов импортируемых функций, по мере необходимости.
Для начала рассмотрим классический импорт, так как без него, как правило, не обходится ни один исполняемый файл. С классическим импортом связано несколько таблиц. На корневую таблицу импортируемых функций указывают элементы IMAGE_DIRECTORY_ENTRY_IMPORT в DATA_DIRECTORY и IMAGE_DIRECTORY_ENTRY_IAT, указывающий на IMPORT_ADDRESS_TABLE (рассматривается далее), начиная с Windows XP.
Элемент DATA_DIRECTORY IMAGE_DIRECTORY_ENTRY_IMPORT указывает на массив расположенных подряд структур IMAGE_IMPORT_DESCRIPTOR, причем последний элемент должен быть нулевым (все поля структуры равны 0). Рассмотрим структуру IMAGE_IMPORT_DESCRIPTOR подробнее:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT }; DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new) // O.W. date/time stamp of DLL bound to (old) DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT } IMAGE_IMPORT_DESCRIPTOR; |
Эта структура справедлива как для PE, так и для PE64. Поле Name - это RVA строки, содержащей имя DLL, из которой будет производиться импорт. Если содержимое поля TimeDateStamp равно нулю, то импорт обрабатывается как обычно, а если -1, то импорт считается привязанным (bound), и к таблице IMAGE_IMPORT_DESCRIPTOR загрузчик вернется только в том случае, если привязанный импорт завершится неудачей. Если TimeDateStamp содержит отличное от 0 и 1 значение, то при совпадении этого значения с таймстампом библиотеки она просто проецируется на адресное пространство процесса без дальнейших вызовов GetProcAddress. При этом считается, что все адреса должны быть записаны в таблицу адресов еще на этапе компиляции. Если же таймстампы не совпали, то импорт обрабатывается по полной программе, как обычно.
Поле ForwarderChain системным загрузчиком игнорируется и может содержать что угодно.
Самые важные поля - это OriginalFirstThunk и FirstThunk. Первое указывает на lookup-таблицу, содержащую имена импортируемых функций, а второе - на таблицу адресов импортируемых функций (сюда загрузчик будет писать текущие адреса функций).
На данном этапе я выкладываю свой C++-класс, упрощающий работу с PE-файлами, который я достаточно давно начал писать. Сейчас он еще не завершен, можно наращивать функционал, но для наглядного вывода таблицы импорта исполняемого файла он вполне подойдет. Вот исходный код, печатающий таблицу импорта любого 32-разрядного файла (для 64-раздядного можете сделать сами, минимум отличий):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
//Необходимые стандартные заголовки #include <iostream> #include <fstream> #include <iomanip> #include <string> #include <sstream> //Заголовки, определяющие класс pe32 и pe64, а также исключения #include "pe3264.h" #include "pe_exception.h" int main(int argc, const char* argv[]) { if(argc != 2) { std::cout << "Usage: sectons.exe pe_file" << std::endl; return 0; } //Открываем PE-файл на чтение std::ifstream pefile; pefile.open(argv[1], std::ios::in | std::ios::binary); if(!pefile.is_open()) { std::cout << "Can't open file" << std::endl; return 0; } try { //Создаем объект класса PE32 pe32 executable(pefile); //Если у него есть импорты if(executable.has_imports()) { //Получаем указатель на массив таблиц IMAGE_IMPORT_DESCRIPTOR const IMAGE_IMPORT_DESCRIPTOR* import_descriptor_array = reinterpret_cast<const IMAGE_IMPORT_DESCRIPTOR*>(executable.section_data_from_rva(executable.directory_rva(IMAGE_DIRECTORY_ENTRY_IMPORT))); //И перебираем их до тех пор, пока не достигнем нулевого элемента while(import_descriptor_array->Characteristics) { //Выведем таймстамп и имя библиотеки std::cout << "DLL Name: " << executable.section_data_from_rva(import_descriptor_array->Name) << std::endl; std::cout << "Import TimeDateStamp: " << import_descriptor_array->TimeDateStamp << std::endl; //Получим указатель на таблицу адресов, //которую должен заполнить загрузчик const DWORD* import_address_table = reinterpret_cast<const DWORD*>(executable.section_data_from_rva(import_descriptor_array->FirstThunk)); //И указатель на лукап-таблицу, которая содержит //имена импортируемых функций. //Стоит обратить внимание на то, что некоторые линкеры //допускают ошибку и оставляют этот указатель нулевым. //Это, в принципе, валидный exe-файл, но в случае //необходимости после загрузки файла уже не удастся восстановить имена //импортируемых функций, так как единственная существующая //в данном случае таблица адресов, являющаяся одновременно и лукап-таблицей, //будет исковеркана загрузчиком const DWORD* import_lookup_table = import_descriptor_array->OriginalFirstThunk == 0 ? import_address_table : reinterpret_cast<const DWORD*>(executable.section_data_from_rva(import_descriptor_array->OriginalFirstThunk)); //Для информации DWORD address_table = import_descriptor_array->FirstThunk; //Переменные для хранения имени импортируемой функции и ее порядкового номера в таблице //экспортируемых функций DLL (hint) //Следует обратить внимание на то, что хинт и ординал - это не одно и то же //Ординал - это некий номер, соответствующий функции, и по этому номеру импорт также может производиться //Подробнее об этом я напишу, когда доберусь до описания экспорта std::string name; WORD hint; std::cout << std::endl << " hint | name/ordinal | address" << std::endl; //Перебор импортируемых функций if(import_lookup_table != 0 && import_address_table != 0) { while(true) { //Тут стоило бы добавить дополнительные проверки, т.к. указатель для кривого exe //может оказаться невалидным, но этот пример демонстрационный //и не стремится быть идеально правильным DWORD address = *import_address_table++; //Если мы достигли конца списка импортируемых функций, то переходим //к следующей библиотеке if(!address) break; DWORD lookup = *import_lookup_table++; //Макрос из WinNT.h, говорит о том, что функция импортируется по ординалу if(IMAGE_SNAP_BY_ORDINAL32(lookup)) { //Если это так, то выведем вместо имени функции ее ординал std::stringstream stream; stream << "#" << IMAGE_ORDINAL32(lookup); name = stream.str(); hint = 0; } else { //В противном случае выведем ее имя name = executable.section_data_from_rva(lookup + 2); hint = *reinterpret_cast<const WORD*>(executable.section_data_from_rva(lookup)); } //Выводим информацию об импортируемой функции std::cout << std::dec << "[" << std::setfill('0') << std::setw(4) << hint << "]" << " " << std::left << std::setfill(' ') << std::setw(30) << name << ":0x" << std::hex << std::right << std::setfill('0') << std::setw(8) << address_table << std::endl; address_table += 4; } } std::cout << "==========" << std::endl << std::endl; //Переходим к следующей библиотеке import_descriptor_array++; } } } catch(const pe_exception& e) { //Если вдруг произошла ошибка std::cout << "Exception: " << e.what() << std::endl; } return 0; } |
Результат работы программы можно увидеть на скриншоте в начале поста. Исходные коды класса и программы прикреплены в конце статьи. Программы для разбора привязанного и отложенного импорта я приводить не буду, они есть в статье Криса Касперски "Путь воина". Я приведу лишь структуры, связанные с этими типами импорта, и некоторые пояснения касательно их.
С привязанным импортом связана всего одна структура (элемент IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT DATA_DIRECTORY указывает на массив таких структр, завершающийся нулевым элементом):
1 2 3 4 5 6 |
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; |
Поле TimeDateStamp содержит временную метку библиотеки для привязанного импорта, и такой импорт будет осуществлен тогда, когда временные метки структуры и библиотеки совпали, либо когда TimeDateStamp = 0. Поле OffsetModuleName - это указатель на имя библиотеки, отсчитываемый от начала массива структур IMAGE_BOUND_IMPORT_DESCRIPTOR. Поле NumberOfModuleForwarderRefs указывает на количество форвардов, назначение этого поля не ясно.
Я сталкивался с exe-файлами, у которых таблицы отложенного импорта находились вне данных секций (т.е. в оверлеях (промежутках между секциями) исполняемого файла), поэтому приходилось дополнительно подгружать их из файла, чтобы считать.
Остается последний механизм импорта - отложенный импорт. Как всегда, элемент IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT DATA_DIRECTORY указывает на массив нижеописанных структр, завершающийся нулевым элементом):
1 2 3 4 5 6 7 8 9 10 11 |
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; |
Описание структуры взято у Криса Касперски, и тут же я позволю себе процитировать его пояснения касательно это структуры, так как ничего подробнее я не нашел, а самому разбираться не приходилось, так как данный механизм импорта используется крайне редко:
Поле grAttrs задает тип адресации, применяющийся в служебных структурах отложенного импорта (0 - VA, 1 - RVA); поле szName содержит RVA/VA-указатель на ASCIIZ-строку с именем загружаемой DLL (тип адреса определяется особенностями реализации конкретного delay helper'а, внедряемого в программу линкером и варьирующегося от реализации к реализации - короче говоря, будьте готовы ко всяким пакостям). В изначально пустое поле phmod загрузчик (все тот же Delay Helper) помещает дескриптор динамически загружаемой DLL.
Поле pIAT содержит указатель на таблицу адресов отложенного импорта, организованную точно также, как и обычная IAT, с той лишь разницей, что все элементы таблицы отложенного импорта ведут к delay load helper'у - специальному динамическому загрузчику, также называемому переходником (thunk), который вызывает LoadLibrary (если только библиотека уже не была загружена), а затем дает GetProcAddress и замещает текущий элемент таблицы отложенного импорта эффективным адресом импортируемой функции, благодаря чему все последующие вызовы данной функции осуществляются напрямую, в обход delay load helper'а.
При выгрузке DLL из памяти, последняя может восстановить таблицу отложенного импорта в исходное состояние, обратившись к ее оригинальной копии, RVA-указатель на которую хранится в поле pUnloadIAT. Если же копии нет, ее указатель будет обращен в ноль.
Поле pINT содержит RVA-указатель на таблицу имен, во всем повторяющую стандартную таблицу имен (см. name Table). То же самое относится и к полю pBoundIAT, хранящим RVA-указатель на таблицу диапазонного импорта. Если таблица диапазонного импорта не пуста и указанная временная метка совпадает с временной меткой соответствующей DLL, системный загрузчик просто проецирует ее на адресное пространство данного процесса и механизм отложенного импорта дезактивируется.
В статье Криса Касперски можно также найти простой дампер таблиц отложенного импорта.
Надеюсь, все это вас не слишком испугало :)
На деле, достаточно понять лишь первый механизм, так как он используется чаще всего и без него, как правило, не обходится ни один исполняемый файл.
Обещанные исходные коды класса для работы с PE и программа для разбора таблиц импорта (всё без комментариев): Скачать.
молодцы ребята постоянно черпаю у вас что-то новое
интересная статейка ))
есть один вопрос не в тему - какой компонент использовали для ведения логов в программе No Ads ?
Если речь о поле внизу формы, то это простой textbox
Извиняюсь что опять не в тему. Как растянуть текстбокс на пару строк я понял, а как сделать вывод по строкам ? Ну например как в том же No Ads пишется по строчке что программа сделала. У меня только заменяет первую строку. Делал так:
this->tb_logs->Text = "Command " + this->tb_command->Text+ " | " + this->tb_1param->Text+ " | "+ this->tb_2param->Text + " sent on " + this->tb_host->Text + ".\n";
logBox.AppendText(text + "\r\n");
Строки не понял = порылся в сорцах = заюзал вот это
this->tb_logs->AppendText(String::Concat(text, "\r\n")); - помогло.
Спасибо за помощь !
у меня проблемка почему то получаю только системные dll, importsStartRVA = GetImgDirEntryRVA(pNTHeader,IMAGE_DIRECTORY_ENTRY_IMPORT);
в чем может быть проблема?
Я не понял вопрос. В чем конкретно проблема, и что такое GetImgDirEntryRVA? Судя по гуглу, оно позволяет лишь определить RVA директории импорта.
"Например, если мы пишем простейший "Hello, world!", который выводит приветствие в MessageBox'е и завершает выполнение, то ему потребуются как минимум две функции: MessageBoxA (или MessageBoxW) из user32.dll и ExitProcess из kernel32.dll."
Поправка: на самом деле и ExitProcess не нужен, достаточно MessageBoxA/MessageBoxW.
invoke MessageBoxA, 0, chr$("Caption"), chr$("Text"), 0
ret
dx, отличная работа, безусловно наиполезнейшая либа. Хотелось бы узнать способна ли она собирать ехе с нуля как болванку... т.е. собираем дос хидер, нт хидер, опшионал хидер, добавляем секции там (код, дата, рсрц етц), настраиваем точку входа на секцию кода, имэджбейс, сайзофимэдж етц. и на выходе получаем готовый пе файл-пустышку. Я извеняюсь если это уже реализованно у вас, я пока не смотрел вашей библиотеки, прошолся бегло взглядом единственное по папке с сэмплами....
С нуля не собирает, хотя думал над этим, да руки не дошли пока. Пока что можно взять готовый PE-файл, удалить у него все секции и работать исходя из этого, так как все поля, флаги редактировать можно.
можно взять болванку например пустого файла на масме и уже на базе его строить свой ехе...
ну хотя мне видится, что в полноценной библиотеке болванка ехе обязательно должна быть:) если появится время, можем списаться, готов посодействовать идеями, и возможно кодом.
Да я думаю, что доработаю этот функционал. Я библиотеку еще не забросил и с момента релиза очень много багов всяких поправил там, функционал нарастил немного, и пока продолжаю :) В хедере pe_base.h даже список TODO заготовил, хотя его пока тоже не начинал делать.
>Я библиотеку еще не забросил и с момента релиза очень много багов всяких поправил там
супер. очень радует ваше творчество. видимо вами движет очень сильная мотивация.
p.s.
мне кажется ваш туду лист еще будет пополняться;) btw релоки с несколькими элементами тоже не видел ни разу.
//Заголовки, определяющие класс pe32 и pe64, а также исключения
#include "pe3264.h"
#include "pe_exception.h"
И где это должен взять читающий ?
Очевидно, скачать по ссылке в конце поста.
Спасибо, не обратил внимание на неё.
Статья хорошая.
Только вот примеры из таких статей всегда приходится переводить на Си.