Загрузка...

Некоторые приемы из малварь-кодинга, которые имеет смысл использовать на практике

Тема в разделе Вирусология создана пользователем Replacer 17 янв 2022. 756 просмотров

Загрузка...
  1. Replacer
    Replacer Автор темы 17 янв 2022 ARTIFICIAL SUICIDE 99 21 июл 2021
    Мне неожиданно захотелось о чем-то написать, но все темы, которые пришли в голову требуют основательного ковыряния и расписывания, а я человек вспыльчивый и могу практически сразу бросить это дело. Решил расписать тут некоторые приемы из малварь-кодинга, которые я использую на практике. Да и не только я, но и многие другие люди.


    Pseudo Random Number Generator

    Константные значения это плохо (однако в них все же бывает необходимость), нельзя, чтоб у в вашем коде постоянно встречались одни и те же значения, поэтому имеет смысл имплементировать генератор псевдослучайных значений. Почему псевдослучайных? Потому что всякий PRNG с ограниченным числом ресурсов имеет свойство зацикливаться и повторять одну и ту же последовательность чисел. Это можно вывести как математически, так и логически, посмотрев на алгоритм генерации. Да, такие генераторы не будут панацеей, но все же помогут в запутывании.

    Классическим вариантом является использование предкомпилированного макросов
    __TIME__
    и
    __LINE__
    . Макрос
    __TIME__
    расширяется в строку, получая время когда препроцессор был запущен. Макрос
    __LINE__
    расширяется до номера текущей строки ввода в виде int. Вот так выглядит классическая имплементация алгоритма PRNG:

    C
    #define PRNG_SEED ((__TIME__[7] - '0') * 1  + (__TIME__[6] - '0') * 10  +         \
    (__TIME__[4] - '0') * 60 + (__TIME__[3] - '0') * 600 + \
    (__TIME__[1] - '0') * 3600 + (__TIME__[0] - '0') * 36000) + \
    (__LINE__ * 100000)

    Что-нибудь специфичнее?


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

    C
    __asm {
    xor ah, ah
    int 1Ah
    rol cx, 8
    xor dx, cx
    xor [prngNum], dx //Немножко энтропии
    }
    Правда, стоит учитывать, что прерывание 1A также получает время системы, так что возможны коллизии у значений, поэтому добавляйте энтропию.

    Хочу сломать себе жизнь!

    У процессоров существуют свои собственные мнемоники, вызывающие аппаратный PRNG (ну или NRBG, но это другая история). А если конкретнее -
    RDRAND
    и
    RDSEED
    . Вы можете попробовать поработать с ними, однако я никогда не видел, чтоб они где-то использовались (кроме системных/аппаратных криптографических модулей).


    Runtime Linking

    Более подробно я расписывал это вот здесь. Идея заключается в том, чтоб "неявно" вызывать функции из системных динамических библиотек. Почему это необходимо? Потому что антивирусы умеют в хуки вызовов функций, а практически все функции из того же WinAPI вызывают под капотом функции из NTAPI и т.д (может быть когда-нибудь напишу об этом, если будет настроение). Таким образом мы можем немножечко "обойти" сканирование антивирусом на весьма интересные функции. Еще одним плюсом будет практически полное избавление от IAT (если мы отключаем CRT, об этом позже). Все сводится к обычной подгрузке dll и указателю на функцию:

    C
    typedef int(__stdcall* Inner_MessageBoxW)(HWND hWnd, LPWSTR lpText, LPWSTR lpCaption, UINT uType);

    VOID APIENTRY Entry(VOID) {
    HMODULE hModule;
    Inner_MessageBoxW I_MessageBoxW;

    hModule = LoadLibrary(L"user32.dll");
    if(hModule != NULL) {
    I_MessageBoxW = (Inner_MessageBoxW)GetProcAddress(hModule, L"MessageBoxW");

    if(I_MessageBoxW != NULL)
    (I_MessageBoxW)(NULL, L"Called from user32.dll", L"Message", MB_OK);

    } else {
    DWORD dwErr = GetLastError();
    ExitProcess(dwErr);
    }
    FreeModule(hModule);
    }
    [IMG]

    Я иногда удивляюсь, что некоторые находят даже какие-то библиотеки для этого, но да ладно. Вашим д/з будет нагенерить темплейтов (если вы плюсовик) для функций с разным количеством аргументов, ведь так работать гораздо удобнее!


    Анти-эмуляция

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

    Проверка на специфические процессы

    Все просто, виртуальные машины запускают специфические процессы при полной инициализации системы. Мы можем получить снапшот всех процессов и пройтись по полученным процессам, найдя необходимые, к примеру:

    C
    BOOL ProcessEnum() {
    HANDLE Snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    PROCESSENTRY32 Process;
    Process.dwSize = sizeof(PROCESSENTRY32);

    if(Process32First(Snapshot, &Process)) {
    while(Process32Next(Snapshot, &Process)) {
    if((!lstrcmp(Process.szExeFile, L"vmd3dservice.exe")) || (!lstrcmp(Process.szExeFile, L"VBoxService.exe")))
    return TRUE;
    }
    }

    return FALSE;
    }
    Hypervisor

    Тоже классический метод анти-эмуляции. Заключается в том, чтоб вызвать мнемонику CPUID со значением 40000000, тем самым получив "Vendor String" в регистры EAX, ECX и EDX. Работает для VMWare (получаем значения "VMwareVMware", ПК: "Microsoft HV"), для других гипервизоров не проверял, но по идее тоже должно работать:

    C
    BOOL HypervisorDetect() {
    BOOL Result = FALSE;
    __asm {
    xor eax, eax
    mov eax, 0x40000000
    cpuid
    cmp ecx, 0x4D566572
    jne NopInstr
    cmp edx, 0x65726177
    jne NopInstr
    mov Result, 0x1
    NopInstr:
    nop
    }
    return Result;
    }
    BIOSVersion

    Гипервизоры имеют специфические значения у BIOS. Вы можете проверять значения
    BIOSVersion
    и
    SystemProductName
    в реестре. Значения находятся в
    HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\SystemInformation
    . Можете порыться в реестре и написать собственный метод, тоже своеобразное д/з для вас.

    Что-то странное?

    Ладно-ладно, я наврал, что не будет чего-то здесь специфического. Существует такая вещь, как IDT (Таблица векторов прерываний). В чем идея? Идея заключается в сохранении таблицы через функцию
    __sidt
    получении 5 и 6 битов таблицы. В ВМ они не будут равны нулям, в отличии от физической машины:

    C
    BOOL IDTRCheck() {
    unsigned char* _idtr[10];

    memset(&_idtr, 0, sizeof(_idtr));

    __sidt(&_idtr);

    if((_idtr[4] && _idtr[5]) != 0x00) {
    return TRUE;
    }

    return FALSE;
    }
    [IMG]

    P.S: на 7 винде существует баг в ядре, что будет выкидывать всегда FALSE, поэтому стоит сохранять таблицу прерываний полностью.


    Анти-отладка

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

    C
    BOOL IsHardwareBreakpointPresent(VOID) {
    BOOL bFlag = FALSE;
    PCONTEXT Context = NULL;

    Context = (PCONTEXT)VirtualAlloc(NULL, sizeof(CONTEXT), MEM_COMMIT, PAGE_READWRITE);

    if (Context == NULL)
    return FALSE; //check GetLastError();

    RtlZeroMemory(Context, sizeof(Context));

    Context->ContextFlags = CONTEXT_DEBUG_REGISTERS;

    if (!GetThreadContext(GetCurrentThreadAlt(), Context))
    goto EXIT_ROUTINE;

    if (Context->Dr0 || Context->Dr1 || Context->Dr2 || Context->Dr3)
    bFlag = TRUE;

    EXIT_ROUTINE:
    if(Context)
    VirtualFree(Context, 0, MEM_RELEASE);

    return bFlag;
    }
    А еще можно флудить на куче, но лучше этого делать не стоит. Вот мой пример на FASM:

    Код
    format PE GUI
    entry entr

    section '.idata' import data readable writeable

    library kernel32, 'kernel32.dll', \
    ntdll, 'ntdll.dll'

    import kernel32, \
    GetProcessHeap, 'GetProcessHeap', \
    ExitProcess, 'ExitProcess'

    import ntdll, \
    RtlAllocHeap, 'RtlAllocateHeap'


    section '.data' data readable writeable

    buf db 256 dup (0)


    section '.code' executable readable writeable

    entr:
    invoke RtlAllocHeap, <invoke GetProcessHeap>, 0, 1024
    mov ecx, [eax + 10]
    invoke ExitProcess, 0

    Хеширование строк

    Не секрет, что открытые строки в малвари - плохо. Поэтому имеет смысл их шифровать. Здесь "хешировать" не подразумевается в "тянем SHA256/MD5/etc.", вполне себе активно используются легкопереносимые функции, такие как SDBM Hash или MurMurHash. Вот, к примеру алгоритм SDBM:

    C
    unsigned int SDBMHash(char *str) {
    unsigned int hash = 0;
    while(*str++)
    hash = str + (hash << 6) + (hash << 16) - hash;

    return hash;
    }
    P.S: для того, чтоб считать хеши в "предкомпилированном" состоянии используется модификатор
    constexpr
    (C++11), в принципе, их всегда так и надо считать.


    No CRT

    Хороший продукт не содержит в себе статической линковки стандартной библиотеки. Это сказывается на оптимизации и в принципе работе продукта. Я расписывал это в теме создания клиппера от backdoortp. Так что мне придется процитировать себя, как бы это странно не звучало:


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

    [CODE=c]VOID __memset(VOID* dest, BYTE value, size_t count) {
    for(volatile int i = 0; i <= count; i++) {
    ((BYTE *)dest)[i] = value;
    }
    }[/CODE][CODE=c]VOID *__alloc(SIZE_T count) {
    LPVOID AllocatedData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, count + 64);

    if(AllocatedData == NULL)
    return ALLOC_ERR;

    return AllocatedData;
    }[/CODE][CODE=c]int _isalpha(char *c) {
    while(*c != '\0') {
    if(((*c >= 65) && (*c <= 90)) || ((*c >= 97) && (*c <= 122))) {
    return 1;
    }
    c++;
    }

    return 0;
    }[/CODE]Забавный факт: некоторые функции из WinAPI, например
    ZeroMemory()
    , вызывают под капотом CRT функции. Конкретно
    ZeroMemory()
    вызывает
    memset
    .


    Обфускация вызовов

    В более профессиональных продуктах присутствует обфускация вызовов функций. Этот пункт также можно объединить с пунктом "Хеширование строк". Идея заключается в парсинге таблицы импортов, поиске имен функций, их обфускации и подгрузке функций через хеш, если кратко. Конкретно здесь была допущена мной неловкая оплошность, вы можете найти метод сами (он мало чем отличается обычно), либо пофиксить её самостоятельно.

    [CODE=c]HMODULE(__stdcall *tmp_LoadLibraryW)(LPCWSTR MODULE_NAME) = NULL;

    HMODULE hashed_LoadLibraryW(LPCWSTR MODULE_NAME) {
    return tmp_LoadLibraryW(MODULE_NAME);
    }

    LPVOID ParseExportTable(LPCWSTR Module, ULONG ApiHash) {
    PIMAGE_DOS_HEADER DosHeader;
    PIMAGE_NT_HEADERS NtHeaders;
    PIMAGE_EXPORT_DIRECTORY ExportDirectory;

    //заголовки
    DosHeader = (PIMAGE_DOS_HEADER)Module;
    NtHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)DosHeader + DosHeader->e_lfanew);
    ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)DosHeader + NtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    PDWORD RVAName;
    PWORD RVAOrdinal;

    //Импорт по ординалам!
    RVAName = (PDWORD)((DWORD_PTR)DosHeader + ExportDirectory->AddressOfNames);
    RVAOrdinal = (PWORD)((DWORD_PTR)DosHeader + ExportDirectory->AddressOfNameOrdinals);

    INT Ordinal = -1;
    WCHAR *ApiName = { 0 };

    //Угадайте, по чему мы проходимся?
    for(DWORD i = 0; i < ExportDirectory->NumberOfNames - 1; i++) {
    ApiName = (PWCHAR)((DWORD_PTR)DosHeader + RVAName[i]);
    const UINT GetMyHash = UnknownHash(ApiName);

    if(ApiName == GetMyHash) {
    Ordinal = (UINT)RVAOrdinal;
    break;
    }
    }

    const ULONG AddressOfFunction = (PDWORD)((DWORD_PTR)DosHeader + ExportDirectory->AddressOfFunctions);
    const ULONG FunctionFound = (LPVOID)((DWORD_PTR)DosHeader + AddressOfFunction[(PUINT)Ordinal]);

    return FunctionFound;
    }

    LPVOID GetMyApi(LPCWSTR Module, ULONG ApiHash) {
    HMODULE krnl32, hDLL;
    LPVOID ApiFunction;

    //Получаем kernel32
    #ifdef _WIN64
    int ModuleList = 0x18;
    int ModuleListFlink = 0x18;
    int KernelBaseAddr = 0x10;
    INT_PTR PEB = __readgsqword(0x60);
    #else
    int ModuleList = 0x0C;
    int ModuleListFlink = 0x10;
    int KernelBaseAddr = 0x10;
    INT_PTR PEB = __readfsdword(0x30);
    #endif

    //Получаем адрес kernel32
    INT_PTR ModList = *(INT_PTR *)(PEB + ModuleList);
    INT_PTR ListFlink = *(INT_PTR *)(PEB + ModuleListFlink);
    INT_PTR KrnlBase = *(INT_PTR *)(PEB + KernelBaseAddr);

    LDR_MODULE *LdrMod = (LDR_MODULE *)ListFlink;

    while(ListFlink != (INT_PTR)LdrMod) {
    LdrMod = (LDR_MODULE *)LdrMod->e[0].Flink;

    if(LdrMod->base != NULL) {
    if(!lstrcmpiW(LdrMod->dllname.Buffer, L"kernel32.dll")) {
    break;
    }
    }
    }

    //Подгружаем LoadLibraryW
    krnl32 = (HMODULE)LdrMod->base;
    const UINT ApiHash_LoadLibraryW = UnknownHash(L"LoadLibraryW");
    tmp_LoadLibraryW = (HMODULE(__stdcall *)(LPCWSTR))ParseExportTable(krnl32, ApiHash_LoadLibraryW);
    hDLL = hashed_LoadLibraryW(Module);

    ApiFunction = (LPVOID)ParseExportTable(hDLL, ApiHash);

    return ApiFunction;
    }[/CODE]

    P.S: LdrMod
    [CODE=c]typedef struct _LDR_MODULE {
    LIST_ENTRY e[3];
    HMODULE base;
    void *entry;
    UINT size;
    UNICODE_STRING dllPath;
    UNICODE_STRING dllname;
    } LDR_MODULE, *PLDR_MODULE;[/CODE]
    Заключение
    Это базовые приемы из мира малвари, так что по сути тут ничего нового нет и вы просто потратили несколько минут своей жизни в пустую, вот. Конечно, наверное стоило расписать о чем-нибудь еще более специфическом, но все, что я затронул, мне пришло в голову сразу, да и на практике это является наиболее распространенными "кусками кода". Все же надеюсь, что кому-то это будет полезно и вы подчеркнете что-нибудь новое.
     
    17 янв 2022 Изменено
  2. MISTER_N_1
    MISTER_N_1 17 янв 2022 3 12 янв 2022
    ничего не понял, но очень интересно :PepeRich:
     
  3. s1and1
    s1and1 19 янв 2022 0 10 ноя 2021
    вот так С превратился в Язык Ассемблера
     
Top