
Beacon によって生成されたファイルをディスクの代わりにメモリへ書き込む CobaltStrike ツールキット
MemFiles は CobaltStrike 用のツールキットで、オペレーターが Beacon プロセスによって生成されたファイルをターゲットシステムのディスクに書き込むのではなく、メモリに書き込むことを可能にします。Windows 7、10、11 で正常にテストされており、対応するサーバーバージョンも問題なく動作するはずです。MemFiles は x64 Beacon のみに対応しています。
これは、NTDLL.dll 内の複数の異なる NtAPI をフックし、それらの API への呼び出しを Beacon プロセスメモリ空間に注入された関数にリダイレクトすることで実現されます。
MemFiles は、Beacon プロセス内にクリーンでアンフックされた NTDLL のコピーがあることを前提としています。EDR フックが残っている Beacon プロセスでの MemFiles の動作可能性については保証されません。MemFiles を使用する前に NTDLL を修復/更新してください!
MemFiles ツールキット内には「特別な」存在しないディレクトリが定義されています。この特別なディレクトリに書き込まれたファイルは MemFiles によってキャプチャされ、メモリに書き込まれ、その後 Teamserver にダウンロードできます。
MemFiles は、Beacon プロセス内で実行され、出力を特定のディレクトリに書き込むように指示できるほとんどの(すべてではありません)ツールと互換性があります。動作に昇格された特権は必要ありません。
これには以下が含まれます:
-BOF
-inline-executeAssembly などを使用してインラインで実行される .NET アセンブリ
-Inline-Execute-PE などを使用してインラインで実行される PE
これらはすべて、関連する NtAPI がフックされている Beacon プロセス内で実行されるため、互換性があります。
MemFiles は以下のようなものでは動作しません:
-execute-assembly
-shell
-run
これらはすべて、NtAPI がフックされていない別のプロセスを起動するため、互換性がありません。
MemFiles は、Beacon プロセス内で実行された場合、Rubeus、SharpHound、Procdump、Powershell などのツールで正常にテストされています。

リポジトリをクローンし、必要に応じて /PIC/Source/NtCreateFile.c と /PIC/Source/NtOpenFile.c の両方の 56 行目で定義されている hookdir 変数を変更します。この変数は、MemFiles に作成中のファイルをインターセプトする必要があることを知らせる「特別な」ディレクトリです。hookdir 変数はデフォルトで "redteam" に設定されています。この変数がターゲットシステム上の実際のディレクトリではないこと、および両方のファイルで同じであることを確認してください!

'make all' を実行して、必要な BOF と PIC 関数の両方をコンパイルします。
MemFiles.cna を CobaltStrike クライアントにロードします。CobaltStrike が実行されているディレクトリがユーザーによって書き込み可能であることを確認してください。MemFiles は、MemFiles が機能するために必要なデータの可用性を確保するために、そこにテキストファイル (memfiles.txt) を作成します。
MemFiles は、Teamserver にコールインする各新しい Beacon にインストールされるように構成できます。これは、MemFiles->Config メニュー項目を使用して実現されます。デフォルトでは、MemFiles は新しい Beacon に自動インストールされません。これはグローバル設定であることに注意してください。2 つのクライアントが Teamserver に接続され、両方に MemFiles.cna がロードされている場合、クライアント A が「Install on beacon initial」設定を切り替えると、その変更はクライアント B にも反映されます!

MemFiles は、BOF を実行する 4 つのターゲット向けコマンドと、プロジェクトのデータ構造を操作する 1 つの内部コマンドで構成されています。
ターゲット向け:
内部データ構造:
meminit は、Beacon プロセスに MemFiles をインストールする役割を担います。
MemFiles によってフックされる NtAPI のリストは次のとおりです:
meminit は以下の主要なアクションを実行します:
memlist は、特定の Beacon について MemFiles によって現在メモリに保存されているすべてのファイルを表示するために使用されます。

いくつかのフィールドが表示されます。ユーザーにとって最も関連性が高く関心のあるものは、ファイルの名前と保存されたデータの長さです。
memfetch は、特定の Beacon について MemFiles によってメモリに保存されたファイルを実際に取得するために使用されます。
デフォルトでは、memfetch は MemFiles によって保存され、「ハンドル」が閉じられているすべてのファイルを取得します。この設計上の選択は、プログラム/アプリケーションが書き込みを完了していないファイルをダウンロードしようとすることに関連する問題を回避するために行われました。
つまり、プログラム/アプリケーションがファイルに対して開いたハンドルを閉じない場合、そのファイルは memfetch によってダウンロードされません。
これは、memfetch で "force" 引数を使用することで軽減できます。つまり、ハンドルの状態に関係なくすべてのファイルをメモリから取得するには、'memfetch force' を使用します。
memfetch がメモリから取得したファイルはダウンロードとして Teamserver に送り返され、CobaltStrike の Downloads タブを介して Teamserver からクライアントに同期できます。
ファイルが Teamserver によってダウンロードされると、Beacon プロセスのメモリから消去され、そのエントリは memlist で表示されなくなります。
memclean は、Beacon プロセスから MemFiles をクリーンアップして削除する役割を担います。
MemFiles の標準的な使用例は、MemFiles をインストールし、Beacon の存続期間中インストールしたままにすることです。ただし、MemFiles をツールと組み合わせてファイル出力をキャプチャして取得し、その後 MemFiles をアンインストールしてそのアーティファクトがメモリに残らないようにしたい場合は、memclean を使用して、meminit が実行される前の元の状態に Beacon プロセスを戻すことができます。
これには以下が含まれます:
memclean がこれらのアクションを実行する前に、MemFiles によってメモリに保存されたファイルを強制的にダウンロードすることに注意してください。MemFiles を単一のツールで使用してから削除する場合は、memfetch の使用をスキップして、memclean だけを使用してファイルの取得と MemFiles の Beacon プロセスからの削除を一度に行うことができます。
memtable は、MemFiles が現在インストールされている Beacon に関する情報を表示および追跡するために使用されます。また、グローバル構成情報も表示します。
各 CobaltStrike クライアントには独自の memtable があります。MemFiles は、接続されているすべての CobaltStrike クライアント間でのデータの同期を確保するために多大な努力を払っており、すべてのオペレーターがすべての Beacon で MemFiles を使用できるようにしています。詳細については、「Design Considerations and Commentary」を参照してください。

