Начало
Хеллоу, уважаемая аудитория. Сегодня мы с вами поговорим о PE-формате исполняемых файлов. Точнее - о таблице импорта. Обсудим, что это такое и в каких случаях знания об этой сущности могут нам пригодиться. Но обо всём по порядку.
Почитывая статьи на местном форуме, мне показалось, что здесь достаточно популярна тема упаковщиков и крипторов. Значит, определённой популярностью пользуются и утилиты для распаковки – снятия этих самых крипторов с исполняемых файлов.
Различных вспомогательных утилит существует великое множество, но статья не о них. В ходе чтения всё же было бы неплохо установить себе утилиту CFF Explorer, которая входит в состав ExplorerSuite http://www.ntcore.com/files/ExplorerSuite.exe
С помощью этой утилиты можно легко посмотреть, с чем нам предстоит работать, красным обведены адрес и размер таблицы, которую мы будем учиться создавать.
Таким образом, статья больше о том, как автоматизировать процесс распаковки какого-то криптора. Зачем это нужно? Предположим, вы покупаете у кого-то некий софт – абсолютно любой. Потом хотите этот же софт передать (или продать) кому-то в пользование. Но вдруг так случается, что он привязан к вашему компьютеру, и на машине вашего товарища (клиента) работать отказывается наотрез. Вы берёте отладчик и хотите выяснить, в чём же там дело? Но дело в том, что сам бинарник чем-то упакован. И нужно его распаковывать. И так – от версии к версии, без конца и края. Если же новые версии выходят достаточно часто, то вы каждый раз будете долбаться с ручной распаковкой. Хорошо бы это дело автоматизировать, верно? Вот тут-то и пригодится умение.
Кратко про распаковку
О распаковке сказано уже чуть более, чем дохрена. Потому не вижу смысла повторять пройденное. Пройдёмся лучше по автоматизации этого процесса.Большинство реверсеров, с которыми мне приходилось общаться, очень хорошо представляют, как автоматизировать поиск ОЕР. Чуть меньшее количество - знают, как сделать дамп памяти. Ещё меньшее количество - могут с ходу сказать, что такое DumpFixer.
Когда дело доходит до восстановления импорта – все как один легко вспоминают ImpRec и/или Scylla Imports Reconstruction. Но вот как автоматизировать их работу – пока на моей памяти не рассказал ещё никто. И это печалит меня, потому что (как вы скоро убедитесь) в этом нет ничего сложного.
Теоретический минимум
Для восстановления таблицы импорта нужно хотя бы в теории представлять, что это такое, и из чего состоит. И вот здесь парадокс – это всё уже давно расписано и разрисовано. http://uinc.ru/articles/41/#23
Даже есть там такая замечательная картинка:
Но то ли статья написана не очень доступно, то ли поисковики плохо её индексируют, но почему-то людям всегда так тяжело её найти и ещё тяжелее в ней разобраться. Потому давайте я вам помогу, и попробую улучшить (как мне кажется) данную схему. Тем не менее, рекомендую во время чтения всегда держать эту картинку перед собой для лучшего понимания происходящего.
Основы основ
Библиотечные функции могут импортироваться либо по имени, либо по ординалу. Если с именем всё более-менее понятно, т.к. у каждой функции есть некое удобное лаконичное имя, например, LoadLibraryA или GetProcAddress. С ординалами всё немного сложнее. Всё, что вам необходимо знать сейчас, это то, что ординал – некий целочисленный номер функции, который задаётся как параметр при билде библиотеки. Более подробно тут: https://msdn.microsoft.com/en-us/library/e7tsx612.aspx
Поскольку имена используются чаще, чем ординалы, рассмотрим пример воссоздания таблицы импорта, где фигурируют как раз имена. Основной здесь является структура:
Код:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Первые два байта (Hint) могут быть нулевыми, тогда как имя нулевым быть не должно. Имя здесь – просто массив char символов, завершается нулевым символом. Но это всё – теория. Допустим, нам нужно разместить такую структуру в памяти. Сделать это может такой метод:
Код:
std::vector<uint8_t> ImportCreator::CreateImportByName(const std::string &ApiName) const
{
uint32_t LengthForString = ApiName.length() + sizeof(char);
uint32_t VectorLength = LengthForString + sizeof(IMAGE_IMPORT_BY_NAME::Hint);
std::vector<uint8_t> result(VectorLength);
memcpy(&result[sizeof(IMAGE_IMPORT_BY_NAME::Hint)],ApiName.data(),ApiName.length());
return result;
}
Можно назвать такое действие сериализацией, если угодно. Структуры IMAGE_IMPORT_BY_NAME могут идти одна за другой (что логично и компактно), но это необязательно. Нужно понимать, что в ОС Windows функция описывается двумя характеристиками:
- именем или ординалом (рассмотрено ранее);
- именем динамической библиотеки, в которой расположена эта самая функция.
Поэтому есть смысл располагать структуры, описывающие функции одной библиотеки, рядом друг с другом.
Предположим, мы определились, что в нашей новой таблице импорта будут располагаться 7 функций из какой-то библиотеки A.dll. Тогда метод CreateImportByName следует запустить 7 раз, сериализируя структуры IMAGE_IMPORT_BY_NAME друг за другом.
Санки
Следующая важная структура, на которую стоит обратить внимание:
Код:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
Данная структура должна содержать адрес IMAGE_IMPORT_BY_NAME, по одной на каждую функцию. Иными словами, если у нас 7 функций, то и IMAGE_THUNK_DATA32 должно быть 7 (и IMAGE_IMPORT_BY_NAME тоже). Следующий метод создаст вектор структур IMAGE_THUNK_DATA32:
Код:
std::vector<IMAGE_THUNK_DATA32>
ImportCreator::CreateImportThunks(const std::vector<std::string> &ApiNames, uint32_t AddressOfPeInMemory)
{
std::vector<IMAGE_THUNK_DATA32> result(ApiNames.size() + 1); //+1 needed to add empty item to show this is the end
uint32_t i = 0;
uint8_t* ImportNameLocation = reinterpret_cast<uint8_t*> (AddressOfPeInMemory);
for (const auto& ApiName:ApiNames)
{
std::vector<uint8_t> ImportByName = CreateImportByName(ApiName);
memcpy(ImportNameLocation,ImportByName.data(),ImportByName.size());
uint32_t Address = rvaOffset.OffsetToRva(reinterpret_cast<uint32_t> ( ImportNameLocation -
reinterpret_cast<uint32_t> ( PeFile.data() ) ) );
IMAGE_THUNK_DATA32 Thunk = {0};
Thunk.u1.AddressOfData = Address;
result[i++] = Thunk;
ImportNameLocation += ImportByName.size();
}
return result;
}
Воу-воу, какие исходные данные?
Логично было бы определиться с тем, что мы имеем перед началом работы. А именно – какие исходные данные для создания таблицы импорта нам нужны?
Тут следует отвлечься и пояснить, какие задачи мы можем решать. Т.е. зачем нам может вообще понадобиться создание своей таблицы импорта. С одной стороны – это распаковка упакованных файлов. В этом случае у нас есть адреса FirstThunk, уже модифицированные загрузчиком. Но идея в том, что мы должны использовать эти FirstThunk, а не создавать свои. Иначе весь код будет нерабочим.
С другой стороны, при создании своих крипторов нам нужна своя таблица импорта, созданная с нуля. Обе эти задачи приводят нас к таким структурам данных:
Код:
struct IMPORT_DESCRIPTOR_X86
{
std::string LibraryName;
uint32_t FirstThunkStartRva;
std::vector<std::string> ApiNames;
};
using IMPORT_TABLE_X86 = std::vector<IMPORT_DESCRIPTOR_X86>;
Простыми словами, нам нужны имена библиотек, также нужны имена функций для каждой библиотеки. Ну и в случае распаковки – нужны адреса FirstThunk.
VA, RVA и FileOffset
Внимательный читатель мог заметить такую конструкцию в вышеприведенном коде:
Код:
rvaOffset.OffsetToRva
Нам сейчас (для краткости) важно понимать, что RVA и FileOffset в общем случае представлены разными числовыми значениями – они неравны. Потому очень важно понимать, в каких случая подставлять RVA, а в каких – FileOffset.
Также при необходимости ручных вычислений может помочь всё тот же CFF Explorer:
Создание таблицы
Таблица импорта состоит из массива таких структур:
Код:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
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;
Здесь мы не затрагиваем Delay Import и Bound Import, потому что в упакованных файлах их просто нет, а если вы создаёте таблицу импорта с нуля, то их нет и подавно.
Важно помнить, что в конце массива должен идти полностью пустой элемент – IMAGE_IMPORT_DESCRIPTOR, заполненный нулями. Также есть два важных элемента структуры – OriginalFirstThunk и FirstThunk, которые мы уже неоднократно упоминали.
Если мы создаём эти элементы, то нам важно понимать, что они (OriginalFirstThunk и FirstThunk) указывают на полностью идентичные массивы адресов IMAGE_IMPORT_BY_NAME. Просто один из этих массивов модифицируется загрузчиком (загрузчик записывает туда адреса функций), а второй – нет. Понимая всё это, можем написать такой код:
Код:
void ImportCreator::MakeNewImportTable(const IMPORT_TABLE_X86 &Import,uint32_t RawAddressOfImport)
{
std::vector<IMAGE_IMPORT_DESCRIPTOR> NewDescriptors;
uint32_t AddressInPe = reinterpret_cast<uint32_t> ( PeFile.data() ) + RawAddressOfImport;
for (const auto& Descriptor:Import)
{
IMAGE_IMPORT_DESCRIPTOR DescriptorReflection = {0};
std::vector<IMAGE_THUNK_DATA32> Thunks = CreateImportThunks(Descriptor.ApiNames,AddressInPe);
uint32_t offset = GetSizeForImageImportByNameArray(Descriptor.ApiNames);
AddressInPe += offset;
uint32_t FirstThunkAddress = rvaOffset.OffsetToRva(AddressInPe -
reinterpret_cast<uint32_t> ( PeFile.data() ) );
DescriptorReflection.FirstThunk = FirstThunkAddress;
memcpy(reinterpret_cast<uint8_t*> (AddressInPe),Thunks.data(),Thunks.size() * sizeof(Thunks[0]));
AddressInPe += Thunks.size() * sizeof(Thunks[0]);
uint32_t OriginalFirstThunkAddress = rvaOffset.OffsetToRva(AddressInPe -
reinterpret_cast<uint32_t> ( PeFile.data() ) );
DescriptorReflection.OriginalFirstThunk = OriginalFirstThunkAddress;
memcpy(reinterpret_cast<uint8_t*> (AddressInPe),Thunks.data(),Thunks.size() * sizeof(Thunks[0]));
AddressInPe += Thunks.size() * sizeof(Thunks[0]);
uint32_t ModuleName = rvaOffset.OffsetToRva(AddressInPe -
reinterpret_cast<uint32_t> ( PeFile.data() ) );
DescriptorReflection.Name = ModuleName;
memcpy(reinterpret_cast<uint8_t*> (AddressInPe),Descriptor.LibraryName.data(),
Descriptor.LibraryName.length() + sizeof(char));
AddressInPe += Descriptor.LibraryName.length() + sizeof(char);
NewDescriptors.push_back(DescriptorReflection);
}
IMAGE_IMPORT_DESCRIPTOR LastEmptyDescriptor = {0};
NewDescriptors.push_back(LastEmptyDescriptor);
memcpy(reinterpret_cast<uint8_t*> (AddressInPe),NewDescriptors.data(),NewDescriptors.size() *
sizeof(NewDescriptors[0]));
uint32_t ImportTableRva = rvaOffset.OffsetToRva(AddressInPe -
reinterpret_cast<uint32_t> ( PeFile.data() ) );
PeUtils::Setx86ImportDirectoryAddress(PeFile,ImportTableRva);
}
https://vlmi.top/attachments/newimport-rar.12484/?temp_hash=9db6dcd6b20af2d225256add39ca5fe4
Здесь лежит сэмпл с новой таблицей импорта. Конечно, файл нерабочий, но до ЕР под отладчиком вполне себе доходит.