
PoC-проект для внедрения shellcode в Hint/Name Table
HintInject — это инструмент для внедрения и загрузки шелл-кода, который я разработал, экспериментируя со структурами PE-файла и таблицы импорта (Import Directory Table). Он принимает сырой шелл-код и разбивает его на фрагменты, помещая их в записи Hint/Name Table, доступные через Import Lookup Table поддельной записи импортируемой DLL в исполняемом файле-загрузчике. Затем загрузчик объединяет эти фрагменты для выполнения шелл-кода. Не знаю, является ли эта техника новой, но я решил поделиться ей ради интереса.
Прежде чем объяснить, как работает HintInject, я хочу кратко описать процесс загрузки DLL, ссылаясь на пост на Stack Overflow.
Чтобы получить информацию о требуемых DLL и функциях/импортах, нужно сначала обратиться к Import Directory Table. Import Directory Table — это таблица записей, по одной для каждой импортируемой DLL. Эти записи содержат указатель на имя импортируемой DLL, указатель на Import Lookup Table, указатель на Import Address Table и другие поля для различной информации.
Проще говоря, Import Lookup Table ведёт к информации об импортах, а Import Address Table — к адресам импортов. Однако, когда исполняемый файл находится на диске или непосредственно перед процессом загрузки DLL, Import Address Table идентична Import Lookup Table. Содержимое Import Address Table перезаписывается адресами импортов во время процесса загрузки DLL.
Не вдаваясь в подробности, взглянем на диаграмму ниже, чтобы увидеть взаимосвязь между этими тремя таблицами.
Если углубиться, то Import Lookup Table не хранит имена импортов напрямую. Для функций, импортируемых по имени, он содержит RVA записей Hint/Name Table. Эти записи хранят имена функций в виде строк ASCII, завершающихся нулём. Поэтому определение структуры записи Hint/Name Table выглядит следующим образом:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Поле Hint здесь — это фактически индекс в таблицу указателей на имена экспорта DLL. Оно используется для ускорения поиска позиции этого импорта. Таким образом, мы можем обобщить взаимосвязь между Import Lookup Table и Hint/Name Table с помощью диаграммы ниже.
В итоге, чтобы найти DLL и её импорты, нужно выполнить следующие шаги:
Недавно, изучая технику ImportDLLInjection, опубликованную x86matthew, я узнал, как добавить поддельную запись в Import Directory DLL, загруженных в память. После этого я захотел разработать небольшой проект, который добавляет поддельную запись напрямую в таблицу импорта бинарного файла на диске — и ради интереса, и для освежения знаний. Просматривая процесс загрузки DLL для этого проекта, я обратил внимание на поле Hint в записях Hint/Name Table, используемых в этом процессе.
Согласно документации MSDN, я узнал, что это поле используется Windows Loader для нахождения адреса данного импорта (импортируемого по имени) непосредственно из таблицы имён экспорта DLL, в которой он находится. Однако в том же документе говорилось, что если функцию не удаётся найти с помощью этого поля, она ищется бинарным поиском в таблице имён экспорта DLL. Основываясь на этой фразе, я решил, что указание неверного значения для этого поля не нарушит процесс загрузки DLL.
Как я упомянул выше, для каждой импортируемой функции существует запись в Hint/Name Table. Другими словами, для каждого импорта у нас есть 2 байта для использования. Объединив несколько импортов, можно получить достаточно полей Hint для хранения вредоносного шелл-кода. В результате можно использовать загрузочный бинарный файл, чтобы встроить шелл-код в записи Hint/Name поддельной записи импортируемой DLL. Этот загрузочный файл может получить доступ к этим записям, чтобы объединить шелл-код для выполнения во время выполнения.
HintInject можно использовать для создания такого загрузчика, который хранит шелл-код в своей таблице Hint/Name. Сначала он создаёт новый раздел с именем .rrdata и копирует текущий каталог импорта в этот раздел. После этого он добавляет новую поддельную запись, импорты которой будут использоваться для хранения входного шелл-кода. Оставшиеся байты раздела используются для хранения Import Lookup Table, Import Address Table, имени DLL и Hint/Name Table новой поддельной записи. На последнем этапе HintInject использует поля Hint импортов для размещения фрагментов входного шелл-кода.
Пример импортов загрузочного бинарного файла:
HintInject.exe с вашим шелл-кодом в сыром формате и путём к выходному файлу, чтобы создать загрузчик шелл-кода: HintInject.exe <Shellcode File> <Output Name>Loader.exe <PID>InjectShellcode в файле HintInjectLoader/Main.cpp.Протестировано на Windows 10 19044 с Visual Studio 2019:
dllNames в файле DllNamesForFakeImports.h.dllNames в файле DllNamesForFakeImports.h. По умолчанию HintInject будет выбирать функции из DLL в соответствии с порядком массива. Другими словами, инструмент сначала выбирает экспортируемые функции из user32.dll; если размер шелл-кода превышает 2 * количество экспортов user32.dll, он также начинает использовать экспорты advapi32.dll и других.static LPCSTR dllNames[] = {"user32.dll","advapi32.dll","gdi32.dll","wininet.dll","comctl32.dll","shell32.dll","wsock32.dll","oleaut32.dll","ws2_32.dll","urlmon.dll"};
Только для авторизованного тестирования безопасности. Несанкционированное использование этого инструмента против систем без явного разрешения является незаконным.