Убить Билла. Изучаем способы принудительного завершения процессов в 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. Вот код функции:

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


Комментарии (2)

  • kill_proc2 - ну такое себе) ExitProcess == TerminateProcess(GetCurrentProcess(), 0)

  • В чём разница между NtTerminateProcess и TerminateProcess? Походу автор кулхацкер.