Terminando procesos en Windows

Para mí, uno de los mejores visores/editores de procesos (si no el mejor) es Process Hacker y una de las herramientas para matar procesos más ‘capaces’ es su Terminator.

En su última versión, Terminator incluye hasta 14 métodos distintos para acabar con la resistencia de casi cualquier proceso. A continuación y gracias a la información publicada en el blog de su autor (el gran wj32), vemos una descripción de la mayoría de estos métodos:

TerminateProcess o NtTerminateProcess

Todo el mundo conoce TerminateProcess. Sólo hay que abrir un identificador para el proceso de destino y llamar a TerminateProcess. En caso de que TerminateProcess esté hookeado (se interceptan las llamadas, mensajes o eventos), se puede llamar a la función equivalente de la API nativa, NtTerminateProcess.

CreateRemoteThread, ExitProcess

Para usar este método hay que encontrar la dirección de ExitProcess dentro del proceso destino. Normalmente la dirección del proceso es la misma que ExitProcess y es posible utilizar GetModuleHandle y GetProcAddress. Después, hay que crear un subproceso dentro del proceso de destino que ejecuta ExitProcess, matando al proceso en cuestión.

NtQuerySystemInformation o ToolHelp32, SetThreadContext

Se recorren los hilos del proceso de destino y se configuran sus contextos utilizando SetThreadContext; modificándolos para que los registros eip apunten a la función ExitProcess.

DuplicateHandle

Se realiza un loop de 0 a 4096 y se llama a DuplicateHandle con ‘Options = 1’, dejando TargetProcess y TargetProcessHandle nulos. Esto cerrará la mayoría, si no todos los identificadores abiertos por el proceso de destino. Este método funciona mejor con aplicaciones complejas como las de software de seguridad pero, por ejemplo, no cerrará el bloc de notas.

CreateJobObject, AssignProcessToJobObject, TerminateJobObject (y sus API nativas equivalentes)

Hay que crear un objeto job utilizando CreateJobObject, asignar su uso al proceso de destino con AssignProcessToJobObject, y terminarlo utilizando TerminateJobObject. Esta técnica funciona bien si NtAssignProcessToJobObject y NtTerminateJobObject no están hookeadas porque NtTerminateJobObject llama a PsTerminateProcess directamente.

NtCreateDebugObject, NtDebugActiveProcess, CloseHandle

La gente normalmente implementa esta técnica mediante el uso de DebugActiveProcess y luego saliendo del proceso actual y hacen esto porque no saben exactamente cómo funciona DebugActiveProcess. Realmente, el kernel32 llama a ntdll que a su vez llama a NtDebugActiveProcess con un objeto debug ya creado. No es necesario por tanto salir del proceso actual a depurar para conseguir matarlo; solo es necesario cerrar el objeto debug. Cuando es cerrado, el kernel matará el proceso de depuración utilizando PsTerminateProcess.

Para implementar esta técnica, se puede crear un objeto debug NtCreateDebugObject (especificando el flag kill-on-close), depurar el proceso usando NtDebugActiveProcess (el manejo del proceso necesita acceso a PROCESS_SUSPEND_RESUME), y cerrar el manejador para el objeto debug a través de CloseHandle. Estas son las definiciones:

#define DEBUG_OBJECT_READEVENT 0x1 # Define DEBUG_OBJECT_READEVENT 0x1
#define DEBUG_OBJECT_PROCESSASSIGN 0x2 # Define DEBUG_OBJECT_PROCESSASSIGN 0x2
#define DEBUG_OBJECT_SETINFORMATION 0x4 # Define 0x4 DEBUG_OBJECT_SETINFORMATION
#define DEBUG_OBJECT_QUERYINFORMATION 0x8 # Define 0x8 DEBUG_OBJECT_QUERYINFORMATION

#define DEBUG_OBJECT_KILLONCLOSE 0x1 # Define DEBUG_OBJECT_KILLONCLOSE 0x1

NTSTATUS NTAPI NtCreateDebugObject( NTAPI NTSTATUS NtCreateDebugObject (
PHANDLE DebugObjectHandle, PHANDLE DebugObjectHandle,
ACCESS_MASK DesiredAccess, ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes, POBJECT_ATTRIBUTES ObjectAttributes,
ULONG Flags Banderas ULONG
); );

NTSTATUS NTAPI NtDebugActiveProcess( NTSTATUS NTAPI NtDebugActiveProcess (
HANDLE ProcessHandle, MANGO ProcessHandle,
HANDLE DebugObjectHandle MANGO DebugObjectHandle
); );


VirtualQueryEx, VirtualProtectEx


Se recorren las regiones de memoria del proceso de destino mediante VirtualQueryEx y se configuran sus protecciones a PAGE_NOACCESS. El programa se colgará en cuanto el contexto cambie en el código en modo usuario, ya que será incapaz de leer cualquier código de la memoria.

VirtualQueryEx, WriteProcessMemory

Se recorren las regiones de memoria del proceso de destino y se escriben datos al azar utilizando WriteProcessMemory.

VirtualAllocEx

Se llama a VirtualAllocEx en bucle hasta que no pueda reservar más memoria en el proceso de destino. Este fallará cuando no sea capaz de reservar más memoria.

PsTerminateProcess

PsTerminateProcess es una función interna en modo kernel que no es exportada por ntoskrnl.exe. Hay que localizarla escaneando la memoria en modo kernel para una firma específica.

ADVERTENCIA: En XP se debe localizar PspTerminateProcess, que es stdcall. En Vista, PsTerminateProcess es thiscall (el primer argumento va en ecx), por lo que se tendrá que modificar el código en ensamblador.


typedef NTSTATUS (*_PsTerminateProcess)( PEPROCESS Process, NTSTATUS ExitStatus );
NTSTATUS typedef (* _PsTerminateProcess) (PEPROCESS Proceso, NTSTATUS ExitS

PspTerminateThreadByPointer


Esta función no se exporta tampoco.

ADVERTENCIA: En XP, hay dos argumentos. En Vista, hay tres.

/* XP */ / * * XP /
typedef NTSTATUS (NTAPI *_PspTerminateThreadByPointer51)( NTSTATUS typedef (NTAPI _PspTerminateThreadByPointer51 *) (
PETHREAD Thread, PETHREAD Hilo,
NTSTATUS ExitStatus NTSTATUS ExitStatus
); );

/* Vista */ / * Vista * /
typedef NTSTATUS (NTAPI *_PspTerminateThreadByPointer60)( NTSTATUS typedef (NTAPI _PspTerminateThreadByPointer60 *) (
PETHREAD Thread, PETHREAD Hilo,
NTSTATUS ExitStatus, NTSTATUS ExitStatus,
BOOLEAN DirectTerminate BOOLEANA DirectTerminate
); );


Y por último, un ejemplo práctico en otro lenguaje

Ahora que ya conocemos un poco estos métodos y que hemos jugado a matar y matar procesos con el Terminator de Process Hacker, ¿por qué no implementar uno de estos métodos nosotros mismos?

El código fuente de Process Hacker, escrito en C/C#, es libre y puede ser reutilizado para nuestros “malignos” propósitos… Pero vamos a implementar por ejemplo el segundo método (TP2) con Python, un lenguaje un poco más flexible para mí.

Para ello y como no soy muy buen programador, hago una pequeña adaptación de este script disponible en Internet y listo:

import win32api
import win32con
import win32process
import win32event
import win32com.client
import ctypes
import sys
import win32pdhutil


def getChildrenPidsOfPid(pid):
print 'getting children of %s' % pid
wmi = win32com.client.GetObject('winmgmts:')
children = wmi.ExecQuery('Select * from win32_process where ParentProcessId=%s' % pid)
pids = []
for proc in children:
print 'child: %s %s' % (proc.Name, proc.Properties_('ProcessId'))
pids.append(proc.Properties_('ProcessId'))
return pids

def SafeTerminateProcess(pid):
success = False
for child in getChildrenPidsOfPid(pid):
SafeTerminateProcess(child)
### this is only for testing, sometimes terminating the children causes parental suicide
import time; time.sleep(5)
print 'trying to terminate pid %s' % pid

handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, int(pid))
exitcode = win32process.GetExitCodeProcess(handle)
if exitcode == win32con.STILL_ACTIVE:
print 'need to terminate pid %s' % pid
hKernel = win32api.GetModuleHandle("Kernel32")
procExit = win32api.GetProcAddress(hKernel, "ExitProcess")
# the following is not (yet?) in win32process:
hRemoteT = ctypes.windll.kernel32.CreateRemoteThread(handle.handle, None, 0, procExit, ctypes.c_void_p(-1), 0, None)
if not hRemoteT:
print 'bad, no thread'
else:
print 'waiting after starting remote thread'
retval = win32event.WaitForSingleObject(handle, 5000) # wait 5 seconds
if retval == win32con.WAIT_TIMEOUT:
print 'argh, timeout elapsed'
win32api.CloseHandle(hRemoteT)
success = True
else:
success = True
print 'pid %s already dead' % pid
win32api.CloseHandle(handle)
print 'success is %s for pid %s' % (success, pid)
return success

if __name__ == '__main__':
if len(sys.argv)>1:
for pid in sys.argv[1:]:
result = SafeTerminateProcess(pid)
if result:
print result
## print "Dumping all processes..."
## win32pdhutil.ShowAllProcesses()
else:
print "Killed %s" % pid
else:
print "Usage: killProcID.py pid ..."


A continuación, vemos el programa en acción, cerrando el proceso de la calculadora:


Ummm… ¿y si cerramos ahora el servicio del antimalware a-squared (a2service.exe)?



Vemos que no es tan fácil, faltan permisos. Si nos fijamos bien en el error devuelto por nuestro script, parece que no hemos podido abrir el proceso correctamente con OpenProcess porque seguramente la función está ‘hookeada’ (práctica habitual realizada por la mayoría de las casas de software de seguridad).

Pero, ¿la única manera de abrir procesos es mediante OpenProcess/NtOpenProcess? Como comenta nuestro amigo wj32 en su blog, la respuesta es (afortunadamente) no.


Se podría abrir el proceso ‘víctima’ con un acceso sincronizado y llamar a la función DuplicateHandle para conseguir los derechos de acceso necesarios. Esto no siempre funcionará porque los fabricantes de software están empezando a hookear ZwDuplicateObject para prevenirlo, pero sigue siendo una alternativa válida en muchos casos.

También, en Windows Vista hay dos funciones de la API nativa llamadas NtGetNextProcess y NtGetNextThread que casi nadie conoce y por tanto no se 'hookean'.

Imaginaros utilizar en nuestro script cualquiera de estas funciones en lugar del clásico OpenProcess…¿seríais capaces de desarrollar un AVKiller?

Comentarios