カーネルexploitに入門しにくい大きな原因が、デバッグ方法がよく分からないという点です。
この節ではgdbを使ってqemu上で動くLinuxカーネルをデバッグする方法を学びます。
まず練習問題LK01のファイルをダウンロードしてください。
root権限の取得
手元でKernel Exploitをデバッグする際、一般ユーザー権限だと不自由なことが多いです。特にカーネルやカーネルドライバの処理にブレークポイントを設定したり、リークしたアドレスが何の関数のアドレスかを調べたりする際、root権限がないとカーネル空間のアドレス情報を取得できません。
Kernel Exploitをデバッグする際は、まずroot権限を取得しましょう。この節の内容は前章の例題の(2)と同じですので、既に解いた方は確認程度に読み流してください。
カーネルが起動すると最初に1つのプログラムが実行されます。このプログラムは設定によりパスは様々ですが、多くの場合/init
や/sbin/init
等に存在します。LK01のrootfs.cpio
を展開すると、/init
が存在します。
1 |
|
ここには特に重要な処理は書かれていませんが、/sbin/init
を実行しています。なお、CTFなどで配布されるような小さい環境では/init
に直接、ドライバをインストールしたりシェルを起動したりといった処理が書かれている場合があります。実際、最後のexec
の行の前に/bin/sh
と書けばカーネル起動時にroot権限でシェルを起動できます。ただし、ドライバのインストールなど他の必要な初期化処理が実行されませんので、今回はこのファイルは書き換えません。
さて、/sbin/init
から最終的には/etc/init.d/rcS
というシェルスクリプトが実行されます。このスクリプトは、/etc/init.d
内にあるS
から始まる名前のファイルを実行していきます。今回はS99pawnyable
というスクリプトが存在します。このスクリプトには様々な初期化処理が書かれていますが、終盤の次の行に注目してください。
1 | setsid cttyhack setuidgid 1337 sh |
この行が今回のカーネルで起動時にユーザー権限でシェルを起動しているコードになります。cttyhack
はCtrl+Cなどの入力を使えるようにしてくれるコマンドです。そしてsetuidgid
コマンドを使ってユーザーIDとグループIDを1337に設定し、/bin/sh
を起動しています。この数字を0(=rootユーザー)に変えます。
1 | setsid cttyhack setuidgid 0 sh |
また、詳細は次章で説明しますが、一部のセキュリティ機構を無効化するために、次の行もコメントアウトして消しておいてください。
1 | -echo 2 > /proc/sys/kernel/kptr_restrict # 変更前 |
変更したらcpioに再びパックして、run.sh
を実行すれば下のスクリーンショットのようにroot権限でシェルが使えるようになっているはずです。(パックの方法は前章を参照)
qemuへのアタッチ
qemuはgdbでデバッグするための機能を搭載しています。qemuに-gdb
オプションを渡し、プロトコル、ホスト、ポート番号を指定してlistenできます。run.sh
を編集して、例えば次のオプションを追加するとローカルホストでTCPの12345番ポートでgdbを待ち受けられます。
1 | -gdb tcp::12345 |
今後の演習では断りなく12345番ポートを利用してデバッグしますが、自分の好きな番号を利用して問題ありません。
gdbでアタッチするにはtarget
コマンドでターゲットを設定します。
1 | pwndbg> target remote localhost:12345 |
これで接続が完了すれば成功です。あとは通常のgdbコマンドを利用してレジスタやメモリの読み書き、ブレークポイントの設定などが可能です。メモリアドレスは「そのブレークポイントを付けたコンテキストでの仮想アドレス」になります。つまり、カーネルドライバやユーザー空間のプログラムが使っている馴染みあるアドレスにそのままブレークポイントを設定して構いません。
今回は対象がx86-64です。もし皆さんのgdbが標準でデバッグ対象のアーキテクチャを認識しない場合、次のようにアーキテクチャを設定できます。(通常は自動で認識してくれます。)
1 | pwndbg> set arch i386:x86-64:intel |
カーネルのデバック
/proc/kallsyms
というprocfsを通して、Linuxカーネル中で定義されたアドレスとシンボルの一覧を見られます。次章のKADRの節でも説明しますが、セキュリティ機構によりカーネルのアドレスはroot権限でも見えないことがあります。
root権限取得の節で既にやりましたが、初期化スクリプトの以下の行をコメントアウトするのを忘れないでください。これをしないとカーネル空間のポインタが見えなくなります。
1 | echo 2 > /proc/sys/kernel/kptr_restrict # 変更前 |
さて、実際にkallsyms
を見てみましょう。量が膨大なのでheadなどで先頭だけ見てみます。
このように、シンボルのアドレス、アドレスの位置するセクション、シンボル名の順に並んで出力されます。セクションは例えば"T"ならtextセクション、"D"ならdataセクションのように表され、大文字はグローバルにエクスポートされたシンボルを表します。これらの文字の詳細な仕様はman nm
で確認できます。
例えば上の図だと0xffffffff81000000が_stext
というシンボルのアドレスであることが分かります。これはカーネルがロードされたベースアドレスにあたります。
では、次にcommit_creds
という名前の関数のアドレスをgrepで探してください。見つかったら0xffffffff8106e390がヒットするはずです。gdbでこの関数にブレークポイントを付けて続行します。
1 | pwndbg> break *0xffffffff8106e390 |
この関数は実は新しくプロセスが作られる時などに呼ばれる関数です。シェルでlsコマンドなどを叩くとブレークポイントでgdbが反応するはずです。
第一引数RDIにはカーネル空間のポインタが入っています。このポインタの指すメモリを見てみましょう。
このように、カーネル空間でもユーザー空間と同じようにgdbのコマンドが利用可能です。pwndbg等の拡張機能も使えますが、もちろんカーネル空間向けに書かれた拡張機能でなければ動かないので注意してください。
カーネルのデバッグ用の機能が搭載されたデバッガなどもあるので、みなさんが好みのデバッガを使ってください。
ドライバのデバッグ
次にカーネルモジュールをデバッグしてみましょう。
LK01にはvulnという名前のカーネルモジュールがロードされています。ロードされているモジュールの一覧と、そのベースアドレスは/proc/modules
から確認できます。
これを見ると、vuln
というモジュールが0xffffffffc0000000にロードされていることが分かります。なお、このモジュールのソースコードとバイナリは配布ファイルのsrc
ディレクトリに存在します。ソースコードの詳細解析は別の章でやりますが、このモジュールの関数にブレークポイントを付けてみましょう。
IDAなどでsrc/vuln.ko
を開くといくつかの関数が見えます。例えばmodule_close
を見ると、相対アドレスは0x20fであることが分かります。
したがって、現在カーネル上では0xffffffffc0000000 + 0x20fにこの関数の先頭が存在するはずです。ここにブレークポイントを付けてみましょう
詳しくは先の章で解析しますが、このモジュールは/dev/holstein
というファイルにマップされています。cat
コマンドを使えばmodule_close
を呼び出せます。ブレークポイントで止まることを確認しましょう。
ドライバのシンボル情報が欲しい場合はadd-symbol-file命令を使って、第一引数に手持ちのドライバ、第二引数にベースアドレスを渡すとシンボル情報を読み込んでくれるよ。関数名を使ってブレークポイントを設定できるね。
1 | # cat /dev/holstein |
stepi
やnexti
といったコマンドも利用できます。このように、カーネル空間のデバッグはアタッチの方法が違うだけで、使えるコマンドやデバッグ方法はユーザー空間と何ら変わりません。
commit_creds
にブレークポイントを止めてRDIレジスタの指すメモリ領域を確認しました。同じことを今度はユーザー権限のシェル(cttyhackでuidを1337にした場合)でgdbを使って確認してみましょう。また、root権限(uid=0)の場合と一般ユーザー権限(uid=1337等)の場合を比べて、
commit_creds
の第一引数に渡されるデータにどのような違いがあるかを確認してください。