Try   HackMD

Linuxカーネルexploit問におけるTIPS

tags: 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カーネルのソースを全て読んだことはなく,必要に応じてチラ見した程度なので,そこまで深い知識は持っていません.もし記事の内容に間違いがありましたら,優しく教えてください.マサカリ投げるのは禁止!

TIPS

カーネルのソースコードリーディング

CTF中にカーネルのソースコードを読む環境を作るのは大変なので,ある程度手順を用意しておくと良いでしょう.私はUbuntu 18.04の上で,GNU Globalを使ってHTMLを生成し,ブラウザ経由で読むというスタイルにしています.

※大量にディスクを食うので注意しましょう.Linuxカーネルのソースコードにタグを付けてHTMLを生成すると,ディレクトリ全体で16GBくらいになります.

※Ubuntu 16.04のaptでインストールされるGNU Globalはバージョンが古く,良さげなオプションが使えないケースがあるため注意しましょう.https://qiita.com/akegashi/items/042c659c856d2092c443

# GNU globalとその拡張をインストール $ pip install Pygments $ apt install global exuberant-ctags # カーネルのソース配置先作成 $ mkdir -p /export/html $ cd /export/html # DLしてタグ付け+HTML生成(gtags/htagsは結構時間がかかるので注意,私の環境だと大体5-10分くらい) $ wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.19.5.tar.xz $ tar xf linux-4.19.5.tar.xz $ cd linux-4.19.5/ $ mv ../linux-4.19.5.tar.xz . # 同じディレクトリに念のためtarballも保管(特に意味は無い) $ gtags --gtagslabel=pygments && htags -afFnso # Webサーバを立てる $ apt install apache2 $ a2enmod cgi $ vi /etc/apache2/mods-enabled/alias.conf <IfModule alias_module> Alias /src/ "/export/html/" <Directory "/export/html/"> Options Indexes FollowSymlinks AllowOverride None Require all granted </Directory> <Directory "/export/html/*/HTML/cgi-bin/"> Options +ExecCGI AddHandler cgi-script .cgi </Directory> </IfModule> $ systemctl restart apache2

後はhttp://localhost/src/linux-4.19.5/HTML/にアクセスすれば,ソースコードが以下のように閲覧できるはずです.
当然ですが閉じた環境で閲覧することを想定してるので,セキュリティ設定は何もしてないに等しいです.このapache設定でサーバを立てた事で,不正アクセス等の被害に遭っても私は責任取れません,ご注意ください.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

尚,GNU Globalのタグ付けはLinuxカーネル以外にも使えるので,私はこんな感じで読みたいソースコードをポイポイ放り込んでいます.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

カーネルのgdbデバッグ

CTFのLinuxカーネルexploit問は,大抵qemu環境で動作する問題です.qemuには起動オプションにgdbを待ち受けるオプションがあるので,これを使うとユーザランドと同じようにgdbでデバッグできます.

[qemu起動側端末] $ qemu-system-x86_64 ... -gdb tcp::1234 # qemu起動時にgdbの接続待ちをしたい場合の指定. もしくは $ qemu-system-x86_64 ... -gdb tcp::1234 -S # -Sはgdbでつなぐまで実行を開始しないというfreezeオプション [gdb接続側端末] $ cat cmd set architecture i386:x86-64:intel # "Remote 'g' packet reply is too long"が出る場合への対応(qemu起動オプションで-Sを付けると起きる) target remote localhost:1234 $ gdb -x cmd # qemu内のカーネルにアタッチ もしくは $ gdb vmlinux -x cmd # デバッグシンボル入りのvmlinuxファイルがあるなら,指定しておくとシンボルも表示してくれる

尚,gdb-pedaを使っていてもアタッチは問題なく出来ますが,当然ながらユーザランドを想定している各種コマンドは利用できなくなります(vmmapなど).

デバッグ対象カーネルのアーキテクチャがホストと異なる場合は,nokia31337の解説資料にあるように,別のqemu上に構築した環境と連携させるなどしてください.gdb-multiarchを使っても良い気がしますが,試したことはありません.

カーネルのページテーブルの確認

カーネルの動作中に,カーネルのメモリマップが見たい時は,Linuxカーネルのソースに含まれるarch/x86/mm/debug_pagetables.cを使うと良いです.

CTFの大会中には使ったことがないですが,カーネルの動作を確認するなど,調査をする際には使ったりします.

