
Ferramenta para fazer man-in-the-middle em memória
A ferramenta MemITM (Mem In The Middle) foi desenvolvida para intercetar facilmente "mensagens" na memória de processos Windows. Desenvolvemos muitas ferramentas de interceção de memória personalizadas para capturar mensagens de rede antes da encriptação, ou mensagens IPC, e para poder inspecioná-las ou alterá-las para fazer fuzzing. Cada ferramenta era muito personalizada, não genérica, implementada em C/ASM e não era fácil de usar/manter/adaptar.
A ferramenta MemITM foi desenvolvida para resolver estes problemas e consiste em:
Podes descarregá-lo (fontes + compilado) aqui: https://github.com/Amossys/MemITM
Digamos que queres intercetar escritas em ficheiros (há formas mais simples de o fazer, mas é para o exemplo) num processo Windows específico. As escritas em ficheiros são realizadas pela função WriteFile, que é exportada por kernelbase.dll, e segue o esquema BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, ...). WriteFile é uma função __fastcall: lpBuffer é apontado por RDX e NumberOfBytesToWrite por R8. Vamos intercetar estes buffers.
abre o teu ficheiro "kernelbase.dll" no IDA Pro, carrega o script generate.idapython.py, e simplesmente executa getHook(LocByName("WriteFile"), "rdx","r8") seguido de updateConfig("config.bin").
executa um notepad e depois, python memitm.py notepad.exe config.bin.
É isso! Um hook foi colocado na função WriteFile, e as funções "logger" e "fuzzer" do memitm.py receberão uma cópia do buffer.
A função getHook permite especificar registos para o buffer e o seu tamanho, mas em alguns casos pode não ser esse o caso. Digamos que queres intercetar syscalls NtCreateFile e obter o nome do ficheiro. NtCreateFile segue o esquema NTSTATUS NtCreateFile( OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, ...), e o nome do ficheiro pode ser obtido por ObjectAttributes->ObjectName->Buffer (o tamanho é ObjectAttributes->ObjectName->Length * sizeof(WCHAR)).
getHook também permite especificar um shellcode no seu parâmetro t1customOpcodes em vez de registos. Este shellcode deve:
RCX;RDX;No caso do NtCreateFile, podemos simplesmente executar este código ASM para fazer o trabalho:
mov rax, [r8+0x10] ; rax é agora ObjectAttributes->ObjectName
mov rcx, [rax + 0x10] ; rcx é agora ObjectAttributes->ObjectName->Buffer
mov rdx, [rax] ; rdx é agora ObjectAttributes->ObjectName->Length
and rdx, 0xFFFF ; Length é um USHORT
shl rdx, 1 ; e deve ser *2
Vamos montar isto (usando, por exemplo, o website de desmontagem online) : 498B4010488B4810488B104881E2FFFF000048D1E2. A nossa chamada getHook deve agora ser getHook(LocByName("NtCreateFile", t1customOpcodes="498B…E2").
Sim! O ficheiro de config pode incorporar várias definições de pontos de interceção, e a função getHook anexa o ficheiro. Podes definir pontos de interceção em múltiplos módulos. Por exemplo, podes intercetar mensagens ALPC e IOCTL ao mesmo tempo.
Poderás diferenciá-los pelo seu "ID de mensagem" nos callbacks python.
Não te preocupes, há também um simples servidor HTTP (logserver.py) e a função httpNetSend que te permitem enviar os teus casos de teste (e modificações) para um host remoto em tempo real.
Bem, podes começar por usar a função bufferBitFlip para... inverter vários bits. Registar mensagens brutas permitir-te-á escrever o teu próprio dissector e começar a implementar manualmente um melhor fuzzer :). Lembra-te: não podes alterar o tamanho do buffer!
Por exemplo, no nosso exemplo WriteFile, a seguinte função fuzzer substituirá "hello" por "world":
def fuzzer(data, msgID=None, pid = 0):
return data.replace("hello","world")
Por exemplo, para mensagens ALPC, podes ter a seguinte configuração 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
O script IDA Pro incorpora um motor de "hooking" simples, que permite mover várias instruções para uma área específica (derivado da nossa ferramenta DIMCT). Deve lidar com muitos casos extremos, como instruções relativas à memória, referências cruzadas, etc., e provavelmente mostrará muitos "can't install hook here" se tentares intercetar blocos básicos realmente pequenos (<5 bytes para binários x86, <12 bytes para x64) que contêm múltiplas referências cruzadas. Para mais informações, basta ler o código-fonte :).
Gera a seguinte informação no ficheiro de config:
RCX/RDX e chama a rotina de log da DLL;O injetor DLL é um simples injetor DLL (coisas CreateRemoteThread/LoadLibraryA).
A própria DLL inicia uma área de memória partilhada e aguarda pelos dados da config (a área de memória partilhada tem 3 campos genéricos: o ID da mensagem da memória partilhada, o tamanho da mensagem e o buffer da mensagem). Uma vez recebida, analisa e instala os hooks na memória, e nenhuma atualização da config será permitida depois disso (deves matar o processo se quiseres configurar novos hooks). Qualquer hook colocado irá parar na função DLL genericHookFunction. Esta função apenas preenche a área de memória (protegida com secções críticas) com os dados do buffer, o seu tamanho e o ID da mensagem (definido nos bits mais altos do campo ID da mensagem da memória partilhada). Para notificar o processo python, são usados 2 eventos para sinalizar novas mensagens e para aguardar que o processo as corrija (timeout de 2 segundos). Se o buffer foi atualizado, o buffer original também é modificado.
O script memitm.py executa o processo injetor e aguarda que a memória partilhada seja criada. Depois envia a configuração e aguarda o evento de mensagem. Quando recebido, a memória partilhada é lida e as funções logger e fuzzer são chamadas com o buffer, o ID do processo e o ID da mensagem. Se a função logger retornar um buffer diferente do original e o seu tamanho for igual, é escrito na área de memória partilhada. O evento "ACK" é então sinalizado, e o script aguarda uma nova mensagem.
Esperamos que esta ferramenta seja útil, quaisquer contribuições/revisões serão apreciadas! Além disso, se és francês e gostas de Rennes, estamos a contratar :)