Категория > Новости > Веселые хеши. Реализуем технику API Hashing, чтобы обдурить антивирус - «Новости»

Веселые хеши. Реализуем технику API Hashing, чтобы обдурить антивирус - «Новости»


15-10-2023, 09:18. Автор: Walkman

прош­лой статье я рас­смот­рел вари­анты сок­рытия 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 += (hash << 10);hash ^= (hash 6);



  • hash — текущее зна­чение хеша;


  • c — текущее зна­чение сим­вола в стро­ке.


Код с фун­кци­ей.


#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

// 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(), искать адрес нуж­ной фун­кции.


Пе­рехо­дим к демонс­тра­ции.


 

Пример



Перейти обратно к новости