Depuración en Windows
Introducción
En este capítulo, vamos a usar PyDbg inspirado en el Libro Gray Hat Python de Justin Seitz, capítulo 4. Estamos en el límite del Hacking y del Forensic. PyDbg ayudará a localizar los datos en los programas, depurar y también a realizar fuzzing, hook de aplicación, etc.
Trabajaremos aquí bajo Windows y por mi parte, en Linux, yo emplearé una máquina virtual VMware para escribir y probar los ejercicios y ejemplos de este capítulo.
Se requiere un conocimiento previo de los sistemas Windows, desensamblador y por supuesto de Python.
Debemos saber cómo asociar el depurador a un proceso bien sea abriendo el ejecutable y arrancándolo, o bien enganchándose al proceso (el programa ya está en ejecución).
Vamos a conectar al proceso cuando este nos permita no tener en cuenta el inicio de la aplicación y centrarnos en una parte específica del código.
Cuando abriremos el programa en el depurador (debugger), controlaremos el proceso desde el arranque; podremos, por lo tanto ver lo que se carga, lo cual resulta muy útil en el análisis de malwares y virus.
El módulo ctypes de Python
El módulo ctypes permite interactuar con librerías escritas en lenguaje C.
Utilizaremos en adelante el módulo ctypes para nuestro depurador (debugger).
El módulo ctypes proporciona métodos para cargar librerías escritas en C y llamar a las funciones de estas librerías, tipos de datos compatibles con el lenguaje C para intercambiar y recuperar las variables con estas funciones.
El módulo ctypes está disponible a partir de la versión 2.5 de Python.
El módulo ctypes exporta un objeto cdll que permite cargar una librería. En el entorno Windows, disponemos además de los objetos windll y oledll.
Objeto cdll proporciona un método LoadLibrary() que permite cargar una librería dinámica.
From ctypes import *
cdll.LoadLibrary("libc6.so.6") # Linux
print windll.kernel32 # windows
print cdll.msvcrt # windows
libc=cdll.msvcrt
Una vez que hemos cargado una librería empleando el método LoadLibrary(), le asignamos un objeto que permitirá llamar a las funciones definidas por la librería.
Este objeto actuará como envoltorio (wrapper) para acceder a las funciones de la librería, pasando a través de punteros.
print windll.kernel32.GetModuleHandleA # Muestra <_FuncPtr object
at 0x...>
print hex(windll.kernel32.GetModuleHandleA(None)) # llamada...
Primer enfoque
¿Cómo crear un proceso en Windows en un depurador?
La función se denomina CreateProcessA(), a la que podemos transmitir los parámetros.
Podremos encontrar en Internet, y en particular en el sitio de MSDN, detalles sobre esta función si así lo queremos.
Veremos aquí los parámetros a transmitir que nos servirán luego.
Los parámetros utilizados son:
-
lpApplicationName
-
lpCommandLine
-
dwCreationFlags
-
lpStartupInfo
-
lpProcessInformation
Los demás parámetros podrán asignarse con NULL.
Los dos primeros parámetros nos permitirán proporcionar la ruta de la aplicación y los comandos que vamos a pasar (en línea de comandos) si es necesario.
dwCreationFlags nos servirá para indicar al proceso que deberá arrancar con el debugger (en modo debug) y los dos últimos parámetros son punteros a las estructuras (STARTUPINFO y PROCESS_INFORMATION) que determinarán la forma en que el proceso debe comenzar y nos darán información tras el arranque del proceso.
Vamos a crear en Python tres programas, un script que definirá las dos estructuras que hemos mencionado antes (mis_definiciones_debugger.py), un script que será el depurador (mi_debugger.py) y un script de prueba para verificar el buen funcionamiento de los dos scripts anteriores.
mis_definiciones_debugger.py
from ctypes import *
WORD = c_ushort
DWORD = c_ulong
LPBYTE = POINTER(c_ubyte)
LPTSTR = POINTER(c_char)
HANDLE = c_void_p
DEBUG_PROCESS = 0x00000001
CREATE_NEW_CONSOLE = 0x00000010
class STARTUPINFO(Structure):
_fields_ = [
("cb", DWORD),
("lpReserved",LPTSTR),
("lpDesktop",LPTSTR),
("lpTitle",LPTSTR),
("dwX",DWORD),
("dwY",DWORD),
("dwXSize",DWORD),
...
Estado de los registros
Una de las principales ventajas de un depurador es poder visualizar el contenido de los registros. Cuando aparece una excepción, debemos ser capaces de determinar el estado de la pila desde cualquier lugar y en cualquier momento.
Para hacer esto, debemos obtener la información del hilo (thread) actual.
La función OpenThread() nos puede ayudar aquí, indicando los parámetros adecuados. Esta función es muy similar a OpenProcess() pero en lugar de transmitirle el PID, tendremos que transmitirle el TID (Thread Identifier).
1. Enumeración de los hilos (threads)
Por esto, debemos ser capaces de enumerar todos los hilos (threads) del proceso. Para esto tenemos a nuestra disposición la función CreateToolhelp32Snapshot() de la DLL kernel32.dll. Podremos, por tanto, obtener por ejemplo la lista de procesos, hilos (threads) y las DLL cargadas.
El parámetro dwFlags indicará a la función lo que deseamos ver (threads, DLL, proceso, heap).
Iniciaremos este parámetro con TH32CS_SNAPTHREAD (valor 0x00000004) para ver los hilos.
El otro parámetro, th32ProcessID, es simplemente el PID del proceso.
Cuando la función tiene éxito, nos devuelve el handle del objeto snapshot.
Una vez que tenemos la lista de threads, podemos enumerarlos.
Para comenzar la enumeración, utilizaremos Thread32First() que recibe dos parámetros: Hsnapshot, el handle devuelto...
Los eventos del debugger
Vamos a regresar a la función WaitForDebugEvent() que nos devolverá la estructura DEBUG_EVENT.
Esta estructura contiene mucha información donde dwDebugEventCode centrará en particular nuestra atención, ya que nos indica el tipo de evento que tuvo lugar.
Mirando el valor de dwDebugEventCode, nos permitirá determinar el evento.
Código del evento |
Valor del código |
Valor de la unión u |
0x1 |
EXCEPTION_DEBUG_EVENT |
u.Exception |
0x2 |
CREATE_THREAD_DEBUG_EVENT |
u.CreateThread |
0x3 |
CREATE_PROCESS_DEBUG_EVENT |
u.CreateProcessInfo |
0x4 |
EXIT_THREAD_DEBUG_EVENT |
u.ExitThread |
0x5 |
EXIT_PROCESS_DEBUG_EVENT |
u.ExitProcess |
0x6 |
LOAD_DLL_DEBUG_EVENT |
u.LoadDll |
0x7 |
UNLOAD_DLL_DEBUG_EVENT |
u.UnloadDll |
0x8 |
OUTPUT_DEBUG_STRING_EVENT |
u.DebugString |
0x9 |
RIP_EVENT |
u.RipInfo |
Añadimos ahora algunas definiciones a mi_debugger3.py, que se convierte en mi_debugger_final.py
Usaremos mi_debugger_final.py hasta el final de este capítulo, las funciones que veremos más adelante ya se han definido.
mi_debugger_final.py
from ctypes import *
from mis_definiciones_debugger_final import *
import sys
import time
kernel32 = windll.kernel32
class debugger():
def __init__(self):
self.h_process = None
self.pid = None
self.debugger_active = False
self.h_thread = None
self.context = None
self.breakpoints = {}
self.first_breakpoint= True
self.hardware_breakpoints = {}
system_info = SYSTEM_INFO()
kernel32.GetSystemInfo(byref(system_info))
self.page_size = system_info.dwPageSize
self.guarded_pages...
Los puntos de parada (breakpoints)
1. Puntos de parada software
Para colocar puntos de parada, debemos ser capaces de escribir y leer en la memoria.
La función ReadProcessMemory() nos puede ayudar, así como WriteProcessMemory().
Empleando estas dos funciones, podemos inspeccionar la memoria.
Los parámetros que debemos proporcionar son lpBaseAddress (dirección donde queremos empezar a leer o escribir), lpBuffer (puntero hacia el dato que queremos leer o escribir), nSize (número total de bytes que queremos leer o escribir).
Usando estas dos funciones, podremos emplear puntos de parada con facilidad en nuestro debugger.
Por lo general, los puntos de parada se ubican en una llamada de función. Para el ejercicio, utilizaremos la llamada a printf().
Para determinar la dirección virtual de una función, utilizaremos GetProcessAddress() que se exportará de kernel32.dll.
Necesitaremos la cabecera de la función de la llamada en la que deseamos colocar el punto de parada; GetModuleHandle() nos ayudará.
Puede ver las definiciones de read_process_memory(), write_process_memory(), bp_set() y func_resolve() en mi_debugger_final.py si desea conocer más detalles al respecto.
Para permitirnos probar el script que hará el bucle con un printf que llamaremos bucle_printf.py, vamos a escribir otro script, mi_test5.py.
mi_test5.py
import mi_debugger_final
debugger=mi_debugger_final.debugger() ...
La librería PyDbg
Hemos visto en las secciones anteriores cómo configurar puntos de parada, pero ¿cómo hacerlo con PyDbg?
Aquí nos podrá ayudar la función bp_set(address, descriptor= ’’ ’’, restore= True,handler=NONE).
El argumento address es la dirección donde queremos configurar el punto de parada software.
El parámetro descriptor es opcional y puede utilizarse para dar un nombre al punto de parada.
El parámetro restore determina si el punto de parada debe ser restaurado a cero de forma automática, después de haber recuperado la cabecera y el parámetro handler define a qué función llamar.
Toda la información de contexto, hilos y procesos será provista por esta clase al llamar a la función.
Usaremos el script bucle_printf.py de las secciones anteriores y, mediante un nuevo script, vamos a leer los valores del contador y sustituirlos por un valor aleatorio comprendido entre 1 y 100.
Vamos a consultar, leer y manipular los datos del proceso objetivo en tiempo real.
printf_random.py
from pydbg import *
from pydbg.defines import *
import struct
import random
def printf_randomizer(dbg):
# Lectura del valor del contador en ESP + 0x8 como un DWORD
parameter_addr = dbg.context.Esp + 0x8
counter = dbg.read_process_memory(parameter_addr,4)
counter = struct.unpack("L",counter)[0]
print "Counter: %d" % int(counter)
random_counter = random.randint(1,100)
random_counter = struct.pack("L",random_counter)[0]
dbg.write_process_memory(parameter_addr,random_counter)
return DBG_CONTINUE
dbg = pydbg()
pid = raw_input("Enter the printf_loop.py PID: ")
dbg.attach(int(pid))
printf_address = dbg.func_resolve("msvcrt","printf")
dbg.bp_set(printf_address,description="printf_address",
handler=pri ntf_randomizer)
dbg.run()
Resultado obtenido por el bucle_printf.py
E:\temp\cap4> python bucle_printf.py
iteración 0 del bucle ...
Puesta en práctica: Hooking
Enunciado
Requisitos previos: PyDbg
Objetivo: crear un hook de Internet Explorer.
Enunciado:
Un hook (literalmente gancho o anzuelo) permite al usuario de un software personalizar el funcionamiento de este último, haciendo que se ejecuten acciones adicionales en momentos determinados.
Deseamos interceptar (sniff) las conexiones SSL de Internet Explorer que utiliza CryptoAPI de Microsoft.
Solución
hook_ssl_pydbg.py
from pydbg import *
from pydbg.defines import *
import utils
import sys
import re
import struct
def ssl_sniff_encryptmessage( dbg, args ):
buffer = ""
addr_bufflen=dbg.context.Esp + 0x2C
bufflen=struct.unpack('L',dbg.read_process_memory(addr_bufflen , 4))[0]
addr=dbg.context.Esp + 0x34
addr_buffer = struct.unpack('L',dbg.read_process_memory(addr, 4))[0]
try:
buffer=dbg.read_process_memory(addr_buffer,bufflen)
buffer=re.sub('.*gzip.*\r\n','',buffer)
dbg.write_process_memory(addr_buffer, buffer)
except:
pass
print "Comando: \n\n%s" % buffer ...