Уроки Разработка вирусов-26. Изучаем кунгфу-1. Скрытие таблицы импорта


X-Shar

:)
Администрация
Регистрация
03.06.2012
Сообщения
6 142
Репутация
8 268
1696070496950.png


Таблица импортных адресов (IAT) содержит информацию о файле PE, такую как используемые функции и DLL, экспортирующие их. Этот тип информации может быть использован для создания сигнатуры и обнаружения двоичного кода.

Например, изображение ниже показывает таблицу импортных адресов примера инъекции шелл-кода.
Файл PE импортирует функции, которые считаются высоко подозрительными. Решения безопасности могут затем использовать эту информацию для детекта.

1696070440518.png


Сокрытие и обфускация IAT - Метод 1

Чтобы скрыть функции от IAT, можно использовать GetProcAddress, GetModuleHandle или LoadLibrary для динамической загрузки этих функций во время выполнения. Приведенный ниже фрагмент загрузит VirtualAllocEx динамически, и поэтому он не появится в IAT при проверке.

C:
typedef LPVOID (WINAPI* fnVirtualAllocEx)(HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);

//...
fnVirtualAllocEx pVirtualAllocEx = GetProcAddress(GetModuleHandleA("KERNEL32.DLL"), "VirtualAllocEx");
pVirtualAllocEx(...);

Хотя это может показаться элегантным решением, по ряду причин это не самый лучший способ:

Во-первых, строка VirtualAllocEx существует в двоичном коде, что может быть использовано для обнаружения использования функции.
GetProcAddress и GetModuleHandleA появятся в IAT, что само по себе используется как сигнатура.

Сокрытие и обфускация IAT - Метод 2

Более элегантным решением является создание собственных функций, которые выполняют те же действия, что и GetProcAddress и GetModuleHandle WinAPIs.
Таким образом, становится возможным динамически загружать функции без появления этих двух функций в IAT.

Функция WinAPI GetProcAddress извлекает адрес экспортированной функции из указанного дескриптора модуля. Функция возвращает NULL, если имя функции не найдено в указанном дескрипторе модуля.

В этой части будет реализована функция, заменяющая GetProcAddress. Прототип новой функции показан ниже.

C:
FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {}

Как работает GetProcAddress

Первый момент, который необходимо учесть, - это то, как адрес функции находится и извлекается с помощью WinAPI GetProcAddress.

Параметр hModule - это базовый адрес загруженной DLL. Это адрес, где модуль DLL находится в адресном пространстве процесса. Учитывая это, адрес функции находится путем перебора экспортированных функций в предоставленной DLL и проверки существования имени целевой функции. Если найдено соответствие, извлеките адрес.

Для доступа к экспортированным функциям необходимо получить доступ к таблице экспорта DLL и просмотреть ее в поисках имени целевой функции.

Таблица экспорта - это структура, определенная как IMAGE_EXPORT_DIRECTORY.

C:
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA от базы изображения
    DWORD   AddressOfNames;         // RVA от базы изображения
    DWORD   AddressOfNameOrdinals;  // RVA от базы изображения
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Для нас актуальны последние три элемента.

AddressOfFunctions - указывает адрес массива адресов экспортированных функций.
AddressOfNames - указывает адрес массива адресов имен экспортированных функций.
AddressOfNameOrdinals - указывает адрес массива порядковых номеров для экспортированных функций.

Как получить каталог экспорта, IMAGE_EXPORT_DIRECTORY

Пример кода:

C:
FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {

    // Делаем это, чтобы избежать приведения каждый раз при использовании 'hModule'
    PBYTE pBase = (PBYTE) hModule;

    // Получаем заголовок DOS и проверяем подпись
    PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
    if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
        return NULL;

    // Получаем заголовки NT и проверяем подпись
    PIMAGE_NT_HEADERS    pImgNtHdrs    = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
    if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return NULL;

    // Получаем необязательный заголовок
    IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;

    // Получаем таблицу экспорта изображения
    // Это каталог экспорта
    PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY) (pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    // ...
}

Доступ к экспортированным функциям

После получения указателя на структуру IMAGE_EXPORT_DIRECTORY можно просмотреть экспортированные функции. Член NumberOfFunctions указывает количество функций, экспортированных hModule. В результате максимальное количество итераций цикла должно соответствовать NumberOfFunctions.

C:
for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){
  // Поиск целевой экспортированной функции
}

Построение логики поиска

Следующим шагом является создание логики поиска для функций. Построение логики поиска требует использования AddressOfFunctions, AddressOfNames и AddressOfNameOrdinals, которые являются массивами, содержащими RVA, ссылающиеся на уникальную функцию в таблице экспорта.

C:
typedef struct _IMAGE_EXPORT_DIRECTORY {
    // ...
    // ...
    DWORD   AddressOfFunctions;     // RVA от базы изображения
    DWORD   AddressOfNames;         // RVA от базы изображения
    DWORD   AddressOfNameOrdinals;  // RVA от базы изображения
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Поскольку эти элементы являются RVA, к базовому адресу модуля, pBase, должен быть добавлен VA. Первые два фрагмента кода должны быть простыми. Они извлекают имя функции и адрес функции соответственно. Третий фрагмент извлекает порядковый номер функции, который объясняется подробно в следующем разделе.

Код:
// Получение указателя на массив имен функций
PDWORD FunctionNameArray     = (PDWORD)(pBase + pImgExportDir->AddressOfNames);

// Получение указателя на массив адресов функций
PDWORD FunctionAddressArray     = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);

// Получение указателя на массив порядковых номеров функции
PWORD  FunctionOrdinalArray     = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

