クラウドコンピューティングが必要ですか? 今すぐ始める

Linux プロセスインジェクション決定版ガイド

Ori David

執筆者

Ori David

November 14, 2024

Ori David

執筆者

Ori David

Ori David は、Akamai で Security Researcher を務めています。彼の調査は、オフェンシブセキュリティ、マルウェア分析、脅威ハンティングに焦点を当てています。

Linux でのさまざまなインジェクション手法に対する認識は、特に Windows と比較すると比較的低いようです。
Linux でのさまざまなインジェクション手法に対する認識は、特に Windows と比較すると比較的低いようです。

はじめに

プロセスインジェクション手法は、攻撃者のツールセットの重要な要素です。攻撃者は、悪性コードを正規のプロセス内で実行して検知を回避したり、リモートプロセスにフックを仕掛けてふるまいを変更したりできます。 

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 では、リモートプロセスのメモリーとのやり取りは、主に ptraceprocfsprocess_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, &regs);

スニペット 1:ptrace を使用してリモートプロセスのレジスターを取得する例

procfs

procfs は、システム上でプロセスを実行するためのインターフェースとして機能する特別な擬似ファイルシステムです。このファイルシステムには、/proc ディレクトリーからアクセスできます(図 1)。

procfs is a special pseudo filesystem that acts as an interface to running processes on the system. It can be accessed through the /proc directory (Figure 1). Fig. 1: A directory listing of the /proc directory on a Linux machine

各プロセスは、PID に従って名前が付けられたディレクトリーとして表されます。このディレクトリーには、プロセスに関する情報を提供するファイルがあります。たとえば、 cmdline ファイルには、プロセスのコマンドラインが保持され、 environ ファイルには、プロセス環境変数などが含まれます。

また、procfs はリモートプロセスのメモリーとやり取りする機能も提供しています。すべてのプロセスディレクトリー内には、プロセスのアドレス空間全体を表す特殊な「 mem 」ファイルがあります。特定のオフセットでプロセスのメモリーファイルにアクセスすることは、同じアドレスでプロセスメモリーにアクセスすることと同じです。

図 2 の例では、xxd ユーティリティーを使用して、指定されたオフセットから始まるプロセス・メモリー・ファイルから 100 バイトを読み取っています。

In the example in Figure 2, we used the xxd utility to read 100 bytes from the process mem file, starting at a specified offset. Fig. 2: Using xxd to read the procfs mem file

GDB を使用してメモリー内の同じアドレスを確認した場合、内容が同じであることがわかります(図 3)。

If we inspect the same address in memory using GDB, we will note that the contents are identical (Figure 3). Fig. 3: Using GDB to inspect the process memory at the same offset we read from the procfs mem file

maps ファイルは、プロセスディレクトリーにあるもう 1 つの興味深いファイルです(図 4)。このファイルには、アドレス範囲やメモリーのアクセス権限など、プロセスアドレス空間内のさまざまなメモリー領域に関する情報が含まれています。

The maps file is another interesting file that can be found in the process directory (Figure 4). Fig. 4: Example contents of a process maps file

次のセクションでは、特定の権限を持つメモリー領域を特定する機能がいかに役立つかを見ていきます。

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)。

We can find such a region by parsing the previously mentioned procfs maps file, and identifying a memory region with execute (x) permissions (Figure 5). Fig. 5: Identifying an executable memory region in the process maps file

実行可能領域には、書き込み可能領域と書き込み不可領域の 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」コマンドを使用して、書き込みメモリー領域と実行メモリー領域を持つプロセスを特定する

The command in Snippet 5 will scan the maps file of all processes on the system and identify regions with write and execute permissions (Figure 6). Fig. 6: Example output of finding processes with WX memory regions

このような領域を特定した後、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 つの手順を実行しました。

  1. GETREGS ptrace 要求を使用して、現在のレジスター値を取得する

  2. 命令ポインターを修正して、ペイロードアドレスを指すようにする(後述するように 2 だけインクリメントされる)

  3. SETREGS 要求を使用して、変更をプロセスに適用する

  // Get old register state.
  struct user_regs_struct regs;
  ptrace(PTRACE_GETREGS, pid, NULL, &regs);
 
  // Modify the instruction pointer to point to our payload
  regs.rip = payload_address + 2;

  // Modify the registers
  ptrace(PTRACE_SETREGS, pid, NULL, &regs);

スニペット 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)。

After this change, RIP will point to the syscall instruction again, causing it to run another time (Figure 7). Fig. 7: The effect of using ptrace on a process during syscall execution

コードの注入中にシステムコールの最中でプロセスを中断してしまうと、問題が発生することがあります。私たちのコードを指すように RIP を変更した後も、カーネルが新しい値を 2 だけデクリメントさせるため、シェルコードの前に 2 バイトのギャップが生じ、これにより、シェルコードが失敗する可能性が高くなります(図 8)。

After we modify RIP to point to our code, the kernel will still decrement the new value by 2, leading to a 2-byte gap before our shellcode, which will likely cause it to fail (Figure 8). Fig. 8: Using ptrace to point RIP to our shellcode results in a junk instruction executing

このふるまいに対応するためには、シェルコードの先頭に 2 つの無操作(NOP)命令を追加し、RIP がシェルコードのアドレス + 2 を指すようにするという 2 つのアクションを実行します。これらの 2 つの手順により、コードが正しく実行されます。

システムコール中にプロセスを中断した場合、カーネルが新しい RIP 値をデクリメントして、私たちの実際のコードに組み込む 2 つの NOPS を含むシェルコードの開始アドレスを指すようになります。

システムコール中にプロセスを中断しなかった場合は、新しい RIP はデクリメントされず、2 つの NOPS がスキップされ、私たちのコードが実行されます。これら 2 つのシナリオを図 9 に示します。

These 2 scenarios are depicted in Figure 9 Fig. 9: Overcoming the ptrace RIP interaction

現行命令の修正

対象:ptrace、procfs mem

procfs のもう 1 つの興味深いファイルは syscall ファイルです。このファイルには、プロセスによって現在実行されているシステムコールに関する情報(システムコール番号、それに渡された引数、スタックポインター、そして(ここでの目的にとって最も興味深い)プロセス命令ポインターが格納されます(図 10)。プロセスが現在システムコールを実行していない場合でも、プロセスのスタックポインターと命令ポインターは syscall ファイルに存在します。

Another interesting file in procfs is the syscall file. This file holds information about the syscall that is currently executed by the process — the syscall number, the arguments that were passed to it, the stack pointer, and (most interesting for our cause) the process instruction pointer (Figure 10). Fig. 10: The structure of the procfs syscall file

この情報により、プロセスの実行フローを制御することができます。次に実行される命令のアドレスがわかれば、それを独自の命令で上書きすることができます。

これを実装するために、攻撃者は次の 4 つの手順を実行できます。

  1. SIGSTOP シグナルを送信して、プロセスの実行を停止する

  2. プロセス syscall ファイルを読み込んで、次に実行される命令のアドレスを特定する

  3. 特定されたアドレスにシェルコードを書き込む

  4. 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)、プロセスの実行フローをハイジャックするために使用できます。

Although the stack memory is not executable (Figure 11), we can still use it to hijack the execution flow of the process. Fig. 11: Identifying the process stack address using the maps file

関数が呼び出されるたびに、呼び出し元関数の戻りアドレスがスタックにプッシュされます。関数が実行を完了すると、プロセッサーはこの戻りアドレスをスタックから取得し、このアドレスにジャンプします(図 12)。

When the function finishes execution, the processor takes this return address from the stack and jumps to it (Figure 12). Fig. 12: Return address on the stack pointing to an address in main

このメカニズムを悪用するためには、スタック上の戻りアドレスを特定し、シェルコードを指す新しいアドレスで上書きします。現在の関数が実行を完了するとすぐに、攻撃者コードが実行されます(図 13)。

To abuse this mechanism, we can identify a return address on the stack and overwrite it with a new address that points to our shellcode. As soon as the current function finishes execution, our code will run (Figure 13). Fig. 13: Overwriting a return address on the stack to point to the attackers code

スタックの先頭を特定するために、前述の procfs syscall ファイルを解析できます。このファイルには、スタック・ポインター・レジスターの値も含まれています。 

この手法を実行するためには、次の 6 つの手順を使用できます。

  1. SIGSTOP シグナルを送信して、プロセスの実行を停止する

  2. procfs syscall ファイルを解析して、プロセスのスタックポインターを特定する

  3. プロセススタックをスキャンし、戻りアドレスを特定する

  4. 前述の書き込みプリミティブのいずれかを使用して、プロセスメモリーにペイロードを注入する

  5. 戻りアドレスをペイロードのアドレスで上書きする

  6. SIGCONT シグナルを送信して、プロセスの実行を再開する

現在の関数が実行を完了すると、ペイロードが実行されます。

