はじめに
Team Enuの鈴木です。今回は2023年2月に開催されたSECCON CTF 2022 Finalsに参加してきたので、そこで出題されたbabyescape(https://github.com/ptr-yudai/ptr-SECCON-CTF-2022-Finals/tree/main/pwnable/babyescape/files/babyescape)というpwn問題について解説します。
概要
chrootで閉じ込められたディレクトリの外側にあるフラグファイルを読む問題です。対象のLinuxマシン上ではroot権限のシェルを使って操作することができますが、seccompとchrootでほかのプロセスやほとんどのファイルへのアクセスが制限されています。
ディレクトリ構成:
問題のソースコード:
seccompの内容:
方針
root権限が与えられていてカーネルモジュールをインストールできるので、カーネルモジュールを使ってカーネル空間のオブジェクトを操作して、chrootを解除します。
カーネルモジュールの作り方
カーネルモジュールを作成するには対象となるバージョンのLinuxカーネルのヘッダーが必要です。
上図で示したように対象の環境で使われているLinuxカーネルのバージョンは6.1.5でした。aptでは6.1.5のlinux-headersはないので、自分でLinuxのソースコードをダウンロードして使用します。Linuxのソースコードはhttps://mirrors.edge.kernel.org/pub/linux/kernel/v6.x/からダウンロードできます。Linuxのソースコードをダウンロードしたら以下のコマンドでカーネルモジュールの作成に必要なファイルを作成します。
cd linux-6.1.5
make defconfig
make modules_prepare
あとは以下のようなMakefileでカーネルモジュールをコンパイルできるようになります。
obj-m += exploit.o
all:
make -C linux-6.1.5 M=$(PWD) modules
clean:
make -C linux-6.1.5 M=$(PWD) clean
ディレクトリ構成:
操作するカーネルオブジェクト
chrootに関係がありそうなカーネルオブジェクトを探します。まずchrootのマニュアルなどを調べると、Linuxはプロセスごとにルートディレクトリを管理しているということが推測できます。Linuxのプロセスごとの状態を保持する構造体としてtask_structがあります。task_structのメンバを調べると気になる構造体のポインタを持っています。
https://elixir.bootlin.com/linux/v6.1.5/source/include/linux/sched.h#L1091
https://elixir.bootlin.com/linux/v6.1.5/source/include/linux/fs_struct.h#L9
chrootに関係するシンボルを調べていくと、ちょうどここを操作するコードが見つかるのでtask_structのfsがルートディレクトリの情報につながっている可能性が高そうです。
https://elixir.bootlin.com/linux/v6.1.5/source/fs/open.c#L577
オフセットの調べ方
今回はカーネル本体とモジュールを別々にコンパイルしていて、コンフィグなどを合わせていないため、構造体内部のオフセットがカーネルとモジュールで異なる可能性が高いです。そこで、カーネルで使っている構造体内部のオフセットを調べて、モジュール側でポインタ演算を使って目的の構造体のメンバにアクセスします。
カーネルで使っている構造体内部のオフセットを調べるには、目的のメンバにアクセスしている特徴的なソースコードを探して、その部分の逆アセンブリを読みます。
例としてtask_structのfsがtask_structの先頭から何バイト目にあるかを調べます。task_structのfsはset_fs_rootの呼び出し時にアクセスされることが分かっています。set_fs_rootは他にも以下のコードから呼び出されています。
https://elixir.bootlin.com/linux/v6.1.5/C/ident/set_fs_root
ほとんどのケースで以下のようにtask_structのfsを第一引数に取るので、逆アセンブリで適当なcall set_fs_rootの直前を調べればtask_structのfsのオフセットがわかりそうです。
https://elixir.bootlin.com/linux/v6.1.5/source/fs/init.c#L76
set_fs_rootのアドレスを/proc/kallsymsで調べて逆アセンブリでそのアドレスを探すと以下のようなコードが見つかります。rdiに代入する際に0x5a8を足したアドレスにアクセスしているので、これがtask_structの先頭からfsまでのオフセットであるとわかります。
解法
chrootによって変化したルートディレクトリを戻したいので、chrootの影響を受けていないプロセスのfsをコピーします。initプロセスは確実に影響を受けていないプロセスなので、init_taskのfsをすべてのtask_structのfsにコピーすればchrootから脱出することができます。完成したコードは以下のようになります。
おわりに
今回はSECCON CTF 2022 Finalで出題されたbabyescapeについて解説しました。作問者の方による解法も公開されていますので、そちらもご覧になることをお勧めします。SECCON CTF 2022 FinalではこのほかにもKing of the Hill形式の問題や、LANケーブルを切断して通信を盗聴する問題など、面白い問題が出題されていました。これからももっと精進して幅広い技術を身に着けたいと思います。