
Herramienta para realizar man-in-the-middle en memoria
La herramienta MemITM (Mem In The Middle) ha sido desarrollada para interceptar fácilmente "mensajes" en la memoria de procesos de Windows. Desarrollamos muchas herramientas personalizadas de intercepción de memoria para capturar mensajes de red antes del cifrado, o mensajes de IPC, y poder inspeccionarlos o alterarlos para hacer fuzzing. Cada herramienta era realmente personalizada, no genérica, implementada en C/ASM y no era fácil de usar/mantener/adaptar.
La herramienta MemITM ha sido desarrollada para abordar estos problemas, y consiste en:
Puedes descargarlo (fuentes + compilado) aquí: https://github.com/Amossys/MemITM
Supongamos que deseas interceptar escrituras de archivos (hay formas más simples de hacerlo, pero es para el ejemplo) en un proceso específico de Windows. Las escrituras de archivos se realizan mediante la función WriteFile, que es exportada por kernelbase.dll, y sigue el esquema BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, ...). WriteFile es una función __fastcall : lpBuffer es apuntado por RDX y NumberOfBytesToWrite por R8. Interceptemos estos búferes.
abre tu archivo "kernelbase.dll" en IDA Pro, carga el script generate.idapython.py, y simplemente ejecuta getHook(LocByName("WriteFile"), "rdx","r8") seguido de updateConfig("config.bin").
ejecuta un bloc de notas y luego, python memitm.py notepad.exe config.bin.
¡Eso es todo! Se ha colocado un hook en la función WriteFile, y las funciones "logger" y "fuzzer" de memitm.py recibirán una copia del búfer.
La función getHook permite especificar registros para el búfer y su tamaño, pero en algunos casos no es así. Supongamos que deseas interceptar llamadas al sistema NtCreateFile y obtener el nombre del archivo. NtCreateFile sigue el esquema NTSTATUS NtCreateFile( OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, ...), y el nombre del archivo se puede obtener mediante ObjectAttributes->ObjectName->Buffer (el tamaño es ObjectAttributes->ObjectName->Length * sizeof(WCHAR)).
getHook también permite especificar un shellcode en su parámetro t1customOpcodes en lugar de registros. Este shellcode debe :
RCX;RDX;En el caso de NtCreateFile, podemos simplemente ejecutar este código ASM para hacer el trabajo:
mov rax, [r8+0x10] ; rax es ahora ObjectAttributes->ObjectName
mov rcx, [rax + 0x10] ; rcx es ahora ObjectAttributes->ObjectName->Buffer
mov rdx, [rax] ; rdx es ahora ObjectAttributes->ObjectName->Length
and rdx, 0xFFFF ; Length es un USHORT
shl rdx, 1 ; y debe ser *2
Montemos esto (usando por ejemplo el sitio web de desensamblado en línea) : 498B4010488B4810488B104881E2FFFF000048D1E2. Nuestra llamada a getHook debería ser ahora getHook(LocByName("NtCreateFile", t1customOpcodes="498B…E2").
¡Sí! El archivo de configuración puede incluir múltiples definiciones de puntos de intercepción, y la función getHook añade al archivo. Puedes establecer puntos de intercepción en múltiples módulos. Por ejemplo, puedes interceptar mensajes ALPC e IOCTL al mismo tiempo.
Podrás diferenciarlos por su "ID de mensaje" en los callbacks de Python.
No te preocupes, también hay un servidor HTTP simple (logserver.py) y la función httpNetSend que te permiten enviar tus casos de prueba (y modificación) a un host remoto en tiempo real.
Bueno, puedes empezar usando la función bufferBitFlip para... voltear varios bits. Registrar mensajes sin procesar te permitirá escribir tu propio disector y comenzar a implementar manualmente un mejor fuzzer :). Recuerda: ¡no puedes cambiar el tamaño del búfer!
Por ejemplo, en nuestro ejemplo de WriteFile, la siguiente función fuzzer reemplazará "hello" por "world":
def fuzzer(data, msgID=None, pid = 0):
return data.replace("hello","world")
Por ejemplo, para mensajes ALPC, podrías tener la siguiente configuración de memitm.py:
def logger(data, msgID=None, pid=0):
totalLen = 0
dataLen = 0
typ = 0
dataInfoOffset = 0
cid = 0
tid = 0
messageID = 0
clientViewSize = 0
# skip the PORT_MESSAGE header (0x18 bytes)
if len(data) > 0x18:
totalLen = struct.unpack("<H",data[:2])[0]
dataLen = struct.unpack("<H",data[2:4])[0]
typ = struct.unpack("<H",data[4:6])[0]
dataInfoOffset = struct.unpack("<H",data[6:8])[0]
zeroInit = struct.unpack("<L",data[4:8])[0]
cid = struct.unpack("<L",data[8:0xC])[0]
tid = struct.unpack("<L",data[0xC:0x10])[0]
messageID = struct.unpack("<L",data[0x10:0x14])[0]
clientViewSize = struct.unpack("<L",data[0x14:0x18])[0]
logMessage = "ALPC message : %x:%x - %x:%x - %d:%d - %x - %x" % (totalLen, dataLen, typ, dataInfoOffset, cid, tid, messageID, clientViewSize)
print logMessage
return
El script de IDA Pro incorpora un simple motor de "hooking", que permite mover varias instrucciones a un área específica (derivado de nuestra herramienta DIMCT). Debe manejar muchos casos límite, como instrucciones relativas a la memoria, referencias cruzadas, etc. y probablemente mostrará muchos "no se puede instalar hook aquí" si intentas interceptar bloques básicos realmente pequeños (<5 bytes para binarios x86, <12 bytes para x64) que contienen múltiples referencias cruzadas. Para más información, solo lee el código fuente :).
Genera la siguiente información en el archivo de configuración:
RCX/RDX y llama a la rutina de registro DLL;El inyector DLL es un simple inyector DLL (cosas de CreateRemoteThread/LoadLibraryA).
La DLL misma inicia un área de memoria compartida y espera los datos de configuración (el área de memoria compartida tiene 3 campos genéricos: el ID de mensaje de memoria compartida, el tamaño del mensaje y el búfer del mensaje). Una vez recibidos, analiza e instala los hooks en memoria, y no se permitirá ninguna actualización de configuración después de esto (debes matar el proceso si deseas establecer nuevos hooks). Cualquier hook colocado terminará en la función DLL genericHookFunction. Esta función simplemente llena el área de memoria (protegida con secciones críticas) con los datos del búfer, su tamaño y el ID de mensaje (establecido en los bits superiores del campo ID de mensaje de memoria compartida). Para notificar al proceso de Python, se utilizan 2 eventos para señalar nuevos mensajes y esperar a que el proceso los parchee (el tiempo de espera es de 2 segundos). Si el búfer ha sido actualizado, el búfer original también se modifica.
El script memitm.py ejecuta el proceso inyector y espera a que se cree la memoria compartida. Luego envía la configuración y espera el evento de mensaje. Cuando se recibe, se lee la memoria compartida y se llaman a las funciones logger y fuzzer con el búfer, el ID del proceso y el ID del mensaje. Si la función logger devuelve un búfer diferente al original y su tamaño es igual, se escribe en el área de memoria compartida. Luego se señala el evento "ACK", y el script espera un nuevo mensaje.
Esperamos que esta herramienta sea útil, ¡cualquier contribución/revisión será apreciada! Además, si eres francés y te gusta Rennes, estamos contratando :)