すべてのプロセスインタラクション手法でスタックを変更できるため、これらすべてを使用してこの手法を実装できます。process_vm_writevv syscall を使用したこの手法の実装は、 リポジトリーをご参照ください。

ROP スタックハイジャック

対象:ptrace、procfs mem ファイル、process_vm_writev

スタックハイジャック手法は、実行可能なメモリーやレジスターを変更せずにプロセスの実行フローをハイジャックできるという点で興味深いものです。それにもかかわらず、それを使用できるようにするためには、実行可能メモリー領域にあるシェルコードにジャンプする必要があります。WX 領域を検索したり(前述のように)、ptrace/procfs mem を使用して書き込み不可能なメモリーに書き込むことができます。

しかし、このようなアクションを避けたい場合はどうすればよいでしょうか?もう 1 つの秘策があります。 リターン指向プログラミング (ROP)です。プロセススタックに書き込む機能を使用して、スタックを ROP チェーンで上書きできるのです(図 14)。プロセスメモリーにすでに存在する実行可能ガジェットに依存しているため、新しい実行可能コードを記述せずにペイロードを構築できます。

But what if we want to avoid these actions? Well, we have another trick up our sleeve — return-oriented programming (ROP). By using our ability to write to the process stack, we can overwrite it with a ROP chain (Figure 14). Fig. 14: Injecting a ROP chain to the process stack

 この手法は、次の 7 つの手順で構成されます。

  1. SIGSTOP シグナルを送信して、プロセスの実行を停止する

  2. procfs syscall ファイルを解析して、プロセスのスタックポインターを特定する

  3. プロセススタックをスキャンし、戻りアドレスを特定する

  4. 前述の書き込みプリミティブのいずれかを使用して、実行権限なしで書き込み可能なメモリー領域にペイロードを注入する

  5. ROP チェーンを構築して mprotect を呼び出し、シェルコード実行可能ファイルのメモリー領域をマークする

  6. ROP チェーンでスタックを上書きする(特定された戻りアドレスのアドレスから開始)

  7. 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)。

Whenever the program calls a function from a remote library, it resolves its memory address by accessing the GOT (Figure 15). Fig. 15: Resolving a library function address using the GOT

このメカニズムは、プロセス実行フローをハイジャックするために攻撃者によって悪用される可能性があります。通常、GOT メモリーは書き込み可能です。つまり、攻撃者は、内部のアドレスをペイロードのアドレスで上書きできます。次に関数がプロセスによって呼び出されると、代わりに攻撃者コードが実行されます(図 16)。

 The GOT memory is normally writable, meaning that an attacker can overwrite any of the addresses inside it with the address of their payload. The next time the function is called by the process, the attacker code will execute instead (Figure 16). Fig. 16: Modifying a function in the GOT to point to the attacker payload

この手法は、次の 4 つの手順で構成されます。

  1. SIGSTOP シグナルを送信して、プロセスの実行を停止する

  2. maps ファイルを解析して、GOT メモリー領域を特定する

  3. セクションのアドレスをペイロードのアドレスで上書きする

  4. SIGCONT シグナルを送信して、プロセスの実行を再開する

 

上書きされた関数が呼び出されると、ペイロードが実行されます。

この攻撃に影響を与える可能性のあるメモリー保護の 1 つは、 full RELROです。この設定を使用してバイナリーをコンパイルすると、GOT メモリーに読み取り専用の権限が与えられ、上書きが防止される可能性があります。

しかし、RELRO はほとんどの場合、この攻撃を防ぐことはできません。

  • ptrace と procfs mem はメモリーのアクセス権限をバイパスするため、RELRO は無関係になります。 

  • RELRO はプロセスバイナリー自体に影響を与えますが、ロードされたライブラリーには影響しません。RELRO を使用せずにコンパイルされたライブラリーをプロセスがロードすると、そのライブラリーは書き込み可能になり、上書きできるようになります。

process_vm_writevv syscall を使用したこの手法の実装は、 リポジトリーをご参照ください。

実行プリミティブの要約

この表には、ここで説明した実行プリミティブのすべてと、それらを実装できる手法の概要が示されています。

The table summarizes all the possible execution primitives that we described, and with which methods they could be implemented. All the possible execution primitives and the methods that could be used to implement them

リモート・プロセス・インタラクションの制限

ここで説明した手法を使用してリモートプロセスを操作する機能を決定する設定は複数あります。このセクションでは、2 つの主な機能について簡単に説明します。

ptrace_scope

ptrace_scope は、リモートプロセスで ptrace を使用できるユーザーを決定する設定です。次の値を指定できます。

