MemITM(内存中间人)工具的开发目的是为了轻松拦截 Windows 进程内存中的“消息”。我们开发了许多自定义的内存拦截工具,用于在加密前捕获网络消息,或拦截 IPC 消息,并能够检查或修改它们以进行模糊测试。每个工具都高度定制、不通用,用 C/ASM 实现,且不易使用、维护和适配。
MemITM 工具旨在解决这些问题,其组成包括:
你可以在这里下载(源码 + 编译好的):https://github.com/Amossys/MemITM
假设你想拦截某个 Windows 进程中的文件写入操作(有更简单的方式,但这里只是举例)。文件写入由 WriteFile 函数完成,该函数由 kernelbase.dll 导出,遵循原型 BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, ...)。WriteFile 是一个 __fastcall 函数:lpBuffer 由 RDX 指向,NumberOfBytesToWrite 由 R8 指向。让我们拦截这些缓冲区。
在 IDA Pro 中打开你的 kernelbase.dll 文件,加载 generate.idapython.py 脚本,然后直接运行 getHook(LocByName("WriteFile"), "rdx","r8") 后再运行 updateConfig("config.bin")。
运行一个记事本,然后执行 python memitm.py notepad.exe config.bin。
这就完成了!在 WriteFile 函数上已放置了钩子,memitm.py 的“logger”和“fuzzer”函数将接收到缓冲区的副本。
getHook 函数允许通过寄存器指定缓冲区及其大小,但在某些情况下并非如此。假设你想拦截 NtCreateFile 系统调用并获取文件名。NtCreateFile 的原型为 NTSTATUS NtCreateFile( OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, ...),可以通过 ObjectAttributes->ObjectName->Buffer 获取文件名(大小为 ObjectAttributes->ObjectName->Length * sizeof(WCHAR))。
getHook 还允许在其 t1customOpcodes 参数中指定一段 shellcode 来代替寄存器。这段 shellcode 必须:
RCX 寄存器;RDX 寄存器;在 NtCreateFile 的情况下,我们可以直接运行以下汇编代码来完成工作:
mov rax, [r8+0x10] ; rax 现在指向 ObjectAttributes->ObjectName
mov rcx, [rax + 0x10] ; rcx 现在指向 ObjectAttributes->ObjectName->Buffer
mov rdx, [rax] ; rdx 现在为 ObjectAttributes->ObjectName->Length
and rdx, 0xFFFF ; Length 是 USHORT 类型
shl rdx, 1 ; 必须乘以 2
对这段代码进行汇编(例如使用在线反汇编网站):498B4010488B4810488B104881E2FFFF000048D1E2。此时 getHook 的调用应为 getHook(LocByName("NtCreateFile", t1customOpcodes="498B…E2")。
可以!配置文件中可以嵌入多个拦截点的定义,而 getHook 函数会向文件追加内容。你可以在多个模块中设置拦截点。例如,可以同时拦截 ALPC 和 IOCTL 消息。
你可以通过它们在 Python 回调函数中的“message ID”来区分它们。
别担心,还有一个简单的 HTTP 服务器(logserver.py)和 httpNetSend 函数,允许你将测试用例(及其修改)实时发送到远程主机。
你可以先使用 bufferBitFlip 函数来……翻转若干位。记录原始消息将允许你编写自己的解析器,并开始手动实现一个更好的模糊器 :) 。记住:你不能改变缓冲区的大小!
例如,在 WriteFile 的示例中,下面的 fuzzer 函数会将 "hello" 替换为 "world":
def fuzzer(data, msgID=None, pid = 0):
return data.replace("hello","world")
例如,对于 ALPC 消息,你可以设置如下的 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
# 跳过 PORT_MESSAGE 头部(0x18 字节)
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
IDA Pro 脚本嵌入了一个简单的“钩子”引擎,允许将若干条指令移动到一个特定区域(源自我们的 DIMCT 工具)。它必须处理许多边界情况,例如内存相对寻址指令、交叉引用等,并且如果你尝试拦截非常小的基本块(x86 二进制小于 5 字节,x64 二进制小于 12 字节)且包含多个交叉引用时,它可能会显示大量“无法在此处安装钩子”的信息。更多信息请阅读源代码 :)。
它会在配置文件中生成以下信息:
RCX/RDX 并调用 DLL 的日志例程;DLL 注入器是一个简单的 DLL 注入器(使用 CreateRemoteThread/LoadLibraryA 机制)。
DLL 本身会初始化一块共享内存区域,并等待配置数据(共享内存区域包含三个通用字段:共享内存消息 ID、消息大小和消息缓冲区)。收到配置后,它解析并在内存中安装钩子,此后不允许再更新配置(要设置新钩子必须终止进程)。任何已放置的钩子都会进入 DLL 的 genericHookFunction 函数。该函数将缓冲区数据、其大小和消息 ID(设置在共享内存消息 ID 字段的高位)填入内存区域(受关键段保护)。为了通知 Python 进程,使用两个事件来指示新消息到达,并等待进程对其进行修补(超时时间为 2 秒)。如果缓冲区已被更新,原来的缓冲区也会被修改。
memitm.py 脚本运行注入器进程,并等待共享内存被创建。然后它发送配置,并等待消息事件。收到事件后,读取共享内存,并调用 logger 和 fuzzer 函数,传入缓冲区、进程 ID 和消息 ID。如果 logger 函数返回的缓冲区与原缓冲区不同且大小相等,则将其写回共享内存区域。然后触发“ACK”事件,脚本等待下一个消息。
我们希望这个工具能派上用场,任何贡献/审查都将受到欢迎!另外,如果你是法国人并且喜欢雷恩,我们正在招聘 :)