meminit コマンドを使用して、Beacon に MemFiles を初期化します。これは、MemFiles->Config メニューでオプションを切り替えることによって自動的に行われるように構成できます。

MemFiles が初期化されると、お気に入りのツールを使用してファイルをメモリに書き込むことができます! その方法は特定のツールによって異なります。複数のファイルを出力するディレクトリを指定できるものもありますが、ツールによって作成される単一のファイルに絶対パスを指定できるものもあります。いくつかの例を以下に示します:
ここでは、SharpHound が生成したすべてのファイルを c:\redteam\ ディレクトリ(特別な MemFiles ディレクトリ)に出力し、ファイルを zip 圧縮しないように指定しています。MemFiles はメモリからファイルを読み取るプログラムをサポートしておらず、書き込みのみをサポートしているため、SharpHound の zip 機能は機能しません。

Rubeus では "dump" コマンドが使用され、すべてのコンソール出力を(特別なディレクトリにある)ファイルに送信するように指示します。

この例では、Inline-Execute-PE を使用して powershell.exe を Beacon プロセスにロードし、'Get-ADUser' を実行してドメインユーザーのリストを取得しています。パイプと 'out-file' を使用して、データをメモリに書き込んでから取得できます。

ファイルを取得するには、memfetch を実行します:

MemFiles の使用が終了したとき、および/または Beacon プロセスにインストールしたままにしたくないときは、memclean を実行します:

上記の例では、まだダウンロードされていないファイルがあったことに注意してください。memclean はこのファイルをダウンロードし、MemFiles をアンインストールする前にメモリから消去します。
memtable を使用して MemFiles のステータスと構成を照会します。長時間の操作中は、memtable から死んだ/古い Beacon のエントリをクリアして、乱雑にならないようにします。