# ビルドに必要なファイル群 $ apt install build-essential # 適当な箇所にディレクトリを作る $ mkdir /tmp/debug_pagetable $ cd /tmp/debug_pagetable # debug_pagetable.cを入手 $ wget https://raw.githubusercontent.com/torvalds/linux/master/arch/x86/mm/debug_pagetables.c # EFI関連のビルドエラーが出る場合があるため無効化 $ sed -i -e '1s/^/#undef CONFIG_EFI\n/' debug_pagetables.c # Makefileを作り,Make $ echo -e 'obj-m += debug_pagetables.o\nBUILD=/lib/modules/$(shell uname -r)/build\nall:\n\tmake -C $(BUILD) M=$(PWD) modules\nclean:\n\tmake -C $(BUILD) M=$(PWD) clean' > Makefile $ make # LKMをロード $ insmod ./debug_pagetables.ko # メモリマップ情報を確認(他にも幾つかファイルがある) $ less /sys/kernel/debug/page_tables/kernel ---[ User Space ]--- 0x0000000000000000-0xffff800000000000 16777088T pgd ---[ Kernel Space ]--- 0xffff800000000000-0xffff9c0000000000 28T pgd 0xffff9c0000000000-0xffff9c1940000000 101G pud ---[ Low Kernel Mapping ]--- 0xffff9c1940000000-0xffff9c1940098000 608K RW NX pte 0xffff9c1940098000-0xffff9c1940099000 4K ro NX pte 0xffff9c1940099000-0xffff9c194009a000 4K ro x pte 0xffff9c194009a000-0xffff9c1940200000 1432K RW NX pte ... # LKMをアンロード $ rmmod debug_pagetables

尚,カーネルのバージョンとアーキテクチャを問題に合わせてビルドすれば,他の環境でもinsmodできると思いますが,特に試した事は無いので,よく分かりません.

bzImage形式のカーネルから圧縮前のvmlinuxを取り出す

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などに食わせて解析ができます.

$ wget https://raw.githubusercontent.com/torvalds/linux/master/scripts/extract-vmlinux $ chmod +x extract-vmlinux $ ./extract-vmlinux vmlinuz > vmlinux

https://blog.packagecloud.io/eng/2016/03/08/how-to-extract-and-disassmble-a-linux-kernel-image-vmlinuz/

/proc/kallsyms

exploitの開発段階では,配布されたカーネルにおける色々な関数やグローバル変数のアドレスを知りたいことがあります.

最近の問題ではkASLRが有効なので,仮にアドレスを知ってもそのままexploitに使うことはできませんが,オフセット計算には使うことが出来ます.ユーザランドでprintf()のアドレスからlibc_baseを求めるのと同じですね.

さて,アドレスを確認するには/proc/kallsymsを使います.但し一般ユーザ権限では,全て0に置換されるので注意しましょう.exploitの開発段階などでは,起動スクリプトを書き換え一時的にroot権限を持たせるなどしてください(後述).

$ less /proc/kallsyms ... 0000000000022880 A runqueues 00000000000233c0 A sched_clock_data 0000000000023400 A osq_node 0000000000023440 A mcs_nodes 00000000000234c0 A rcu_bh_data 0000000000023680 A rcu_sched_data 0000000000023840 A csd_data 0000000000023880 A call_single_queue 00000000000238c0 A cfd_data 0000000000023900 A softnet_data 0000000000023ac0 A rt_uncached_list 0000000000023b00 A rt6_uncached_list 0000000000024000 A kvm_apic_eoi 0000000000024040 A steal_time 0000000000024080 A apf_reason 0000000000025000 A __per_cpu_end ffffffffa7200000 T startup_64 ffffffffa7200000 T _stext ffffffffa7200000 T _text ffffffffa7200030 T secondary_startup_64 ffffffffa72000e0 T verify_cpu ffffffffa72001e0 T start_cpu0 ffffffffa72001f0 T __startup_64 ffffffffa7200380 T pvh_start_xen ffffffffa7201000 T hypercall_page ...

尚,AとかTとかの意味は,man nmで調べられます.大文字は外部公開されるシンボル(external),小文字は内部で閉じたシンボル(local)の意味です.

T/t: text
B/b: bss
D/d: data
R/r: Read-only
A: 絶対的な値
W: ウィークシンボル
V: ウィークシンボル

起動スクリプトの修正による一時的なroot権限

CTFのカーネル問でテスト用に一時的にroot権限を得る方法です.hogehoge.cpioのようなqemuに渡すファイルシステムイメージを弄れば何とでもなりますが,cpioを展開して編集してまた固める,という手順は大変です.

ファイルシステムイメージには,大抵以下の様な起動スクリプト(init)が入っています.置かれているパスは色々違う可能性がありますが,どこにあっても大差はありません.