0:プロセスは、同じ UID を持つ限り、システム上の他のプロセスにアタッチできます。

1:通常のプロセスは、子プロセスにのみアタッチできます。特権プロセス( CAP_SYS_PTRACEを持つ)は、関連のないプロセスにアタッチできます。これは、多くのディストリビューションのデフォルト設定です。

2:CAP_SYS_PTRACE を持つプロセスだけがプロセスにアタッチできます。この機能は通常、root にのみ付与されます。

3:リモートプロセスへのアタッチは無効になります。

この設定は、その名前に反し、リモートプロセスの procmem ファイルにアクセスし、process_vm_writev を使用する機能にも影響します。

「dumpable」(ダンプ可能)属性

Linux のすべてのプロセスでは「ダンプ可能」属性が設定されています。この属性は、デフォルトでは true に設定されています。プロセスは 特定の状況下で自動的にダンプ不可になりますが、 prctlを呼び出して手動でダンプ不可に設定することもできます。

ダンプ不可のプロセスには、前述のいかなる手法でもリモートアクセスすることはできません。この設定は他の設定より優先されます。ダンプ不可のプロセスはリモートで変更できません。

プロセスのリカバリーに関する注意事項

ここで説明したすべてのインジェクション手法では、プロセスレジスターの変更、実行可能メモリーの上書き、スタック上の戻りアドレス、GOT など、何らかの方法でプロセス状態を変更する必要があります。これらのアクションはすべて、プロセスの通常の実行フローを変更し、ペイロードが終了した後に予期しないふるまいを引き起こします。

これは、注入されたペイロードとともにターゲットプロセスを継続して実行したい場合には、問題となる可能性があります。プロセスが正常に動作し続けることを保証するためには、元の状態を復元する必要があります。一般的なリカバリーフローは、次の 8 つの手順で構成されます。

  1. リモート読み取りプリミティブを使用して、上書きする予定のメモリーコンテンツをバックアップする

  2. プロセスレジスターの現在の内容をバックアップする(ptrace またはシェルコードを使用して実行)

  3. ペイロードを実行する(例:コードを別のスレッドで実行する共有オブジェクト(SO)ファイルをロードする)

  4. ペイロードが完了したら、実行が完了したことを注入プロセスに示す(割り込みを発生させることで実装)

  5. リモートプロセスを一時停止する

  6. プロセスレジスターの状態を復元する

  7. 上書きしたメモリーを復元する

  8. プロセスの実行を再開する

実装の詳細は、使用するインジェクション手法によって多少異なりますが、この一般的な概要に従う必要があります。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> ディレクトリーからの読み込みと同じであることがわかります。

In Figure 17, we can see that reading the syscall file from the /proc/<pid> directory is equivalent of reading from the /proc/<pid>/task/<pid> directory, which represents the main thread of the process. Fig. 17: Example of using the /proc/<pid>/task directory

process_vm_writev

繰り返しになりますが、このシステムコールの正当な使用のベースラインを構築することで、異常な逸脱を特定できます。他のプロセスのメモリーに書き込む不明なプロセスは、疑わしいとみなして分析する必要があります。

プロセスの異常を検知する

プロセスインジェクションを直接検知するだけでなく、その副作用を検知することもできます。コードがリモートプロセスに注入されると、そのふるまいが変わります。プロセスで実行される通常のアクションに加えて、ペイロードのアクションも同じプロセスで実行されるようになります。

このふるまいの変化が、検知の機会を提供する可能性があります。通常のプロセスのふるまいのベースラインを構築することで、それからの疑わしい逸脱を特定し、コードインジェクションが発生した可能性があることを示唆することができます。このようなふるまいの例としては、異常な子プロセスの生成、以前に表示されなかった SO ファイルのロード、異常なポートを介した通信などがあります。

Akamai の研究者は このアプローチを文書化 し、ネットワークの異常を分析することによってコードインジェクションを特定する方法を実演しました。

まとめ

攻撃者には、Linux マシンでインジェクション攻撃を実行するためのオプションが多数あります。これらの手法は攻撃者にとって非常に有用であるとともに、防御者に貴重な検知の機会を提供します。Linux マシンに確実なロギング機能および検知機能を実装することで、組織はセキュリティ体制を大幅に改善することができます。



Ori David

執筆者

Ori David

November 14, 2024

Ori David

執筆者

Ori David

Ori David は、Akamai で Security Researcher を務めています。彼の調査は、オフェンシブセキュリティ、マルウェア分析、脅威ハンティングに焦点を当てています。