はじめにで強調したように、MemFiles が機能するには Beacon プロセス内にクリーンな NTDLL のコピーが必要です。これは、NtFunction 内の元のバイトを読み取り、特定のバイトをトランポリンにコピーし、そのトランポリンが後で MemFiles が干渉すべきではない NtFunction への通常の呼び出しを完了するために使用されるためです。このテーマについては、「Technical Details, Design Considerations, and Commentary」セクションで詳しく説明します。
MemFiles は各ファイルに対して最初に 1048576 バイトを割り当てます。データがメモリに書き込まれると、より大きなファイルを保持するために、必要に応じてこの割り当てを拡張できます。
MemFiles 構造体に保存されるファイル名は、置換 NtCreateFile 関数に渡される引数から解析されます。MemFiles はこれをかなり単純な方法で行います。ファイルパス引数で「特別な」ディレクトリを見つけ、その末尾に移動し、「特別な」ディレクトリとファイル名を区切る '\' 文字を考慮してポインタを 1 つインクリメントします。たとえば、パス 'C:\users\tom\redteam\myfile.txt' では、MemFiles は 'redteam' を見つけ、バックスラッシュ文字を考慮し、ファイル名として 'myfile.txt' を選択します。
MemFiles はファイルパス内の先行ディレクトリを気にしません。'C:\redteam\myfile.txt' と 'c:\users\tom\appdata\local\redteam\myfile.txt' は、MemFiles にとって同様に有効なパスです。
MemFiles がファイルパスからファイル名を解析する方法を考えると、MemFiles はサブディレクトリの作成をサポートしていないことに注意する必要があります。つまり、MemFiles は、たとえば c:\redteam\mynewdir\file1.txt、c:\redteam\mynewdir\file2.txt、c:\redteam\mysecondir\file3.txt などを作成しようとするツールでは正しく機能しません。
MemFiles によってフックされる NtAPI は、さまざまなプログラムが I/O 操作に使用するものとして識別されています。前述のように、MemFiles/このフックされた NtAPI のセットで正常にテストされたツール/機能は、SharpHound、Rubeus、Powershell、Procdump、BOF、ファイル書き込み操作を実行する一般的な C プログラム、および CobaltStrike の bupload_raw コマンド(オペレーターがリモートファイルの場所を指定できる)です。確かに、MemFiles をそのまま動作させる他のツールもあります。他のものは互換性がありません。
一見すると、Windows のファイル作成プロセスは単純に見えます: NtCreateFile->NtWriteFile->NtClose。 このプロジェクトを掘り下げてすぐに、他にも多くの API が関与していることがわかり、さらに面白いことに、関与する他の API はプログラムによって異なります。一部のプログラムは、I/O プロセスの一部として Win32 API SetFilePointerEx を呼び出し、それが NtAPI NtSetInformationFile を呼び出します。SharpHound を含む .NET プログラムなど、他のプログラムは NtFlushBuffersFile を呼び出すことになります。
ファイルを作成して書き込むための単一の共通 API 呼び出しチェーンがないため、MemFiles が使用されるツールによっては非互換性の余地が生まれます。プログラムが、MemFiles がフックしていない別の NtAPI を呼び出し、置換 NtCreateFile 関数で MemFiles によって作成された偽のハンドルを渡すと、「Invalid Handle」エラーが発生して実行が停止する可能性があります。他のプログラムは、MemFiles が適切に偽装/置換しないより複雑なアクションを実行します。すでに特定されている互換性のないものの 1 つは ADExplorer.exe です。
ADExplorer.exe は、Active Directory の列挙に使用される署名済みの Microsoft バイナリです。実行中、ADExplorer は指定された出力ファイルにデータを書き込み、後でそれを参照してから、最終的に実行終了時に最終出力をファイルに書き込みます。出力ファイルを一種のキャッシュとして使用するため、ファイルにすでに書き込んだデータを読み取ることができなければならず、それを単純で予測可能な方法で行う可能性は非常に低いです。
MemFiles は現在、プログラムまたはアプリケーションがメモリ内ファイルを読み取ることをサポートしていませんが、カスタム NtReadFile 関数をさらに発展させ、MemFiles 構造体に追加の変数/データ追跡を追加することで、これは可能になるはずです。
ADExplorer は、生成するファイルのサイズという点でもう 1 つの課題を提示します。大規模なエンタープライズ環境では、出力ファイルが 1GB を超える可能性があります。MemFiles はプログラム的にはこれを処理できるはずですが、そのようなユースケースを意図しているわけではありません。
間違いなく、コミュニティは MemFiles が正しく機能しないツールを発見するでしょう。互換性のないプログラム/ツールと、それを実行した状況(つまり、BOF なのか、inline-executeAssembly、Inline-Execute-PE などを介してなのか)を詳述した issue を開くことをお勧めします。MemFiles を拡張して動作させられるかどうかを確認できるようにします。
MemFiles に関連する IOC には以下が含まれますが、これらに限定されません:
VirtualAlloc を使用したメモリの割り当て
WriteProcessMemory を使用したデータの書き込み
割り当てられたメモリの保護を RW と RX の間で変更
NTDLL.dll 内のメモリの上書き
MemFiles は本格的な EDR に対して開発またはテストされたわけではありません。利用可能だったのは Microsoft Defender です。そうは言っても、Beacon がファイルを生成するために実行しているツール/プログラムの方が、MemFiles がそのファイルをメモリにキャプチャまたは保存することよりもアラートの対象になる可能性が高いでしょう。NTDLL 内のメモリの上書き/NtAPI のフックは、一部の製品が問題にするかもしれないものだと思いますが、これを裏付ける証拠はありません。MemFiles がキャプチャする/すべきファイルに関係のないフックされた NtFunction への呼び出しについては、syscall は引き続き NTDLL.dll アドレス空間内から発行されます。セキュリティ製品は、この領域外から行われた syscall を検出して警告するためです。
MemFiles によってメモリに保存されたファイルはエンコードも暗号化もされないことに注意してください。AV/EDR がメモリ内の生成ファイルにアラートを発する実際のユースケース/事例が特定された場合、この機能を追加できます。
私は数ヶ月前、KFiveFour の Tradecraftcon でのカンファレンストークで、インメモリファイルシステムのコンセプトを初めて知りました。スピーカー (@DexterGerig) が、クライアントサーバーモデルを使用してインメモリファイルシステムを作成する POC をデモンストレーションしました。 そのようなプロジェクトの私が思い描いていた機能の半分は、前回のメジャーリリースである Inline-Execute-PE でカバーされていました。 残りの半分、つまりツールによって生成されたファイルをキャプチャしてディスクではなくメモリに保存できるというアイデアは、そのプロジェクトでは実現されず、明らかな理由から非常に望ましい機能であり続けました。
MemFiles は私にとって非常に困難な挑戦でした。このプロジェクト以前は、デバッガーに費やした時間はごくわずかで、アセンブリの理解もなく、API フッキングも理解していなかったからです。 このプロジェクトでは、10〜20 時間に及ぶ行き詰まりに何度か遭遇しましたが、辛抱強く取り組んだおかげで、ありがたいことに乗り越えることができました。 おそらく最も効率的な方法ではありませんが、デバッガーにかなり慣れ、アセンブリ、レジスタ、スタック、呼び出し規約に関して、コンピューターが内部でどのように動作するかについての理解を深めることができました。
以下は、MemFiles に組み込まれた、より重要な技術的詳細と設計上の考慮事項のいくつかに関する技術的な詳細な解説です。
Windows でのファイル作成は NtCreateFile から始まります。NtCreateFile には目的のファイルのパスが渡され、その代わりに Windows はその場所にファイルを作成し、ハンドルを提供します。返されたハンドルは、ファイルを含む後続のすべての呼び出し(たとえば、NtWriteFile や NtClose)で使用されます。
インターセプトして改ざんしたい呼び出しと、そのままにしておきたい呼び出しをこれらのすべての API への呼び出しから分離する方法を考えるにあたり、NtCreateFile 呼び出しでキーワードを探すことにしました。これは、NtCreateFile 呼び出しのファイルパスの一部として、一意で存在しないディレクトリを指定することで実現されました。フックが実行を置換 NtCreateFile 関数にリダイレクトすると、NtCreateFile への引数として渡されたファイルパスにその一意の「キーワード」が存在するかどうかが調べられます。見つかった場合、MemFiles はこの NtCreateFile 呼び出しがディスクではなくメモリに配置されるべきファイルに関連するものであると認識します。これが発生すると、MemFiles はいくつかの変数を初期化し、ファイルが使用するために最初の 1MB のメモリを割り当てますが、最も重要なのは、MemFiles 構造体で指定されたファイル名に偽のハンドルを関連付け、この偽のハンドルを呼び出し元に返すことです。
MemFiles によってフックされた他のすべての NtAPI について、対応する置換 NtFunction は引数として渡されたハンドルを調べ、それが MemFiles 構造体に存在するかどうかを確認します。ハンドルが MemFiles 構造体に存在する場合(MemFiles によって生成された偽のハンドルは、実際のハンドルと交差しないように十分に偽物です)、MemFiles はこの呼び出しをメモリ内ファイルに関するものとして識別し、それに応じて動作します。### フックの理論と置換関数 ディスクに書き込まれるはずだったファイルをメモリ内に書き込むために、MemFiles はプログラムがファイルを作成しようとする際に作られる特定の API への呼び出しを傍受する必要があります。API フッキングは非常に長い間存在しており、多くの EDR 製品がその中核機能として積極的に使用しています。特定の API への呼び出しは EDR のアドレス空間にリダイレクトされ、そこでは API 呼び出しとそれに渡された変数の分析が行われます。EDR がその呼び出しを悪意があると判断した場合(たとえば攻撃ツールやキルチェーンの一部である場合)、呼び出しの完了を防ぎ、アラートを発報します。EDR がその呼び出しが良性であると判断した場合、リダイレクト元に実行をパッチして戻し、API 呼び出しが元々意図されたとおりに完了するようにします。単純な例えとして、友人に手紙を郵送したが、友人が受け取る前に第三者がそれを開封して読み、手紙に違法な内容が含まれていると判断した場合、友人は手紙を受け取ることができず、警察に通報される、というものがあります。
MemFiles も同じ理論に従いますが、アラート(または理論上の警察の関与)はありません。API フッキングは通常、ユーザーランドで可能な限り低いレベル、すなわち NTDLL.dll 内の NtFunctions に対して実装されます。フッキングが行われる前の NtCreateFile を見てみましょう。
すべての NtFunction は、システムコール番号を除いて同一です。この例ではその番号は 55 です。システムコール番号は NtFunction ごとに異なり、また Windows のバージョンによっても変わる可能性があることにも注意してください。この OS(Windows 11)における NtCreateFile のシステムコール番号は 55 ですが、Windows 10 では異なる可能性があり(そして Windows 7 では確実に異なります)。
TEST 命令と JNE 命令に注目する価値があります。これらは、NtFunction が通常の syscall 命令を使用すべきか、それともレガシーな INT 2E 命令を使用すべきかを判断するために存在します。klezvirus 氏の投稿 SysWhispers is dead, long live SysWhispers! から引用します:
さて、興味深い部分です。関数は SharedUserData[0x308](BYTE PTR DS:[7FFE0308])が 1 に設定されているかどうかをチェックします。SharedUserData は、カーネルモード構造体 KUSER_SHARED_DATA を参照するシンボルです。
KUSER_SHARED_DATA 構造体は、ユーザーモードソフトウェアと情報を共有するために使用される固定(または事前定義)メモリ空間を定義します。これはもちろん、ユーザーランドコードが毎回ユーザーモードとカーネルモードの実行を切り替えるオーバーヘッドなしに、特定のグローバルシステム情報を利用できるようにするために行われました。
インデックス 0x308 の値は syscall 命令を表し、これは Windows 1511 以降のすべての Windows バージョンでサポートされています。想像できるかもしれませんが、1511 より前のすべての Windows バージョンでは、syscall を実行する標準的な方法は割り込み int 2Eh を呼び出すことでした。
...
Windows が現在 1511 よりはるかに上のバージョンであるにもかかわらず、なぜこの int 2Eh がまだ存在するのか疑問に思っているなら、それはこの命令が今でも使用されているからです。実際、HVCI(Hypervisor-protected Code Integrity)が有効な場合、SharedUserData[0x308] は 0 に設定され、syscall 命令の代わりに int 2Eh が使用されます。これは主にパフォーマンス上の理由によるもので、Ring3 から Ring0 への切り替えがどちらの命令を使用するかによって行われる方法に起因します。
このトピックについて Twitter でさらなる説明を求めたところ、@yarden_shafir 氏は次のように述べました:

要するに、NtFunction 内のすべての命令は、ある時点で必要になる可能性があります(おそらく最後にある到達不能と思われるマルチバイト NOP は例外かもしれません)。そして、NtFunction 内の命令を上書きする場合、それらを保存し、最終的な syscall(状況によっては INT 2E)を実行する前にどこかの時点でそれらを実行することを保証する必要があります。
syscall を実行する前に、システムコール番号が RAX に移動されることに注意してください(スクリーンショットでは EAX と表示されています)。この前に RAX がスタックにプッシュされていないため、私は(おそらく素朴にも)システムコール番号がそこに移動される前に RAX に含まれていた値は、syscall が発行された後には重要ではない、または必要ないと想定しました。これは良いニュースです。syscall が発行される前に RAX にシステムコール番号が含まれていることを保証すれば、RAX レジスタを自由に使用できることを意味するからです。
実行をカスタムコード/置換 NtFunction にリダイレクトするために、元の NtAPI の一部を上書きし、置換 NtFunction のアドレスを RAX に移動してから、JMP 命令を使用してそのコードに移動します:
MOV 命令と JMP 命令には 12 バイトが必要です。NtAPI の最初の 12 バイトを上書きして他の命令を壊すことになるため、NtAPI の適切な間隔とアライメントを維持するために、それらの命令は NOP に置き換えられています。
プログラムが NtCreateFile を呼び出すと、実行は置換 NtFunction にジャンプします。
MemFiles は、フックされた API のリダイレクト先となる置換関数の配置場所に関して、EDR が行うフッキングの方法とは異なります。多くの EDR は自社の DLL をプロセスに読み込みます。フックされた API は、この読み込まれた DLL のアドレス空間にリダイレクトされ、そこで分析が行われます。このプロジェクトの全体的な目的がディスクへのファイル展開を避けることだったため、DLL をディスクに配置し、Beacon プロセスにそれを読み込ませて置換 NtFunction にアクセスできるようにするのは、あまり良い方法とは思えませんでした。メモリから DLL を読み込むことを可能にする 数年前の POC が利用可能であり、これは私たちのニーズにとって実行可能な戦略ですが、このプロジェクトはメンテナンスされておらず、いくつかの問題があるようです。さらに、これは 1200 行のコードであり、BOF 形式に変換するのは大変な作業です。
簡単な余談ですが、置換 NtFunction は BOF 内に配置することはできません。CobaltStrike は BOF を読み込み、実行し、完了するとプロセスメモリから消去します。プロセスがフックされた NtAPI のいずれかを呼び出すたびに呼び出せる、メモリ内に永続的な関数が必要であるため、BOF では機能しません。
私がたどり着いた答えは、位置独立コード(PIC)でした。名前が示すように、特定の場所にロードされることや、各部分が他と特定の関係で配置されることを必要とする通常の実行ファイルとは対照的に、PIC はメモリ内のどこにでも配置して実行できます。これにより、置換 NtFunction を PIC 実行ファイルとして記述し、Beacon プロセスに注入し、フックされた NTAPI への呼び出しがあったときにフックが実行をその関数にリダイレクトする、という道が開かれます。
これらの PIC NtFunction のテンプレートは、Cracked5pider 氏の ShellcodeTemplate プロジェクトから来ています。
ベースプロジェクトからの注目すべき逸脱点の 1 つは、ベースプロジェクトが完全な PIC 実行ファイルの作成を中心に設計されていることです。つまり、スタックポインタを保存し、スタックに領域を作成し、実行ファイル内に含まれる指定された置換 NtFunction を呼び出し、その関数が戻ったらスタックポインタを復元する ASM が含まれています。PIC 実行ファイルによって行われる call 命令は問題を引き起こします。なぜなら、それによって呼び出しが行われた場所(PIC 実行ファイルの ASM 内)の戻りアドレスがスタックにプッシュされ、後で遭遇する ret 命令が、元の NtAPI の呼び出し元ではなく、PIC 実行ファイル内へ実行を戻してしまうからです。
これを軽減するために、ShellcodeTemplate プロジェクトの ASM ファイルを編集し、スタックのセットアップ、関数の呼び出し、関数の実行完了後のスタックポインタの復元に関連する ASM を削除しました。その結果、NtAPI に配置されたフックは、プログラムによって元の NtAPI が呼び出されたときと同じ状態のスタックとレジスタで、実行を直接置換 NtFunction にジャンプさせます(私たちの JMP に使用される RAX を除く)。
元の ShellcodeTemplate ASM:

