
Tool zur Durchführung eines Man-in-the-Middle im Speicher
Das MemITM (Mem In The Middle) Tool wurde entwickelt, um "Nachrichten" im Speicher von Windows-Prozessen einfach abzufangen. Wir haben viele benutzerdefinierte Speicher-Interception-Tools entwickelt, um Netzwerknachrichten vor der Verschlüsselung oder IPC-Nachrichten zu erfassen und sie inspizieren oder verändern zu können, um Fuzzing durchzuführen. Jedes Tool war sehr spezifisch, nicht generisch, in C/ASM implementiert und nicht einfach zu verwenden/zu warten/anzupassen.
Das MemITM-Tool wurde entwickelt, um diese Probleme zu lösen, und besteht aus:
Sie können es hier herunterladen (Quellen + kompiliert): https://github.com/Amossys/MemITM
Angenommen, Sie möchten Dateischreibvorgänge in einem bestimmten Windows-Prozess abfangen (es gibt einfachere Wege, dies zu tun, aber das dient als Beispiel). Dateischreibvorgänge werden von der Funktion WriteFile durchgeführt, die von kernelbase.dll exportiert wird, und folgt dem Schema BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, ...). WriteFile ist eine __fastcall-Funktion: lpBuffer wird von RDX und NumberOfBytesToWrite von R8 gezeigt. Lassen Sie uns diese Puffer abfangen.
Öffnen Sie Ihre kernelbase.dll-Datei in IDA Pro, laden Sie das Skript generate.idapython.py und führen Sie einfach getHook(LocByName("WriteFile"), "rdx","r8") gefolgt von updateConfig("config.bin") aus.
Starten Sie einen Editor (notepad) und führen Sie dann python memitm.py notepad.exe config.bin aus.
Das war's! Ein Hook wurde auf die Funktion WriteFile gesetzt, und die "Logger"- und "Fuzzer"-Funktionen von memitm.py erhalten eine Kopie des Puffers.
Die Funktion getHook erlaubt die Angabe von Registern für den Puffer und seine Größe, aber in manchen Fällen ist das nicht der Fall. Angenommen, Sie möchten NtCreateFile-Syscalls abfangen und den Dateinamen erhalten. NtCreateFile folgt dem Schema NTSTATUS NtCreateFile( OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, ...), und der Dateiname kann durch ObjectAttributes->ObjectName->Buffer (Größe ist ObjectAttributes->ObjectName->Length * sizeof(WCHAR)) erhalten werden.
getHook erlaubt auch die Angabe eines Shellcodes im Parameter t1customOpcodes anstelle von Registern. Dieser Shellcode muss:
RCX legen;RDX legen;Im Fall von NtCreateFile können wir einfach diesen ASM-Code ausführen, um die Arbeit zu erledigen:
mov rax, [r8+0x10] ; rax ist jetzt ObjectAttributes->ObjectName
mov rcx, [rax + 0x10] ; rcx ist jetzt ObjectAttributes->ObjectName->Buffer
mov rdx, [rax] ; rdx ist jetzt ObjectAttributes->ObjectName->Length
and rdx, 0xFFFF ; Length ist ein USHORT
shl rdx, 1 ; und muss *2 getan werden
Lassen Sie uns dies assemblieren (z. B. mit der Online-Disassemblierungs-Website): 498B4010488B4810488B104881E2FFFF000048D1E2. Unser getHook-Aufruf sollte nun sein: getHook(LocByName("NtCreateFile", t1customOpcodes="498B…E2").
Ja! Die Config-Datei kann mehrere Definitionen von Interceptionspunkten enthalten, und die Funktion getHook hängt sie an die Datei an. Sie können Interceptionspunkte in mehreren Modulen setzen. Sie können zum Beispiel gleichzeitig ALPC- und IOCTL-Nachrichten abfangen.
Sie können sie durch ihre "message ID" in den Python-Callbacks unterscheiden.
Keine Sorge, es gibt auch einen einfachen HTTP-Server (logserver.py) und die Funktion httpNetSend, die es Ihnen ermöglicht, Ihre Testfälle (und Änderungen) in Echtzeit an einen entfernten Host zu senden.
Nun, Sie können beginnen, die Funktion bufferBitFlip zu verwenden, um... ein paar Bits zu kippen. Das Loggen von Roh-Nachrichten ermöglicht es Ihnen, Ihren eigenen Dissector zu schreiben und manuell einen besseren Fuzzer zu implementieren :). Denken Sie daran: Sie können die Puffergröße nicht ändern!
Zum Beispiel wird in unserem WriteFile-Beispiel die folgende fuzzer-Funktion "hello" durch "world" ersetzen:
def fuzzer(data, msgID=None, pid = 0):
return data.replace("hello","world")
Zum Beispiel könnten Sie für ALPC-Nachrichten das folgende memitm.py-Setup haben:
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
Das IDA Pro-Skript enthält eine einfache "Hooking"-Engine, die es ermöglicht, mehrere Anweisungen in einen bestimmten Bereich zu verschieben (abgeleitet von unserem DIMCT-Tool). Es muss viele Randfälle behandeln, wie speicherrelative Anweisungen, Querverweise usw., und wird wahrscheinlich viele "can't install hook here"-Meldungen anzeigen, wenn Sie versuchen, wirklich kleine Basisblöcke (<5 Bytes für x86-Binärdateien, <12 Bytes für x64-Binärdateien) abzufangen, die mehrere Querverweise enthalten. Für weitere Informationen lesen Sie einfach den Quellcode :).
Es erstellt die folgenden Informationen in der Config-Datei:
RCX/RDX platziert und die DLL-Log-Routine aufruft;Der DLL-Injektor ist ein einfacher DLL-Injektor (CreateRemoteThread/LoadLibraryA-Zeug).
Die DLL selbst initialisiert einen Shared-Memory-Bereich und wartet auf die Config-Daten (der Shared-Memory-Bereich hat 3 generische Felder: die Shared-Memory-Nachrichten-ID, die Nachrichtengröße und den Nachrichtenpuffer). Sobald sie empfangen werden, parst sie die Config und installiert die Hooks im Speicher, und danach ist keine Config-Aktualisierung mehr erlaubt (Sie müssen den Prozess beenden, wenn Sie neue Hooks einrichten möchten). Jeder gesetzte Hook landet in der DLL-Funktion genericHookFunction. Diese Funktion füllt den Speicherbereich (geschützt mit kritischen Abschnitten) mit den Pufferdaten, seiner Größe und der Nachrichten-ID (gesetzt in höheren Bits des Shared-Memory-Nachrichten-ID-Feldes). Um den Python-Prozess zu benachrichtigen, werden 2 Ereignisse verwendet, um neue Nachrichten zu signalisieren und auf das Patchen durch den Prozess zu warten (Timeout beträgt 2 Sekunden). Wenn der Puffer aktualisiert wurde, wird auch der ursprüngliche Puffer modifiziert.
Das Skript memitm.py führt den Injektorprozess aus und wartet auf die Erstellung des Shared-Memorys. Es sendet dann die Konfiguration und wartet auf das Nachrichten-Ereignis. Wenn es empfangen wird, wird der Shared-Memory gelesen und die Funktionen logger und fuzzer werden mit dem Puffer, der Prozess-ID und der Nachrichten-ID aufgerufen. Wenn die logger-Funktion einen anderen Puffer als den ursprünglichen zurückgibt und seine Größe gleich ist, wird er in den Shared-Memory-Bereich geschrieben. Dann wird das "ACK"-Ereignis signalisiert, und das Skript wartet auf eine neue Nachricht.
Wir hoffen, dass dieses Tool nützlich sein wird, jede Mitarbeit/Überprüfung ist willkommen! Auch falls Sie Franzose sind und Rennes mögen, wir stellen ein :)