Linux プロセスインジェクション決定版ガイド
はじめに
プロセスインジェクション手法は、攻撃者のツールセットの重要な要素です。攻撃者は、悪性コードを正規のプロセス内で実行して検知を回避したり、リモートプロセスにフックを仕掛けてふるまいを変更したりできます。
Windows マシンでのプロセスインジェクションのトピックは徹底的に調査されており、比較的よく認識されています。Linux マシンの場合、事情は異なります。優れた リソース も この トピックについてはいくつか書かれていますが、Linux でのさまざまなインジェクション手法に対する認識は、特に Windows と比較すると比較的低いようです。
私たちは、SafeBreach の Amit Klein 氏と Itzik Kotler 氏が執筆した『 Windows プロセスインジェクションの概要 』からインスピレーションを得て、Linux プロセスインジェクションの包括的なドキュメントを提供することを目指しています。ここでは、ライブで実行中のプロセスをターゲットとする手法である「真のプロセスインジェクション」に焦点を当てますつまり、 ディスク上のバイナリーを変更したり、 特定の環境変数を使用してプロセスを実行したり、 プロセスを読み込むプロセスを悪用したりする必要がある手法は除外します。
Linux でのプロセスインジェクションを容易にする OS の機能と、それによって可能になるさまざまなインジェクションプリミティブについて説明します。前述した手法について説明するとともに、 以前に文書化されていなかったインジェクションバリアントをハイライトします。最後に、ハイライトされている手法の検知と緩和戦略について説明します。
このブログ投稿に加えて、この投稿で説明されているさまざまなインジェクションプリミティブ用の概念実証(PoC)コードの包括的なセットが含まれている、GitHub リポジトリー もリリースしています。これらの無害な PoC は、手法の悪意ある実装がどのように見えるかを理解するためのものであり、検知機能を作成し、テストするのに役立ちます。詳細については、プロジェクトの READMEをご参照ください。
Linux インジェクションと Windows インジェクションの比較
Windows マシンでの既知のインジェクション手法の数は膨大で、 APC キュー や NTFS トランザクション から アトムテーブル や スレッドプールまで、増え続けています。Windows は、攻撃者がリモートプロセスとやり取りできるようにする(およびそれらのプロセスを注入の対象とする)多数のインターフェースを公開しています。
Linux の世界では状況が大きく異なります。リモートプロセスとのやり取りは、システムコールの小さなセットに限定されており、Windows マシンでインジェクションを容易にする機能の多くはどこにも見つかりません。 リモートプロセスでのメモリーの割り当て や リモートメモリー保護の変更を行うための API は存在せず、 リモートスレッドを作成する API など当然ありません。
この違いは、インジェクション攻撃の構造に影響します。Windows では、通常、プロセスインジェクションは、割り当て→書き込み→実行の 3 つの手順で構成されます。まず、コードの格納に使用されるリモートプロセスにメモリーを割り当て、次にこのメモリーにコードを書き込み、最後に実行します。
Linux では、最初の手順である割り当てを実行することができません。リモートプロセスでメモリーを割り当てる直接的な方法がありません。そのため、インジェクションフローは若干異なり、上書き→実行→回復となります。リモートプロセス内の 既存のメモリー をペイロードで上書きし、ペイロードを実行し、プロセスの以前の状態を回復して、正常に実行を継続できるようにします。
リモートプロセスとのやり取り手法
Linux では、リモートプロセスのメモリーとのやり取りは、主に ptrace、 procfs、 process_vm_writevの 3 つの手法に制限されています。次のセクションでは、それぞれについて簡単に説明します。
ptrace
ptrace は、リモートプロセスのデバッグに使用されるシステムコールです。開始プロセスでは、デバッグされたプロセスメモリーおよびレジスターを検査および変更できます。GDB などのデバッガーは、ptrace を使用してデバッグ対象のプロセスを制御するように実装されています。
ptrace は、 ptrace 要求コードで指定されたさまざまな操作をサポートします。いくつかの重要な例として、PTRACE_ATTACH(プロセスにアタッチ)、PTRACE_PEEKTEXT(プロセスメモリーからの読み取り)、PTRACE_GETREGS(プロセスレジスターを取得)があります。スニペット 1 は、ptrace の使用例を示しています。
// Attach to the remote process
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
wait(NULL);
// Get registers state
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
スニペット 1:ptrace を使用してリモートプロセスのレジスターを取得する例
procfs
procfs は、システム上でプロセスを実行するためのインターフェースとして機能する特別な擬似ファイルシステムです。このファイルシステムには、/proc ディレクトリーからアクセスできます(図 1)。
各プロセスは、PID に従って名前が付けられたディレクトリーとして表されます。このディレクトリーには、プロセスに関する情報を提供するファイルがあります。たとえば、 cmdline ファイルには、プロセスのコマンドラインが保持され、 environ ファイルには、プロセス環境変数などが含まれます。
また、procfs はリモートプロセスのメモリーとやり取りする機能も提供しています。すべてのプロセスディレクトリー内には、プロセスのアドレス空間全体を表す特殊な「 mem 」ファイルがあります。特定のオフセットでプロセスのメモリーファイルにアクセスすることは、同じアドレスでプロセスメモリーにアクセスすることと同じです。
図 2 の例では、xxd ユーティリティーを使用して、指定されたオフセットから始まるプロセス・メモリー・ファイルから 100 バイトを読み取っています。
GDB を使用してメモリー内の同じアドレスを確認した場合、内容が同じであることがわかります(図 3)。
maps ファイルは、プロセスディレクトリーにあるもう 1 つの興味深いファイルです(図 4)。このファイルには、アドレス範囲やメモリーのアクセス権限など、プロセスアドレス空間内のさまざまなメモリー領域に関する情報が含まれています。
次のセクションでは、特定の権限を持つメモリー領域を特定する機能がいかに役立つかを見ていきます。
process_vm_writev
リモートプロセスのメモリーとやり取りする 3 番目の手法は、 process_vm_writev システムコールです。このシステムコールを使用すると、リモートプロセスのアドレス空間にデータを書き込むことができます。
process_vm_writev はローカルバッファーへのポインターを受け取り、その内容をリモートプロセス内の指定されたアドレスにコピーします。process_vm_writev の使用例をスニペット 2 に示します。
// Initialize local and remote iovec structs used to perform the syscall
struct iovec local[1];
struct iovec remote[1];
// Place our data in the local iovec
local[0].iov_base = data;
local[0].iov_len = data_len;
// Point the remote iovec to the address in the remote process
remote[0].iov_base = (void *)remote_address;
remote[0].iov_len = data_len;
// Write the local data to the remote address
process_vm_writev(pid, local, 1, remote, 1, 0);
スニペット 2:process_vm_writev を使用してリモートプロセスにデータを書き込む
リモートプロセスへのコードの書き込み
他のプロセスとやり取りするためのさまざまな手法を理解したところで、コードインジェクションの実行にそれらをどのように使用できるかを見てみましょう。インジェクション攻撃の最初のステップは、シェルコードをリモートプロセスのメモリーに書き込むことです。前述したように、Linux では、リモートプロセスで新しいメモリーを割り当てる直接的な方法はありません。つまり、新しいメモリーセクションを作成できないため、ターゲットプロセスの既存のメモリーを活用しなければなりません。
コードを実行するためには、実行権限を持つメモリー領域にコードを書き込む必要があります。このような領域は、前述した procfs maps ファイルを解析し、実行(x)権限を持つメモリー領域を特定することで見つけることができます(図 5)。
実行可能領域には、書き込み可能領域と書き込み不可領域の 2 種類があります。以下のセクションでは、それぞれをいつどのように使用できるかを説明します。
RX メモリーへのコードの書き込み
対象:ptrace、procfs mem
理想的には、コードを記述して実行できるように、書き込み権限と実行権限を持つメモリー領域を特定します。実際には、WX メモリーを割り当てるのは不適切と考えられるため、ほとんどのプロセスにはこのような権限を持つ領域がありません。通常は、権限は読み取りと実行に制限されています。
興味深いことに、この制限は前述の 2 つの手法(ptrace と procfs mem)を使用して解除できます。これらのメカニズムはどちらも、メモリーのアクセス権限を回避し、書き込み権限がなくても任意のアドレスに書き込むことができるように実装されています。このふるまいの詳細については、 こちらのブログ投稿をご参照ください。
つまり、書き込み権限に関係なく、ptrace または procfs mem を使用して、コードをリモート実行可能メモリー領域に書き込むことができます。
ptrace
ペイロードをリモートプロセスに書き込むには、POKETEXT または POKEDATA ptrace 要求を使用できます。これらの要求の動作は同一で、リモートプロセスのメモリーにデータワードを書き込むことができます。繰り返し呼び出すことで、ペイロード全体をターゲットプロセスのメモリーにコピーできます。この例をスニペット 3 に示します。
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
wait(NULL);
// write payload to remote address
for (size_t i = 0; i < payload_size; i += 8, payload++)
{
ptrace(PTRACE_POKETEXT, pid, address + i, *payload);
}
スニペット 3:ptrace POKETEXT を使用して、リモートプロセスのメモリーにペイロードを書き込む
procfs mem
procfs を使用してリモートプロセスにペイロードを書き込むには、正しいオフセットで mem ファイルに書き込むだけです。mem ファイルに加えられた変更は、プロセスメモリーに適用されます。これらの操作を実行するためには、通常のファイル API を使用します(スニペット 4)。
// Open the process mem file
FILE *file = fopen("/proc/<pid>/mem", "w");
// Set the file index to our required offset, representing the memory address
fseek(file, address, SEEK_SET);
// Write our payload to the mem file
fwrite(payload, sizeof(char), payload_size, file);
スニペット 4:procfs mem ファイルを使用して、リモートプロセスのメモリーにデータを書き込む
WX メモリーへのコードの書き込み
対象:ptrace、procfs mem、process_vm_writev
前述したように、ptrace と procfs mem はどちらもメモリーのアクセス権限を回避し、書き込み不可のメモリー領域にコードを書き込むことを可能にします。しかし、process_vm_writev ではそうではありません。process_vm_writev はメモリーのアクセス権限に準拠しているため、書き込み可能なメモリー領域にのみデータを書き込むことができます。
このため、書き込み可能な領域を検索することが唯一のオプションです。すべてのプロセスにこのような領域が含まれているわけではありませんが、そのような領域が含まれるプロセスは確実に見つけることができます。
スニペット 5 のコマンドは、システム上のすべてのプロセスの maps ファイルをスキャンし、書き込み権限と実行権限を持つ領域を特定します(図 6)。
find /proc -type f -wholename "*/maps" -exec grep -l "wx" {} +
スニペット 5:「find」コマンドを使用して、書き込みメモリー領域と実行メモリー領域を持つプロセスを特定する
このような領域を特定した後、process_vm_writev を使用してコードをその領域に書き込むことができます(スニペット 6)。
// Initialize local and remote iovec structs used to perform the syscall
struct iovec local[1];
struct iovec remote[1];
// Place our payload in the local iovec
local[0].iov_base = payload;
local[0].iov_len = payload_len;
// Point the remote iovec to the address of our wx memory region
remote[0].iov_base = (void *)wx_address;
remote[0].iov_len = payload_len;
// Write the local data to the remote address
process_vm_writev(pid, local, 1, remote, 1, 0);
スニペット 6:process_vm_writev を使用して、リモート WX 領域にペイロードを書き込む
リモート実行フローのハイジャック
リモートプロセスのメモリーにコードを書き込んだ後、コードを実行する必要があります。次のセクションでは、これを実現するために使用できるさまざまな手法について説明します。
ここでの調査では、amd64 マシンに焦点を当てました。他のアーキテクチャでは若干の違いが当てはまるかもしれませんが、一般的な概念は変わらないはずです。
プロセス命令ポインターの修正
対象:ptrace
Ptrace を使用してプロセスにアタッチすると、そのプロセスの実行が一時停止され、命令ポインターを含むプロセスレジスターを検査および変更できます。これは、SETREGS および GETREGS ptrace 要求を使用して実行できます。プロセスの実行フローを修正するためには、ptrace を使用して、シェルコードのアドレスへの命令ポインターを修正できます。
スニペット 7 の例では、次の 3 つの手順を実行しました。
GETREGS ptrace 要求を使用して、現在のレジスター値を取得する
命令ポインターを修正して、ペイロードアドレスを指すようにする(後述するように 2 だけインクリメントされる)
SETREGS 要求を使用して、変更をプロセスに適用する
// Get old register state.
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
// Modify the instruction pointer to point to our payload
regs.rip = payload_address + 2;
// Modify the registers
ptrace(PTRACE_SETREGS, pid, NULL, ®s);
スニペット 7:ptrace SETREGS を使用して、命令ポインターをペイロードに向ける
SETREGS は、プロセスレジスターを変更する「伝統的」かつ最も文書化された方法ですが、もう 1 つの ptrace 要求である POKEUSER を使用してこれを実行することもできます。
POKEUSER 要求を使用すると、プロセスの USER エリア にデータを書き込むことができます。これは、レジスターを含むプロセスに関する情報を含む構造体( sys/user.hで定義)です。POKEUSER を正しいオフセットで呼び出すことで、命令ポインターをコードのアドレスで上書きし、以前と同じ結果を得ることができます(スニペット 8)。
// calculate the offset of the RIP register, based on the USER struct definition
rip_offset = 16 * sizeof(unsigned long);
ptrace(PTRACE_POKEUSER, pid, rip_offset, payload_address + 2);
スニペット 8:ptrace POKEUSER を使用して、命令ポインターをペイロードに向ける
POKEUSER を使用して RIP を変更する実装は、 リポジトリーをご参照ください。
RIP += 2:いつ、なぜ?
スニペット 7 およびスニペット 8 に示すように、RIP をペイロードのアドレスに変更する際に、それを 2 だけインクリメントすることも行っています。これを行うのは、 ptrace の興味深いふるまい に対応するためです。ptrace を使用してプロセスからデタッチした後、RIP の値が 2 だけデクリメントすることがあります。これが発生している理由を理解してみましょう。
ptrace を使用してプロセスにアタッチすると、カーネルで現在実行されているシステムコールが中断されることがあります。システムコールが正しく実行されるようにするために、プロセスからデタッチしたときにカーネルが再実行します。
システムコールの実行中、RIP はすでに次の実行命令を指しています。システムコールを再実行するために、カーネルは RIP の値を 2(amd64 でのシステムコール命令のサイズ)だけデクリメントします。この変更後、RIP がシステムコール命令を再度指し示し、この命令が再度実行されます(図 7)。
コードの注入中にシステムコールの最中でプロセスを中断してしまうと、問題が発生することがあります。私たちのコードを指すように RIP を変更した後も、カーネルが新しい値を 2 だけデクリメントさせるため、シェルコードの前に 2 バイトのギャップが生じ、これにより、シェルコードが失敗する可能性が高くなります(図 8)。
このふるまいに対応するためには、シェルコードの先頭に 2 つの無操作(NOP)命令を追加し、RIP がシェルコードのアドレス + 2 を指すようにするという 2 つのアクションを実行します。これらの 2 つの手順により、コードが正しく実行されます。
システムコール中にプロセスを中断した場合、カーネルが新しい RIP 値をデクリメントして、私たちの実際のコードに組み込む 2 つの NOPS を含むシェルコードの開始アドレスを指すようになります。
システムコール中にプロセスを中断しなかった場合は、新しい RIP はデクリメントされず、2 つの NOPS がスキップされ、私たちのコードが実行されます。これら 2 つのシナリオを図 9 に示します。
現行命令の修正
対象:ptrace、procfs mem
procfs のもう 1 つの興味深いファイルは syscall ファイルです。このファイルには、プロセスによって現在実行されているシステムコールに関する情報(システムコール番号、それに渡された引数、スタックポインター、そして(ここでの目的にとって最も興味深い)プロセス命令ポインターが格納されます(図 10)。プロセスが現在システムコールを実行していない場合でも、プロセスのスタックポインターと命令ポインターは syscall ファイルに存在します。
この情報により、プロセスの実行フローを制御することができます。次に実行される命令のアドレスがわかれば、それを独自の命令で上書きすることができます。
これを実装するために、攻撃者は次の 4 つの手順を実行できます。
SIGSTOP シグナルを送信して、プロセスの実行を停止する
プロセス syscall ファイルを読み込んで、次に実行される命令のアドレスを特定する
特定されたアドレスにシェルコードを書き込む
SIGCONT シグナルを送信して、プロセスの実行を再開する
スニペット 9 では、このプロセスの擬似コードを提供しています。
// Suspend the process by sending a SIGSTOP signal
kill(pid, SIGSTOP);
// Open the syscall file
FILE *syscall_file = fopen("/proc/<pid>/syscall", "r");
// Extract the instruction pointer from the syscall file
long instruction_pointer = ...
// Write our payload to the address of the current instruction pointer using
procfs mem
FILE *mem_file = fopen("/proc/<pid>/mem", "w");
fseek(mem_file, instruction_pointer, SEEK_SET);
fwrite(payload, sizeof(char), payload_size, mem_file);
// Resume execution by sending a SIGCONT signal
kill(pid, SIGCONT);
スニペット 9:procfs mem を使用して、命令ポインターの現在のアドレスでプロセスメモリーを変更し、プロセスの実行フローをハイジャックする
スニペット 9 の例では、この手法を procfs mem ファイルを使用して実装していますが、ptrace POKETEXT を使用して、ペイロードをメモリーに書き込むこともできることに注意することが大切です。
前述のように、process_vm_writev はメモリーのアクセス権限によって制限されており、書き込み可能なメモリー領域のみを変更できます。WX メモリー領域から実行されているコードが見つかる可能性は低いため、このプリミティブの process_vm_writev の信頼性が低下します。procfs mem ファイルを使用したこの手法の実装は
こちら でご確認ください。
スタックハイジャック
対象:ptrace、procfs mem ファイル、process_vm_writev
もう 1 つの興味深いメモリー領域は、プロセススタックです。この領域は、maps ファイルを使用して特定することもできます。スタックメモリーは実行可能ではありませんが(図 11)、プロセスの実行フローをハイジャックするために使用できます。
関数が呼び出されるたびに、呼び出し元関数の戻りアドレスがスタックにプッシュされます。関数が実行を完了すると、プロセッサーはこの戻りアドレスをスタックから取得し、このアドレスにジャンプします(図 12)。
このメカニズムを悪用するためには、スタック上の戻りアドレスを特定し、シェルコードを指す新しいアドレスで上書きします。現在の関数が実行を完了するとすぐに、攻撃者コードが実行されます(図 13)。
スタックの先頭を特定するために、前述の procfs syscall ファイルを解析できます。このファイルには、スタック・ポインター・レジスターの値も含まれています。
この手法を実行するためには、次の 6 つの手順を使用できます。
SIGSTOP シグナルを送信して、プロセスの実行を停止する
procfs syscall ファイルを解析して、プロセスのスタックポインターを特定する
プロセススタックをスキャンし、戻りアドレスを特定する
前述の書き込みプリミティブのいずれかを使用して、プロセスメモリーにペイロードを注入する
戻りアドレスをペイロードのアドレスで上書きする
SIGCONT シグナルを送信して、プロセスの実行を再開する
現在の関数が実行を完了すると、ペイロードが実行されます。
すべてのプロセスインタラクション手法でスタックを変更できるため、これらすべてを使用してこの手法を実装できます。process_vm_writevv syscall を使用したこの手法の実装は、 リポジトリーをご参照ください。
ROP スタックハイジャック
対象:ptrace、procfs mem ファイル、process_vm_writev
スタックハイジャック手法は、実行可能なメモリーやレジスターを変更せずにプロセスの実行フローをハイジャックできるという点で興味深いものです。それにもかかわらず、それを使用できるようにするためには、実行可能メモリー領域にあるシェルコードにジャンプする必要があります。WX 領域を検索したり(前述のように)、ptrace/procfs mem を使用して書き込み不可能なメモリーに書き込むことができます。
しかし、このようなアクションを避けたい場合はどうすればよいでしょうか?もう 1 つの秘策があります。 リターン指向プログラミング (ROP)です。プロセススタックに書き込む機能を使用して、スタックを ROP チェーンで上書きできるのです(図 14)。プロセスメモリーにすでに存在する実行可能ガジェットに依存しているため、新しい実行可能コードを記述せずにペイロードを構築できます。
この手法は、次の 7 つの手順で構成されます。
SIGSTOP シグナルを送信して、プロセスの実行を停止する
procfs syscall ファイルを解析して、プロセスのスタックポインターを特定する
プロセススタックをスキャンし、戻りアドレスを特定する
前述の書き込みプリミティブのいずれかを使用して、実行権限なしで書き込み可能なメモリー領域にペイロードを注入する
ROP チェーンを構築して mprotect を呼び出し、シェルコード実行可能ファイルのメモリー領域をマークする
ROP チェーンでスタックを上書きする(特定された戻りアドレスのアドレスから開始)
SIGCONT シグナルを送信して、プロセスの実行を再開する
現在の関数が実行を完了すると、ROP チェーンが実行され、シェルコードが実行可能になり、その関数にジャンプします。
この概念は、AON Cyber Labs の Rory McNamara 氏が、procfs mem インジェクションを扱った自著の ブログ記事 の中で実証したものです。
この手法では、書き込み不可のメモリー領域を変更する必要はありません。したがって、process_vm_writev を含むプロセスとやり取りするためのどの手法を使用しても実行できます。process_vm_writev を使用したこの手法の実装は
こちら でご確認ください。私たちが知る限り、process_vm_writev システムコールのみに依存するインジェクション手法の公開デモンストレーションは、これが初めてです。
GOT ハイジャック
対象:ptrace、procfs mem ファイル、process_vm_writev
通常書き込み可能なもう 1 つの興味深いメモリーセクションは GOT です。グローバル・オフセット・テーブル(GOT)は、動的にリンクされた ELF ファイルの再配置プロセスの一部として使用されるメモリーセクションです。ここでは詳細には触れず、私たちの目的に関連する部分(プログラムによってインポートされた機能のアドレスを格納するセクション)に焦点を当てます。プログラムは、リモートライブラリーから関数を呼び出すたびに、GOT にアクセスしてそのメモリーアドレスを解決します(図 15)。
このメカニズムは、プロセス実行フローをハイジャックするために攻撃者によって悪用される可能性があります。通常、GOT メモリーは書き込み可能です。つまり、攻撃者は、内部のアドレスをペイロードのアドレスで上書きできます。次に関数がプロセスによって呼び出されると、代わりに攻撃者コードが実行されます(図 16)。
この手法は、次の 4 つの手順で構成されます。
SIGSTOP シグナルを送信して、プロセスの実行を停止する
maps ファイルを解析して、GOT メモリー領域を特定する
セクションのアドレスをペイロードのアドレスで上書きする
SIGCONT シグナルを送信して、プロセスの実行を再開する
上書きされた関数が呼び出されると、ペイロードが実行されます。
この攻撃に影響を与える可能性のあるメモリー保護の 1 つは、 full RELROです。この設定を使用してバイナリーをコンパイルすると、GOT メモリーに読み取り専用の権限が与えられ、上書きが防止される可能性があります。
しかし、RELRO はほとんどの場合、この攻撃を防ぐことはできません。
ptrace と procfs mem はメモリーのアクセス権限をバイパスするため、RELRO は無関係になります。
RELRO はプロセスバイナリー自体に影響を与えますが、ロードされたライブラリーには影響しません。RELRO を使用せずにコンパイルされたライブラリーをプロセスがロードすると、そのライブラリーは書き込み可能になり、上書きできるようになります。
process_vm_writevv syscall を使用したこの手法の実装は、 リポジトリーをご参照ください。
実行プリミティブの要約
この表には、ここで説明した実行プリミティブのすべてと、それらを実装できる手法の概要が示されています。
リモート・プロセス・インタラクションの制限
ここで説明した手法を使用してリモートプロセスを操作する機能を決定する設定は複数あります。このセクションでは、2 つの主な機能について簡単に説明します。
ptrace_scope
ptrace_scope は、リモートプロセスで ptrace を使用できるユーザーを決定する設定です。次の値を指定できます。
0:プロセスは、同じ UID を持つ限り、システム上の他のプロセスにアタッチできます。
1:通常のプロセスは、子プロセスにのみアタッチできます。特権プロセス( CAP_SYS_PTRACEを持つ)は、関連のないプロセスにアタッチできます。これは、多くのディストリビューションのデフォルト設定です。
2:CAP_SYS_PTRACE を持つプロセスだけがプロセスにアタッチできます。この機能は通常、root にのみ付与されます。
3:リモートプロセスへのアタッチは無効になります。
この設定は、その名前に反し、リモートプロセスの procmem ファイルにアクセスし、process_vm_writev を使用する機能にも影響します。
「dumpable」(ダンプ可能)属性
プロセスのリカバリーに関する注意事項
ここで説明したすべてのインジェクション手法では、プロセスレジスターの変更、実行可能メモリーの上書き、スタック上の戻りアドレス、GOT など、何らかの方法でプロセス状態を変更する必要があります。これらのアクションはすべて、プロセスの通常の実行フローを変更し、ペイロードが終了した後に予期しないふるまいを引き起こします。
これは、注入されたペイロードとともにターゲットプロセスを継続して実行したい場合には、問題となる可能性があります。プロセスが正常に動作し続けることを保証するためには、元の状態を復元する必要があります。一般的なリカバリーフローは、次の 8 つの手順で構成されます。
リモート読み取りプリミティブを使用して、上書きする予定のメモリーコンテンツをバックアップする
プロセスレジスターの現在の内容をバックアップする(ptrace またはシェルコードを使用して実行)
ペイロードを実行する(例:コードを別のスレッドで実行する共有オブジェクト(SO)ファイルをロードする)
ペイロードが完了したら、実行が完了したことを注入プロセスに示す(割り込みを発生させることで実装)
リモートプロセスを一時停止する
プロセスレジスターの状態を復元する
上書きしたメモリーを復元する
プロセスの実行を再開する
実装の詳細は、使用するインジェクション手法によって多少異なりますが、この一般的な概要に従う必要があります。Adam Chester 氏の Linux ptrace インジェクションブログ投稿 には、ptrace ベースインジェクションの後のプロセスリカバリーの詳細な例が示されています。
本稿の目的は、守る側が使用して手法を理解し、適切に検知することができるように、インジェクション手法の概要を説明することでした。焦点が防御にあるため、攻撃者が手法を完全に武器化するために必要な、異なる手法に対するリカバリー手順は詳しく説明しないことにしました。
検知と緩和
先ほど説明したように、攻撃者が Linux マシンでプロセスインジェクションを実行できるようにする手法は数多くあります。幸いにも、これらすべての手法では異常なアクションを実行する必要があるため、これが検知の機会となります。次のセクションでは、Linux でのプロセスインジェクションを検知して緩和するために実装できるさまざまな戦略について詳しく説明します。
「インジェクションシステムコール」
本稿では、ptrace、procfs、process_vm_writev という 3 つの手法を使用してリモートプロセスとやり取りしました。これらの手法は悪意をもって使用される可能性があるため、監視する必要があります。
Linux マシンにロギングソリューションを導入することが重要となります。システムコールの実行監視は、 Sysmon for Linux または Aqua Security の Tracee (すでに、この投稿で説明されている多くの手法をカバーする ルールを実装 している)などの eBPF ベースのロギングユーティリティーを使用して有効にできます。
ロギングを確立した後、組織が環境での「インジェクションシステムコール」の通常の使用状況を分析し、既知の有効な使用事例のベースラインを構築することを推奨します。このようなベースラインが作成された後は、そのベースラインからの逸脱を調査して、潜在的な攻撃を除外する必要があります。システムごとのその他の考慮事項については、次のセクションで説明します。
理想的には、可能な場合は ptrace_scope を使用して、これらのシステムコールの使用を制限するか、完全に阻止します。
ptrace
ほとんどの本番環境では、ptrace システムコールの使用は非常にまれです。有効な ptrace の使用のベースラインを確立した後、異常な ptrace の使用状況を分析することをお勧めします。
次の ptrace 要求は、リモートプロセスの変更が可能にするもので、非常に疑わしいと考えられます。
POKEDATA/POKETEXT
POKEUSER
SETREGS
procfs
procfs mem ファイルへの書き込みには、正当な使用例がいくつかありますが、このふるまいはあまり一般的ではありません。有効なユースケースのベースラインを構築したら、異常な書き込み操作を分析することをお勧めします。
また、 /proc/<pid>/task procfs ディレクトリーを検討することも重要です。このディレクトリーは、プロセスのさまざまなスレッドに関する情報を公開します。各スレッドには独自の procfs ディレクトリーがあり、ここで説明した主要な procfs ファイル(mem、maps、syscall ファイルなど)がすべて含まれています。
図 17 では、/proc/<pid> ディレクトリーから syscall ファイルを読み込むことが、プロセスのメインスレッドを表す /proc/<pid>/task/<pid> ディレクトリーからの読み込みと同じであることがわかります。
process_vm_writev
繰り返しになりますが、このシステムコールの正当な使用のベースラインを構築することで、異常な逸脱を特定できます。他のプロセスのメモリーに書き込む不明なプロセスは、疑わしいとみなして分析する必要があります。
プロセスの異常を検知する
プロセスインジェクションを直接検知するだけでなく、その副作用を検知することもできます。コードがリモートプロセスに注入されると、そのふるまいが変わります。プロセスで実行される通常のアクションに加えて、ペイロードのアクションも同じプロセスで実行されるようになります。
このふるまいの変化が、検知の機会を提供する可能性があります。通常のプロセスのふるまいのベースラインを構築することで、それからの疑わしい逸脱を特定し、コードインジェクションが発生した可能性があることを示唆することができます。このようなふるまいの例としては、異常な子プロセスの生成、以前に表示されなかった SO ファイルのロード、異常なポートを介した通信などがあります。
Akamai の研究者は このアプローチを文書化 し、ネットワークの異常を分析することによってコードインジェクションを特定する方法を実演しました。
まとめ
攻撃者には、Linux マシンでインジェクション攻撃を実行するためのオプションが多数あります。これらの手法は攻撃者にとって非常に有用であるとともに、防御者に貴重な検知の機会を提供します。Linux マシンに確実なロギング機能および検知機能を実装することで、組織はセキュリティ体制を大幅に改善することができます。