MemFiles ASM:

フックされた各 NtAPI には独自の PIC NtFunction があり、次のいずれかに必要なロジックが含まれています:
A. 偽のハンドルの作成、メモリへのデータの書き込み、MemFiles 構造体内の変数の変更など、MemFiles 固有のアクションを実行する
または
B. 実行をトランポリンに導き、API 呼び出しを本来の経路に戻して、syscall を実行できるように NTDLL にパッチして戻す
置換 NtFunction の中には、他のものよりも複雑なものがあります。フックされた NtAPI への呼び出しが MemFiles に関係するものである場合、NtCreateFile や NtQueryVolumeInformationFile など一部の関数は、MSDN ドキュメント、テスト結果、およびある程度の推測/常識に従って、NtAPI に引数として渡された変数を変更します。NtClose や NtReadFile などの他の関数は、MemFiles によって作成された偽のハンドルを渡すことで必然的に発生する「無効なハンドル」エラーを回避するために、元の呼び出し元に単に STATUS_SUCCESS を返します。
フックされた NtAPI への呼び出しが MemFiles に関係しない場合は、物事を本来の経路に戻すために実行をトランポリンに導く必要があります:

トランポリンは、API がフックされたことにより元の NtAPI 内で実行されなかったすべての命令を実行する役割を担います。これには、元のフックによって部分的または完全に上書きされた命令も含まれます。API のフッキングは、必要な命令をすべて実行するための十分なスペースがないという問題をすぐに引き起こす可能性があります。トランポリンはこの問題の緩和にも役立ちます。元の NtAPI にジャンプして戻る前に、レジスタやスタックをセットアップするための任意の数のアクションを実行できるからです。NtCreateFile トランポリンは以下で確認できます:

