kernel
ctf
pwn
この記事は,CTF Advent Calendar 2018 の3日目の記事です.
2日目は@xrekkusuさんの「WebAssemblyちょっとやる」でした.
CTFのカーネルexploit問を解く上では,当然ですがカーネルを読んだり,カーネルをデバッグする必要があります.しかしユーザランドのそれとは勝手が違うので,初めて試した際には色々問題に直面することでしょう.また残念なことに,カーネルを開発している方とCTFerとでは,状況も情報も視点も違うので,欲しい情報が中々見つけられなかったりするのです.
そんな背景もあって,自分が過去にLinuxカーネルをデバッグした際にハマった点はなるべくメモっていたのですが,この記事はそのメモを読みやすくしたものです.ハマった点だけつらつら書かれているので,ハマらなかった点については特に書かれていません.
最近はLinuxカーネルのexploit問もそこそこ出題されるようになりましたね.でも日本勢で解いているチームはほとんどなく,(入門編のような記事を除くと)日本語のWrite-upや資料もあまり見かけません.
そこで今回は自分のメモの供養がてら,Linuxカーネルexploitに対する話を,5日分の記事として公開することにしました(当初は3日分だったのですが,長くなったので分割しました).
初日のネタは概要に記述したとおりで,過去ハマった点に対するメモを公開します.
尚,実際にカーネルexploit問を解く手順などについては,nokia31337の解説資料で図を入れて解説しているので,そちらを御覧ください(2014年当時のARMにおけるLinuxカーネルExploit問です).
また私はLinuxカーネルのソースを全て読んだことはなく,必要に応じてチラ見した程度なので,そこまで深い知識は持っていません.もし記事の内容に間違いがありましたら,優しく教えてください.マサカリ投げるのは禁止!
CTF中にカーネルのソースコードを読む環境を作るのは大変なので,ある程度手順を用意しておくと良いでしょう.私はUbuntu 18.04の上で,GNU Globalを使ってHTMLを生成し,ブラウザ経由で読むというスタイルにしています.
※大量にディスクを食うので注意しましょう.Linuxカーネルのソースコードにタグを付けてHTMLを生成すると,ディレクトリ全体で16GBくらいになります.
※Ubuntu 16.04のaptでインストールされるGNU Globalはバージョンが古く,良さげなオプションが使えないケースがあるため注意しましょう.https://qiita.com/akegashi/items/042c659c856d2092c443
後はhttp://localhost/src/linux-4.19.5/HTML/
にアクセスすれば,ソースコードが以下のように閲覧できるはずです.
当然ですが閉じた環境で閲覧することを想定してるので,セキュリティ設定は何もしてないに等しいです.このapache設定でサーバを立てた事で,不正アクセス等の被害に遭っても私は責任取れません,ご注意ください.
尚,GNU Globalのタグ付けはLinuxカーネル以外にも使えるので,私はこんな感じで読みたいソースコードをポイポイ放り込んでいます.
CTFのLinuxカーネルexploit問は,大抵qemu環境で動作する問題です.qemuには起動オプションにgdbを待ち受けるオプションがあるので,これを使うとユーザランドと同じようにgdbでデバッグできます.
尚,gdb-pedaを使っていてもアタッチは問題なく出来ますが,当然ながらユーザランドを想定している各種コマンドは利用できなくなります(vmmap
など).
デバッグ対象カーネルのアーキテクチャがホストと異なる場合は,nokia31337の解説資料にあるように,別のqemu上に構築した環境と連携させるなどしてください.gdb-multiarch
を使っても良い気がしますが,試したことはありません.
カーネルの動作中に,カーネルのメモリマップが見たい時は,Linuxカーネルのソースに含まれるarch/x86/mm/debug_pagetables.c
を使うと良いです.
CTFの大会中には使ったことがないですが,カーネルの動作を確認するなど,調査をする際には使ったりします.
尚,カーネルのバージョンとアーキテクチャを問題に合わせてビルドすれば,他の環境でもinsmodできると思いますが,特に試した事は無いので,よく分かりません.
CTFでは問題環境のvmlinuz
(bzImage形式)が同梱されていることがあります.しかしbzImage形式のファイルは圧縮されているため,メモリ上で動作しているカーネルそのものではありません.
IDA Pro等で解析するには圧縮前のvmlinux
を取り出す必要がありますが,bzImage形式のファイルには,解凍用のコードなどが複数くっついているため,簡単には取り出すことが出来ません.参考:http://wiki.bit-hive.com/linuxkernelmemo/pg/bzImage
このvmlinux
の取り出しを自動化するツールがあります.Linuxカーネルのソースに含まれるscripts/extract-vmlinux
がそれで,圧縮されたカーネルを含むファイル(vmlinuz
など)から,圧縮前のカーネル(vmlinux
)を取り出せば,IDA Proなどに食わせて解析ができます.
/proc/kallsyms
exploitの開発段階では,配布されたカーネルにおける色々な関数やグローバル変数のアドレスを知りたいことがあります.
最近の問題ではkASLRが有効なので,仮にアドレスを知ってもそのままexploitに使うことはできませんが,オフセット計算には使うことが出来ます.ユーザランドでprintf()
のアドレスからlibc_base
を求めるのと同じですね.
さて,アドレスを確認するには/proc/kallsyms
を使います.但し一般ユーザ権限では,全て0に置換されるので注意しましょう.exploitの開発段階などでは,起動スクリプトを書き換え一時的にroot権限を持たせるなどしてください(後述).
尚,A
とかT
とかの意味は,man nm
で調べられます.大文字は外部公開されるシンボル(external),小文字は内部で閉じたシンボル(local)の意味です.
CTFのカーネル問でテスト用に一時的にroot権限を得る方法です.hogehoge.cpio
のようなqemuに渡すファイルシステムイメージを弄れば何とでもなりますが,cpioを展開して編集してまた固める,という手順は大変です.
ファイルシステムイメージには,大抵以下の様な起動スクリプト(init
)が入っています.置かれているパスは色々違う可能性がありますが,どこにあっても大差はありません.
ファイルシステムイメージをバイナリエディタで開いて,起動スクリプトっぽいものを探してsetsid cttyhack setuidgid ...
を実行する部分を探します.
これを以下の様に修正するのが楽です.1000
を0000
にするので(uid=0だからと言って1文字の0
にしないのがミソ),これ以降のデータがずれるということも起きません.このファイルシステムイメージを使ってqemuを起動すれば,最初からrootになります.
apt install dwarves
でインストール可能なpahole
というツールは,vmlinux
から構造体のアライメント情報を抜き出す事ができます.
ただしこのvmlinux
は,デバッグシンボル付きでビルドされていなければなりません.
CTFでは大抵サイズ削減目的でstrip
されていることが多いため,実際にはあまり役立たないかもしれませんが,TIPSとして書いておきます.
カーネルパニック時のダンプに,アセンブリコードらしきものが含まれるケースがあります.
以下の例だと33行目ですね.
この情報からアセンブリコードを復元するには,Linuxカーネルのソースに含まれるscripts/decodecode
を使うのがお手軽です.但しAT&T記法なので注意しましょう.
Linuxカーネルのソースに含まれる,tools/vm/slabinfo.c
を使うと,slab情報を表示する良い感じのツールが手に入ります.
他にもオプションは多数あり,/proc/slabinfo
を見るよりも情報量が多いです.
少し古めのPCにおいてqemuを使ってカーネルを起動しようとした場合に,正しく起動できない場合があります.
色々理由はありますが,その一つに「当時のCPUでは未対応の命令が,配布されたカーネル内に含まれている」というケースがあるようです.私のように8年も前のPCを未だに使っていると,こういう事が起きるのです.
この場合,VM環境の上でqemuを動かすと,回避できることがあります.
VMware Playerの設定からCPUの項目で,以下のようにIntel-VTxを仮想化しておき,UbuntuなどのLinuxを起動し,その上でqemuを使って起動すると良いでしょう.
VirtualBoxでも似たような事は設定できると思いますが,使ってないので良くわかりません.
gdbでカーネルをデバッグ中に,ブレークポイントで停止してからシングルステップ(ni
やsi
)を行うと,必ずnative_apic_mem_write
の中に飛んでしまって,まともにデバッグできないことがあります.
これはqemuの起動オプションに--enable-kvm
がついていることが原因なので,消すと良いでしょう.動作速度は5分の1くらいになってしまいますが,とりあえずまともにデバッグすることができるようにはなります.背に腹は変えられないので,動作速度については我慢しましょう.
https://stackoverflow.com/questions/46069388/kernel-debugging-gdb-step-jumps-out-of-function
gdbでデバッグする際は,qemuの起動オプションの-append
節に,nokaslr
をつけておくと良いです.
kASLRが有効な場合,vmlinux
ファイルを指定していてもシンボル情報を見ることができないためです.
https://kernhack.hatenablog.com/entry/2018/02/15/223027
kASLRが無効な場合,カーネルの.text
セグメントのベースアドレスは固定です.
x64では0xFFFFFFFF81000000
,x86では0xc0000000
です.ARMも確か0xc0000000
だったと思います.
リークしたアドレスからオフセットを特定する場合や,メモリを全域走査する際のベースに指定するなど,もしかしたら使う機会があるかもしれません.
尚,これらは仮想アドレスの話であり,物理アドレスではありません.物理アドレスはcat /proc/iomem
で確認することができますが,閲覧にはroot権限が必要です.
linuxではデフォルトでSLUBアロケータが使われます.昔はSLABだったらしいですが,今ではSLUBです.
kmalloc()
もkmem_cache_alloc()
も,内部的にはいずれもSLUBで管理されているので,細かい動作は抜きにしても,SLUBの構造は簡単に知っておくと良いと思います(引用元のリンクは下に貼ってあります).
SLUB,つまりLinuxカーネルのヒープも,ユーザランドのヒープと同じくフリーリストを使って解放済みの領域を管理しています.
このフリーリストは単方向リンクリストであり,解放済みメモリの先頭8byteがまさにそのnext
です(図ではFP
と書かれている部分がそれ).
もしUAFなどで解放済みメモリの内容を上書きすることができるなら,先頭8byteを別の位置に向けるだけで,リンクリストを破壊することが出来ます.
フリーリストの整合性チェックなどは特にないはずなので,この状態で何度かkmalloc()
を呼び出させてメモリを確保すれば,next
を向けた先からメモリを確保する(=kmalloc()
が変な位置のメモリを返す)こともできるでしょう.
但し,最近はCONFIG_SLAB_FREELIST_RANDOM
なんていう設定も実装されており(デフォルト有効),フリーリストがランダムな順序に変更されるケースがあるので,注意しましょう.
参考資料:
http://events.linuxfoundation.org/sites/events/files/slides/slaballocators-japan-2015.pdf
https://www.ibm.com/developerworks/jp/linux/library/l-linux-slab-allocator/index.html
http://www.coins.tsukuba.ac.jp/~yas/coins/os2-2010/2011-01-11/
http://softwaretechnique.jp/OS_Development/kernel_development14_1.html
https://kernhack.hatenablog.com/entry/2016/08/20/004534
いかがでしたでしょうか.大した情報ではありませんが,カーネルデバッグ時の参考になれば幸いです.
明日も私が記事を担当します.もう少しpwner向けの内容でして,KPTI回避のテクニックの話(
最近のLinuxカーネルexploit問に対するテクニック集1)です.良ければ見てくださいね.