Гайд Lazy_importer - простой способ скрыть импорты и вызовы внутри программы

0Z0SK0

Участник
Автор темы
36
15
Lazy_importer
C++ (header only) библиотека для сокрытия импорта системных (и не только) DLL и вызовов из них.

Введение:
Для начала необходимо ответить на вопрос: Зачем скрывать импорты DLL и(или) вызовы из них?
Простой ответ: чтобы усложнить жизнь ревёрсеру ваших программ.

Более раскрытый ответ: чтобы запутать ревёрсера во время "вскрытия" вашей программы, лишив его информации об используемых системных (и не только) библиотеках, о вызывах из них. Данная информация играет одну из ключевых ролей в ревёрсе программы, примеры её использования различны, начиная от банальных хуков функций внутри сторонних длл, заканчивая использованием дыр в этих же библиотеках (например пустые пространства кода внутри сегментов длл, для последующего встраивания туда шелл-кода).



Преимущества:
  • Не выделяет виртуальную память для какой-либо функции (или при загрузке DLL).
  • В исполняемом виде представляет из себя достаточно простой asm-код.
  • Не оставляет defined strings при импорте/использовании функций библиотеки.
  • Легковстраиваемая в любой проект (представляет из себя одиночный hpp файл).
  • Интуитивно понятные названия inlined-функций.

Использование:
Вызов любой из функций осуществляется при помощи скрытого метода LI_FN, он динамичен, принимает аргументы и возвращает значения оригинальной функции.
Принцип работы заключается в извлечении оригинальной функции из библиотеки, с последующим встраиванием функции в непосредственный код программы, предварительно подвергнув её hash-morping`у. При традиционном использовании, функция импортируется из определенной библиотеки (указанной в PE-заголовках) и не находится напрямую в исполняемом коде.


C++:
// Использование скрытым методом, не отображается в разделе импортов PE-заголовков.
LI_FN(OutputDebugStringA)("string");

// Оригинальное использование, отобразится в любом дебаггере при изучении PE-заголовков,
// в разделе импорты.
OutputDebugStringA("string");

Лучше всего это демонстрируется при reverse-engineering`е исполняемого кода.

При традиционном способе всё отлично проявляется.
1677095224124.png



При использовании LI_FN, импортированная функция никак не отображается, и при исполнении представляет из себя примерно подобный код

Код:
for ( i = *(_QWORD **)(*(_QWORD *)(__readgsqword(0x60u) + 24) + 16i64); ; i = (_QWORD *)*i )
  {
    v1 = i[6];
    v2 = *(unsigned int *)(*(signed int *)(v1 + 60) + v1 + 136);
    v3 = (_DWORD *)(v2 + v1);
    if ( v2 + v1 != v1 )
    {
      LODWORD(v4) = v3[6];
      if ( (_DWORD)v4 )
        break;
    }
LABEL_8:
    ;
  }
  while ( 1 )
  {
    v4 = (unsigned int)(v4 - 1);
    v5 = -2128831035;
    v6 = (char *)(v1 + *(unsigned int *)((unsigned int)v3[8] + 4 * v4 + v1));
    v7 = *v6;
    v8 = (signed __int64)(v6 + 1);
    if ( v7 )
    {
      do
      {
        ++v8;
        v5 = 16777619 * (v5 ^ v7);
        v7 = *(_BYTE *)(v8 - 1);
      }
      while ( v7 );
      if ( v5 == -973690651 )
        break;
    }
    if ( !(_DWORD)v4 )
      goto LABEL_8;
  }
  ((void (__fastcall *)(const char *))(v1
                                     + *(unsigned int *)(v1
                                                       + (unsigned int)v3[7]
                                                       + 4i64 * *(unsigned __int16 *)(v1 + (unsigned int)v3[9] + 2 * v4))))("hello world");


LI_FN помимо обычного использования, позволяет различными способами оптимизировать ваш код и имеет для этого определенные суб-параметры.
C++:
// cached параметр вычисляет результат выполнения при первом использовании,
// и в дальнейшем возвращает сохраненное значение.
LI_FN(OutputDebugStringA)("string").cached()