最も明らかなのは、最初のフックによって上書きされた 3 つの命令が、トランポリンの最初の 3 つの命令として見られることです:
MOV R10, RCX
MOV EAX, 55
TEST BYTE PTR DS:[7FFE0308], 1
前述のとおり、このすべての操作における大きな要件は、syscall を発行する前にシステムコール番号(上記の例では 55)が RAX(EAX)にあることです。しかし、元の NtAPI に実行を戻すために JMP 命令を使用する必要があるという問題があります。重要な情報を保持しておらず、その目的に利用できる別のレジスタがあるかもしれませんが、フックしている多数の NtAPI(それぞれがレジスタを異なる方法で使用している可能性がある)を考慮すると、一貫して安全な選択肢は見つかりませんでした。安全な選択肢は、引き続き RAX を使用することです。RAX に保存されているシステムコール番号をスタックにプッシュすることで、これを行えます。次に、元の NtAPI 内でジャンプして戻りたいアドレスを RAX に移動し、JMP 命令を使用して NTDLL に戻ります:

NtAPI にインストールされるフックには POP RAX 命令が含まれており、トランポリンを使用してジャンプするのはこの場所です。この命令を実行すると、スタックの先頭から RAX にシステムコール番号が復元され、syscall を発行する準備が整います。元のフックされていない NtAPI の JNE 命令がまだここにあることに注意してください。ZF フラグを設定し、JNE を実行するかどうか(syscall を飛び越えて INT 2E にジャンプするかどうか)を決定する対応する TEST 命令は、トランポリン内で実行されました。このように配置することで、NtAPI のフックを成功させ、実行を置換 PIC NtFunction にリダイレクトする一方で、フッキングの結果として何もスキップしたり、機能を失ったりしないことを保証できます。
ここまでに説明した内容を簡単に要約すると、MemFiles が初期化されるとき、MemFiles の機能にとって重要な情報を含む構造体が Beacon プロセスメモリ内に作成されます。この構造体の情報は、MemFiles のライフサイクルを通じて継続的に参照され、各置換 PIC NtFunction や、MemFiles によってメモリに保存されたファイルを照会・取得するために使用される BOF も含まれます。そのため、構造体が作成されると、構造体があるメモリアドレスが Teamserver に送り返されます:

このアドレスを memtable に保存した後、後続の MemFiles コマンド(memlist、memfetch、memclean)は、このアドレスを引数として BOF に送信し、構造体を特定して参照できるようにします。しかし、PIC NtFunction はどのようにして構造体を特定するのでしょうか?
明白な問題は、PIC NtFunction が MemFiles 構造体のメモリアドレスを必要とする一方で、構造体が作成される時点ではすでにコンパイルされているということです。MemFiles の初期実装では、MemFiles の初期化を 2 つの別々の BOF に分割することでこの問題に取り組みました。最初の BOF は構造体を作成してアドレスを Teamserver に送り返し、Teamserver は Aggressor スクリプトの魔法のような処理でアドレスを解析し、各 NtFunction のソースコードファイルに挿入して、最終的な PIC NtFunction に再コンパイルしました。2 番目の BOF は、完成した PIC NtFunction を送信し、実際の NtAPI の注入とフッキングを行いました。
見た目が悪く、余分な時間がかかることに加えて、複数の Beacon が同時に MemFiles を初期化しようとする状況では、実際の問題が発生する可能性があります。Beacon 1 が NtFunction ソースコードファイルをパッチして再コンパイルしている最中に、Beacon 2 が自身の MemFiles 構造体アドレスをコールバックした場合、事態は複雑になる可能性があります。
この問題に対するエレガントな解決策は、PIC NtFunction に対してバイナリパッチを実行することです。ここでは、MemFiles 構造体アドレスがコンパイル済みの NtFunction にパッチされ、実行時にアクセス可能になります。これを容易にするために、各 NtFunction にプレースホルダ文字列が書き込まれます:

この変数は、xxd のようなツールを使用してコンパイル済みコード内で確認できます:

meminit コマンドが実行されると、各 PIC NtFunction が InstallHooks BOF と共に Beacon に送信されます。この BOF は MemFiles 構造体の作成を担当します。構造体を作成した後、各 PIC NtFunction に対して patchAddr 関数を呼び出します。patchAddr は、A の文字列を見つけて、それを構造体アドレスの文字列表現に置き換える役割を担います。実際には、先ほどのスクリーンショットのメモリアドレスを使用すると、pFileInfoStr 変数は次のようになります:
char* pFileInfoStr = GET_SYMBOL( "00000178BC106860" );
この文字列表現は、その後、実際の 16 進数値、つまりメモリアドレスに変換できます。BOF は _strtoi64 API を使用してこれを非常に簡単に実現します:

もちろん、PIC NtFunction にとってはこれほど簡単にはいきません。
この短いアセンブリスニペットは、PIC NtFunction の 1 つの終端を示しています。JMP RAX 命令に注目してください。これは PIC NtFunction がトランポリンを呼び出しているものです(つまり、MemFiles がスプーフィング/干渉しなかった呼び出しです):

理由はわかりませんが、_strtoi64(または stroull や atoll などの類似 API)を使用しようとすると、この JMP RAX 命令が CALL RAX 命令に変わってしまいました。これには「コンピュータがどのように動作するか」の深いレベルに関わる正当な理由があるはずですが、この一見取るに足らない変更によってかなりの数のものが壊れてしまいます。MemFiles 構造体アドレスの文字列表現を、内部に保存された値を利用できるように実際の 16 進数値に変換する方法を見つけようと 15 時間以上費やした後、この StackOverflow の投稿 に出会いました。そこでは、コメント投稿者がマイクロコントローラ向けに設計された文字列を uint32 に変換するカスタムルーチンを提供していました。ありがたいことに、これは変更なしで uint64 でも機能し、そして最も重要なことに、PIC NtFunction 内の後半にある JMP RAX 命令を CALL に変更することなく保持しました。最終的なスニペット:

