В этой статье мы с тобой напишем пол­ноцен­ную пер­мутиру­ющую реали­зацию тех­ники API Hashing, что поз­волит раз и нав­сегда скрыть импорты от глаз анти­виру­са. Впер­вые тех­нику API Hashing я уви­дел в вирусе‑шиф­роваль­щике Revil, проб­лема лишь в том, что хеши от фун­кций были впи­саны в код, а это поз­воля­ет лег­ко соз­давать сиг­натуры. Давай сде­лаем так, что­бы все дан­ные генери­рова­лись на лету, тог­да у анти­виру­са не будет шан­сов!

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
// ASCII
DWORD HashStringDjb2A(_In_ PCHAR String)
{
ULONG Hash = INITIAL_HASH;
INT c;
while (c = *String++)
Hash = ((Hash << INITIAL_SEED) + Hash) + c;
return Hash;
}
// Unicode
DWORD 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
// ASCII
UINT32 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;
}
// Unicode
UINT32 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
// ASCII
DWORD HashStringLoseLoseA(_In_ PCHAR String)
{
ULONG Hash = 0;
INT c;
while (c = *String++) {
Hash += c;
Hash *= c + INITIAL_SEED;
}
return Hash;
}
// Unicode
DWORD 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 5
UINT32 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»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии