knqyf263's blog

自分のためのメモとして残しておくためのブログ。

LinuxカーネルをCLionでデバッグする

概要

Linuxカーネル脆弱性はよく見つかるので、たまにLinuxカーネルデバッグしたいときがあると思います。printkデバッグでも良いんですが、いちいちビルドするの面倒だし良い感じにステップ実行できれば良いのにと思っていました。

思うだけで何一つ行動に移していなかったのですが、今回SACK Panicの脆弱性調査のために気合い入れてデバッグ環境を作ったので残しておきます。

環境はVagrantで作っていて、デバッガはkgdbを使っています。正直細かい話はよく分かってないのでカーネル詳しい方々の説明を見たほうが良いですが、とりあえず動かしたければ役に立つかもしれません。

カーネルデバッグなんかググればアホほど記事あるだろうと思ってたのですが、意外とステップ実行頑張ってる人は少なさそうに見えました。なので綺麗にステップ実行できるようになるまでにもそこそこ苦労しました。

環境

ホスト環境

VM環境

参考URL

詳細

Vagrant

まず最初にVagrantVMを起動します。以下のようなVagrantfileを作ります。Vagrantは既に使える前提です。

$ cat Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-14.04"
  config.vm.network "private_network", ip: "192.168.33.20"
  config.vm.provider "virtualbox" do |vb|
    vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
    vb.customize ["modifyvm", :id, "--uart2", "0x2F8", "4"]
    vb.customize ["modifyvm", :id, "--uartmode2", "tcpserver", "1234"]
    vb.memory = "2048"
  end
end

重要なのは--uart辺りで、これはVMのシリアルポートをホスト側から利用するための設定になります。VirtualBox 5.0からシリアルポートのTCP/IPバックエンド機能が使えるようになったらしく、それを利用してCLionから繋いでいます。

起動してVMにログインします。

$ vagrant up
$ vagrant ssh

Linuxカーネル

この時点で既にkgdbを利用可能な状態ではあります。実際にkgdbに関連するオプションを確認すると全て有効になっています。

vagrant@vagrant:~$ grep -i kgdb /boot/config-4.4.0-31-generic
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
# CONFIG_KGDB_TESTS is not set
CONFIG_KGDB_LOW_LEVEL_TRAP=y
CONFIG_KGDB_KDB=y

このまま繋ぐことは可能なのですが、1つ問題があります。カーネルビルド時に最適化オプションが有効になっているため、ステップ実行しても行が飛びまくります。最適化により変数もいくつかoptimized outされて消えており、中身が見えない状態になります。

Cのプログラム書いてgdbとか動かしたことある人は分かると思いますが、あまりステップ実行のメリットが無くなってしまうため-O0などを付けるのがオススメです。そのため、このUbuntu 14.04はそのまま利用せずLinuxソースコードを落としてきて自分でビルドします。

ビルドに必要なものを入れてソースコードをcloneします。cloneし終わったら3.12にチェックアウトします。バージョンは3.12じゃなくても別に大丈夫だと思います。

vagrant@vagrant:~$ sudo apt-get -y update
vagrant@vagrant:~$ sudo apt-get install build-essential git libncurses5-dev
vagrant@vagrant:~$ git clone --depth=1 -b v3.12 https://github.com/torvalds/linux.git linux-3.12
vagrant@vagrant:~$ cd linux-3.12

カーネルの設定を行います。loadmodconfigは今ロードされているモジュールを有効にするように自動でconfigureしてくれるやつらしいです。 実行すると色々聞かれますが全部Nで大丈夫でした。デフォルトがNなのでEnter連打するだけです。

vagrant@vagrant:~$ make localmodconfig

次に一部の設定を変えるためにmenuconfigを行います。これはGUI風の画面で諸々の設定をいじれるのですが、自分はデフォルトのままで問題なかったです(記憶が失われただけで過去に設定したのかもしれない)。

vagrant@vagrant:~$ make menuconfig