簡単のためこれまで言及しませんでしたが、NtAPI のフォーマットは長年にわたって変化しています。Windows 7 で使用されているような初期バージョンは、現代のものよりもはるかに短く、32 バイトではなくわずか 16 バイトです:

これにより、MemFiles が NtAPI をフックする方法と、トランポリンの構築方法の両方に変更が必要になります。MemFiles が扱っている NtAPI のバージョンを特定するために、InstallHooks BOF はまず対象の NtAPI のアドレスを解決し、その場所から 32 バイトを読み取ります。フォーマットが異なると、syscall 命令の位置も NtAPI のバージョン間で異なります。特定のバイトオフセットで syscall の有無を確認することで、MemFiles は現代の NtAPI 実装とレガシー NtAPI 実装のどちらを扱っているかを判断し、それに応じて動作できます:

フッキング後、レガシーな NtCreateFile API は次のようになります:

そして、レジスタをセットアップしてから NtCreateFile にジャンプして戻るために使用されるトランポリン:

全体的に、この手法はほぼ同じですが、はるかにタイトで、余裕があまりありません。すべてを収めて正しく機能させるために、syscall 命令を NtCreateFile 内で移動する必要があったことに注意してください。syscall 命令は依然として NTDLL 内の NtCreateFile API メモリ空間に存在しますが、API の 9 番目と 10 番目のバイトから、API の 14 番目と 15 番目のバイトに移動しました。すべてを適切に詰め込むために、マルチバイト NOP は犠牲になっています。
ファイル I/O 操作に関連する API のいくつかは直感的で、特定してフックするのは簡単でしたが、他ははるかに捉えどころがなく、WinDbg や x64dbg で何時間もアセンブリをステップ実行して、どの API が呼び出されているかを特定しようとする必要がありました。この作業にはもっと効率的な方法があったはずだと思いますが、私はやりながら学んでいました。さて、おまけとして、後から考えればすぐに終わるはずだった、SharpHound を20時間にわたって動作不能にしていた NtAPI を特定するまでの過程を詳しく説明しようと思います。.NET プログラムである SharpHound は、「The handle is invalid」と判断したことに起因する、非常に見苦しいコールスタックを吐き出しました。.NET はプログラマに優しい言語ですが、ほとんどの(すべての?)機能は変換され、結局は Win32 API(そして結果として NtAPI)を経由するため、他の何かと同様に観察してフックすることができます。

「Invalid Handle」エラーは、SharpHound が使用している NtAPI のうち、私がフックしていないものがある(既存の PIC NtFunctions が正しく動作していないのとは対照的に)ことを明確に示していたため、それが何かを突き止めようと試みました。これまで他のツールで使っていた手法は、まず NtCreateFile にブレークポイントを設定し、自分の「特別な」ディレクトリに関係する呼び出しを見つけるというものでした。そこからプログラムをステップ実行し(迷子になることが多かったため、通常は数回)、プログラムが次に呼び出す関数を確認しました。この方法に従うことで、NtQueryVolumeInformationFile、NtQueryInformationFile、NtSetInformationFile が呼び出されており、フックする必要があることを発見しました。
SharpHound は、タスクの多くを非同期に実行するという、さらに厄介な点があります。そのため、複数のファイルが同時に処理されるため、単一のファイルが API 呼び出しを経由する直線的なステップを追跡するのがはるかに難しくなります。さらに、NtCreateFile 呼び出しの後にプログラムをステップ実行すると、NtCreateFile 呼び出しが行われたスレッドは最終的に終了し、その後の NtWriteFile 呼び出し(およびこの検索の対象である他の未知の API 呼び出し)は別のスレッドで発生することがわかりました。これが検索プロセスをさらに困難にしています。
.NET をほんの少し扱っただけだった私は、エラーで生成されたコールスタックの解析に精通しておらず、非同期によってさらに見苦しくなったものについてはなおさらでした。20時間に及ぶ問題の中で、以前の戦略では進展がなかったため、私は何度もそのコールスタックに戻り、ゆっくりとではありますが確実に理解を深めていきました。上部から数えて「--- End of stack trace...」行で区切られた3つ目の「チャンク」あたりに、「at Sharphound.Writers.JsonDataWriter...」という行が見えます。これにより、Github で公開されている SharpHound のコード内の相対的な開始位置がわかりました。名前が示すように、その SharpHound 関数は JSON 出力をファイルに書き込む処理を扱っていました。データがファイルに正常に書き出されていないことはすでに認識していたので、これは新しい情報ではありませんでした。コールスタックを1レベル上にたどると、次に関連する行は「at System.IO.Streamwriter.」でした。System.IO プレフィックスは、これが SharpHound 固有の関数ではなく .NET に組み込まれていることを示していました。コールスタックの最上部を見ると、「at System.IO.FileStream.FlushOSBuffer()」という行が目に飛び込んできました。FlushOSBuffer を Google で検索して、何か見つかるか試してみることにしました。
その結果、Microsoft の .NET ドキュメントの filestream.cs にたどり着きました。そこには FlushOSBuffer の定義がありました:

これは Win32 API の FlushFileBuffers を呼び出しているようです。Win32Native.FlushFileBuffers の定義は、Win32Native ドキュメントを参照することで見つけることができます:

P/Invoke で .NET を扱ったことがある人なら、この形式に気づくでしょう。これで、問題の .NET 関数である System.IO.Filestream.FlushOSBuffer() が呼び出す Win32 API がわかりました。KERNEL32!FlushFileBuffers にブレークポイントを設定して SharpHound を実行するとこれが確認でき、ステップ実行することで、FlushFileBuffers が内部で NtFlushBuffersFile を呼び出していることがすぐにわかりました。この API をフックすることで、SharpHound が抱えていた問題は解消され、出力ファイルをメモリに書き込みながら正常に実行できるようになりました。
このプロジェクトの重要な部分は、ファイルがメモリ内にある状態で、実際に CobaltStrike Teamserver にダウンロードできることです。当然のことながら、通常の CobaltStrike の download コマンドは、実際には存在しないファイルパスでは機能しません。今の知識で考えると、解決策はおそらく、メモリ内のファイルが書き込みだけでなく読み取りもできるように、置き換え用の PIC NtReadFile コードを拡張することにあります。当時その知識がなかったため、ファイルを実際に取得できるかどうかが大きな障害となっていました。
偶然、EspressoCake が書いた BOF に出会いました。そこには私の目を引く関数が含まれていました:

コードを見ると、文書化されていない Beacon CALLBACK オプションを使用しているようです:

この関数により、BOF はクライアントからではなくターゲットから Teamserver へのファイルのダウンロードを開始できます。この機能(後になって、@Cr0Eax、@EthicalChaos、@anthemtotheego を含む数人の協力によるものであると知りました)により、以前存在した大きな障害が取り除かれました。メモリ内ファイルに対してターゲットシステムからファイル転送を開始する手段が得られたからです。このコードスニペットは将来も役立つと思います。関わったすべての方に多大な感謝を捧げます。
このプロジェクトの難しい点の1つは、Team Server に接続されているすべての CobaltStrike クライアントが MemFiles 機能を利用できるようにすることでした。MemFiles のデータは MemFiles.cna によって作成される構造体に格納され、このツールを使用したい各クライアントに読み込む必要があります。その結果、これらのデータ構造は Team Server 上ではなく、各クライアント内に存在します。このデータが単一の集中管理場所(TS)に存在していれば、各クライアントからそれを取得するのは簡単で、この問題はすべて発生しなかったでしょう。CobaltStrike チームが MemFiles のような機能を正式に CobaltStrike に統合するなら、間違いなくこの方向を取ると思います。しかし、これはコミュニティ製のアドオンであるため、あるものでやりくりするしかありません。
各 CobaltStrike クライアントが、Beacon 内の MemFiles ステータスと構成に関する最新かつ正確なデータを確実に保持するために考慮しなければならないシナリオがいくつかあります。
TS に新しく接続し、現在の memtable を必要とする新しいクライアント
単一のクライアントだけが TS に接続されており、CobaltStrike を再起動する(そのためクライアントのメモリに保存されている memtable を失う)ケース
クライアント A が MemFiles データに変更を加え、それをクライアント B に伝える必要があるケース
これらのシナリオに対処するため、多角的なアプローチが取られました。単一の CobaltStrike クライアントだけが TS に接続されている(つまり、memtable データを持つ唯一の存在である)ケースを処理するために、クライアントが memtable を変更する(meminit、memclean)たびに、memtable の内容を CobaltStrike ディレクトリ内のローカルテキストファイルにも書き出します。クライアントが終了/再起動した場合、または MemFiles.cna が再読み込みされた場合は、まずローカルの memtable.txt ファイルを読み取って、インメモリの memtable を設定しようとします。
複数のクライアントが TS に接続されていて、新しいクライアントが参加した場合(Event Log による)、各クライアントは TS に接続されているすべてのユーザーのリストを取得し、アルファベット順に並べ替えます。そのリストの最初にあるクライアントが「Broadcast」クライアントとして選択され、5秒待った後(新しいクライアントが初期化してローカルの memtable.txt を読み取るため)、自身の memtable の各エントリについて Event Log にメッセージ(Actions)を送信します。他のすべてのクライアント(Broadcast クライアント以外)はこれらのメッセージを読み取り、ブロードキャスト情報で自分の memtable を更新します。これには、既存のエントリの更新に加えて、それぞれの memtable に含まれていない追加エントリの追加も含まれます。
MemFiles を含む通常の操作も、Event Log でのメッセージ送信に依存しています。クライアント A が meminit を実行すると、関連するすべての memtable 情報を含むメッセージがブロードキャストされます。すべてのクライアントは、「on Event_Action」フックを使用してこれらのブロードキャストされた Event Log メッセージを解析し、それぞれの memtable を更新します。meminit がその BOF の実行を終了すると、MemFiles データにも変更が加えられます。これらの変更は Beacon によってクライアントに通知され(例: meminit の実行後、Beacon が pMemAddrs 構造体のメモリ位置をコールバックする)、そのため接続されているすべてのクライアントから認識でき、各クライアントは「on Beacon_Output」フックを使用してそれぞれの memtable を更新します。
これらの個別の取り組みが組み合わさることで、MemFiles は複数のクライアント間で重要なデータを効率的かつ確実に同期できます。
このプロジェクトは、以下の個人とプロジェクトの貢献なしには実現しませんでした: