Snyk Fetch the Flag CTF 2023 解説:Off the SETUID
Carlos Polop
Yago Gutiérrez
2023年11月30日
0 分で読めますFetchに参加してくれてありがとうございます! Fetch the Flag CTFに参加した何千人ものプレイヤーの皆さん、おめでとうございます。Snykの2023年Fetch the Flagに参加し、Off the SETUIDチャレンジの答えをお探しなら、ここがぴったりです。解法を一緒に見ていきましょう!
見慣れない環境に迷い込んでしまいました。この環境にあるものだけで切り抜けられるでしょうか?それとも、ただ生き延びるだけで精いっぱいでしょうか……
rootユーザーのホームディレクトリからフラグを取り出してください。
これは妙な問題です。Webチャレンジでしょうか、それともカーネルのpwnでしょうか?
qemuのスクリプト、コンパイル済みのカーネル(対応する.diffもあります)、そしてinitramfsイメージ(initプログラムのコードもあります)が用意されています。まずはinitramfsを展開して、中を見てみましょう。
ディストリビューションを使うか、使わないか
なるほど、ディストリビューションなしです。シェルも、よく使うユーティリティも、何もありません。initプログラムは明らかにコンパイル済みです(シェルがないので、スクリプトではありません)。ただし、ソースコードも用意されています。
一般的なinitスクリプトと同じ処理を行っています。デバイスを初期化し、procfsとsysfs(noexec付き)をマウントし、rootfsを読み取り専用で再マウントして、ネットワークを初期化します。その後、ユーザーuid=100として、ルートディレクトリを/var/runに設定したHTTPサーバーをポート8080で起動します。
/var/runにあるのはindex.phpだけです。
まとめると、このHTTPサーバーにはPHPコードインジェクションの脆弱性があることは明らかです。以下のようなコードを使えば、PHPの対話型リバースシェルを取得できます。
しかし、フラグはrootだけが読み取れるため、読むことができません。そこで、権限昇格の方法を見つける必要があります。fun_setuid.diffの登場です。
fun_setuid()
.diffファイルを見ると、カーネルに新しいsyscallが追加されています。
簡単に言うと、このsyscallを使うと、プロセスのuidを現在の値より大きい値に変更できます(または、プロセスがCAP_SETUID capabilityを持っている場合に変更できます)。
このsyscallはprepare_user_creds()というヘルパー関数を使います。この関数はさらにprepare_cred()を使って新しいstruct credを割り当て、ユーザーの情報を書き換えて返します。エラーが発生した場合は、エラーコードの負の値を返します。そのためsyscall関数は、資格情報を確定する前に戻り値が負でないことを確認します(下記の設計上の注意点を参照)。また、要求されたuidが無効(つまり-1)でないことも確認し、無効な場合はNULLを返します。
設計上の注意点
この設計には、ほかにも2つの重大な問題があります。
Cでアドレスとゼロを比較しても機能しません。まったく意味がありません。アドレスは符号なしの型として扱われるため、負かどうかを確認するのは無意味です。そのため、コンパイラーはif文を完全に削除します。
たとえコンパイラーが削除しなかったとしても、このコードは動きません。Linuxのx86ではカーネルアドレスは常に負だからです。そのため、
prep_user_creds()で問題が見つからなくても、返されるstruct cred*ポインターは負の値と解釈され、確定されません。
資格情報の扱い
問題は、syscall関数がこの最後の可能性を確認しないことです。そのため、資格情報構造体へのポインターとしてNULLポインターを確定してしまう可能性があります。initramfsを変更してbusybox,を追加し、sysctl vm.mmap_min_addrを実行すれば、このシステムでプロセスがマッピングできる最小アドレスを確認できます。さらにデバッグが必要な場合は、/proc/config.gzも利用できます。
mmap_min_addr=0であり、さらにrun.shスクリプトを見ると、VMではSMAPが有効になっていないことが分かります。これは好都合です。NULLアドレスをマッピングし、そこにrootの資格情報構造体を配置してからfun_setuid(-1)を呼び出せば、資格情報ポインターをNULL、つまり偽造した資格情報に向けられます。
capabilityを変更する必要はありませんが、せっかくなので、追加点を狙って変更してもよいでしょう。ただし、cred->usage=1(参照カウンターのようなもの)を設定することが非常に重要です。そうしないと、カーネルはこれをUAFバグだと判断します(BUG_ON(atomic_read(&new->usage) < 1))。
また、このプロセスを終了させてはいけません。終了すると、カーネルがこの構造体を解放しようとするため、問題が起きます。だからシェルを起動できないのです(execve()を呼び出すと、最終的に現在のプロセスが終了します)。そのため、最後にpause()を呼び出しています。
しかし、まだ問題があります。エクスプロイトをアップロードして実行する場所がありません。エクスプロイトをシェルコードに変換し、PHPコードからPHPプロセスのprocfsのmemファイルに書き込めば、任意のネイティブコードを実行できます。そこからmmap()でNULLをマッピングし、偽の資格情報を配置してfun_setuid(-1)を呼び出せます。PHPスクリプトからは、これらの操作を実行できないからです……
でも、それだけでは面白くありません。
Memexec
数カ月前、ディストリビューションのない環境を、特定の状況下で回避する方法について、DEFCON 31で講演しました。そのために、私(Yago Gutiérrez)はmemexecというツールを開発しました。これを使うと、任意のプログラムをPHP上でファイルレス実行できます。
この対話型PHPセッションを貼り付けるだけです。これで、バイナリのURIとプログラムの引数配列を受け取るmemexec()関数が使えるようになります。たとえば、busyboxを置いたHTTPサーバーを用意して、システム内でコマンドを実行できます。
重要:VMにはSSLライブラリがないため、HTTPSは使用しないでください。
さあ、今度こそこのカーネルを攻略しましょう。
Fetchを盛り上げてくれてありがとう!
2023年のFetch the Flagに参加したすべてのチームに、心から感謝します!皆さんに会えて本当にうれしかったです。@Arget(GHでは@arget13)、@carlospolop(こちらもGHでは@carlospop)で、いつでも連絡してください。
2023年のその他のチャレンジの解説はこちらです。ぜひご覧ください!