参考URLにある通り、以下の設定が有効になっていれば大丈夫です。

Kernel Hacking ---> 
  <*> KGDB: kernel debugger

Device Drivers ---> 
  Network device support --->     
   <*> Network console logging support

設定が終わったらビルドします。CPU複数ないと並列化の恩恵受けられないので、時間かかるようなら上のVagrantfile内でvb.cpus = 4とかにしておくと良いと思います。

vagrant@vagrant:~$ make -j8

ビルドが終わったらmake installします。

vagrant@vagrant:~$ sudo make modules_install
vagrant@vagrant:~$ sudo make install

次に起動するカーネルを切り替える必要があります。起動時にgrubの画面で切り替えても良いのですが、面倒なのでdefault設定を変えておきます。

vagrant@vagrant:~$ grep "menuentry " /boot/grub/grub.cfg
menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-267a57a4-b544-4eb5-afdc-3a36cd2cd265' {
        menuentry 'Ubuntu, with Linux 4.4.0-31-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.4.0-31-generic-advanced-267a57a4-b544-4eb5-afdc-3a36cd2cd265' {
        menuentry 'Ubuntu, with Linux 4.4.0-31-generic (recovery mode)' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.4.0-31-generic-recovery-267a57a4-b544-4eb5-afdc-3a36cd2cd265' {
        menuentry 'Ubuntu, with Linux 3.12.0' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-3.12.0-advanced-267a57a4-b544-4eb5-afdc-3a36cd2cd265' {
        menuentry 'Ubuntu, with Linux 3.12.0 (recovery mode)' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-3.12.0-recovery-267a57a4-b544-4eb5-afdc-3a36cd2cd265' {
menuentry 'Memory test (memtest86+)' {
menuentry 'Memory test (memtest86+, serial console 115200)' {

これ未だにどうやって切り替えるのが一番良いのか不明なのですが、とりあえずgrepすると上のように4.4.0-31と3.12.0の2つがあることが分かります。4.4.0-31は元々のUbuntuカーネルで、3.12.0が今ビルドしたやつなのでそちらに切り替えます。

/etc/default/grub 内のGRUB_DEFAULTを書き換えます。

vagrant@vagrant:~$ sudo vim /etc/default/grub
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 3.12.0"

Ubuntu, with Linux 3.12.0 だけ書いていたらAdvanced optionsも付けろって怒られたので付けてます。GRUB_DEFAULT=1とかでも動く気はします(CentOSとかはいつもそうやってたので)。

ちなみにソースコードを少しいじってmake installし直すと3.12.0+になります。さらにmake installしても3.12.0++とかにはならず+のままでした。

そしてKASLRが有効だとメモリがランダマイズされてkgdbがうまく動かないので無効化します。これも上と同様 /etc/default/grub に書きます。

vagrant@vagrant:~$ sudo vim /etc/default/grub
GRUB_CMDLINE_LINUX="nokaslr"

最後にgrubの設定を更新します。

vagrant@vagrant:~$ sudo update-grub

一旦再起動します。

vagrant@vagrant:~$ sudo reboot

カーネル入れ替えるとVirtualBoxのファイル共有機能が使えなくなっているので、入れ直しておきます。

vagrant@vagrant:~$ sudo apt-get update
vagrant@vagrant:~$ sudo apt-get -y install --reinstall virtualbox-guest-dkms

rebootコマンドで起動していたら何故かファイル共有がうまく動かなくて困っていたのですが、おとなしくvagrantコマンドでやっておくほうが良いようでした。

ということでもう一度再起動します。

vagrant@vagrant:~$ exit
$ vagrant reload

Linuxカーネルの設定としてはこれで終わりです。

CLion

デバッグ実行のためにCLionを使います。VSCodeでも何でも良いとは思いますが、何やかんや機能が豊富そうという理由で今回はCLionを使っています。

適当にインストールします。

CLion: A Cross-Platform IDE for C and C++ by JetBrains

次にソースコードも必要なので持ってきます。再度cloneしても良いのですが、既にVM内にあるので持ってきます。gitでcloneした場合でもvmlinuxは必要なので結局VM内から持ってくることになります。

VM内の /vagrant に置くとホスト側のVagrantfileある位置に動悸されます。このlinuxnのソースを適当な位置に移動させます。自分は ~/src/github.com/knqyf263 に移動させましたが、各自の好きな場所で良いです。

$ vagrant ssh
vagrant@vagrant:~$ sudo cp -r linux-3.12/ /vagrant/
vagrant@vagrant:~$ exit
$ mv linux-3.12 ~/src/github.com/knqyf263/

CLionで ~/src/github.com/knqyf263/linux-3.12/ を開きます。

f:id:knqyf263:20190701185144p:plain

GDBのリモートデバッグの設定をします。右上の「Add Configuration」からポップアップを開いて左上の「+」ボタンを押して「GDB Remote Debug」を選びます。

以下のように設定しておきます。

  • Name: kernel
  • GDB: /usr/local/bin/gdb
  • 'target remote' args: localhost:1234
  • Symbol file: /Users/teppei/src/github.com/knqyf263/linux-3.12/vmlinux
  • Path mappings:

「Name」は何でも良いので適当に決めます。

GDB」の箇所はとても重要です。CLionはgdb/lldbが同梱されており、デフォルトだとDebuggerはBundledなものが指定されています。

めっちゃハマったのですが、同梱のやつだとうまく動きません。何か方法があるかもしれないですが、とりあえずbrewgdbを入れておけば動くようになります。

$ brew install gdb

インストールできたら /usr/local/bin/gdb を使うように設定しておきます。「Custom GDB executable」にして /usr/local/bin/gdb を指定すればOKです。

次に「target remote」ですが、これは最初にVagrantlocalhost:1234にシリアルポートを開くように設定したのでそちらに繋いでいます。違うポートにしていたらそれに合わせて変えて下さい。

「Symbol file」はビルド時に得られたvmlinuxをMac側に持ってきて設定する必要があります。これも自分linuxのソースをmvした場所を指定する必要があるので、自分の置いたディレクトリに合わせて設定して下さい。

「Path mappings」ではリモートのpathとローカルのpathをマッピングする必要があります。この設定がうまく出来なくてかなりハマりました。

以下のような感じになると思います。

f:id:knqyf263:20190701123756p:plain

最適化の無効化

ここまでの設定で既にデバッグは可能なのですが、先程も述べたようにこのままだと最適化のせいでステップ実行が綺麗に動きません。そこでLinuxカーネル全体を-O0で最適化無効にしようとしたのですが、どうやらLinuxカーネルは-O0でコンパイルできないようです。

lists.kernelnewbies.org

Yes, it doesn't work :)

って言われてます。実際に自分もやってみたらうまく行かずでした。ここは誰かカーネルに詳しい人に助けてほしいです。

ですが、上の参考URLに貼ったslideshareを見ると特定のファイル単位での無効化なら可能なようです。

CFLAGS_(オブジェクトファイル名) = コンパイルオプション

こんな感じで書けるみたいです。今回無効にしたかったのはtcp_input.cだったので以下のようにMakefileに追記しました。

vagrant@vagrant:~$ vim linux-3.12/net/ipv4/Makefile
...
CFLAGS_tcp_input.o = -O0

再度ビルドとインストールを行います。

vagrant@vagrant:~$ cd linux-3.12/
vagrant@vagrant:~$ make -j8
vagrant@vagrant:~$ sudo make modules_install
vagrant@vagrant:~$ sudo make install

先程も述べたように3.12.0+になっているのでgrubの設定を変えておきます。

vagrant@vagrant:~$ sudo vim /etc/default/grub
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 3.12.0+"
...

更新します。

vagrant@vagrant:~$ sudo update-grub

あとはリロードすればOKです。

vagrant@vagrant:~$ exit
$ vagrant reload

そしてvmlinuxを忘れないようにMacに持ってきます。ソースコードビルドし直したあとにこれを忘れて30分ぐらい溶かす、ということを自分は5回ぐらいやってます。疲れてくると忘れがち。

$ vagrant ssh
vagrant@vagrant:~$ sudo cp linux-3.12/vmlinux /vagrant/
vagrant@vagrant:~$ exit
$ mv vmlinux ~/github.com/knqyf263/linux-3.12/

デバッグ

念の為カーネルバージョンを確認しておきます。

$ vagrant ssh
vagrant@vagrant:~$ uname -a
Linux vagrant 3.12.0+ #2 SMP Fri Jun 28 04:03:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

3.12.0+になっていればOKです。

では実際にデバッグしてみます。以下のコマンドを打つとkgdbを待ち受けるようになります。VM側は何もコマンド打てなくなりますが、デバッガで繋げば動かせるようになるので焦らなくて大丈夫です。

vagrant@vagrant:~$  echo ttyS1,115200 | sudo tee /sys/module/kgdboc/parameters/kgdboc
vagrant@vagrant:~$  echo g | sudo tee /proc/sysrq-trigger

今回は net/ipv4/tcp_input.cデバッグしたかったので、適当にtcp_ack関数内にブレークポイントを貼っておきます。

f:id:knqyf263:20190701184917p:plain

右上の虫ボタンを押すとVMに繋がりブレークポイントで止まります。

f:id:knqyf263:20190701185029p:plain

あとはステップ実行なり何なり好き勝手出来ます。変数の中身も見放題なので大分捗るようになります。

ただ、やはりカーネルだからなのかステップ実行は少し時間がかかります。なので自分は常に見たい値はprintkで一部出力したりしてました。その場合はビルドし直してvmlinuxを持ってくるのですが、同時にソースコードも持ってこないと行がずれてしまうので気をつけて下さい。

あと欲しい値見る時は「Variables」で右クリックすると「Evaluation Expression」というのがあるので、それを使ったりしてたのですがマクロの場合は評価後の値を見れませんでした。これもやり方ありそうですが、とりあえず分からなかったのでソースコードに単にマクロの結果を変数に代入するだけの行を追加したりして凌いでました。

struct  tcp_skb_cb *cb = TCP_SKB_CB(skb);

こんな感じ。

自分は /net/ipv4/tcp_input.c 以外にも /net/ipv4/tcp_output.c も-O0を付けつつprintkデバッグしてました。ただ、skbuff.cは-O0付けたら動かなかったので辛かったです。

何かたまにデバッガの接続が切れてVMが動かなくなることがあります。再度CLionから繋ぐことで動くこともあれば、どうやっても復帰できないときもあるのでそういう時は大人しくVM再起動してます。

あとVirtualBoxのせいか動作が不安定で突然クラッシュすることもありました。もっと新しいLinuxのバージョン使えば安定するのかも...?

そして、あまりLinuxカーネル関係なくIntelliJ IDEAのテクニックになりますが、ブレークポイントはConditionが書けます。カーネル内の変数を使ってif文書いたり出来るということです。

f:id:knqyf263:20190701150513p:plain

これは目からウロコで、特定の閾値を超えたときだけ見たい場合とかいつも困ってたのですがこれ使ってから劇的に便利になりました。ブレークポイント間での依存関係も指定でき、特定のブレークポイントにヒットするまで他のブレークポイントも無効にできます。

Linuxカーネルは特に雑にブレークポイント指定するとヒットしまくってボタンを連打する人になりがちなので本当に助かりました。これなしでは生きていけないぐらい便利な機能でした。

まとめ

ハマりまくってやっと動くようになったので、やり方をまとめておきました。ググった感じあまりこの辺りやってる人見つからなかったのですが、何か他に良いやり方あれば教えて下さい。