// safe параметр позволяет безопасно использовать вызовы функций без unhandled exception,
// т.к при неверном выполнении функции, она попросту возвратит значение 0, не вызывая краш.
LI_FN(OutputDebugStringA)("string").safe()

// in параметр в отличие от других, позволяет принудительно указать источник функции,
// таким источник может выступать предварительно загруженный модуль.
// В данном примере, функция вызывается непосредственно из библиотеки Kernel32.DLL,
// тогда как без использования параметра in, программа попытается найти требуемую
// функцию в уже загруженных иных библиотеках.
LI_FN(OutputDebugStringA)("string").in("Kernel32.dll")

// Остальные параметры представляют из себя комбинацию описанных выше
LI_FN(OutputDebugStringA)("string").safe_cached()
LI_FN(OutputDebugStringA)("string").in_cached("Kernel32.dll")
LI_FN(OutputDebugStringA)("string").in_safe("Kernel32.dll")
LI_FN(OutputDebugStringA)("string").in_safe_cached("Kernel32.dll")

Но, если функция не найдена ни в одной из импортированных библиотек, что в этом случае?
Параметр in в таком случае так-же не поможет, вам необходимо предварительно загрузить требуемые библиотеки при помощи стандартных способов.
Если же не загрузить их, ваш исполняемый код вызовет краш и программа завершится.
C++:
// Предварительно загружаем требуемую нам библиотеки скрытым способом
LI_FN("LoadLibraryA")("Kernel32.DLL");

// Теперь, мы можем использовать любую из функций внутри этой библиотеки
LI_FN("OutputDebugStringA")("string").in(LI_MODULE("Kernel32.DLL").get())

При использовании LI_FN важно помнить о дефайнах/макросах функций.
Например OutputDebugString является обёрткой для двух различных функций - OutputDebugStringA и OutputDebugStringW в зависимости от используемой многобайтовой или иной кодировки.
При вызове дефайна OutputDebugString напрямую, ваш исполняемый код так-же вызовет краш.

В таком случае необходимо вызывать функцию напрямую, избегая различные обёртки.
C++:
// crash!!!
LI_FN(OutputDebugString)("string")
 
// всё ok!
LI_FN(OutputDebugStringA)("string")
 
// всё ok!
LI_FN(OutputDebugStringW)(L"string")

Не стоит забывать и о APISET-библиотеках, такой является Ole32.DLL. В данном случае необходимо вызывать функции из библиотеки combase.dll.
C++:
// crash!!!
// apiset библиотека
LI_FN(LoadLibraryA)("Ole32.DLL");
LI_FN(CoCreateGuid).in(LI_MODULE("Ole32.DLL").get())(&guid);

// всё ok!
LI_FN(LoadLibraryA)("combase.dll");
LI_FN(CoCreateGuid).in(LI_MODULE("combase.dll").get())(&guid);

Предирективы:
Lazy_Importer позволяет указать различные дефайны для эффективного и правильного использования своих фунций.

C++:
// позволяет импортировать библиотеки вне зависимости от регистра в их названии,
// без использования данной предирективы, следует соблюдать регистр, иначе последует краш.
#define LAZY_IMPORTER_CASE_INSENSITIVE

// заменяет get() на параметр safe()
#define LAZY_IMPORTER_CACHE_OPERATOR_PARENS

// добавляет более жесткие ограничения при поиске функции из импортированных библиотек.
#define LAZY_IMPORTER_HARDENED_MODULE_CHECKS


Библиотека: https://github.com/JustasMasiulis/lazy_importer
Автор этой блестящей библиотеки: JustasMasiulis и сообщество GitHub


C++:
// Загружаем библиотеку libcurl.dll
const char* libraryPath = (Core::getCurrentDirectory() + XorStr("\\libcurl.dll")).c_str();
HMODULE library = LI_FN(LoadLibraryA)(libraryPath);

