eBPF Summit 2021 にてCTFが開催された。 CTFをやったことが無い人間が「eBPFならちょっと面白そう」と思って取り組んでみたところ、一応解けたので小さな成功体験になった、という話。
以降ではStage2の解決過程を記しているので注意
学び
Stage1
- skbを編集するようなprogを補助輪付きでありながらも書けた
- iproute2でさくっとtcにattachできると知る
- security groupsなどをここで書くと良さそう
Stage2
- bpftoolはeBPF関連のシステムコールのCLIラッパーであり実行環境での運用や開発時のデバッグで重宝する
- eBPF用coreutilsと言っても過言ではない便利さ
/sys/fs/bpf
配下を初めてみて実際にpinされたmapが作られることを知った- attach_typeはよく分からなかった
Stage1
WireGuardのデバイス経由で 100.202.1.1 1138
と通信しパスフレーズを取り出したいがiptablsのフィルタにより遮断されている。
eBPFプログラムを書いてこのフィルタを迂回せよ、という問題。
通信経路はこんな感じ。
wg0 (100.202.1.2/24)
|
| filter
| -d 100.202.1.1/32 -p udp -m udp -j DROP
v
namespace berpaffyl
wg0 (100.202.1.1/24)
/usr/bin/server :1138 100.202.1.2
フィルタの内容
root@ee7b43bf78d8:/# iptables-save -c
# Generated by iptables-save v1.8.7 on Sat Aug 21 22:00:04 2021
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
[1:29] -A OUTPUT -d 100.202.1.1/32 -p udp -m udp -j DROP
COMMIT
# Completed on Sat Aug 21 22:00:04 2021
下記のようにUDPの通信はフィルタされて固まることが確認できる。
root@ee7b43bf78d8:/# echo | netcat -u 100.202.1.1 1138
下記のディレクトリにREADMEやbpfプログラムの足場があるので、これらを駆使して100.202.1.1の1138ポートでLISTENしているサーバと通信する
root@ee7b43bf78d8:/# ls -l /bpf
total 28
-rw-r--r--. 1 root root 591 Aug 16 08:51 Makefile
-rw-r--r--. 1 root root 247 Aug 16 08:51 README
-rw-r--r--. 1 root root 2116 Aug 17 13:01 bpf.c
-rw-r--r--. 1 root root 8128 Aug 16 08:51 bpf_helper_defs.h
-rw-r--r--. 1 root root 7756 Aug 16 08:51 bpf_helpers.h
bpf.cを読むとwg0デバイスにingress, egressそれぞれattachされるプログラムが記述されており、
/* TODO solution */
と記載されたegress側のプログラムだけ書き込めば良いことが解る。
このStage1の問題はingressプログラムでやっていることを読むと、egress側がどう振る前ばいいか自ずと把握できるようになっている。 のため、解決方法は割愛。
Stage2
Stageと同様にTCPサーバと通信してパスフレーズを読みだす問題。 セキュリティプログラムが起動されておりTCPの接続が拒否されるので、なんとかしてセキュリティプログラムを停止させる必要がある。
Stage1と違ってヒントらしきものは無いので環境を確認しながら解法を見つけることになる。
localhostで動作しているプログラムからパスフレーズを読めるか確認してみる。
root@09e8e01f6e47:/# curl localhost:1977
curl: (7) Failed to connect to localhost port 1977: Connection refused
listenしているがconn refusedになる。
どうも通信経路の間に何かが入っててSYN ACKを拒否している模様。
ps
で状況をみるとこの拒否の仕掛けを仕込んでそうなプログラム derefence-droid
が大量に起動している。
root@f34b43eefdef:/# ps aux | head -10
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.3 2.5 9840868 25592 pts/0 Ssl 22:27 0:02 /usr/bin/container
root 12 0.0 0.4 1077164 4876 pts/0 Sl 22:27 0:00 /usr/bin/password-server :1977
root 19 0.0 0.0 2668 856 pts/0 S 22:27 0:00 /usr/bin/defense-droid
root 22 0.0 0.0 2668 856 pts/0 S 22:27 0:00 /usr/bin/defense-droid
root 25 0.0 0.0 2668 856 pts/0 S 22:27 0:00 /usr/bin/defense-droid
root 35 0.0 0.0 2668 792 pts/0 S 22:27 0:00 /usr/bin/defense-droid
root 36 0.0 0.0 2668 792 pts/0 S 22:27 0:00 /usr/bin/defense-droid
root 73 0.0 0.0 2668 792 pts/0 S 22:27 0:00 /usr/bin/defense-droid
root 74 0.0 0.0 2668 848 pts/0 S 22:27 0:00 /usr/bin/defense-droid
試しに自分でもこのプログラムを実行してみて何者かを観てみる。
root@8c0037161c3d:/# /usr/bin/defense-droid
bpf prog fd from 2059: 8
passing fd 8
received fd 4
bpf link fd from 2059: 9
passing fd 9
received fd 5
libbpf: failed to pin map: File exists
bpf_map__pin: File exists
どうやらeBPFプログラムをopenしているらしいことが解る。 これが何かのフィルタ処理を行っているらしい。 このeBPFプログラムを何とかしてdetachかunloadなどすれば良さそう。
/sys/fs
とかに何か出てなかったっけと思って探してみるとpinされた何かが見える。
root@8c0037161c3d:/# ls -l /sys/fs/bpf/defense-droid-doc
-rw------- 1 root root 0 Aug 21 00:32 /sys/fs/bpf/defense-droid-doc
なんとかしてプログラムをdetachしたいのでpinnedなプログラム等を見つける方法を探してみる。
eBPF: retrieve fd
of the pinned bpf program
なるほどbpftoolを使うようだと解る。
root@316f78963fd1:/# bpftool prog
6: kprobe name connect4_hook tag a23eac2b87717a62 gpl
loaded_at 2021-08-21T12:32:23+0000 uid 0
xlated 336B jited 230B memlock 4096B map_ids 5,4
btf_id 5
metadata:
kprobeにattachされたプログラムがあるようだ。
ちなみにmapもみることができる。名前からさっきの def_droid_doc
がmapであると解った。
root@8c0037161c3d:/# bpftool map show
3: array name def_droid_doc flags 0x0
key 4B value 1536B max_entries 1 memlock 4096B
btf_id 5
4: array name connect_.rodata flags 0x480
key 4B value 2664B max_entries 1 memlock 8192B
btf_id 5 frozen
5: array name connect_.bss flags 0x400
key 4B value 16B max_entries 1 memlock 8192B
btf_id 5
このmapの中身がdumpできそうなのでのぞいてみる
root@316f78963fd1:/# bpftool map dump id 3
~略~
"content_5": "Each defense droid has different [f]eature [d]esignators and only one can employ the Imperial Advanced Protection (TM) as a [f]eature [d]esignator at a time. The others are decoys. Refer to the .proc status panel.",
~略~
フィルタ処理の本体は大量のdefense-droid
のうち一つだけでロードされており、後はデコイらしい。
kprobe系はbpftool perf
で情報を見ることができるらしいので見てみる。
bpftool-perf
root@316f78963fd1:/# bpftool perf show
pid 439 fd 5: prog_id 6 kprobe func __sys_connect offset 0
実際にロードしているプログラムを持つpidが解る。 確かにprocfsを見ると何かそれらしきfdを開いている。
root@316f78963fd1:/# ls -l /proc/439/fd/
total 0
lr-x------ 1 root root 64 Aug 21 13:07 0 -> /dev/null
l-wx------ 1 root root 64 Aug 21 13:07 1 -> /dev/null
l-wx------ 1 root root 64 Aug 21 13:07 2 -> /dev/null
lrwx------ 1 root root 64 Aug 21 13:07 4 -> anon_inode:bpf-prog
lrwx------ 1 root root 64 Aug 21 13:07 5 -> 'anon_inode:[perf_event]'
このeBPFプログラムを何とかして閉じる or 止めるなどしたい。 それらしいコマンドが無いか見てみる。
root@f34b43eefdef:/# bpftool prog help
bpftool prog detach PROG ATTACH_TYPE [MAP]
ATTACH_TYPE := { msg_verdict | stream_verdict | stream_parser |
flow_dissector }
PROG
はid 6
などで指定、MAP
もpinned /sys/fs/bpf/defense-droid-doc
などで指定。ATTACH_TYPE
だけが何を指定したらいいのか解らないので雰囲気でやってみる。
root@f34b43eefdef:/# bpftool prog detach id 6 msg_verdict pinned /sys/fs/bpf/defense-droid-doc
Error: failed prog detach from map
失敗。
まあ、開いているkprobeのプログラムはpid 439
のプロセスが持ち主なので、そりゃ外部から閉じれる訳がなさそう。
何かヒントがないかなと思い、プログラムコードのダンプを見てみる。
root@f34b43eefdef:/# bpftool prog dump xlated id 6
~略~
; if (old.sin_port == bpf_htons(1977)) {
20: (69) r1 = *(u16 *)(r7 +2)
; if (old.sin_port == bpf_htons(1977)) {
21: (b7) r11 = 831077056
~略~
; old.sin_port = bpf_htons(1980);
27: (6b) *(u16 *)(r7 +2) = r1
~略~
どうやら接続先ポートが1977
なら1980
の未開放ポートに書き換えることでconnection refusedを起こさせているらしい。
が、特にプログラムの停止をするためのヒントは得られず。。。
うーむ、とりあえずそのプロセスを止めるしかないのでkillしたら、、、
root@316f78963fd1:/# kill 439
curl localhost:1977
でパスフレーズを取れるようになった。。
こんな解き方でいいのか不安になったので 答え合わせ の動画を確認してみたが、やっぱりkill
で止めてたので正解だったらしい。
反省点
Stage2 ヒントのつかみ忘れ
最初のサーバプロセス起動用のコンテナを-d
のdaemon移動にしてしまったため、解析に有用な情報が表示されているのを見逃した。
下記のようなコマンドでサーバプロセスを含むコンテナとbashを起動する。
sudo docker run --privileged --name ctf-3 --rm --tty --interactive "quay.io/isovalent/ebpf-summit-2021-ctf-challenge-3"
するとbash起動時に下記のようなメッセージが表示されbpftoolやtcpconnect.btなどが使えることが示唆される。
[22:27:57] Imperial Advanced Protection (TM) deployed.
[22:27:57] Master password securely stored at "curl localhost:1977".
[22:27:57] Diagnostic tool "tcpconnect.bt" loaded and ready to be used.
[22:27:57] Control panel "bpftool" activated.
が、-d
つきでdocker run
をしてこのメッセージを見逃しており、bpftoolを使い始めるまでに時間がかかった。
Stage1と同様にサーバ用コンテナはdaemon起動して置けば端末開けておく必要がなくて楽だと思って勝手に-d
をつけたのが失敗だった。
Stage2 eBPFプログラムをなんとかunloadしようとした
対象のeBPFプログラムはpinされておらず、openしているプロセスを止めて解放するしか無いのだが、この辺の仕掛けがあまり頭に入っていなかった。 なので、bpftoolでなんとかプログラムをdetachかunloadできるだろうと思い込んで、余計に調査をして時間をとった。
感想とこれから
自分でも解けるくらいなので難易度は大したことなかったようだ。(本編の動画でもStage2は10~20分くらいで解いている)
eBPFの開発やデバッグに使う道具立てとしてbpftoolがとても有用なことが解ったのは収穫だった。
また、これまでeBPFプログラムでパケットのDROPをさせる簡単なsocketフィルタ処理しか書いたことがなかったので、今回のようにパケットを編集するStage1のtcフィルタを触れたのはいい経験となった。
まだ観れていない本編の他のプレゼンを少しずつみながら、引き続きlibbpf-rsを使ってeBPFプログラムをたまに試作してみよう。 今回Day2ではLize RiceがA Load Balancer from scratchにてXDPベースのLBを15分くらいで作っていたのをみると、なんか自分でも少しできそうな気がしてくるのだった。(という簡単さを伝える意味ではLizのプレゼンはとても良かったと思う)