
Outil pour réaliser un homme du milieu en mémoire
L'outil MemITM (Mem In The Middle) a été développé pour intercepter facilement les "messages" dans la mémoire des processus Windows. Nous avons développé de nombreux outils d'interception mémoire personnalisés pour capturer les messages réseau avant chiffrement, ou les messages IPC, et pouvoir les inspecter ou les modifier pour faire du fuzzing. Chaque outil était vraiment spécifique, non générique, implémenté en C/ASM et n'était pas facile à utiliser/maintenir/ adapter.
L'outil MemITM a été développé pour résoudre ces problèmes et se compose de :
Vous pouvez le télécharger (sources + compilé) ici : https://github.com/Amossys/MemITM
Disons que vous souhaitez intercepter les écritures de fichiers (il existe des moyens plus simples de le faire, mais c'est pour l'exemple) dans un processus Windows spécifique. Les écritures de fichiers sont effectuées par la fonction WriteFile, qui est exportée par kernelbase.dll, et suit le schéma BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, ...). WriteFile est une fonction __fastcall : lpBuffer est pointé par RDX et NumberOfBytesToWrite par R8. Interceptons ces buffers.
ouvrez votre fichier "kernelbase.dll" dans IDA Pro, chargez le script generate.idapython.py, et exécutez simplement getHook(LocByName("WriteFile"), "rdx","r8") suivi de updateConfig("config.bin").
lancez un bloc-notes, puis python memitm.py notepad.exe config.bin.
C'est tout ! Un hook a été placé sur la fonction WriteFile, et les fonctions "logger" et "fuzzer" de memitm.py recevront une copie du buffer.
La fonction getHook permet de spécifier des registres pour le buffer et sa taille, mais dans certains cas, ce n'est pas possible. Disons que vous souhaitez intercepter les appels système NtCreateFile et récupérer le nom de fichier. NtCreateFile suit le schéma NTSTATUS NtCreateFile( OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, ...), et le nom du fichier peut être obtenu par ObjectAttributes->ObjectName->Buffer (la taille est ObjectAttributes->ObjectName->Length * sizeof(WCHAR)).
getHook permet également de spécifier un shellcode dans son paramètre t1customOpcodes au lieu de registres. Ce shellcode doit :
RCX ;RDX ;Dans le cas de NtCreateFile, on peut simplement exécuter ce code ASM pour faire le travail :
mov rax, [r8+0x10] ; rax est maintenant ObjectAttributes->ObjectName
mov rcx, [rax + 0x10] ; rcx est maintenant ObjectAttributes->ObjectName->Buffer
mov rdx, [rax] ; rdx est maintenant ObjectAttributes->ObjectName->Length
and rdx, 0xFFFF ; Length est un USHORT
shl rdx, 1 ; et doit être multiplié par 2
Assemblons cela (en utilisant par exemple le site web de désassemblage en ligne) : 498B4010488B4810488B104881E2FFFF000048D1E2. Notre appel à getHook devrait maintenant être : getHook(LocByName("NtCreateFile", t1customOpcodes="498B…E2").
Oui ! Le fichier de configuration peut embarquer plusieurs définitions de points d'interception, et la fonction getHook ajoute au fichier. Vous pouvez définir des points d'interception dans plusieurs modules. Par exemple, vous pouvez intercepter les messages ALPC et IOCTL en même temps.
Vous pourrez les différencier par leur "ID de message" dans les callbacks python.
Ne vous inquiétez pas, il y a aussi un simple serveur HTTP (logserver.py) et la fonction httpNetSend qui vous permettent d'envoyer vos cas de test (et modifications) vers un hôte distant en temps réel.
Eh bien, vous pouvez commencer par utiliser la fonction bufferBitFlip pour... inverser plusieurs bits. La journalisation des messages bruts vous permettra d'écrire votre propre dissecteur et de commencer à implémenter manuellement un meilleur fuzzer :). Rappelez-vous : vous ne pouvez pas changer la taille du buffer !
Par exemple, dans notre exemple WriteFile, la fonction fuzzer suivante remplacera "hello" par "world":
def fuzzer(data, msgID=None, pid = 0):
return data.replace("hello","world")
Par exemple, pour les messages ALPC, vous pouvez avoir la configuration memitm.py suivante :
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
Le script IDA Pro intègre un simple moteur de "hooking", qui permet de déplacer plusieurs instructions vers une zone spécifique (dérivé de notre outil DIMCT). Il doit gérer de nombreux cas particuliers, comme les instructions relatives à la mémoire, les références croisées, etc., et affichera probablement beaucoup de "can't install hook here" si vous essayez d'intercepter de très petits blocs de base (<5 octets pour les binaires x86, <12 octets pour les binaires x64) qui contiennent plusieurs références croisées. Pour plus d'informations, lisez simplement le code source :).
Il génère les informations suivantes dans le fichier de configuration :
RCX/RDX et appelle la routine de journalisation de la DLL ;L'injecteur DLL est un simple injecteur DLL (truc CreateRemoteThread/LoadLibraryA).
La DLL elle-même initie une zone de mémoire partagée et attend les données de configuration (la zone de mémoire partagée a 3 champs génériques : l'ID de message de la mémoire partagée, la taille du message et le buffer du message). Une fois reçu, elle analyse et installe les hooks en mémoire, et aucune mise à jour de configuration ne sera autorisée après cela (vous devez tuer le processus si vous voulez mettre en place de nouveaux hooks). Tout hook placé aboutira à la fonction DLL genericHookFunction. Cette fonction remplit simplement la zone mémoire (protégée par des sections critiques) avec les données du buffer, sa taille et l'ID de message (défini dans les bits de poids fort du champ ID de message de la mémoire partagée). Afin de notifier le processus python, 2 événements sont utilisés pour signaler de nouveaux messages et pour attendre que le processus les patche (timeout de 2 secondes). Si le buffer a été mis à jour, le buffer original est également modifié.
Le script memitm.py exécute le processus d'injection et attend que la mémoire partagée soit créée. Il envoie ensuite la configuration et attend l'événement de message. Lorsqu'il est reçu, la mémoire partagée est lue et les fonctions logger et fuzzer sont appelées avec le buffer, l'ID du processus et l'ID du message. Si la fonction logger renvoie un buffer différent de l'original et que sa taille est égale, il est écrit dans la zone de mémoire partagée. L'événement "ACK" est alors signalé, et le script attend un nouveau message.
Nous espérons que cet outil sera utile, toute contribution/relecture sera appréciée ! Aussi, si vous êtes français et aimez Rennes, nous recrutons :)