FILE* fp;
long httpStatusCode = 0;

CURL* curl;

// Инициализация CURL.
curl = LI_FN(curl_easy_init).get()();

// Если CURL готов к работе.
if (curl)
{
    setDefaultCURLOptions(curl);

    // Открываем файл вывода.
    fp = fopen(out, XorStr("wb"));

    LI_FN(curl_easy_setopt).get()(curl, CURLOPT_URL, url);
    LI_FN(curl_easy_setopt).get()(curl, CURLOPT_WRITEFUNCTION, Core::curlWriteToFile);
    LI_FN(curl_easy_setopt).get()(curl, CURLOPT_WRITEDATA, fp);

    CURLcode result = LI_FN(curl_easy_perform).get()(curl);
    LI_FN(curl_easy_getinfo).get()(curl, CURLINFO_RESPONSE_CODE, &httpStatusCode);

    if (result == CURLE_OK && httpStatusCode == 200)
    {
        // Процедуры очистки и закрытия файла.
        LI_FN(curl_easy_cleanup).get()(curl);
        fclose(fp);

        return true;
    }
    else
    {
        Network::lastError = httpStatusCode;

        LI_FN(curl_easy_cleanup).get()(curl);
        fclose(fp);
        return false;
    }
}
LI_FN(curl_easy_cleanup).get()(curl);
 
Последнее редактирование:

EclipsedFlow

Известный
Проверенный
1,040
459
Не совсем понимаю о чем ты, и как это относится к теме.
Я так понимаю что если у тебя какая-то ошибка выскочит или краш на пустом месте и тебе будет нужно искать причину чтобы это исправить. Ты попросту её не найдешь так как runtime-debug -> press F или это будет очень сложно сделать.
 
  • Нравится
Реакции: AeSiK256

0Z0SK0

Участник
Автор темы
36
15
Я так понимаю что если у тебя какая-то ошибка выскочит или краш на пустом месте и тебе будет нужно искать причину чтобы это исправить. Ты попросту её не найдешь так как runtime-debug -> press F или это будет очень сложно сделать.
Для этого предусмотрены методы safe и nt_safe для LI_FN вызова, и тоже самое для LI_MODULE.
Возвращающие 0 при неправильном выполнении функции, минуя появление undefined behavior.

https://github.com/JustasMasiulis/lazy_importer/blob/master/include/lazy_importer.hpp:
struct safe_module_enumerator {
        using value_type = const detail::win::LDR_DATA_TABLE_ENTRY_T;
        value_type* value;
        value_type* head;

        LAZY_IMPORTER_FORCEINLINE safe_module_enumerator() noexcept
            : safe_module_enumerator(ldr_data_entry())
        {}

        LAZY_IMPORTER_FORCEINLINE
        safe_module_enumerator(const detail::win::LDR_DATA_TABLE_ENTRY_T* ldr) noexcept
            : value(ldr->load_order_next()), head(value)
        {}

        LAZY_IMPORTER_FORCEINLINE void reset() noexcept
        {
            value = head->load_order_next();
        }

        LAZY_IMPORTER_FORCEINLINE bool next() noexcept
        {
            value = value->load_order_next();

            return value != head && value->DllBase;
        }
    };
 

colby57

Активный
11
111
Более раскрытый ответ: чтобы запутать ревёрсера во время "вскрытия" вашей программы, лишив его информации об используемых системных (и не только) библиотеках, о вызывах из них.
Увы, но реверсеру данная библиотека ничем не мешает, более того, она оставляет важный паттерн за собой из-за которого можно отследить все дальнейшие вызовы, а именно - обращение к PEB.

Более подробно как находить все вызовы импортов от LazyImport можно прочитать у меня в статье -> https://yougame.biz/threads/247347/

Хорошим аналогом для этой библиотеки может послужить любой коммерческий протектор, тот же VMProtect или Code Virtualizer с лёгкой вм, которые не будут так открыто палить ваш вызов