
ExportHider: генерация таблицы экспорта во время выполнения для скрытия экспортируемых функций из DLL-файла.
ExportHider генерирует шаблон DLL на C++, содержащий заглушку кода, которая позволяет скрыть экспортируемые функции из каталога экспорта DLL на файловой системе. После добавления определений функций и компиляции файла вы не увидите скрытые экспортируемые функции через программы просмотра PE-файлов, такие как CFF Explorer. Однако, поскольку заглушка кода в шаблоне воссоздает каталог экспорта во время выполнения, легитимные вызовы GetProcAddress будут выполняться успешно. Этот метод работает только для динамической загрузки DLL или случаев использования пользовательского загрузчика DLL.
Обычно, когда вы хотите определить экспортируемую функцию в файлах DLL (на C или C++), вы просто ставите ключевое слово __declspec(dllexport) перед именем функции или создаете .def-файл. После компиляции компилятор создает специальную таблицу, называемую каталогом экспорта (Export Directory), для хранения информации, связанной с экспортируемыми функциями. Структура каталога экспорта показана ниже:
Когда процесс хочет использовать функцию из DLL-файла, загрузчик Windows (Windows Loader) просто анализирует эту структуру и импортирует запрошенные функции, используя массивы AddressOfFunctions, AddressOfNames и AddressOfNameOrdinals.
Конкретная процедура импорта функции подробно описана в статье блога ferreirasc, но кратко: для функции, импортируемой по имени, загрузчик перебирает массив AddressOfNames (значения этого массива — это просто RVA) и ищет заданное имя. Как только загрузчик находит совпадение на позиции "i", он обращается к i-му индексу массива AddressOfNameOrdinals и получает порядковый номер (ordinal), связанный с этой функцией. Имея порядковый номер, загрузчик обращается к массиву AddressOfFunctions по позиции значения порядкового номера, чтобы наконец получить RVA, связанный с импортируемой функцией.
Ключевой момент здесь заключается в том, что все эти операции поиска и доступа со стороны загрузчика Windows при вызове LoadLibrary выполняются после того, как DLL отображена в адресное пространство процесса. Во время отображения DLL весь файл DLL, включая его PE-заголовки, записывается в память, и загрузчик анализирует заголовки в памяти для доступа к каталогу экспорта. Это означает, что если сама DLL сможет перезаписать свои PE-заголовки в памяти, чтобы изменить адрес каталога экспорта (просто 0-й индекс DataDirectory) после присоединения к процессу, загрузчик сможет искать функции для импорта в произвольном каталоге экспорта, а не в том, который был добавлен компилятором.
__ _ _ _
/__\_ ___ __ ___ _ __| |_ /\ /(_) __| | ___ _ __
/_\ \ \/ / '_ \ / _ \| '__| __|/ /_/ / |/ _` |/ _ \ '__|
//__ > <| |_) | (_) | | | |_/ __ /| | (_| | __/ |
\__/ /_/\_\ .__/ \___/|_| \__\/ /_/ |_|\__,_|\___|_|
|_|
от @R0h1rr1m
Использование: C:\Users\Public\DLLDemo\ExportHider.exe:
-h | --help Показать справку.
-i | --input <Путь к входному файлу> Путь к файлу со списком имен функций, которые нужно скрыть. (Обязательно)
-o | --output <Путь к выходному файлу> Путь для сохранения шаблона DLL. (Обязательно)
-n | --name <Имя DLL> Имя DLL для каталога экспорта. (Обязательно)
-c | --count <Количество других функций> Количество других экспортируемых функций, которые не будут скрыты.
Что касается параметра -i | --input <Input Path>, вам нужно указать путь к входному файлу, в котором построчно хранятся имена функций для скрытия. Пример содержимого такого файла:
TestFunction1
TestFunction2
TestFunction3
Что касается параметра -c | --count, если вы не хотите скрывать все экспортируемые функции (т.е. есть некоторые функции, экспортированные с помощью __declspec(dllexport) или .def-файла, и они отображаются в каталоге экспорта DLL на файловой системе через программы просмотра PE-файлов), просто укажите, сколько их, используя этот параметр, так как инструменту нужна эта информация для вычислений в памяти.
Если вы хотите поэкспериментировать с техникой, есть два интересных момента, с которыми я столкнулся при разработке проекта. Возможно, вам нужно знать их перед изменением проекта:
GetProcAddress с именем. Из-за этого имена всех экспортируемых функций, включая скрытые, должны быть отсортированы в массиве AddressOfNames. В противном случае функция GetProcAddress возвращает NULL. Поэтому я использовал алгоритм пузырьковой сортировки для сортировки элементов этого массива.AddressOfNames, массив AddressOfFunctions, массив DataDirectory и некоторые другие поля требуют значений в относительных виртуальных адресах (RVA). Вдобавок к этому, они хранят эти значения RVA в полях размером DWORD. Когда вы выделяете область памяти для нового произвольного каталога экспорта с помощью функций динамического выделения памяти, таких как VirtualAlloc или HeapAlloc, полученные адреса будут далеки от отображенной области DLL, и значения RVA не помещаются в поля размером DWORD, что приводит к целочисленному переполнению. Поэтому я использовал глобальные переменные типа массива байтов в шаблоне DLL для удовлетворения потребностей в памяти.Моей первой целью при создании этого проекта было сгенерировать DLL, у которой отсутствуют экспорты (или полностью отсутствует таблица экспорта), но которая все равно успешно загружается вновь созданным процессом. Я думал, что такое поведение может предоставить новую среду для полезных нагрузок DLL sideloading. Однако я не смог найти ни функции, ни способа, при котором заглушка исправления каталога экспорта выполняется до того, как загрузчик Windows проверит экспортируемые функции DLL.
Более технически, я заметил следующий поток и вызов функций для каждой DLL в разделах кода, связанных с загрузчиком Windows в NTDLL (собрано из моих старых заметок; могут быть некоторые ошибки, так как я не эксперт по реверс-инжинирингу):
1. LdrpMapDll - Здесь DLL отображается в адресное пространство процесса. Все DLL, удовлетворяющие условию имени DLL,
напрямую помещаются в память, без предварительной проверки версии на файловой системе.
2. LdrpSnapModule - Здесь загрузчик Windows начинает разрешать импорты. Для каждого дескриптора импорта он анализирует
PE-структуру, проверяет таблицу экспорта, выполняет бинарный поиск импортируемой функции, вычисляет её RVA,
и записывает её адрес в соответствующую запись таблицы импорта (IAT) вызывающего процесса во время этой функции.
3. LdrpDoPostSnapWork - Если шаг 2 успешен для каждой импортируемой функции, в этой функции выполняются защита памяти,
инициализация TLS, включение CFG.
4. LdrpInitializeNode - Если шаг 3 успешен, на этом шаге выполняются функции связывания модулей.
5. LdrpCallTlsInitializers - Здесь вызываются обратные вызовы TLS перед функцией DllMain.
6. LdrpCallInitRoutine - Здесь впервые вызывается сам DllMain для импортированной DLL. В исходном
решении эта функция слишком поздна для исправления таблицы экспорта.
Когда вы запускаете исполняемый файл, который импортирует DLL, если загрузчик Windows не может найти требуемое имя функции в таблице экспорта DLL, он останавливает выполнение и не выполняет функции, которые выполняются после LdrpSnapModule.
Для случая DLL sideloading мы не можем модифицировать вызывающий процесс; таким образом, единственная возможность динамически исправить таблицу экспорта — найти возможность выполнения кода между функциями LdrpMapDll и LdrpSnapModule, потому что загрузчик немедленно останавливается во время проверок функции LdrpSnapModule. Я пробовал обратные вызовы TLS, загрузку второй DLL, перенаправленные экспорты и другие обходные пути, но ни один из них не помог мне найти такое место, поэтому, к сожалению, этот метод не работает напрямую для DLL sideloading или статически импортированных DLL. Если вы найдете решение или жизнеспособный обходной путь для этой проблемы, я буду очень рад изучить его — будь то обсуждение идеи, совместное обдумывание подхода или реализация. Любой вклад в этом направлении будет высоко оценен.
Один из возможных способов использования этого метода для случаев DLL sideloading — разделить всю работу на две DLL: прокси-библиотека (Proxy DLL) и полезная нагрузка (Payload DLL). Прокси-библиотека принимает имя, ожидаемое EXE, и имеет видимые экспорты, удовлетворяющие статической проверке импорта загрузчика, в то время как полезная нагрузка содержит реальную скрытую функциональность, имеет отсутствующие экспорты и восстанавливает свою таблицу экспорта в DllMain. Я не считаю это хорошим обходным путем для этой проблемы, поэтому не реализовал его.
Только для авторизованного тестирования безопасности. Неправомерное использование этого инструмента против систем без явного разрешения незаконно.