カーネルexploitに入門しにくい大きな原因が、デバッグ方法がよく分からないという点です。
この節ではgdbを使ってqemu上で動くLinuxカーネルをデバッグする方法を学びます。

まず練習問題LK01のファイルをダウンロードしてください。

root権限の取得

手元でKernel Exploitをデバッグする際、一般ユーザー権限だと不自由なことが多いです。特にカーネルやカーネルドライバの処理にブレークポイントを設定したり、リークしたアドレスが何の関数のアドレスかを調べたりする際、root権限がないとカーネル空間のアドレス情報を取得できません。
Kernel Exploitをデバッグする際は、まずroot権限を取得しましょう。この節の内容は前章の例題の(2)と同じですので、既に解いた方は確認程度に読み流してください。

カーネルが起動すると最初に1つのプログラムが実行されます。このプログラムは設定によりパスは様々ですが、多くの場合/init/sbin/init等に存在します。LK01のrootfs.cpioを展開すると、/initが存在します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/sh
# devtmpfs does not get automounted for initramfs
/bin/mount -t devtmpfs devtmpfs /dev

# use the /dev/console device node from devtmpfs if possible to not
# confuse glibc's ttyname_r().
# This may fail (E.G. booted with console=), and errors from exec will
# terminate the shell, so use a subshell for the test
if (exec 0</dev/console) 2>/dev/null; then
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
fi

exec /sbin/init "$@"

ここには特に重要な処理は書かれていませんが、/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
2
-echo 2 > /proc/sys/kernel/kptr_restrict    # 変更前
+#echo 2 > /proc/sys/kernel/kptr_restrict # 変更後

変更したらcpioに再びパックして、run.shを実行すれば下のスクリーンショットのようにroot権限でシェルが使えるようになっているはずです。(パックの方法は前章を参照)

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
2
echo 2 > /proc/sys/kernel/kptr_restrict     # 変更前
#echo 2 > /proc/sys/kernel/kptr_restrict # 変更後

さて、実際にkallsymsを見てみましょう。量が膨大なのでheadなどで先頭だけ見てみます。

/proc/kallsymsの先頭

このように、シンボルのアドレス、アドレスの位置するセクション、シンボル名の順に並んで出力されます。セクションは例えば"T"ならtextセクション、"D"ならdataセクションのように表され、大文字はグローバルにエクスポートされたシンボルを表します。これらの文字の詳細な仕様はman nmで確認できます。
例えば上の図だと0xffffffff81000000が_stextというシンボルのアドレスであることが分かります。これはカーネルがロードされたベースアドレスにあたります。

では、次にcommit_credsという名前の関数のアドレスをgrepで探してください。見つかったら0xffffffff8106e390がヒットするはずです。gdbでこの関数にブレークポイントを付けて続行します。

1
2
pwndbg> break *0xffffffff8106e390
pwndbg> conti

この関数は実は新しくプロセスが作られる時などに呼ばれる関数です。シェルでlsコマンドなどを叩くとブレークポイントでgdbが反応するはずです。

commit_credsでブレークポイントにより停止する様子

第一引数RDIにはカーネル空間のポインタが入っています。このポインタの指すメモリを見てみましょう。

commit_credsでのメモリの確認

このように、カーネル空間でもユーザー空間と同じようにgdbのコマンドが利用可能です。pwndbg等の拡張機能も使えますが、もちろんカーネル空間向けに書かれた拡張機能でなければ動かないので注意してください。
カーネルのデバッグ用の機能が搭載されたデバッガなどもあるので、みなさんが好みのデバッガを使ってください。

ドライバのデバッグ

次にカーネルモジュールをデバッグしてみましょう。
LK01にはvulnという名前のカーネルモジュールがロードされています。ロードされているモジュールの一覧と、そのベースアドレスは/proc/modulesから確認できます。

/proc/moudlesの中身

これを見ると、vulnというモジュールが0xffffffffc0000000にロードされていることが分かります。なお、このモジュールのソースコードとバイナリは配布ファイルのsrcディレクトリに存在します。ソースコードの詳細解析は別の章でやりますが、このモジュールの関数にブレークポイントを付けてみましょう。
IDAなどでsrc/vuln.koを開くといくつかの関数が見えます。例えばmodule_closeを見ると、相対アドレスは0x20fであることが分かります。

IDAで見たmodule_close関数

したがって、現在カーネル上では0xffffffffc0000000 + 0x20fにこの関数の先頭が存在するはずです。ここにブレークポイントを付けてみましょう

gdbでmodule_close関数にブレークポイントを付ける

詳しくは先の章で解析しますが、このモジュールは/dev/holsteinというファイルにマップされています。catコマンドを使えばmodule_closeを呼び出せます。ブレークポイントで止まることを確認しましょう。

オオカミくん

ドライバのシンボル情報が欲しい場合はadd-symbol-file命令を使って、第一引数に手持ちのドライバ、第二引数にベースアドレスを渡すとシンボル情報を読み込んでくれるよ。関数名を使ってブレークポイントを設定できるね。

1
# cat /dev/holstein

stepinextiといったコマンドも利用できます。このように、カーネル空間のデバッグはアタッチの方法が違うだけで、使えるコマンドやデバッグ方法はユーザー空間と何ら変わりません。


本章ではcommit_credsにブレークポイントを止めてRDIレジスタの指すメモリ領域を確認しました。同じことを今度はユーザー権限のシェル(cttyhackでuidを1337にした場合)でgdbを使って確認してみましょう。
また、root権限(uid=0)の場合と一般ユーザー権限(uid=1337等)の場合を比べて、commit_credsの第一引数に渡されるデータにどのような違いがあるかを確認してください。