Понимание порядковых номеров

Порядковый номер функции - это целочисленное значение, представляющее позицию функции в таблице экспортированных функций в DLL. Таблица экспорта организована в виде списка (массива) указателей на функции, каждой функции присваивается порядковое значение на основе ее положения в таблице.

1696071493725.png


Важно отметить, что порядковое значение используется для идентификации адреса функции, а не ее имени. Таблица экспорта работает таким образом, чтобы обрабатывать случаи, когда имя функции недоступно или не уникально. Кроме того, получение адреса функции с помощью ее порядкового номера происходит быстрее, чем с помощью ее имени. По этой причине операционная система использует порядковый номер для извлечения адреса функции.

Например, адрес VirtualAlloc равен FunctionAddressArray[порядковый номер VirtualAlloc], где FunctionAddressArray - это указатель массива адресов функции, извлеченный из таблицы экспорта.

Учитывая это, следующий фрагмент кода выведет порядковое значение каждой функции в массиве функций указанного модуля.

C:
// Получение указателя на массив имен функций
PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);

// Получение указателя на массив адресов функций
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);

// Получение указателя на массив порядковых номеров функции
PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

// Цикл по всем экспортированным функциям
for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){

    // Получение имени функции
    CHAR* pFunctionName        = (CHAR*)(pBase + FunctionNameArray[i]);

    // Получение порядкового номера функции
    WORD wFunctionOrdinal = FunctionOrdinalArray[i];

    // Вывод
    printf("[ %0.4d ] NAME: %s -\t ORDINAL: %d\n", i, pFunctionName, wFunctionOrdinal);
}

Частичная демонстрация GetProcAddressReplacement

Хотя GetProcAddressReplacement еще не завершена, теперь она должна выводить имена функций и их соответствующие порядковые номера.
Чтобы проверить, что было построено до сих пор, вызовите функцию с следующими параметрами:
C:
GetProcAddressReplacement(GetModuleHandleA("ntdll.dll"), NULL);

1696071820499.png


Как ожидалось, имя функции и порядковый номер функции выводятся на консоль.

Порядковый номер к адресу

С помощью порядкового номера функции можно получить адрес функции.

C:
// Получение указателя на массив имен функций
PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);

// Получение указателя на массив адресов функций
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);

// Получение указателя на массив порядковых номеров функции
PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

// Цикл по всем экспортированным функциям
for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){

    // Получение имени функции
    CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);

    // Получение порядкового номера функции
    WORD wFunctionOrdinal = FunctionOrdinalArray[i];

    // Получение адреса функции через ее порядковый номер
    PVOID pFunctionAddress = (PVOID)(pBase + FunctionAddressArray[wFunctionOrdinal]);

    printf("[ %0.4d ] NAME: %s -\t ADDRESS: 0x%p  -\t ORDINAL: %d\n", i, pFunctionName, pFunctionAddress, wFunctionOrdinal);
}

Для проверки функциональности откройте notepad.exe с помощью xdbg и проверьте экспорт ntdll.dll.

1696071694137.png


Изображение выше показывает, что адрес A_SHAUpdate равен 0x00007FFD384D2D10 как в xdbg, так и с использованием функции GetProcAddressReplacement. Однако обратите внимание, что порядковые номера для функции отличаются из-за того, что загрузчик Windows генерирует новый массив порядковых номеров для каждого процесса.

Код GetProcAddressReplacement

Последний фрагмент кода, необходимый для завершения функции, - это способ сравнения экспортированных имен функций с целевым именем функции, lpApiName. Это легко сделать с помощью strcmp. Затем, наконец, верните адрес функции при совпадении.

C:
FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {

    // Делаем это, чтобы избежать приведения каждый раз при использовании 'hModule'
    PBYTE pBase = (PBYTE)hModule;

    // Получение заголовка DOS и проверка подписи
    PIMAGE_DOS_HEADER    pImgDosHdr        = (PIMAGE_DOS_HEADER)pBase;
    if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
        return NULL;

    // Получение заголовков NT и проверка подписи
    PIMAGE_NT_HEADERS    pImgNtHdrs        = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
    if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return NULL;

    // Получение необязательного заголовка
    IMAGE_OPTIONAL_HEADER    ImgOptHdr    = pImgNtHdrs->OptionalHeader;

    // Получение таблицы экспорта изображения
    PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY) (pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    // Получение указателя на массив имен функций
    PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);

    // Получение указателя на массив адресов функций
    PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);

    // Получение указателя на массив порядковых номеров функции
    PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

    // Цикл по всем экспортированным функциям
    for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){

        // Получение имени функции
        CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);

        // Получение адреса функции через его порядковый номер
        PVOID pFunctionAddress    = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);

        // Поиск указанной функции
        if (strcmp(lpApiName, pFunctionName) == 0){
            printf("[ %0.4d ] FOUND API -\t NAME: %s -\t ADDRESS: 0x%p  -\t ORDINAL: %d\n", i, pFunctionName, pFunctionAddress, FunctionOrdinalArray[i]);
            return pFunctionAddress;
        }
    }

    return NULL;
}

Финальная демонстрация GetProcAddressReplacement

Изображение ниже показывает результаты обеих функций GetProcAddress и GetProcAddressReplacement, ищущих адрес NtAllocateVirtualMemory. Как ожидалось, оба результата указывают на правильный адрес функции, и поэтому успешно была создана пользовательская реализация GetProcAddress.

1696071938088.png


Теперь давайте попробуем реализовать функцию GetModuleHandle

Функция GetModuleHandle извлекает дескриптор для указанной DLL. Функция возвращает дескриптор к DLL или NULL, если DLL не существует в вызывающем процессе.

В этом модуле будет реализована функция, которая заменит GetModuleHandle.

Прототип новой функции показан ниже.

C:
HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName){}

Как работает GetModuleHandle

Тип данных HMODULE - это базовый адрес загруженной DLL, который указывает, где DLL расположена в адресном пространстве процесса. Следовательно, цель функции-заменителя - извлечь базовый адрес указанной DLL.

Блок окружения процесса (PEB) содержит информацию о загруженных DLL, в частности, член PEB_LDR_DATA Ldr структуры PEB.

Таким образом, начальный шаг - получить доступ к этому члену через структуру PEB.

PEB в 64-битных системах

Напомним, что указатель на структуру PEB находится в структуре блока окружения потока (TEB).

1696072271078.png


В 64-битных системах смещение к указателю структуры TEB хранится в регистре GS. Следующее изображение из x64dbg.

1696072250157.png


Метод 1: Извлечение PEB в 64-битных системах

Существуют два разных способа извлечения PEB. Первый метод включает в себя извлечение структуры TEB, а затем получение указателя на PEB. Этот подход можно выполнить с помощью макроса __readgsqword(0x30) в Visual Studio, который считывает 0x30 байт из регистра GS, чтобы достичь указателя на структуру TEB.

C:
// Метод 1
PTEB pTeb = (PTEB)__readgsqword(0x30);
PPEB pPeb = (PPEB)pTeb->ProcessEnvironmentBlock;

Метод 2: Извлечение PEB в 64-битных системах

Следующий метод извлекает структуру PEB напрямую, пропуская структуру TEB, используя макрос __readgsqword(0x60) в Visual Studio, который считывает 0x60 байт из регистра GS.

C:
// Метод 2
PPEB pPeb2 = (PPEB)(__readgsqword(0x60));

Это можно сделать, потому что элемент ProcessEnvironmentBlock находится на 0x60 (hex) или 96 байтах от начала структуры TEB.

Метод 1: Извлечение PEB в 32-битных системах.

Так же, как и в 64-битных системах, существуют два способа извлечения PEB.

Первый метод включает в себя получение структуры TEB, а затем получение структуры PEB с помощью макроса __readfsdword(0x18) в Visual Studio, который считывает 0x18 байт из регистра FS.

C:
// Метод 1
PTEB pTeb = (PTEB)__readfsdword(0x18);
PPEB pPeb = (PPEB)pTeb->ProcessEnvironmentBlock;

Метод 2: Извлечение PEB в 32-битных системах

Второй метод получает PEB напрямую, пропуская структуру TEB, используя макрос __readfsdword(0x30) в Visual Studio, который считывает 0x30 байт из регистра FS.

Код:
// Метод 2
PPEB pPeb2 = (PPEB)(__readfsdword(0x30));

0x30 (hex) это 48 байт, которые являются смещением элемента ProcessEnvironmentBlock от 32-битной структуры TEB. Тип данных PVOID составляет 4 байта в 32-битных системах.

Перечисление DLL

После извлечения структуры PEB следующий шаг - получить доступ к члену PEB_LDR_DATA Ldr. Напомним, что этот член содержит информацию о загруженных DLL в процессе.

Структура PEB_LDR_DATA

Структура PEB_LDR_DATA показана ниже. Важным членом этой структуры является LIST_ENTRY InMemoryOrderModuleList.

C:
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

Структура LIST_ENTRY
Структура LIST_ENTRY, показанная ниже, представляет собой двусвязный список, который по сути аналогичен массивам, но упрощает доступ к соседним элементам.

C:
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

Двусвязные списки используют элементы Flink и Blink в качестве указателей на начало и конец списка соответственно. Это означает, что Flink указывает на следующий узел в списке, тогда как элемент Blink указывает на предыдущий узел в списке. Эти указатели используются для перемещения по связанному списку в обоих направлениях. Зная это, чтобы начать перечисление этого списка, следует начать с доступа к его первому элементу, InMemoryOrderModuleList.Flink.

Согласно определению Microsoft для члена InMemoryOrderModuleList, указывается, что каждый элемент в списке является указателем на структуру LDR_DATA_TABLE_ENTRY.

Структура LDR_DATA_TABLE_ENTRY

Структура LDR_DATA_TABLE_ENTRY представляет собой DLL внутри связанного списка загруженных DLL для процесса. Каждый LDR_DATA_TABLE_ENTRY представляет собой уникальную DLL.

C:
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks; // двусвязный список, содержащий порядок загрузки модулей в память
PVOID Reserved2[2];
PVOID DllBase;
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName; // структура 'UNICODE_STRING', содержащая имя файла загруженного модуля
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

Логика реализации

На основе всего вышеупомянутого требуются следующие действия:

1. Извлечь PEB
2. Извлечь член Ldr из PEB
3. Извлечь первый элемент в связанном списке

Код:
HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName) {

// Получение peb
#ifdef _WIN64 // если компиляция как x64
PPEB pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32 // если компиляция как x32
PPEB pPeb = (PEB*)(__readfsdword(0x30));
#endif// Получение Ldr
PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->Ldr);

// Получение первого элемента в связанном списке, который содержит информацию о первом модуле
PLDR_DATA_TABLE_ENTRY    pDte    = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
}

Поскольку каждый pDte представляет собой уникальную DLL внутри связанного списка, возможно перейти к следующему элементу с помощью следующей строки кода:

C:
pDte = (PLDR_DATA_TABLE_ENTRY)(pDte);

Эта строка кода может показаться сложной, но все, что она делает - это разыменовывает значение, хранящееся по адресу, на который указывает pDte, а затем приводит результат к указателю на структуру PLDR_DATA_TABLE_ENTRY. Это просто так работают связанные списки, что-то вроде следующего изображения.

1696073063046.png


Перечисление DLL - код
Приведенный ниже фрагмент кода извлекает имя DLL, уже загруженных в вызывающий процесс. Функция ищет целевой модуль, szModuleName. Если совпадение найдено, функция возвращает дескриптор к DLL (HMODULE), в противном случае - возвращает NULL.

C:
MODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName) {

// Получение PEB
#ifdef _WIN64 // если компиляция как x64
PPEB pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32 // если компиляция как x32
PPEB pPeb = (PEB*)(__readfsdword(0x30));
#endif// Получение Ldr
PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->Ldr);

// Получение первого элемента в связанном списке, который содержит информацию о первом модуле
PLDR_DATA_TABLE_ENTRY    pDte    = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);

while (pDte) {
    // Если не null
    if (pDte->FullDllName.Length != NULL) {
           // Вывод имени DLL
        wprintf(L"[i] \"%s\" \n", pDte->FullDllName.Buffer);
    }
    else {
        break;
    }
    // Следующий элемент в связанном списке
    pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
}

return NULL;
}

1696073091272.png


Чувствительность имен DLL к регистру

Изучив вывод, можно легко заметить, что некоторые имена DLL написаны заглавными буквами, а некоторые - нет, что влияет на возможность получения базового адреса DLL (HMODULE). Например, если кто-то ищет DLL KERNEL32.DLL и передает Kernel32.DLL, функция wcscmp будет рассматривать обе строки как разные.

Чтобы устранить эту проблему, была создана вспомогательная функция IsStringEqual, которая принимает две строки, преобразует их в представление в нижнем регистре, а затем сравнивает их в этом состоянии. Она возвращает true, если обе строки равны, и false в противном случае.

Код:
BOOL IsStringEqual (IN LPCWSTR Str1, IN LPCWSTR Str2) {

WCHAR   lStr1    [MAX_PATH],
        lStr2    [MAX_PATH];

int        len1    = lstrlenW(Str1),
        len2    = lstrlenW(Str2);

int        i        = 0,
        j        = 0;

// Проверка длины. Не хотим переполнить буферы
if (len1 >= MAX_PATH || len2 >= MAX_PATH)
    return FALSE;

// Преобразование Str1 в строку в нижнем регистре (lStr1)
for (i = 0; i < len1; i++){
    lStr1[i] = (WCHAR)tolower(Str1[i]);
}
lStr1[i++] = L'\0'; // завершение null

// Преобразование Str2 в строку в нижнем регистре (lStr2)
for (j = 0; j < len2; j++) {
    lStr2[j] = (WCHAR)tolower(Str2[j]);
}
lStr2[j++] = L'\0'; // завершение null

// Сравнение строк в нижнем регистре
if (lstrcmpiW(lStr1, lStr2) == 0)
    return TRUE;

return FALSE;
}

Базовый адрес DLL

Получение базового адреса DLL требует ссылки на структуру LDR_DATA_TABLE_ENTRY. К сожалению, в официальной документации Microsoft отсутствуют большие фрагменты структуры. Поэтому, чтобы лучше понять структуру, Результаты для структуры можно найти

C:
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
WORD LoadCount;
WORD TlsIndex;
union {
LIST_ENTRY HashLinks;
struct {
PVOID SectionPointer;
ULONG CheckSum;
};
};
union {
ULONG TimeDateStamp;
PVOID LoadedImports;
};
PACTIVATION_CONTEXT EntryPointActivationContext;
PVOID PatchInformation;
LIST_ENTRY ForwarderLinks;
LIST_ENTRY ServiceTagLinks;
LIST_ENTRY StaticLinks;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;

Базовый адрес DLL - это InInitializationOrderLinks.Flink, хотя имя этого не предполагает, но, к сожалению, Microsoft любит сбивать с толку людей. Сравнив этот член с официальной документацией Microsoft по LDR_DATA_TABLE_ENTRY, можно увидеть, что базовый адрес DLL - это зарезервированный элемент (Reserved2[0]).

С этим в виду, функция-заменитель GetModuleHandle может быть завершена.

Функция-заменитель GetModuleHandle

GetModuleHandleReplacement - это функция, которая заменяет GetModuleHandle. Она будет искать заданное имя DLL, и если оно загружено процессом, возвращает дескриптор к DLL.

C:
HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName) {

// Получение PEB
#ifdef _WIN64 // если компиляция как x64
PPEB pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32 // если компиляция как x32
PPEB pPeb = (PEB*)(__readfsdword(0x30));
#endif// Получение Ldr
PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->Ldr);
// Получение первого элемента в связанном списке (содержит информацию о первом модуле)
PLDR_DATA_TABLE_ENTRY pDte = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);

while (pDte) {

    // Если не null
    if (pDte->FullDllName.Length != NULL) {

        // Проверка на равенство
        if (IsStringEqual(pDte->FullDllName.Buffer, szModuleName)) {
            wprintf(L"[+] Найдена Dll \"%s\" \n", pDte->FullDllName.Buffer);
#ifdef LDR_DATA_TABLE_ENTRY return (HMODULE)(pDte->InInitializationOrderLinks.Flink);
#else return (HMODULE)pDte->Reserved2[0];
#endif // STRUCTS}

        // wprintf(L"[i] \"%s\" \n", pDte->FullDllName.Buffer);
    }
    else {
        break;
    }

    // Следующий элемент в связанном списке
    pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);

}

return NULL;
}

Одна часть кода, которая не была объяснена, показана ниже. Эта часть кода определяет, используется ли версия структуры LDR_DATA_TABLE_ENTRY от Microsoft или та, что из структур ядра Windows Vista. В зависимости от того, какая из них была использована, имя члена меняется.

Код:
#ifdef LDR_DATA_TABLE_ENTRY return (HMODULE)(pDte->InInitializationOrderLinks.Flink);
#else return (HMODULE)pDte->Reserved2[0];
#endif // STRUCTS

Демо

1696074752719.png



Вот мы написали две функции GetProcAddressReplacement и GetModuleHandleReplacement, которые заменяли GetProcAddress и GetModuleHandle. Этого достаточно для выполнения Run-Time Dynamic Linking, что скрывает импортированные функции от IAT. Однако строки, используемые в коде, показывают, какие функции используются. Например, строка ниже использует функции для извлечения VirtualAllocEx.

C:
GetProcAddressReplacement(GetModuleHandleReplacement("ntdll.dll"),"VirtualAllocEx")

Решения безопасности легко могут извлекать строки из скомпилированного двоичного файла и распознавать, что используется VirtualAllocEx. Чтобы решить эту проблему, к обеим функциям GetProcAddressReplacement и GetModuleHandleReplacement будет применен алгоритм хэширования строк. Вместо выполнения строковых сравнений для получения указанного базового адреса модуля или адреса функции, функции будут работать с хеш-значениями.

Реализация JenkinsOneAtATime32Bit

Функции GetProcAddressReplacement и GetModuleHandleReplacement в этом модуле переименованы в GetProcAddressH и GetModuleHandleH соответственно. Эти обновленные функции используют алгоритм хеширования строк Jenkins One At A Time для замены имени функции и модуля на хеш-значение, которое их представляет. Напомним, что этот алгоритм был использован через функцию JenkinsOneAtATime32Bit, которая была представлена в уроке скрытия строк.

Хеширование строк

Чтобы использовать функции, показанные в этой статье, необходимо получить хеш-значение имени модуля (например, User32.dll) и хеш-значение имени функции (например, MessageBoxA). Это можно сделать, сначала выводя хешированные значения на консоль. Убедитесь, что алгоритм хеширования использует ту же начальную точку.

C:
int main(){
printf("[i] Хеш "%s" : 0x%0.8X \n", "USER32.DLL", HASHA("USER32.DLL")); // Имя модуля в верхнем регистре
printf("[i] Хеш "%s" : 0x%0.8X \n", "MessageBoxA", HASHA("MessageBoxA"));

return 0;
}

Вышеуказанная основная функция выведет следующее:

Хеш "USER32.DLL" : 0x81E3778E
Хеш "MessageBoxA" : 0xF10E27CA

Эти хеш-значения теперь можно использовать с функциями ниже.

Использование

Функции будут использоваться таким же образом, за исключением того, что теперь передается хеш-значение, а не строковое значение.

C:
// 0x81E3778E - это хеш USER32.DLL
// 0xF10E27CA - это хеш MessageBoxA
fnMessageBoxA pMessageBoxA = GetProcAddressH(GetModuleHandleH(0x81E3778E),0xF10E27CA);

Функция GetProcAddressH

GetProcAddressH - это функция, эквивалентная GetProcAddressReplacement, основное отличие которой заключается в том, что хеш-значения алгоритма хеширования строк JenkinsOneAtATime32Bit используются для сравнения экспортируемых имен функций с входным хешем.

Также стоит отметить, что код использует два макроса, чтобы сделать код более чистым и легким для обновления в будущем.

HASHA - Вызов HashStringJenkinsOneAtATime32BitA (ASCII)
HASHW - Вызов HashStringJenkinsOneAtATime32BitW (UNICODE)

#define HASHA(API) (HashStringJenkinsOneAtATime32BitA((PCHAR) API))
#define HASHW(API) (HashStringJenkinsOneAtATime32BitW((PWCHAR) API))

Исходя из этого, GetProcAddressH показан ниже.

Функция принимает два параметра:

hModule - дескриптор модуля DLL, содержащего функцию.
dwApiNameHash - хеш-значение имени функции, чтобы получить ее адрес.

C:
FARPROC GetProcAddressH(HMODULE hModule, DWORD dwApiNameHash) {

if (hModule == NULL || dwApiNameHash == NULL)
    return NULL;

PBYTE pBase = (PBYTE)hModule;

PIMAGE_DOS_HEADER         pImgDosHdr              = (PIMAGE_DOS_HEADER)pBase;
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
    return NULL;

PIMAGE_NT_HEADERS         pImgNtHdrs              = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
    return NULL;

IMAGE_OPTIONAL_HEADER     ImgOptHdr              = pImgNtHdrs->OptionalHeader;

PIMAGE_EXPORT_DIRECTORY   pImgExportDir          = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

PDWORD  FunctionNameArray    = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
PDWORD  FunctionAddressArray    = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
PWORD   FunctionOrdinalArray    = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++) {
    CHAR*    pFunctionName       = (CHAR*)(pBase + FunctionNameArray[i]);
    PVOID    pFunctionAddress    = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);

    // Хеширование каждого имени функции pFunctionName
    // Если оба хеша равны, значит, мы нашли нужную функцию
    if (dwApiNameHash == HASHA(pFunctionName)) {
        return pFunctionAddress;
    }
}

return NULL;
}

GetModuleHandleH

Функция GetModuleHandleH такая же, как и GetModuleHandleReplacement, основное отличие состоит в том, что хеш-значения алгоритма хеширования строк JenkinsOneAtATime32Bit будут использоваться для сравнения перечисленных имен DLL с входным хешем.

Обратите внимание на то, как функция преобразует строку в FullDllName.Buffer в верхний регистр, следовательно, параметр dwModuleNameHash должен быть хеш-значением имени модуля в верхнем регистре (например, USER32.DLL).

C:
HMODULE GetModuleHandleH(DWORD dwModuleNameHash) {

if (dwModuleNameHash == NULL)
    return NULL;
#ifdef _WIN64
PPEB pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32
PPEB pPeb = (PEB*)(__readfsdword(0x30));
#endif

PPEB_LDR_DATA            pLdr  = (PPEB_LDR_DATA)(pPeb->Ldr);
PLDR_DATA_TABLE_ENTRY    pDte  = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);

while (pDte) {

    if (pDte->FullDllName.Length != NULL && pDte->FullDllName.Length < MAX_PATH) {

        // Преобразование `FullDllName.Buffer` в строку верхнего регистра
        CHAR UpperCaseDllName[MAX_PATH];

        DWORD i = 0;
        while (pDte->FullDllName.Buffer[i]) {
            UpperCaseDllName[i] = (CHAR)toupper(pDte->FullDllName.Buffer[i]);
            i++;
        }
        UpperCaseDllName[i] = '\0';

        // хеширование `UpperCaseDllName` и сравнение хеш-значения с тем, что во входном `dwModuleNameHash`
        if (HASHA(UpperCaseDllName) == dwModuleNameHash)
            return pDte->Reserved2[0];

    }
    else {
        break;
    }

    pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
}

return NULL;
}

Демо

Это демо использует GetModuleHandleH и GetProcAddressH для вызова MessageBoxA.

C:
#define USER32DLL_HASH 0x81E3778E
               #define MessageBoxA_HASH 0xF10E27CAint

main() {

// Загрузите User32.dll в текущий процесс, чтобы GetModuleHandleH работал
if (LoadLibraryA("USER32.DLL") == NULL) {
    printf("[!] LoadLibraryA не удалось с ошибкой : %d \n", GetLastError());
    return 0;
}

// Получение дескриптора user32.dll с помощью GetModuleHandleH
HMODULE hUser32Module = GetModuleHandleH(USER32DLL_HASH);
if (hUser32Module == NULL){
    printf("[!] Не удалось получить дескриптор User32.dll \n");
    return -1;
}

// Получение адреса функции MessageBoxA с помощью GetProcAddressH
fnMessageBoxA pMessageBoxA = (fnMessageBoxA)GetProcAddressH(hUser32Module, MessageBoxA_HASH);
if (pMessageBoxA == NULL) {
    printf("[!] Не удалось найти адрес указанной функции \n");
    return -1;
}

// Вызов MessageBoxA
pMessageBoxA(NULL, "Создание вредоносных программ с RuSfera", "Вау", MB_OK | MB_ICONEXCLAMATION);

printf("[#] Нажмите <Enter>, чтобы выйти ... ");
getchar();

return 0;
}

1696075705061.png


Использование хэширования API для маскировки IAT реализации является эффективным методом. Однако иногда замена самого WinAPI, если это возможно, может улучшить скрытность IAT, уменьшая количество хэш-значений, а также уменьшая потенциальные эвристические сигнатуры, связанные с алгоритмом хэширования API. Кроме того, реализация пользовательского кода для функции WinAPI может использоваться в различных реализациях, что упрощает автоматизацию общего процесса скрытия IAT.

Можно посмотреть реализацию некоторых API здесь:

Генерация хешей во время компиляции программы
В этой статье хеширования API хеши функций и модулей генерировались перед их добавлением в код. К сожалению, это может занять много времени, и этого можно избежать, используя хеширование API во время компиляции.

Кроме того, в статье выше хеши были жестко закодированы, что позволяет системам безопасности использовать их в качестве индикаторов заражения, если они не обновляются в каждой реализации. Однако при хешировании API во время компиляции динамические хеши генерируются каждый раз при компиляции двоичного файла.

Оговорка

Этот метод работает только с проектами на C++ из-за использования ключевого слова constexpr. Оператор constexpr в C++ используется для указания на то, что функция или переменная может быть вычислена на этапе компиляции. Кроме того, оператор constexpr на функциях и переменных улучшает производительность приложения, позволяя компилятору выполнять определенные вычисления на этапе компиляции, а не во время выполнения.

Пошаговое руководство по хешированию во время компиляции

Ниже приведены шаги, необходимые для реализации хеширования во время компиляции.

Создание функций во время компиляции

Первым шагом является преобразование используемых хеш-функций в функции, выполняемые во время компиляции, с использованием оператора constexpr. В данном случае алгоритм хеширования Dbj2 будет изменен для использования оператора constexpr.

C:
#define SEED 5 // Функция хеширования Djb2 во время компиляции (WIDE)
constexpr DWORD HashStringDjb2W(const wchar_t* String) {
    ULONG Hash = (ULONG)g_KEY;
    INT c = 0;
    while ((c = *String++)) {
        Hash = ((Hash << SEED) + Hash) + c;
    }

    return Hash;
}

// Функция хеширования Djb2 во время компиляции (ASCII)
constexpr DWORD HashStringDjb2A(const char* String) {
    ULONG Hash = (ULONG)g_KEY;
    INT c = 0;
    while ((c = *String++)) {
        Hash = ((Hash << SEED) + Hash) + c;
    }

    return Hash;
}

Неопределенная переменная, g_KEY, используется в качестве начального хеша в обеих функциях.

g_KEY - это глобальная переменная constexpr и она случайным образом генерируется функцией RandomCompileTimeSeed (объясняется ниже) при каждой компиляции двоичного файла.

Генерация случайного значения начального числа RandomCompileTimeSeed используется для генерации случайного значения начального числа на основе текущего времени. Он делает это, извлекая цифры из макроса TIME, который является предопределенным макросом в C++, который расширяется до текущего времени в формате ЧЧ:ММ:СС. Затем функция RandomCompileTimeSeed умножает каждую цифру на разную случайную константу и складывает их все вместе, чтобы получить окончательное значение начального числа.

C:
// Генерировать случайный ключ на этапе компиляции, который используется как начальный хеш
constexpr int RandomCompileTimeSeed(void)
{
    return '0' * -40271 +
        __TIME__[7] * 1 +
        __TIME__[6] * 10 +
        __TIME__[4] * 60 +
        __TIME__[3] * 600 +
        __TIME__[1] * 3600 +
        __TIME__[0] * 36000;
};

// Компилируемое время случайного начального числа
constexpr auto g_KEY = RandomCompileTimeSeed() % 0xFF;

Создание макросов

Затем определите два макроса, RTIME_HASHA и RTIME_HASHW, которые будут использоваться функцией GetProcAddressH во время выполнения для сравнения хешей. Макросы следует определить следующим образом.
C:
#define RTIME_HASHA( API ) HashStringDjb2A((const char*) API)       // Вызов HashStringDjb2A
#define RTIME_HASHW( API ) HashStringDjb2W((const wchar_t*) API)    // Вызов HashStringDjb2W

Как только функция хеширования во время компиляции будет установлена, следующим шагом будет объявление значений хеша во время компиляции в переменных. Для автоматизации процесса будут реализованы два макроса.

C:
#define CTIME_HASHA( API ) constexpr auto API##_Rotr32A = HashStringDjb2A((const char*) #API);
#define CTIME_HASHW( API ) constexpr auto API##_Rotr32W = HashStringDjb2W((const wchar_t*) L#API);

Оператор преобразования в строку

Символ # известен как оператор преобразования в строку. Он используется для преобразования параметра макропроцессора в строковый литерал.

Например, если макрос CTIME_HASHA вызван с аргументом SomeFunction, как HASHA(SomeFunction), выражение #API будет заменено строковым литералом "SomeFunction".

Оператор объединения

Оператор ## известен как оператор объединения. Он используется для объединения двух макросов в один макрос.
Оператор ## используется для объединения параметра API со строкой "_Rotr32A" или "_Rotr32W" соответственно, чтобы формировать окончательное имя определяемой переменной.

Например, если макрос CTIME_HASHA вызван с аргументом SomeFunction, как HASHA(SomeFunction), оператор ## объединит API с "_Rotr32A" для формирования окончательного имени переменной SomeFunction_Rotr32A.

Демонстрация расширения макроса

Чтобы лучше понять, как работают предыдущие макросы, на изображении ниже показан пример использования макроса CTIME_HASHA для создания хеша для MessageBoxA, создавая переменную под названием MessageBoxA_Rotr32A, которая будет содержать значение хеша во время компиляции.

1696076790446.png


Хеширование во время компиляции - Код

Собрав все вместе, код будет выглядеть следующим образом.

C:
#include <Windows.h>
#include <stdio.h>
#include <winternl.h>
#define SEED 5 // генерировать случайный ключ (используется как начальный хеш)
constexpr int RandomCompileTimeSeed(void)
{
    return '0' * -40271 +
        __TIME__[7] * 1 +
        __TIME__[6] * 10 +
        __TIME__[4] * 60 +
        __TIME__[3] * 600 +
        __TIME__[1] * 3600 +
        __TIME__[0] * 36000;
};

constexpr auto g_KEY = RandomCompileTimeSeed() % 0xFF;

// Функция хеширования Djb2 во время компиляции (WIDE)
constexpr DWORD HashStringDjb2W(const wchar_t* String) {
    ULONG Hash = (ULONG)g_KEY;
    INT c = 0;
    while ((c = *String++)) {
        Hash = ((Hash << SEED) + Hash) + c;
    }

    return Hash;
}

// Функция хеширования Djb2 во время компиляции (ASCII)
constexpr DWORD HashStringDjb2A(const char* String) {
    ULONG Hash = (ULONG)g_KEY;
    INT c = 0;
    while ((c = *String++)) {
        Hash = ((Hash << SEED) + Hash) + c;
    }

    return Hash;
}

// макросы хеширования во время выполнения
#define RTIME_HASHA( API ) HashStringDjb2A((const char*) API)
#define RTIME_HASHW( API ) HashStringDjb2W((const wchar_t*) API)
// макросы хеширования во время компиляции (используются для создания переменных)
#define CTIME_HASHA( API ) constexpr auto API##_Rotr32A = HashStringDjb2A((const char*) #API);
#define CTIME_HASHW( API ) constexpr auto API##_Rotr32W = HashStringDjb2W((const wchar_t*) L#API);

FARPROC GetProcAddressH(HMODULE hModule, DWORD dwApiNameHash) {

    PBYTE pBase = (PBYTE)hModule;

    PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
    if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
        return NULL;

    PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
    if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return NULL;

    IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;

    PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
    PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
    PWORD FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

    for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++) {
        CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
        PVOID pFunctionAddress = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);

        if (dwApiNameHash == RTIME_HASHA(pFunctionName)) { // проверка значения хеша во время выполнения
            return (FARPROC)pFunctionAddress;
        }
    }

    return NULL;
}

Демо

1696077000659.png
 

Вложения

  • 1696076940334.png
    1696076940334.png
    78.8 КБ · Просмотры: 5
Последнее редактирование:

yum

Пользователь
Форумчанин
Регистрация
30.11.2023
Сообщения
47
Репутация
3
Почему используется для FunctionOrdinalArray тип PWORD , если в структуре это DWORD. А для остальных используется PDWORD , но тут понятно , в структуре они DWORD
 

X-Shar

:)
Администрация
Регистрация
03.06.2012
Сообщения
6 142
Репутация
8 268
Почему используется для FunctionOrdinalArray тип PWORD , если в структуре это DWORD. А для остальных используется PDWORD , но тут понятно , в структуре они DWORD
PWORD используется в данном контексте, а не PDWORD, потому что AddressOfNameOrdinals в структуре IMAGE_EXPORT_DIRECTORY указывает на массив порядковых номеров функций, которые являются 16-битными целыми числами. В Windows API тип WORD определен как 16-битное беззнаковое целое, тогда как DWORD является 32-битным беззнаковым целым. Поскольку порядковые номера функций экспорта являются 16-битными значениями, для их представления используется тип PWORD (указатель на WORD), а не PDWORD (указатель на DWORD). Это обеспечивает корректное считывание 16-битных значений из массива порядковых номеров.
 

yum

Пользователь
Форумчанин
Регистрация
30.11.2023
Сообщения
47
Репутация
3
Поскольку каждый pDte представляет собой уникальную DLL внутри связанного списка, возможно перейти к следующему элементу с помощью следующей строки кода:

C:
pDte = (PLDR_DATA_TABLE_ENTRY)(pDte);
Подскажите пожалуйста как это работает , не могу понять как мы получаем следующий элемент при помощи этой конструкции? InMemoryOrderModuleList хранится Flink , который по сути является следующем элементом. Почему мы используем ту конструкцию для получение следующего элемента. Это конечно хорошо , то что там описывается , но даже в статье говорится , что это просто так работает, но я хочу понять как это работает.
 
Последнее редактирование:

yum

Пользователь
Форумчанин
Регистрация
30.11.2023
Сообщения
47
Репутация
3
Код:
#ifdef LDR_DATA_TABLE_ENTRY return (HMODULE)(pDte->InInitializationOrderLinks.Flink);
#else return (HMODULE)pDte->Reserved2[0];
#endif // STRUCTS
А есть ли не такие костыльные способы проверки структуры ?
 

X-Shar

:)
Администрация
Регистрация
03.06.2012
Сообщения
6 142
Репутация
8 268
Подскажите пожалуйста как это работает , не могу понять как мы получаем следующий элемент при помощи этой конструкции? InMemoryOrderModuleList хранится Flink , который по сути является следующем элементом. Почему мы используем ту конструкцию для получение следующего элемента. Это конечно хорошо , то что там описывается , но даже в статье говорится , что это просто так работает, но я хочу понять как это работает.
Как вариант для понимания можно в дебагере или принтами распечатать эти адреса и содержимое структур.
 
Автор темы Похожие темы Форум Ответы Дата
X-Shar Введение в разработку вредоносных программ 1
X-Shar Введение в разработку вредоносных программ 6
X-Shar Введение в разработку вредоносных программ 0
X-Shar Введение в разработку вредоносных программ 2
X-Shar Введение в разработку вредоносных программ 0
X-Shar Введение в разработку вредоносных программ 0
X-Shar Введение в разработку вредоносных программ 0
X-Shar Введение в разработку вредоносных программ 0
X-Shar Введение в разработку вредоносных программ 0
X-Shar Введение в разработку вредоносных программ 1
X-Shar Введение в разработку вредоносных программ 0
X-Shar Введение в разработку вредоносных программ 0
X-Shar Введение в разработку вредоносных программ 0
X-Shar Введение в разработку вредоносных программ 0
X-Shar Введение в разработку вредоносных программ 2
X-Shar Введение в разработку вредоносных программ 2
X-Shar Введение в разработку вредоносных программ 0
X-Shar Введение в разработку вредоносных программ 3
X-Shar Введение в разработку вредоносных программ 2
X-Shar Введение в разработку вредоносных программ 0
Похожие темы
Уроки Разработка вирусов-35.Обход EDRs.Последняя тема цикла
Уроки Разработка вирусов-34.Обход Windows defender
Уроки Разработка вирусов-33.Уменьшение вероятности детекта зверька
Уроки Разработка вирусов-32.Открываем врата ада
Уроки Разработка вирусов-31.Обход виртуальных машин
Уроки Разработка вирусов-30.Черпаем силы в антиотладке
Уроки Разработка вирусов-29. Предельная техника-2. Практика. Реализуем техники инъекции через сисколы
Уроки Разработка вирусов-28. Предельная техника. Разборка с сисколами
Уроки Разработка вирусов-27.Кунгфу-2.Изучаем API Hooking
Уроки Разработка вирусов-25. Скрытие строк
Уроки Разработка вирусов-24. Изучаем технику Spoofing
Уроки Разработка вирусов-23. Контроль выполнения полезной нагрузки
Уроки Разработка вирусов-22.Изучаем технику Stomping Injection
Уроки Разработка вирусов-21.Инъекция отображаемой памяти
Уроки Разработка вирусов-20.Вызов кода через функции обратного вызова
Уроки Разработка вирусов-19.Изучаем технику APC Injection
Уроки Разработка малвари-18.Определение PID нужного процесса, или перечисления процессов
Уроки Разработка вирусов-17.Изучаем технику Thread Hijacking
Уроки Разработка вирусов-16.Разборка с цифровой подписью зверька
Уроки Разработка вирусов-15. Прячем Payload в реестре
Верх Низ