コード実行を支援する技術「BPF」を悪用して動作するマルウェアの仕組みを解説
BPF(Berkley Packet Filtering)を利用するマルウェアやルートキットについて、サイバー犯罪の事例や概念実証を交えて解説します。BPFは、クラウドサービスのオペレーティングシステムやカーネル内部でのコード実行を支援する仕組みです。BPFの不正利用を検知する手法についても詳しく解説します。
BPF(Barkeley Packet Filtering)は、プログラムによってカーネル内でコードを実行するための強力な仕組みであり、LinuxやBSD系(Berkeley Software Distribution)を含む近代的なオペレーティングシステム(OS)に対応しています。近いうちに、Windowsにも対応すると予測されます。このBPFについて、セキュリティ対策チームではまだ広く認知されていないかも知れません。しかし、サイバー犯罪グループ側はすでにBPFを攻撃手段として不正利用し始めています。実際にBPFを用いた攻撃の概念実証(Proof of Concept)がすでに公開されているだけでなく、一部の犯罪グループは、BPFによって動くマルウェアを展開し、特定業界への攻撃を行っています。防御チームでは、BPFによる新たな脅威に備えるため、その攻撃手段や仕組みに関する理解を深め、セキュリティ対策や手続きを強化することが求められます。
BPFの概要
BPFは、仮想マシン(VM:Virtual Machine)によってバイトコードを読み取る機能を備えたカーネルエンジンの一種です。もともとはネットワーク用パケットフィルタとして開発されたものですが、後により広範な用途に対応できるように、「eBPF(extended BPF)」の名前でリブランドされました。現在、「BPF」という用語は、「eBPF」と同種の技術自体を指す意味で用いられます。一方、前身であるパケットフィルタに対しては、「cBPF(classic BPF)」という別の用語が充てがわれました。最近に入ってeBPFの名前やロゴが広く認知されるようになりましたが、その技術自体は、開発者の間で今なお「BPF」と呼ばれています。本稿では、「BPF」という言葉を、cBPFとeBPF双方のコードを指す意味で使用します。
カーネルBPFエンジンの構成要素として、BPF命令(インストラクション)のインタプリタと、これらの命令を端末固有の機械語に変換する実行時コンパイラが挙げられます。この構成上、通常のLinux用実行ファイルはある時点でBPFバイトコードの配列をバッファ領域内に確保し、特別なシステムコールを呼び出すことで、当該のBPFプログラム(eBPFのプログラムやcBPFのフィルタ)をカーネル側にロードします。一連の動作や仕組みについては、後に詳しく説明します。
eBPFがLinuxに対して行うことは、JavaScriptがWebサイトに対して行うことと同等であるという見方が、開発者の間に存在します。言い方を変えると、eBPFは、稼働中のカーネルに対して機能を付け加えるものと見なせます。以降、これによって何が実現されるのかについて解説します。
BPFで実現できること
トレンドマイクロは2023年7月にマルウェア「BPFDoor」に関する記事を公開し、バックドア機能が感染端末内で稼働する際に、cBPFフィルタがどのように動作するかについて述べました。cBPFの機能はネットワークデータのフィルタリングに限定されますが、eBPFではその制約が取り払われ、パフォーマンスの測定、システムコールのフック(独自の処理を追加する仕組み)、可観測性(システムの状態を監視する機能)、さらにはセキュリティ領域にまで手を広げることが可能です。このようにBPFはカーネルレベルでコードを実行する汎用的な技術へと進化したため、何が実現できるかを正確に定義することは難しくなりました。端的に言えば、ロードするコードの内容に依存します。セキュリティ観点では、攻撃者がBPFを利用してルートキットやマルウェアを強化する可能性があります。例えば、感染端末におけるプロセスID(PID)の隠蔽、システムコールへの割り込み、通信データに対する処理、カーネルモジュールの隠蔽など、多くの用途が考えられます。
ここで、攻撃シナリオの具体例を挙げます。まず、標的端末に配備されたルートキットが、eBPFプログラムをロードします。本プログラムは、標的システム上で特定のTCP(Transmission Control Protocol)パケットを待ち構え、受信したタイミングでバックドア機能を立ち上げます。こうしたパケットを処理するコードは、カーネル内に留まります。結果、当該のeBPFプログラムはシステム管理者のみならず、セキュリティ製品からも隠蔽されることとなります。発見するには、システム内のBPFに関わる領域を直接解析し、不審な挙動やイベントの存在を見極める必要があります。
BPFの利用法
eBPFを用いたソフトウェアの開発に役立つツールとして、ライブラリ「libbpf C」が挙げられます。本ライブラリは、eBPFコードのロードを担うプログラムによって呼び出されます。現時点でさまざまなプログラミング言語に対応し、Go、Python、Rustによるラッパーの利用例が確認されています。本ライブラリは、システムコール「SYS_bpf」を介してeBPFプログラムをロードします。
eBPFプログラムにはさまざまな種別が存在します。マルウェア解析において特に重要な種別を下記に示します。
- BPF_PROG_TYPE_SOCKET_FILTER:ネットワーク用パケットフィルタリングを行うプログラム
- BPF_PROG_TYPE_TRACEPOINT、BPF_PROG_TYPE_RAW_TRACEPOINT、BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE:既存のカーネル・トレースポイントに追加するプログラム
- BPF_PROG_TYPE_KPROBE:ユーザの要求にあったトレースポイントが存在しない場合に有用な「kprobe(Kernel Probe)プログラム」
- BPF_PROG_TYPE_XDP:流入パケットに対するディープパケットインスペクション(DPI:Deep Packet Inspection)やパケット処理の機能を提供
- BPF_PROG_TYPE_SCHED_CLS:上記と同様だが、流入、流出パケットの双方に対応
- BPF_PROG_TYPE_SYSCALL:システム機能を呼び出せるプログラム
例として、マルウェアがカーネルのシステムコールをフックするケースについて考えます。ここで、対象システムコールのトレースポイントがカーネル側からすでに提供されているのであれば(以下を参照)、種別「BPF_PROG_TYPE_TRACEPOINT」のeBPFプログラムをロードします。一方、対象システムコールのトレースポイントが存在しない場合は、代わりに種別「BPF_PROG_TYPE_KPROBE」のeBPFプログラムをロードします。
/sys/kernel/debug/tracing/events/syscalls
eBPFプログラムのロード後、それをカーネルイベントに紐付ける必要があります。このために、SYS_perf_event_openを呼び出します。また、イベントの有効化にはSYS_ioctlを使用します。
なお、cBPFフィルタをロードする際にはSYS_setsockoptを呼び出します。これについては、後に詳しく説明します。はじめに、eBPFを用いるルートキットについて解説します。
eBPFによるルートキットの概念実証
eBPFによるルートキットの概念実証として、調査時に確認された4種を取り上げ、その機能や仕組みについて解説します。
1. Boopkit
Kris Novaが概念実証として作成したBoopkitは、各種コンポーネントから構成されます。
ユーザ空間のプログラム(通常のELF実行ファイル):
- Boopkit-boop:攻撃者側の端末で稼働するTCPクライアント
- Boopkitサーバ:被害端末で稼働
「eBPFプローブ」はeBPFバイトコードの小さな一要素であり、カーネル側に動的にロードされます。これらのプローブが、Boopkitサーバによって下記4種のトレースポイントにロードされます。
- tp/tcp/tcp_bad_csum:チェックサム不正のあるTCPパケットが標的システムに届いた際にBoopkitを呼び出す目的で使用される。実行対象のコマンドはパケットに同梱され、マッピング領域にロードされる。
- tp/tcp/tcp_receive_reset:Boopkitを呼び出す第2の手段であり、RSTフラグを含むSYNパケットに反応する。
- tp/syscalls/sys_enter_getdents64、tp/syscalls/sys_exit_getdents64:システムコール「getdents64()」をフックし、Boopkit自身のディレクトリが一覧表示されないようするために使用される。
BoopkitがeBPFプローブのロード処理や設定を行う際には、ライブラリ「libbpf」に含まれる下記の関数を使用します。
- bpf_map_update_elem()
- bpf_object__attach_skeleton()
- bpf_program__attach()
- bpf_program__fd()
- ring_buffer__poll()
- bpf_map__name()
- bpf_map_delete_elem()
- bpf_object__destroy_skeleton()
- ring_buffer__new()
- bpf_map_get_next_key()
- bpf_object__open()
- bpf_object__next_map()
- bpf_map_lookup_elem()
- bpf_object__open_skeleton()
- bpf_object__load()
- bpf_map__fd()
- bpf_program__section_name()
- bpf_object__load_skeleton()
- bpf_program__name()
- bpf_object__next_program()
上記の関数はバイナリの実行中に呼び出されるものですが、その全てがバイナリのソースコードから明示的に呼び出されるわけではありません。例えば、ライブラリ「libbpf」に含まれるプログラマ向けのC言語マクロが起因となって呼び出されるものも、少数ながら存在します。本ライブラリは、最後にシステムコール「SYS_bpf()」を呼び出します(amd64アーキテクチャの0x141)。
下記に、BoopkitがBPFコードをロードし、先述のカーネルイベント「tcp_bad_csum」に紐付ける際の動作を、アーキテクチャ寄りの視点で確認した結果を示します。
eBPFプログラムのロード後、Boopkitサーバは下記の処理をループで実行します。
- eBPFプログラムがロードされていることを確認
- eBPFのマップ領域にコマンドが存在するかをチェック
- TCPパケットとして届いたコマンドを実行
Boopkitクライアントは、実行対象コマンドを「magicパケット」の内部に埋め込み、サーバ側で開かれている任意のポートに向けて送信します。このようにして、攻撃者は感染端末をリモートから完全にコントロールできるようになります。
サーバ(感染端末)側で稼働しているソフトウェアは、一連の動作に影響を与えません。「magicパケット」を受信した時点で、ルートキットがすぐに稼働するためです。この処理は、当該パケットがファイアウォールの制御に回される前の段階で行われます。また、パケット自体がカーネル側で無視されても、受信したコマンドはそのまま実行されます。
2. Bad BPF
ツール「Bad BPF」はルートキットではありませんが、通常のルートキットに相当する機能を備えています。具体的には下記を実行することが可能です。
- bpfdos:SYS_ptraceを用いて任意のプロセスにSIGKILLを送信する。
- exechijack:SYS_execveをフックし、「/a」の配下からプログラムを実行させる。
- pidhide:SYS_getdentsをフックして特定のPIDを隠蔽する。
- sudoadd:SYS_openat、SYS_read、SYS_closeをフックし、コマンド「sudo」を介してユーザの権限昇格を行う。
- textreplace:仮想ファイルシステムの文字列を書き換える。「/proc」、「/sys」などの配下に存在するものを隠すことが可能になる。
- textreplace2:textreplaceと同様に動作するが、ランタイム設定情報の格納先としてマップを利用する。
- writeblocker:SYS_writeをフックする。
上述したELFプログラムの全てが、以下の亜種のeBPFプログラムをロードします。
BPF_PROG_TYPE_TRACEPOINT
そのために、コマンド「BPF_PROG_LOAD」を添えてSYS_bpfを呼び出します。続いて、同じ手続をSYS_perf_event_openとSYS_ioctlに適用します。
3. ebpfkit
ebpfkitは高度な機能を備えたルートキットであり、以下の亜種に相当する多数のプログラムをロードします。
BPF_PROG_TYPE_KPROBE
BPF_PROG_TYPE_XDP
BPF_PROG_TYPE_SCHED_CLS
一方で、以下についてはあまり使用しません。図2に、ebpfkitがSYS_bpfの呼び出す際の様子を示します。
BPF_PROG_TYPE_TRACEPOINT
ebpfkitはXDPを用いることで、開かれたポート宛て(デフォルトでは8000/tcp)に届いたパケットを傍受し、クライアント側とのやり取りを行います。ポートが開かれていることを確認するため、ebpfkitは小さなWebサーバから配布されます。
ebpfkitは特にkprobesを多用します。そのため、正常なシステムの出力(図3)とebpfkitに感染したシステムの出力(図4)を比較すると、両者の差異が明確に表れます。
図4は、p___x64_sys_bpf__1530のkprobeが作成されたことを示します。本kprobeによってSYS_bpfがフックされる結果、ユーザ側でeBPFプログラムの一覧を取得しても、ebpfkitに関する情報は除外され、その存在を確認できなくなりま
4. TripleCross
ルートキット「TripleCross」は主に以下の亜種のプログラムをロードしますが、その際に、上で示した概念実証とは異なった手法を利用します。
BPF_PROG_TYPE_TRACEPOINT
BPF_PROG_TYPE_XDP
具体的には、コマンド「BPF_OBJ_PIN(0x06)」を添えてSYS_bpfを呼び出すことにより、オブジェクトを固定(pin)します。図5に、TripleCrossの起動時における呼び出し例を示します。
ebpfkitと同様、TripleCrossは通信関連の処理にXDPを使用します。
eBPFのインプラントを検知
eBPFプログラムの各種属性は、構造体「bpf_prog_info」に定義されています。その内容として、32ビットのプログラムIDを筆頭に、タイプ情報、命令数、ロード時刻(システムブート以降の経過時間をナノ秒単位で保持)、オーナーのユーザID(UID)などが挙げられます。これらの属性情報は、稼働中のeBPFプログラムが不正であるかを判定する際に役立ちます。例えば、環境にもよりますが、1分以上に渡ってロードされているeBPFプログラムは、不正である可能性が疑われます。また、ロード時刻を使用することで、eBPFプログラムがロードされる度にアラートを出すことも可能となります。
Linux開発者の協力により、上述したeBPFプログラムの各種属性を取得、表示するコマンドラインツール「bpftool」が提供されています。正常なLinuxシステムで余分なBPFプログラムが稼働していない場合、bpftoolの出力は図6のようになることが期待されます。
bpftoolのフレームワークでは、各eBPFプログラムについて下記のフィールドが存在します。
- id:eBPFプログラムの識別用IDを表し、32ビットの符号なし整数で定義される。
- type:プログラムの種別を表し、32ビットの符号なし整数で定義される。詳細は、ソース「bpf.h」の列挙体「bpf_prog_type」に記載されている。
- name:プログラム名を表し、16桁の文字列で定義される。
- tag:タグ情報であり、16桁の16進数文字列で定義される。
- gpl_compatible:プログラムがGPLに準拠しているかを示し、ブール(真偽)値で定義される。
- loaded_at:秒単位でのロード時刻を表す。先述の構造体「bpf_prog_info」に含まれるシステムブート以降の経過時間をもとに算出される。
- UID:作成者のユーザIDを表す。
- bytes_xlated:eBPFプログラムに含まれる命令のサイズを表す。
- jited:機械語にコンパイルされている場合は「true」が指定される。
- bytes_jited:機械語アセンブリにおける命令のサイズを表す。
- bytes_memlock:メモリ内に固定されたアドレス空間のサイズを表す。
- pids:当該プログラムをロードしたプロセスのID(PID)を表す。
上記の中で、フィールド「loaded_at」が重要な意味を持ちます。システム起動時にロードされるBPFプログラムについては、多くの場合、そのロード時刻とブート時刻が一致すると考えられます。実際に一致しているかを調べる際は、図7のように、オプション「-b」付きのコマンド「who」を利用できます。なお、システム起動からBPFプログラムのロードまでには多少の処理が発生するため、ロード時刻がブート時刻より数秒遅れる場合もあります。
上述の例では、BPFプログラムのロード時刻がブート時刻に近いことが示されます。以上の他、ロードを行ったプロセスIDに相当するフィールド「pids」も、解析において重要な意味を持ちます。PIDが1(systemd:システムデーモン)でない場合は、不正の可能性を念頭に調査する必要があります。
図8に、Boopkitに感染したシステムでbpftoolを実行した際の出力を示します。
Boopkitは、プロセス一覧の取得コマンド「ps」などから、自身のPIDを隠蔽します。しかし、bpftoolを用いた場合は、依然として当該のPIDが表示されます。
Boopkitの場合、BPFプログラムの名称からも、発生している事象に関するヒントが得られます。しかし、検知の手法としては、確実性に欠けると考えられます。
bpftoolはJSON形式の出力をサポートしているため、PIDが2以上のプロセスからロードされたBPFプログラムの一覧を容易に取得できます。
eBPFプログラムによってハッシュマップを含む共有保存領域が使用された場合、そのマップIDも、bpftoolから確認できます。中身を解析することで、マルウェアの設定情報を得られる場合もあります。例えば先述の図8では、以下のプログラムによって3種のマップID(627、628、630)が使用されています。Boopkitの動作上、3つ目のマップに設定情報が格納されます。図10に示す通り、その中身についても、bpftoolから確認可能です。
handle_getdents_patch
プログラマの立場からeBPFプログラムを解析する場合は、その手段としてシステムコール「SYS_bpf」を直接呼び出す方式と、libbpfなどのツール経由で間接的に呼び出す方式が存在します。図11に、後者に対応するコード例を示します。
上図では、可読性のためにエラー対応処理を割愛しています。以下の関数は、稼働しているeBPFプログラムのIDを逐次取得するために、
bpf_prog_get_next_id()
そして以下の関数が、取得されたIDの詳細情報を得るために使用されます。
bpf_obj_get_info_by_fd()
また、稼働端末におけるライブラリ・フックの影響を回避するため、本検知プログラムは静的にリンクすることが望まれます。さらに、本検知法を回避する手段も、依然として存在します。例えばシステムコール「SYS_bpf」をフックした状態で他のeBPFルートキットを利用する手口や、権限の高い「リング0」を用いる手口が挙げられます。実際に、ebpfkitが当該の手口を用いることで知られています。一般的なルールとして、この手のルートキットに感染したシステムは、もはや信頼できないものと見なさざるを得ません。
cBPFのマルウェア
冒頭で挙げたcBPFは、eBPFよりも機能面で劣り、その処理対象はネットワークに限定されます。しかし、トレンドマイクロの調査によると、挙動の痕跡を消去する仕組みを備えたマルウェアファミリとして「BPFDoor」と「Symbiote」の2種が確認されています。
cBPFフィルタをロードする際には、汎用的なeBPFプログラムのロード時と異なり、関数「SYS_setsockopt」を呼び出し、オープン・ネットワークソケットのオプションを設定します。図12に、本関数のプロトタイプ定義を示します。
ELFプログラムがcBPFフィルタをロードする際には、図12に示すsetsockopt()の第3引数として以下を指定します。
SO_ATTACH_FILTER(0x1a)
また、第4引数「optval」として、下記構造体へのポインタを指定します。
上図に示される構造体「sock_filter」の定義を下図に示します。
図13に示す構造体「sock_fprog」のフィールド「len」は、構造体「sock_filter」の配列の要素数を指します。別の言い方をすると、cBPFの命令数に相当します。配列全体のサイズは「len * 8(バイト)」となります。
以降、一連の仕組みに基づいて不正なcBPFフィルタがどのようにロードされるかについて解説します。
BPFDoor
マルウェア「BPFDoor」は、SYS_cloneを用いて子プロセスを作成し、そこからSYS_setsockoptを呼び出すことでcBPFフィルタをロードします。この手続きは、SYS_socketの呼び出し直後に行われます。
この後、バックドアがSYS_recvを用いてコマンドを受信します。
ロードするcBPFフィルタの内容は、BPFDoorの検体に応じて異なる場合があります。さらに、2種のcBPFフィルタをまとめてロードするパターンも確認されました。これらのフィルタは、不正な通信データをネットワーク監視機能から隠蔽する目的で使用されます。
Symbiote
Symbioteは、通信データの検知を回避する目的で単一のcBPFフィルタを使用します。さらに本マルウェアは、パケット監視ツール「tcpdump」がcBPFのバイトコードをコンパイルしてロードする点を逆手に利用し、環境変数「LD_PRELOAD」を介して関数「setsockopt」をフックします。これにより、setsockoptが作成するいかなるフィルタについても、cBPFフィルタが自動で付加されるようになります。図16に、フックされた関数「setsockopt()」の内容を示します。
ユーザが正規なフィルタをロードしようとすると、上図の関数「apply_filter」が呼び出され、不正なcBPFフィルタがロードされます。図17に、本関数の内容を示します。
ハッキングツール
本調査では、cBPFフィルタを用いる以下のようなSSH用ブルートフォース・ツールが発見され、先述のライブラリ「libpcap」に静的リンクを張っていることが分かりました。
f2a96cdd228e1279f612d61b756863fea5adde977ad92b8e2a26352fa88feb18
e4f87188ba73acc5706f5af8a2e295a4b8c31883743f249cb57a4d89ae5735d0
しかし、当初の目的からは外れるため、詳細解析の対象外としました。トレンドマイクロでは、本ツールを以下として検知します。
cBPFフィルタの検知
ソケットに紐付けられたcBPFフィルタを検知する上では、ツール「ss」が有用です。
図18の内容より、PID「319731」のプロセス「tcpdump」がRaw型パケットのソケットを開き、そこに24個の命令からなるcBPFフィルタを追加したことが分かります。
まとめ
BPFを用いるマルウェアの勢いは、着実に強まっています。今後、cBPFよりもさらに強力なeBPFによるマルウェアが多数出現するものと予想されます。
eBPFそのものは強力かつ有用な技術ですが、マルウェアやルートキットによる検知回避の手段として不正利用される可能性もあります。そのため、本技術を用いる不審なプログラムに対しては、十分な警戒を払う必要があります。最近ではeBPFのセキュリティに関する議論も行われ、特に脆弱性を突いた攻撃に対する防御法が、重要トピックの1つに挙がっています。しかし、脆弱性のみに焦点を絞ることなく、そもそも稼働中のカーネルから不正なeBPFプログラムをいかに検知するかについても、検討する価値があるでしょう。例えば、SYS_openの正規なフックと不正なフックは、どのように区別できるでしょうか。双方とも公開状態のトレースポイントを利用する可能性があり、SYS_openがフックされているだけで不正な活動と決めつけることはできません。正しく判別するためには、eBPFプログラム自体と、そのロード処理の双方を含めて、丁寧に分析を行う必要があります。
本稿で提示した検知法については、さらにテストを行い、偽陽性(不正でないものを不正と誤識別する)の排除にも力を入れる必要があります。別の課題として、今後eBPFの用途が拡大し、システム上で稼働するeBPFプログラムの数や種類が増えるにつれて、その中から不正なものを特定する作業も複雑化してしまう点が挙げられます。eBPFを用いるツールとして、現状では特にコンテナツールやネットワーク監視ツールなどが考えられます。将来的に攻撃者は、不正なeBPFプログラムを評判の高い正規なeBPFプログラムに偽装する、不正なeBPFコードを正規なコードに埋め込むなどの手口を行使する可能性があります。サイバー犯罪者にとって、eBPFは新たな可能性を生み出す貴重な技術として映るでしょう。
防御チーム側にとって重要なことは、cBPFやeBPFのバイトコードやアセンブリに関する理解を深め、スキルを高めることです。そうすることで、仮にBPFによるマルウェアの脅威が発生したとしても、的確な対処法をより的確に見出だせるようになると考えられます。
参考記事:
How BPF-Enabled Malware Works
By: Fernando Mercês
翻訳:清水 浩平(Core Technology Marketing, Trend Micro™ Research)