warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
В прошлой статье я рассмотрел варианты сокрытия IAT путем получения адресов функции и их вызова «напрямую» через LoadLibrary(
и GetProcAddress(
, но проблема в том, что сами строки, содержащие имена функций, никак не спрятаны. Поэтому любой уважающий себя антивирусный продукт все равно сможет понять, что мы задумали.


Чтобы предотвратить это, добрые хакеры придумали технику API Hashing, с которой сейчас и познакомимся.
Простейший API Hashing
Раньше мы получали адрес нужной функции через кастомный GetProcAddress(
. Вот как это выглядит.
FARPROC myGetProcAddress(HMODULE hModule, LPCSTR lpProcName) { PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule; PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dosHeader->e_lfanew); PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD* addressOfFunctions = (DWORD*)((BYTE*)hModule + exportDirectory->AddressOfFunctions); WORD* addressOfNameOrdinals = (WORD*)((BYTE*)hModule + exportDirectory->AddressOfNameOrdinals); DWORD* addressOfNames = (DWORD*)((BYTE*)hModule + exportDirectory->AddressOfNames); for (DWORD i = 0; i < exportDirectory->NumberOfNames; ++i) { if (strcmp(lpProcName, (const char*)hModule + addressOfNames[i]) == 0) { return (FARPROC)((BYTE*)hModule + addressOfFunctions[addressOfNameOrdinals[i]]); } } return NULL;}
API Hashing заключается в том, что мы вместо имени функции (lpProcName
) будем передавать хеш от нее. Фактически изменится лишь следующая строка.
if (strcmp(lpProcName, (const char*)hModule + addressOfNames[i]) == 0)
И станет вот такой:
if (strcmp(lpProcName, HASH((const char*)hModule + addressOfNames[i])) == 0)
Аналогичным образом все происходит и с GetModuleHandle(
.
Но что это вообще за хеш, откуда его взять? Хеш генерируется с помощью конкретного алгоритма хеширования, можешь выбрать любой на свой вкус. Приведу несколько примеров.
Выбор алгоритма
Итак, берем алгоритм Djb2. Это очень быстрый алгоритм хеширования.
hash = ((hash << 5) + hash) + c
-
hash
— текущее значение хеша; -
<
— сдвиг вправо;< -
c
— текущее значение символа в строке.
Вот функция, которая позволяет хешировать строку.
#include <Windows.h>#include <iostream>#define INITIAL_HASH 3731 // Для рандомизации#define INITIAL_SEED 7// ASCIIDWORD HashStringDjb2A(_In_ PCHAR String){ ULONG Hash = INITIAL_HASH; INT c; while (c = *String++) Hash = ((Hash << INITIAL_SEED) + Hash) + c; return Hash;}// UnicodeDWORD HashStringDjb2W(_In_ PWCHAR String){ ULONG Hash = INITIAL_HASH; INT c; while (c = *String++) Hash = ((Hash << INITIAL_SEED) + Hash) + c; return Hash;}int main() { LPWSTR str = (LPWSTR)L"HelloWorld"; std::cout << HashStringDjb2W(str); return 0;}
Теперь давай попробуем алгоритм JenkinsOneAtATime32Bit. Он перебирает символы входной строки и постепенно обновляет хеш‑значение в соответствии с каждым полученным символом.
hash += c;hash += (hash << 10);hash ^= (hash >> 6);
-
hash
— текущее значение хеша; -
c
— текущее значение символа в строке.
Код с функцией.
#include <Windows.h>#include <iostream>#define INITIAL_SEED 7// ASCIIUINT32 HashStringJenkinsOneAtATime32BitA(_In_ PCHAR String){ SIZE_T Index = 0; UINT32 Hash = 0; SIZE_T Length = lstrlenA(String); while (Index != Length) { Hash += String[Index++]; Hash += Hash << INITIAL_SEED; Hash ^= Hash >> 6; } Hash += Hash << 3; Hash ^= Hash >> 11; Hash += Hash << 15; return Hash;}// UnicodeUINT32 HashStringJenkinsOneAtATime32BitW(_In_ PWCHAR String){ SIZE_T Index = 0; UINT32 Hash = 0; SIZE_T Length = lstrlenW(String); while (Index != Length) { Hash += String[Index++]; Hash += Hash << INITIAL_SEED; Hash ^= Hash >> 6; } Hash += Hash << 3; Hash ^= Hash >> 11; Hash += Hash << 15; return Hash;}int main() { LPWSTR str = (LPWSTR)L"HelloWorld"; std::cout << HashStringJenkinsOneAtATime32BitW(str); return 0;}
Не утомился? Давай посмотрим еще один пример. В каком‑то проекте, чуть ли не в Hell’s Gate, я встречал алгоритм LoseLose. Он вычисляет хеш‑значение входной строки, перебирая каждый символ в ней и суммируя значения ASCII каждого символа.
hash = 0;hash += c; // Для каждого символа в строке
Хеш‑значение, полученное в результате алгоритма LoseLose, представляет собой целое число, уникальное для входной строки. Но все равно есть вероятность коллизии. Чтобы решить эту проблему, формула алгоритма была обновлена, как показано ниже.
hash = 0;hash += c;hash *= c + 2;
Вот пример функции, которая генерирует подобный хеш.
#include <Windows.h>#include <iostream>#define INITIAL_SEED 2// ASCIIDWORD HashStringLoseLoseA(_In_ PCHAR String){ ULONG Hash = 0; INT c; while (c = *String++) { Hash += c; Hash *= c + INITIAL_SEED; } return Hash;}// UnicodeDWORD HashStringLoseLoseW(_In_ PWCHAR String){ ULONG Hash = 0; INT c; while (c = *String++) { Hash += c; Hash *= c + INITIAL_SEED; } return Hash;}int main() { LPWSTR str = (LPWSTR)L"HelloWorld"; std::cout << HashStringLoseLoseW(str); return 0;}
И, думаю, последний вариант — Rotr32(
.
#include <Windows.h>#include <iostream>#define INITIAL_SEED 5UINT32 HashStringRotr32SubA(UINT32 Value, UINT Count){ DWORD Mask = (CHAR_BIT * sizeof(Value) - 1); Count &= Mask;#pragma warning( push )#pragma warning( disable : 4146) return (Value >> Count) | (Value << ((-Count) & Mask));#pragma warning( pop )}INT HashStringRotr32A(_In_ LPCSTR String){ INT Value = 0; for (INT Index = 0; Index < strlen(String); Index++) Value = String[Index] + HashStringRotr32SubA(Value, 7); return Value;}UINT32 HashStringRotr32SubW(UINT32 Value, UINT Count){ DWORD Mask = (CHAR_BIT * sizeof(Value) - 1); Count &= Mask;#pragma warning( push )#pragma warning( disable : 4146) return (Value >> Count) | (Value << ((-Count) & Mask));#pragma warning( pop )}INT HashStringRotr32W(_In_ LPCWSTR String){ INT Value = 0; for (INT Index = 0; Index < wcslen(String); Index++) Value = String[Index] + HashStringRotr32SubW(Value, 7); return Value;}int main() { LPWSTR str = (LPWSTR)L"HelloWorld"; std::cout << HashStringRotr32W(str); return 0;}
Для API Hashing ты просто инициализируешь строку с именем функции, а затем загоняешь в любой из описанных выше алгоритмов. После получения хеша ты должен добавить его в свою программу и, используя кастомный GetProcAddress(
, искать адрес нужной функции.
Переходим к демонстрации.
Пример
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»