
プロセスの開始から終了までシェルコードを見えなくするメモリベースの回避技術。
プロセス開始から終了までシェルコードを見えなくするメモリベースの回避手法。
このシェルコード自己注入のPoCを共有し、Red Teamingに役立つ可能性のあるAV/EDR回避のコンセプトを紹介したいと考えました。数週間前、私はShellGhostと名付けたカスタムのメモリ内回避手法を思いつきました。この手法は、__プロセス開始から終了まで「見えない」シェルコードを実行するコード__が必要だったことに端を発しています。
ShellGhostは、ソフトウェアブレークポイントと組み合わせたベクタード例外処理(VEH)に依存し、 スレッドの実行を周期的に停止し、実行されたブレークポイントをRC4暗号化されたシェルコード命令に置き換え、命令を復号し、メモリ保護をRXに戻した後に実行を再開します。その後のEXCEPTION_BREAKPOINTが発生すると、例外ハンドラは以前のシェルコード命令を新しいブレークポイントに置き換えるため、割り当てメモリは完全なシェルコードを暗号化されていない状態で公開することはありません。これは、最初はREAD/WRITEとしてマークされたプライベートメモリページ内で行われます。
RW PRV割り当ては、PE-SieveやMonetaなどのメモリスキャナによって「侵害の兆候(IOC)」とは見なされません。割り当てがRXになりページがスキャンされても、ブレークポイント以外は見つかりません。これはシェルコードが実際に実行されている間に起こります。次の画像は、リバースシェルが動作しているが、MonetaによってIOCが見つからない(バイナリが署名されていないこと以外は)ことを示しています。

Pe-Sieveでプロセスをスキャンしようとすると、さらに良い結果が得られます。

シェルコードマッピングはShellGhostの中核機能です。この戦術により、スレッドはメモリ内でシェルコード全体を露出させることなく、断続的に命令を実行できます。これは、スレッドが実行する個々のシェルコード命令の位置が、割り当てられたメモリページ内の特定のブレークポイントの位置と対応しているため可能です。ShellGhostは、スレッドのRIPから割り当てられたメモリページのベースアドレスまでの相対仮想アドレス(RVA)を計算し、それを暗号化されたシェルコード/暗号化された命令のベースアドレスに加算することで、この位置を解決します。置き換えられるブレークポイントの数は常に同じではなく、各命令が正しく生成および解釈されるために必要なオペコードの数(QUOTA)によって異なります。例えば、'POP RBP'命令は'5D'に等しく、これは1つのブレークポイントのみが置き換えられることを意味します。対照的に、'JMP RAX'命令はオペコード'FF E0'を必要とするため、2つのブレークポイントが置き換えられます。このため、以下のCデータ構造を作成しました。
typedef struct CRYPT_BYTES_QUOTA {
DWORD RVA; // offset to encrypted instruction
DWORD quota; // number of opcodes that generate the instruction
} CRYPT_BYTES_QUOTA, * PCRYPT_BYTES_QUOTA;
ブレークポイントはすぐに命令に置き換えられるわけではありません。命令は実行前に復号ルーチンを経る必要があるからです。ここでDWORD quotaが重要になります。ShellGhostは、現在広く使われている'SystemFunction032'を使用してRC4復号を実行します。XORとは異なり、RC4は単一バイトの暗号化方式ではありません。つまり、シェルコードを一度に暗号化および復号することはできません。これも、各命令を個別に処理するもう一つの理由です。ブレークポイントが置き換えられた後、SystemFunction032が必要とするバッファ長は'instruction quota'と等しくなります。これは、特定の命令を構成するオペコードの数を表します。例えば、次のスニペットを考えてみてください。
CRYPT_BYTES_QUOTA instruction[200];
instruction[5].quota = 2
USTRING buf = { 0 }; // will contain the buffer to be decrypted and its length
USTRING key = { 0 }; // will contain the RC4 key and length
buf.Length = 2 // buffer length, or length of the instruction to be decrypted
シェルコード命令番号5は2つのオペコードで構成されているため、バッファ長2がSystemFunction032に渡されることがわかります。これは、SystemFunction032への単一の呼び出しでシェルコード全体を復号しようとすると完全に破損するため、重要です。
シェルコードはコンパイル前にShellGhost_mapping.pyでマッピングする必要があります。このスクリプトは各命令を抽出し、小さな独立したシェルコードとして扱います。命令は1つずつ暗号化され、unsigned charとしてまとめてC形式で出力されます。結果はCコード内にハードコードできます。以下は、calc.exe用の暗号化されたMSFシェルコード命令の例です。

このシェルコードには98個の命令があるため、98個のCRYPT_BYTES_QUOTA構造体が宣言されます。コードが実行されるとき、これらの構造体は適切な命令のRVAとQUOTAで埋められる必要があります。'-1'パラメータは、マッピングスクリプトにこれを行うコードの断片を出力するように指示します。

Metasploit x64シェルコードは、通常、命令の間にWinAPIの文字列パラメータを保存します。つまり、Winexecを呼び出すMSF x64シェルコードは、最初のパラメータ文字列をスタックに配置するために末尾にヌルバイトを持つ一連のバイトをプッシュしません。むしろ、RCXレジスタ(最初のパラメータ)は、次の画像のように、シェルコード自体の内部のポインタです。

これは、文字列に関連する位置のブレークポイントが解決されないことを意味します。RIPがその位置に到達することはないからです。実際、このコードはRIPが通過する実際のシェルコード命令を解決し、命令のように実行されることのないパラメータは解決しません。これを修正するために、MSFシェルコードは常に呼び出すWinAPIへのポインタをRAXレジスタに格納し、その後レジスタ自体にジャンプすることに気づきました。したがって、ShellGhost VEHが解決されたブレークポイントが'JMP RAX'であり、RCXレジスタがシェルコード内部の位置へのポインタを含んでいることを検出すると、RCXが指すものも解決しようとします。その後、実行は割り当てられたメモリに戻されず、代わりにRAX(WinAPIアドレス)がRIPにコピーされ、WinAPIからスレッド実行が再開されるため、'JMP RAX'は上書きされ、割り当てられたメモリはRWのままになります。これは、WaitForSingleObjectを呼び出すリバースシェルに必要です。これにより、'JMP RAX'の後でスレッドがスリープし、シェルが存続している間メモリがRXのままになるためです。次のコードスニペットには、ShellGhostがWinAPIパラメータ文字列を含むRCXレジスタを調整し、MSFシェルコードが関数呼び出し(ここではWinExec)を正しく発行できるようにするために満たす必要のある2つの条件が含まれています。
<snip>
if (*(PWORD)exceptionData->ContextRecord->Rip == 0xe0ff) // if RIP is 'JMP RAX'
<snip>
if ((contextRecord->Rcx >= (DWORD_PTR)allocation_base) && (contextRecord->Rcx <= ((DWORD_PTR)allocation_base + sizeof(sh)))) // if RCX is inside the allocation
<snip>
RDX、R8、R9(2番目、3番目、4番目のパラメータ)はまだカバーされていません。
ShellcodeFluctuationは、非常によく似たメモリ内回避のコンセプトです。同様に、ここでの割り当てメモリはRWからRXへ「変動」します。対照的に、ShellGhostは以下の改善点を導入しています:
ただし、ShellGhostは完璧な手法とは言えません。これらの手法すべてに共通する最大の欠点、すなわち__実行中のある時点でプライベートな実行可能メモリが必要になること__を依然として抱えています。Foliageのようなより高度な手法はすでにこの回避策を見つけています。さらに、ソフトウェアブレークポイントで埋め尽くされたメモリ割り当ては、YARAルールで検出される可能性があります。次の画像は、MonetaがRX PRV割り当てのIOCを正しく検出していることを示しています。

EDRソリューションを回避する場合、メモリスキャンは全体像の一部に過ぎません。IOCが完全に存在しないからといって、この手法を使用したバイナリが特定のEDRに対して効果的であるとは限りません。私の知る限り、ソリューションがバイナリを現在の方法で起動することさえ許可しない状況に遭遇したことがあります。裏を返せば、IOCは常に正確な指標とは限らず、一部は誤検出である可能性があります。とはいえ、これは単なる生の手法であり、読者が評価してくれることを願うインスピレーションです。Red Teamerは、EDRのコンポーネントと同様に、メモリ内回避はエンジンの1つのコンポーネントに過ぎないことを知っています。
コンパイルにはインクリメンタルリンクを無効にする必要があります。このVSプロジェクトには、すべてのコンパイラ/リンカオプションが既に設定されています。