ファイルシステムイメージをバイナリエディタで開いて,起動スクリプトっぽいものを探してsetsid cttyhack setuidgid ...を実行する部分を探します.

これを以下の様に修正するのが楽です.10000000にするので(uid=0だからと言って1文字の0にしないのがミソ),これ以降のデータがずれるということも起きません.このファイルシステムイメージを使ってqemuを起動すれば,最初からrootになります.

setsid cttyhack setuidgid 1000 sh ↓ setsid cttyhack setuidgid 0000 sh

構造体のアライメント特定

apt install dwarvesでインストール可能なpaholeというツールは,vmlinuxから構造体のアライメント情報を抜き出す事ができます.
ただしこのvmlinuxは,デバッグシンボル付きでビルドされていなければなりません.
CTFでは大抵サイズ削減目的でstripされていることが多いため,実際にはあまり役立たないかもしれませんが,TIPSとして書いておきます.

$ pahole -C 'prog_entry' vmlinux die__process_inline_expansion: tag not supported (INVALID)! struct prog_entry { int target; /* 0 4 */ int when_to_branch; /* 4 4 */ struct filter_pred * pred; /* 8 8 */ /* size: 16, cachelines: 1, members: 3 */ /* last cacheline: 16 bytes */ }; $

カーネルパニックからのアセンブリ復元

カーネルパニック時のダンプに,アセンブリコードらしきものが含まれるケースがあります.
以下の例だと33行目ですね.

[ 64.460285] BUG: unable to handle kernel paging request at ffff9c3e00000001 [ 64.460285] IP: __kmalloc+0x9d/0x1a0 [ 64.461285] PGD 3cf29067 P4D 3cf29067 PUD 0 [ 64.461285] Oops: 0000 [#1] SMP PTI [ 64.461285] Modules linked in: list(O) [ 64.461285] CPU: 3 PID: 1084 Comm: exp Tainted: G O 4.16.0-rc7+ #17 [ 64.461285] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014 [ 64.461285] RIP: 0010:__kmalloc+0x9d/0x1a0 [ 64.461285] RSP: 0018:ffffa407004efe40 EFLAGS: 00010282 [ 64.461285] RAX: 0000000000000000 RBX: ffff9c3e00000001 RCX: 0000000000007fc9 [ 64.461285] RDX: 0000000000007fc8 RSI: 0000000000000000 RDI: 0000000000023b40 [ 64.461285] RBP: 00000000014202c0 R08: ffff9c3effda3b40 R09: 0000000000000000 [ 64.461285] R10: ffffa407004efee0 R11: 0000000000000000 R12: ffff9c3efb9c6000 [ 64.461285] R13: 0000000000000100 R14: ffff9c3efd001700 R15: ffffffffc006c1d5 [ 64.461285] FS: 0000000000000000(0000) GS:ffff9c3effd80000(0000) knlGS:0000000000000000 [ 64.461285] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [ 64.461285] CR2: ffff9c3e00000001 CR3: 000000007b93e000 CR4: 00000000001006e0 [ 64.461285] Call Trace: [ 64.461285] add_item+0x35/0xb0 [list] [ 64.461285] do_vfs_ioctl+0x8b/0x5e0 [ 64.461285] ? security_file_ioctl+0x3f/0x60 [ 64.461285] ? selinux_bprm_set_creds+0x290/0x290 [ 64.461285] SyS_ioctl+0x6f/0x80 [ 64.461285] do_syscall_64+0x5b/0x100 [ 64.461285] entry_SYSCALL_64_after_hwframe+0x3d/0xa2 [ 64.461285] RIP: 0033:0x400245 [ 64.461285] RSP: 002b:0000000000605fd0 EFLAGS: 00000202 ORIG_RAX: 0000000000000010 [ 64.461285] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 0000000000400245 [ 64.461285] RDX: 0000000000606018 RSI: 0000000000001337 RDI: 0000000000000003 [ 64.461285] RBP: 0000000000605fd0 R08: 0000000000000000 R09: 0000000000000000 [ 64.461285] R10: 0000000000000000 R11: 0000000000000202 R12: 0000000000000000 [ 64.461285] R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000 [ 64.461285] Code: b9 00 00 00 49 63 46 20 49 8b 3e 48 8d 4a 01 49 8b 1c 04 4c 89 e0 65 48 0f c7 0f 0f 94 c0 84 c0 74 bb 48 85 db 74 0b 49 63 46 20 <48> 8b 04 03 0f 18 08 f7 c5 00 80 00 00 0f 85 a5 00 00 00 49 63 [ 64.461285] RIP: __kmalloc+0x9d/0x1a0 RSP: ffffa407004efe40 [ 64.461285] CR2: ffff9c3e00000001 [ 64.461285] ---[ end trace b407fdb8bce751c3 ]--- [ 64.461285] Kernel panic - not syncing: Fatal exception [ 64.461285] Kernel Offset: 0xf000000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff) [ 64.461285] Rebooting in 1 seconds..

この情報からアセンブリコードを復元するには,Linuxカーネルのソースに含まれるscripts/decodecodeを使うのがお手軽です.但しAT&T記法なので注意しましょう.

$ wget https://raw.githubusercontent.com/torvalds/linux/master/scripts/decodecode $ chmod +x decodecode $ echo 'Code: b9 00 00 00 49 63 46 20 49 8b 3e 48 8d 4a 01 49 8b 1c 04 4c 89 e0 65 48 0f c7 0f 0f 94 c0 84 c0 74 bb 48 85 db 74 0b 49 63 46 20 <48> 8b 04 03 0f 18 08 f7 c5 00 80 00 00 0f 85 a5 00 00 00 49 63 ' | ./decodecode Code: b9 00 00 00 49 63 46 20 49 8b 3e 48 8d 4a 01 49 8b 1c 04 4c 89 e0 65 48 0f c7 0f 0f 94 c0 84 c0 74 bb 48 85 db 74 0b 49 63 46 20 <48> 8b 04 03 0f 18 08 f7 c5 00 80 00 00 0f 85 a5 00 00 00 49 63 All code ======== 0: b9 00 00 00 49 mov $0x49000000,%ecx 5: 63 46 20 movslq 0x20(%rsi),%eax 8: 49 8b 3e mov (%r14),%rdi b: 48 8d 4a 01 lea 0x1(%rdx),%rcx f: 49 8b 1c 04 mov (%r12,%rax,1),%rbx 13: 4c 89 e0 mov %r12,%rax 16: 65 48 0f c7 0f cmpxchg16b %gs:(%rdi) 1b: 0f 94 c0 sete %al 1e: 84 c0 test %al,%al 20: 74 bb je 0xffffffffffffffdd 22: 48 85 db test %rbx,%rbx 25: 74 0b je 0x32 27: 49 63 46 20 movslq 0x20(%r14),%rax 2b:* 48 8b 04 03 mov (%rbx,%rax,1),%rax <-- trapping instruction 2f: 0f 18 08 prefetcht0 (%rax) 32: f7 c5 00 80 00 00 test $0x8000,%ebp 38: 0f 85 a5 00 00 00 jne 0xe3 3e: 49 rex.WB 3f: 63 .byte 0x63 Code starting with the faulting instruction =========================================== 0: 48 8b 04 03 mov (%rbx,%rax,1),%rax 4: 0f 18 08 prefetcht0 (%rax) 7: f7 c5 00 80 00 00 test $0x8000,%ebp d: 0f 85 a5 00 00 00 jne 0xb8 13: 49 rex.WB 14: 63 .byte 0x63 $

slab情報の表示

Linuxカーネルのソースに含まれる,tools/vm/slabinfo.cを使うと,slab情報を表示する良い感じのツールが手に入ります.

$ wget https://raw.githubusercontent.com/torvalds/linux/master/tools/vm/slabinfo.c $ gcc slabinifo.c -o slabinfo # 情報を閲覧 $ ./slabinfo # 最終的に同じSLUBキャッシュを見ている構造体の一覧を確認 $ ./slabinfo -a

他にもオプションは多数あり,/proc/slabinfoを見るよりも情報量が多いです.

qemuでカーネルが起動できない場合

少し古めのPCにおいてqemuを使ってカーネルを起動しようとした場合に,正しく起動できない場合があります.
色々理由はありますが,その一つに「当時のCPUでは未対応の命令が,配布されたカーネル内に含まれている」というケースがあるようです.私のように8年も前のPCを未だに使っていると,こういう事が起きるのです.

この場合,VM環境の上でqemuを動かすと,回避できることがあります.
VMware Playerの設定からCPUの項目で,以下のようにIntel-VTxを仮想化しておき,UbuntuなどのLinuxを起動し,その上でqemuを使って起動すると良いでしょう.

VirtualBoxでも似たような事は設定できると思いますが,使ってないので良くわかりません.

カーネルデバッグ時に,niやsi命令がうまく動作しない

gdbでカーネルをデバッグ中に,ブレークポイントで停止してからシングルステップ(nisi)を行うと,必ず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)です.良ければ見てくださいね.