eBPF Summit 2021 CTF Stage1, Stage2を解いてみた

2021-08-22 / [linux] [ebpf]

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 }

PROGid 6などで指定、MAPpinned /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のプレゼンはとても良かったと思う)