
メモリ内で中間者攻撃を行うツール
MemITM(Mem In The Middle)ツールは、Windowsプロセスメモリ内の「メッセージ」を簡単にインターセプトするために開発されました。暗号化前のネットワークメッセージやIPCメッセージをキャプチャし、それらを検査したり、ファジングのために変更したりするために、多くのカスタムメモリインターセプションツールを開発してきました。各ツールは非常にカスタムで汎用性がなく、C/ASMで実装されており、使用・保守・適応が容易ではありませんでした。
MemITMツールはこれらの問題に対処するために開発され、以下で構成されています:
こちらからダウンロードできます(ソース+コンパイル済み):https://github.com/Amossys/MemITM
特定のWindowsプロセスにおけるファイル書き込みをインターセプトしたいとします(実現するにはもっと簡単な方法がありますが、例として)。ファイル書き込みは kernelbase.dll によってエクスポートされる WriteFile 関数によって行われ、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") を実行します。
notepadを実行し、次に 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 パラメータでシェルコードを指定することもできます。このシェルコードは:
RCX レジスタに配置する。RDX レジスタに配置する。NtCreateFile の場合、このASMコードを実行して処理できます:
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")。
はい!configファイルには複数のインターセプションポイント定義を埋め込むことができ、getHook 関数はファイルに追加します。複数のモジュールにインターセプションポイントを設定できます。例えば、ALPCメッセージとIOCTLメッセージを同時にインターセプトできます。
Pythonコールバック内で「メッセージ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
# 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
IDA Proスクリプトにはシンプルな「フッキング」エンジンが組み込まれており、いくつかの命令を特定の領域に移動できます(DIMCT ツールから派生)。メモリ相対命令や相互参照など、多くのコーナーケースを処理する必要があり、複数の相互参照を含む本当に小さな基本ブロック(x86バイナリで5バイト未満、x64で12バイト未満)をインターセプトしようとすると、「ここにフックをインストールできません」というメッセージが多数表示される可能性があります。詳細はソースコードをお読みください。
configファイルに次の情報を生成します:
RCX/RDX に配置し、DLLのログルーチンを呼び出すインターセプションフック領域(別名 "T1 トランポリン")DLLインジェクターはシンプルなDLLインジェクターです(CreateRemoteThread/LoadLibraryA の仕組み)。
DLL自体は共有メモリ領域を初期化し、configデータを待ちます(共有メモリ領域には3つの汎用フィールドがあります:共有メモリメッセージID、メッセージサイズ、メッセージバッファ)。受信後、configを解析してメモリにフックをインストールし、その後はconfigの更新は許可されません(新しいフックを設定するにはプロセスを強制終了する必要があります)。配置されたフックはすべて genericHookFunction DLL関数に着地します。この関数は、バッファデータ、そのサイズ、およびメッセージID(共有メモリメッセージIDフィールドの上位ビットに設定)でメモリ領域(クリティカルセクションで保護)を埋めるだけです。Pythonプロセスに通知するために、2つのイベントが使用され、新しいメッセージのシグナリングと、プロセスがそれらをパッチするのを待機します(タイムアウトは2秒)。バッファが更新された場合、元のバッファも変更されます。
memitm.py スクリプトはインジェクタープロセスを実行し、共有メモリが作成されるのを待ちます。その後、設定を送信し、メッセージイベントを待ちます。受信されると、共有メモリが読み取られ、logger および fuzzer 関数がバッファ、プロセスID、メッセージIDとともに呼び出されます。logger 関数が元のバッファとは異なるバッファを返し、そのサイズが等しい場合、共有メモリ領域に書き込まれます。その後 "ACK" イベントがシグナリングされ、スクリプトは新しいメッセージを待ちます。
このツールがお役に立てば幸いです。コントリビューションやレビューを歓迎します!また、もしフランス人でレンヌが好きなら、採用しています :)