# bata24/gefの機能紹介とか ###### tags: `gef` `gdb` `ctf` # はじめに この記事は,[CTF Advent Calendar 2022](https://adventar.org/calendars/7550) の8日目の記事です. 7日目は[だこつ(@y011d4)](https://twitter.com/y011d4)さんの「[面白かった Crypto問 2022](https://blog.y011d4.com/20221207-crypto-highlight)」でした. 今年はCrypto勢の投稿が多いですね.最近のCrypto問は全くわからずついて行けてないので,Crypto勢の皆さまは凄いなと思います(小並感). # タイトルのbata24/gefって何? https://github.com/bata24/gef のことです. もう少し説明すると,「gdb拡張スクリプトとして有名所の1つである[hugsy/gef](https://github.com/hugsy/gef)を,勝手にforkして色々機能を追加したもの」です. 作成に至った背景としては,CTF Advent Calendar 2020で「[gefを改造した話](https://hackmd.io/@bata24/rJVtBJsrP)」という記事にしているので,そちらをご覧ください. # 何故Advent Calendarのネタにしたの? このスクリプトを世にリリースしてから2年が経ちました. 2年も経つと,各機能を実装しようとした背景とか苦労話などもそこそこ溜まってきます.それらを忘れないうちにどこかに書き留めておこうと思ったからです. - 何故その機能をつくろうと思ったのか - 技術的にどうやってるのか みたいな話題を提供したかったのです. こういった話題は,このスクリプトを使ってない方でも楽しめるでしょうし,機能開発のきっかけが特定のCTFの問題だったりするので,CTF Advent Calendarで公開することにしました. 全てのコマンドについて触れると膨大になってしまうので,思い入れのあるコマンドを中心に解説していこうかと思います. ---- では,早速書いていきます. # pagewalkコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/pagewalk-arm64.png) qemu-system連携時に利用できる,ページテーブルをダンプする機能です. きっかけはHITCON CTF 2018のSuper Hexagonを復習していた時です.Write-upを読み進める過程で[aarch64-pagewalk.py](https://github.com/grant-h/gdbscripts/blob/master/aarch64/aarch64-pagewalk.py)というものに出会いました.これについては衝撃を受けました.以下その理由です. まずそれまで知っていた私の知識は以下の通りでした。 - qemu-systemのqemuモニタには`info mem`コマンドがあり,pagewalkっぽいことができます. - しかしx86/x64しか実装されておらず,ARMやAArch64では使えません. - またx86/x64であっても,出力が長すぎます. - アドレスが0x1000だけ違う似たようなエントリが,ずーっと数千行垂れ流されることがあり,読みやすいとは言えません. - ページテーブルに含まれる,各種フラグを詳細に表示することができません. - ページテーブルは階層構造を持ちますが,その各階層(レベル)の解釈状況を表示することができません. - 最近のCTFのカーネル問/VMエスケープ問では,ページテーブルを書き換えるようなテクニックも必要になってきています. - しかしページテーブルの情報を綺麗に表示してくれる機能は見たことがありませんでした. qemuの`info mem`はつかえねーなー,都度メモリを自分で追いかけるしか無いかなーと思っていた矢先,スクリプトによる力技で解決するという方針に出会い,感動しました.スクリプトでこんな事ができるとは思っていなかったのです. 色々調べていると,pagewalkを行うために必要な情報は,システムレジスタとメモリの読み取りだけで賄えることがわかりました.つまりアーキテクチャさえ分かるのなら,ゲストOSがなんであるかには依存せず,gdbの機能だけで実装可能なのです. これは汎用性があるぞと思い,CPUマニュアルとにらめっこしながら,1ヶ月くらい掛けてできたのが`pagewalk`コマンドです(その後何度かリファクタリングはしています).出力はなるべく短くすっきりと,それでいて必要な情報は得られるようにしています. また,以下のような機能をつけました. - 各レベルの詳細表示(デフォルトでは表示しない) - エントリをマージしない(デフォルトではマージする) - 出力のフィルタ機能(最終結果のみで判定) - 物理アドレスでソート(デフォルトは仮想アドレスでソート) - 物理アドレスの連続性を無視してマージ(デフォルトでは仮想アドレスも物理アドレスも連続する同一フラグを持つエントリのみマージ) - 指定した仮想アドレスのエントリに達するまでに辿った各レベルのエントリをトレースして表示 - パース時のデバッグ的な出力を抑制 ![](https://i.imgur.com/mnhxn1O.png) 最初はAArch64,続いてx64,さらにx86,ARMまで対応しました.それ以外のアーキは詳細なマニュアルを見つける事ができず,断念しました.まあCTFではこの4つのアーキさえ対応していれれば十分でしょう. ちなみに`pagewalk`のソフトウェア実装はかなり大変で,コード量はとても多くなってしまいました.コメントが多いこともありますが,4つのアーキで合計4000行近くをスクラッチで書いています.まあ処理の記述が大変というよりは単に分岐が多くて複雑なだけなんですが. 尚,同じような機能を持つ[gdb-pt-dump](https://github.com/martinradev/gdb-pt-dump)というのも後から知ったのですが,これはx86とARMに対応していません.x86とARMのpagewalkを実装し今でも使えるのは,bata24/gef以外にないと思っています. また私のスクリプトでは検出できている領域が,gdb-pt-dumpでは出力に含まれないことがあるのも確認しました.その領域には確かにメモリが存在してメモリアクセスできたので,おそらくgdb-pt-dumpのバグだと思われます.気になる方はそれぞれの出力を比べてみてください. またx86_64限定の話っぽいのですが,ある1つの物理アドレスに対して65536個(?)の異なる仮想アドレスがマップされていることがあります.表示が長くなる原因の一つはこれなのですが,幸いアドレスは連続でないながらも似てはいるので,`*`でまとめるなどの工夫をしています. ※どうやらKASAN用のシャドウメモリらしいです.環境によってはページテーブルを数百万エントリも利用することがあります.愚直にパースするとOOMが発生するため,現在はデフォルトでパースをスキップするようにしています. # ksymaddr-remoteコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/ksymaddr-remote.png) qemu-system連携時に利用できる,シンボルのないカーネルでもシンボルのアドレスを表示する機能です. シンボルのないLinuxカーネルをデバッグするという状況は,CTFではよくあることです.対処法としてはkASLRを無効化し,rootでログインできるように問題イメージを書き換えてから,`/proc/kallsyms`の情報をダンプして控えておいて,その情報をもとにデバッグするというのが私がいつも行う方法でした. 毎回面倒だなーと思っていたわけですが,そもそもシンボルのないカーネルをデバッグするのはCTFプレイヤーくらいです.ググってもそういう時のテクニックなんて都合の良い情報が見つかるはずもなく,解決策もなかなか思いつかなかったのでした. そんな時,binjaのとある方に昔教えてもらったことを思い出しました.それは「シンボルがstripされたカーネルでも`/proc/kallsyms`で表示するための情報はカーネル内に保持している」ということです.つまり,これをパースすればリアルタイムにシンボルのアドレスを特定できるのではないか,とひらめいたのがきっかけです. `pagewalk`コマンドを実装したことで,qemu-systemで動くLinuxは,そのメモリマップがわかるようになっています.だいたいパターンも見えてきて,ヒューリスティックではありますが,Linuxカーネル自体がメモリのどこにあるかも特定できるようになりました. 後はkallsyms用の情報を見つけることができれば,そのデータをパースすることで,全てのアドレスをリアルタイムに得ることができるはずです. カーネルのソースを読んで,色々検討した結果,以下の配列のアドレスや値を求めていけば,kallsymsの内容をパースするのに必要な情報を揃えられそうなことがわかりました. 1. kallsyms_relative_base 2. kallsyms_num_syms 3. kallsyms_names 4. CONFIG_KALLSYMS_ABSOLUTE_PERCPU が有効か無効か 5. kallsyms_offsets 6. kallsyms_markers 7. kallsyms_token_table 8. kallsyms_token_index 実際にやってみたところ,長い試行錯誤を経てなんとか達成することができました. 特に苦労したのは,これらを特定するためにメモリをスキャンする過程で,メモリ上に値が格納される微妙なパターンの違いが色々あることです. - kallsymsの情報として,絶対アドレスを格納するケース - kallsymsの情報として,ベース値からの相対アドレスを格納するケース - メモリを疎に利用するケース - i386環境で,全ての値が16バイトalign - AArch64環境で,全ての値が256バイトalign(贅沢…) - kallsyms_relative_baseなどの格納位置が想定と異なるケース - パディングの有無 - その他細かいパターン違いがたくさん これらのパターンは,過去のCTFで出題された,手元にあった30種類くらいのqemuイメージで動作確認する過程で見つけたものです.なぜこんなにパターンがあるのか理由はわかりませんでしたが,とりあえず見つけたパターンは片っ端から対応していきました. 2023/3/23追記: さてその後もたまに見つかるバグを潰したりしていたわけですが,どうにも判定ロジックが複雑になりすぎメンテが大変になりました.そこでロジックの見直しをしたところ,後述の[vmlinux-to-elf](https://github.com/marin-m/vmlinux-to-elf)に同梱の`kallsyms_finder.py`がスマートなロジックで導出していたので,こちらのロジックへ切り替えました. 2023/07/25追記: カーネル6.2で新たにテーブルが増えました(`kallsyms_seqs_of_names`).またカーネル6.4で`kallsyms_offsets`の配置される位置が変わりました.これらにも対応しています.どのように配置が変わったのかは,gefのソースコードを見てください.コメント付きで詳しく解説しています. # ksymaddr-remote-applyコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/ksymaddr-remote-apply.png) `ksymaddr-remote`コマンドができたことで,求めたシンボルをgdb上にインポートしたいと思うようになりました.`ksymaddr-remote`コマンドでシンボルとそのアドレスは求められても,実際には毎回アドレスの生の値を直接使う必要があり,大変なことには変わらないと思ったのです. さて手元には,シンボルとそのアドレスのペアが大量にあります.これをgdbにシンボル情報としてインポートしたいのですが,そんな都合の良い機能はgdbにはありませんでした(あっても良さそうなのに). しかし当たり前ですが,gdbには,シンボル付きのELFであればその中にあるシンボルを読み取ってインポートする機能があります(`add-symbol-file`コマンド).そこで考えたのが「空のELFを作り,objcopyでシンボルとアドレスをそのELFに埋め込んで,そのELFをgdbにロードすればいいのでは?」ということです. ということで実装してみたらできました.正確には,シンボルを追加するためのELFを作って`add-symbol-file`コマンドを発行する機能自体は`add-symbol-temporary`コマンドとして独立に実装し,`ksymaddr-remote-apply`コマンドからはその`add-symbol-temporary`コマンドを呼ぶようにしています. このテクニックは,idaのデコンパイル結果をgdbに反映する機能を提供する[decomp2dbg](https://github.com/mahaloz/decomp2dbg/blob/edf69c24c0489910a3688b9ab724837fa5d0ff29/decomp2dbg/clients/gdb/symbol_mapper.py#L33)でも使われており,ソースにアイデア元として私の名前が書かれています.嬉しいですね. # vmlinux-to-elf-applyコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/vmlinux-to-elf-apply1.png) `ksymaddr-remote-apply`コマンドを実装して満足していたところ,[vmlinux-to-elf](https://github.com/marin-m/vmlinux-to-elf)の存在を知りました. `ksymaddr-remote`/`ksymaddr-remote-apply`で実装したものとほぼ同じ方針で,しかも対応するイメージも多く,またヘッダのないカーネルからELF形式に復元するということまでやっています. 完全に私の作った機能の上位互換なわけですが,`ksymaddr-remote`を自分で実装した私にとっては,コードを読まずとも何をやっているのかが大体わかるわけです.これは良いツールだと思ったので,取り込みました. `pagewalk`で求めた,カーネルが含まれるであろうメモリをダンプしてから,`vmlinux-to-elf`を呼びだしてシンボル付きのELFを生成し,そのELFをgdbに取り込むことでシンボルとアドレスをインポートできるようにしました.かなり安定して動作してくれるので,非常に便利な機能の一つです. こう書くと私の作った`ksymaddr-remote`/`ksymaddr-remote-apply`はお払い箱のようにも見えますが,一応メリットはあります.それはパースが比較的早いこと.10秒もあればパースは終わります.`vmlinux-to-elf`はかなり正確にシンボルを特定して復元してくれるのですが,場合によっては数分かかることもあり,何度も実行する用途には向いてないのです. 一応対策として,`vmlinux-to-elf`で生成したELFを/tmpに保存しておき使い回すようにはしました.デバッグ対象カーネルを再起動させた後にシンボルを再インポートするケースでは,kASLRでアドレスが変わり得ますが,ベースアドレスからの各関数のオフセット自体は変わりません.つまりベースアドレスを修正して読み込むだけで良く,この機能はgdbの`add-symbol-file`コマンドにあります.従って時間のかかる`vmlinux-to-elf`を動かす必要がなくなるので,かなり時間短縮できます. とはいえFGKASLRが有効な環境など,毎回アドレスがオフセットごとガラッと変わる環境では`ksymaddr-remote`のほうがストレスなく使えて良いので,こちらの機能も残しています. # slub-dumpコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/slub-dump.png) どの問題だったかおぼえていないのですが,LinuxのスラブアロケータのUAFを使って解くカーネル問題で,スラブアロケータのフリーリストのパーサがほしいなと思ったことがありました.[salt](https://github.com/PaoloMonti42/salt)の存在は知っていたのですが,開発は停止しており,最近のカーネルには適用できなくなっていました. そこで,saltのコードを参考にしながら新規に開発したのがこの機能です. 最近のSLUBでは,解放済みチャンクが保持するリンクリストのポインタのオフセットがチャンク先頭にないケース(上図内の`offset`の値)や,そのポインタを暗号化しているケース(上図内の`random (xor key)`の値)があります.saltが未対応なのはこの辺りの処理なので,これらを考慮しないといけません. すでに`ksymaddr-remote`によって必要なシンボルが特定できていたため,とっかかり自体は簡単でした.しかし構造体のメンバがカーネルのバージョンによって結構変わることもあって,メモリ内の構造をもとに,ヒューリスティックにその差分を吸収する必要があり,そこが結構難しかったです. 現在のカーネルにも対応しており,シンボルが無くても利用可能なSLUBのフリーリストダンプ機能は,他に見たことがないので,これもなかなか良い機能だと思っています. 2023/05/16追記: SLUBだけでなく、SLAB,SLOB,SLUB-TINYのダンプ機能も追加しました.SLOBやSLABはそろそろカーネルから削除されますけど,一応昔の問題向けということで残しています. # thunk-tracerコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/thunk-tracer.png) zer0pts CTF 2022 のkRCEという問題を解く際に作った機能です. リモートからAAW(Arbitrary Adress Write; 任意アドレスへの書き込み)が達成できるとしましょう.つまり好きな長さのデータを好きなアドレスへ一度だけ書き込めるのです.このとき,どこをどのように書き換えるべきでしょうか. 状況にもよりますが,スタック上のポインタやリターンアドレス,SLUBから確保されたチャンク内のポインタは,実行の度に位置が変わり得るアドレスに配置される可能性が高く,あまりよろしくありません.どちらかといえば,kASLRさえ突破できれば確実に特定可能な,グローバルな.data領域などにあるアドレスが望ましいです. さてカーネルには書き換えると嬉しいポインタやデータがいくつかあり,それらを書き換えるテクニックがいくつか知られています.例えば`ptmx_fops->XXX`の書き換え,`modprobe_path`の書き換え,などが有名ですね(前者はもう対策されて使えなくなってるかも). しかしこういったテクニックの多くはローカル権限昇格向けのテクニックであり,ポインタやデータを書き換えた後にユーザランドでの操作が必要となるものばかりです.つまりログインすらできていない今回のケースでは使えません.従って今回のようなケースでは,カーネル内で自動的に関数ポインタを呼びだしてもらい,そこからROPへ持ち込み,愚直に色々やる必要があります. では,そのROPを引き起こすために書き換えるべきアドレスはどこでしょうか.RWな関数ポインタで,自動的に呼び出される,そんな都合の良い箇所はあるのでしょうか.あるとしたら,それを見つけるためにはどうすればよいでしょうか.こんな事を考えたのがきっかけです. さてx86/x64では,カーネル内に`__x86_indirect_thunk_XXX`(XXXは汎用レジスタ名)という関数があります.以下のように,特定のレジスタ(以下の例では`$rax`)の内容をスタックに配置して,そこにリターンするといったヘルパー関数です. ``` ... mov rax, qword ptr [...] call __x86_indirect_thunk_rax ... <__x86_indirect_thunk_rax>: mov qword [rsp], rax ret ``` 上記の例ですが,要は`jmp rax`の代わりとして用いられるコードです.無駄なコードにも見えますが,`CONFIG_RETPOLINE`が有効だと存在する,Spectre対策のコードです.大抵の環境では有効ですね. これらのthunk関数は,我々の知りたいことを解決してくれる特殊な関数です.本来は`jmp rax`のような間接ジャンプなわけですから,動的に決まるアドレスへジャンプしているわけです.つまりジャンプ先のアドレスは,どこかのメモリから持ってきているか,それらを用いて計算した結果ということになります. 大抵はこの呼び出しの直前にメモリから読み取っていると思われるので,他の汎用レジスタが読み取り元のアドレスを保持している可能性が十分考えられます.つまり飛び先のレジスタを`$rax`とした時の,`mov rax, qword ptr [rbx]`という命令列における`$rbx`のようなケースですね.もしくは`mov rax, rdi; mov rax, qword ptr [rax]`という命令列における`$rdi`のようなケースかもしれません(命令列実行後,`$rax`は飛び先のアドレスを保持するため格納元のアドレスはすでに破壊されていますが,`$rdi`がまだ保持してくれているケースです). パターンは色々あると思いますが,要は他の汎用レジスタの値がアドレスとして有効な値ならば,その指す先を確認すれば良いのです.もしかしたら,関数ポインタを保持するRWなメモリがあるかも知れません. ということで,都合の良い関数ポインタを探すためにこの機能を作ったのでした. これによってthunk関数経由でカーネルが呼び出す関数ポインタの一覧を洗い出したところ,意外なことに,自動的に(定期的に)呼び出されるグローバルでRWな関数ポインタがそこそこ存在していることがわかりました.これらのポインタを書き換えることで,自動的にRIPを奪うことができるので,あとはstack pivotしてROPに持ち込めば良いことになります. 思ったより上手く実装できた機能の一つで,カーネルのROPの起点を探すのが格段に楽になりました. # xsm/wsmコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/xsm.png) ![](https://raw.githubusercontent.com/bata24/gef/dev/images/wsm.png) DEFCON CTF 2022 qualsのteedium walletを復習するときに作った機能です.Write-upはここにありますので,気になる方は参考にどうぞ.https://hackmd.io/@bata24/BJ3nuVEu5 ARMのTrustZone内,つまりセキュアワールドで動作するOP-TEEと呼ばれるOSがあります. OP-TEEをデバッグする時にはセキュアワールド向けに確保されたメモリをデバッグすることになる訳ですが,セキュアワールド向けのメモリは特殊でして,ノーマルワールドにいるLinuxカーネルをデバッグしているときには,gdbからもセキュアワールドのメモリを見ることはできません. この仕様はほんと意味不明でして,qemu-systemでフルエミュレーションしてる場合は,gdbスタブからセキュアワールドのメモリへ制限なくアクセスできるべきだと思ってるのですが,そうはいかないようです. まあノーマルワールドにいる時は,セキュアワールド向けのページテーブルのルートのアドレスがわからないので,仕方のないことかも知れないのですが(実際AArch64向けのqemu-systemでは不可能),ARM向けのqemu-systemではなぜかセキュアワールド向けのページテーブルのルートを個別に保持しているので,技術的には可能なはずだと思っています. さてセキュアワールドでブレークさせれば,ページテーブルのルートが切り替わるので,gdbからそれらのメモリを見ることができるようにはなります.しかしセキュアワールドでブレークさせるのは,それなりの手順が必要で大変というか面倒です.ノーマルワールドにいながら,セキュアワールドのメモリを読み書きできないか,と考えたのがきっかけです. よくよく考えると,全てqemu-systemでエミュレートしている関係上,エミュレータであるqemu-systemのメモリの何処かにはセキュアワールドのメモリもあるはずです.つまり,gdbからqemu-system自体のメモリをパースすれば良いのではないかと気づきました. 色々調べていると,qemuモニタが提供する`monitor gpa2hva`というコマンドを見つけました.これはゲストの物理メモリとして割り当てたメモリが,qemu-systemの仮想メモリのどこに存在するか,そのアドレスを知ることができる機能です.つまりゲストの物理アドレスがわかれば,ホストの`/proc/<pid of qemu-system>/mem`経由で,セキュアワールドのメモリにアクセスすることができるわけです. あとはセキュアワールドの物理メモリアドレスが分かればよいのですが,これは`monitor info mtree -f`からわかります.`virt.secure-ram`という領域がそれです.必要な情報が手に入ったので,あとは実装するだけとなりました.ということで実装したのが`xsm`/`wsm`です. なお`wsm`コマンドを実装するときに知ったのですが,`/proc/<pid of qemu-system>/mem`経由でデータを書き換えても,その内容はゲストから見えるメモリとして即座には反映されません.qemuが持つキャッシュをクリアしないとだめなようで,これに気づくまで時間がかかりました. ちなみに,`xsm`は`x`コマンドの**S**ecure **M**emory版の意味です.`wsm`は**W**rite to **S**ecure **M**emoryです. # v2p/p2vコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/v2p-p2v.png) `xsm`コマンドの実装によって,ノーマルワールドにいながらセキュアワールドのメモリの中身を読み取ることができるようになりました. これはつまり「ノーマルワールドにいながらセキュアワールドのページテーブルを`pagewalk`できる」ようになったことを意味します. もともと`pagewalk`自体は実装できていたので,`pagewalk`にセキュアワールド向けのメモリ読み取り処理を追加することで,セキュアワールド向けのページテーブルも`pagewalk`ができるようになりました. さて`pagewalk`が達成できれば,仮想アドレスと物理アドレスの紐づけができたことになります.つまりノーマルワールドにいながら,セキュアワールドの仮想アドレスから物理アドレスへの変換,またその逆変換も達成できます. これを達成できると,更にいろんな機能の実装に役立ちそうだということで,実装することにしました. 仮想アドレスと物理アドレスを相互変換するコマンドなので,実際にはセキュアワールドに関係ないアドレスに対しても使えるコマンドなのですが,きっかけはこういった一連の流れによるものでした. # bsmコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/bsm.png) `v2p`/`p2v`コマンドを実装したことによって,ノーマルワールドにいながら,セキュアワールドの仮想アドレスと物理アドレスを任意に変換できるようになりました. そこで次に思いついたのが,ノーマルワールドにいながらセキュアワールドのアドレスにブレークポイントを仕掛ける機能です. OP-TEEはセキュアワールドで動作するOSなわけですが,当然kASLRがあり,起動する度に仮想アドレスが毎回変わります.しかし仮想アドレスは物理アドレスから特定でき(`p2v`),セキュアワールドが利用する物理アドレス自体も既に特定できています(`xsm`). それらの情報を元に,OP-TEEの仮想アドレスにブレークポイントを仕掛けるコードを実装したのがこのコマンドです. なお`bsm`は`b`コマンドの**S**ecure **M**emory版の意味です. # optee-break-taコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/optee-break-ta.png) 先の`bsm`は,単に物理アドレスを仮想アドレスに変換して,ブレークポイントを仕掛けるものです.事前に物理アドレスがわかっているのはOP-TEEのカーネルだけなので,そのOP-TEEのkASLRを突破するのには使えます. しかしOP-TEEにはユーザランドもあります.当然ユーザランドにもASLRがありますが,こちらへのブレークポイントを仕掛けることはこの時点ではできていませんでした. なぜなら,セキュアワールドのユーザランドのコードは,その呼び出しの都度ロードされるものだからです. ノーマルワールドにいながらそこにブレークポイントを仕掛けたい訳ですが,そもそもセキュアワールドのコードを呼び出していない状況では,メモリ上にロードすらされておらず,アドレスを知ることが困難です.ノーマルワールドで言えば,ELFが起動する前にそのELFのASLRを突破するのは無理だ,というようなイメージです. OP-TEEのドキュメントを漁って色々と検討した結果,カーネル内の`thread_enter_user_mode`に達したところでブレークさえできれば,その時にはユーザランドのコードがメモリ上にロードされていることから,ユーザランドのASLRのベースが分かる事に気が付きました. つまり,まずカーネルの`thread_enter_user_mode`にブレークポイントを仕掛け,今後そこでブレークした時に,「ユーザランドのコードがロードされてるであろうアドレスにブレークポイントを仕掛ける」という処理を行わせればいいわけです. `thread_enter_user_mode`のオフセットだけは,事前にIDAで解析するなどして求めておく必要がありますが,同じOP-TEEのカーネルイメージを使う限りは一度求めたら変化しない値なので,それほど手間ではないでしょう. OP-TEE固有の処理を利用しているので,全てのセキュアワールドで利用できるわけではありませんが,OP-TEEはTrustZoneで利用されるデファクトスタンダード的なカーネルであることから,実装する価値はあります. これを実際に実装したのがこのコマンドです. ちなみにセキュアワールドのユーザランドのコードのことを,TrustletとかTrusted App(TA)と言います.コマンド名が`optee-break-ta`なのは,OP-TEEのTAにブレークポイントを仕掛けるよ,という意図です.セキュアワールドの実装にはOP-TEE以外もあるため,意図的に`optee-`というプリフィックスをつけています. # optee-bget-dumpコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/optee-bget-dump.png) OP-TEEのユーザランドのコードが使うライブラリにもmalloc/freeがあるのですが,glibcではないので,当然アロケータのアルゴリズムも異なります. bgetと呼ばれるアロケータなのですが,これをダンプする機能が欲しかったので,実装しました. DEFCON CTF 2022 qualsのteedium walletを復習するために,`xsm`/`wsm`から始まって,`pagewalk`の修正,`v2p`/`p2v`,`bsm`,`optee-break-ta`,そして`optee-bget-dump`と様々な機能追加をしたわけですが,なかなかいい感じにできたので満足しています. # partition-alloc-dumpコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/partition-alloc-dump.png) 0CTF 2020のchromium fullchainを復習するときに作った機能です. chromiumではglibcのmallocやfreeはあまり使わず,独自メモリアロケータであるpartition allocを主に利用しています.chromium問で,特にUse-After-Free系のバグを悪用する場合には,このアロケータのフリーリストのダンプ機能が欲しくなるわけです. ソースとにらめっこしながらなんとか実装し,一旦は完成したのですが,当時のchromeはまさにpartition alloc周りを更新し続けており,1ヶ月もするとガラッとコードや構造が変わってしまっていました. 半年後くらいにGoogle CTF 2021のfullchainを解くときには,全く使えなくなっており,慌てて修正したのを覚えています.それ以降もpartition alloc周りのコードは頻繁に更新され続けたため,この更新に追いつくよう,不定期にchromiumのソースを調べてpartition alloc周りのコードを更新し続けることにしました. chromiumにはstable, dev, betaの開発ラインがあり,それぞれでpartition allocの構造が異なることもしばしばあったので,以下の3つのコマンドでその差分を吸収するようにしました. - partition-alloc-dump-stable - partition-alloc-dump-dev - partition-alloc-dump-beta 2023年1月現在では開発が落ち着いたのか,開発ラインごとの大きな差分も殆ど発生しなくなりました.そのため3つのコマンドは統合し,現在では`partition-alloc-dump`というコマンド一つになっています. なお,partition allocには`fast_malloc_root_`, `buffer_root_`, `array_buffer_root_`とglibcで言うarenaみたいなものが3種類あります(昔は`layout_root_`もありましたが今は削除されています). シンボルがあればこれらrootは容易に特定できるものの,chromium問ではシンボルが消されていることもしばしばありました.手動で探索することは辛いので,シンボルがなくてもヒューリスティックにこれらrootのアドレスを求められるようにしてあります. # musl-dumpコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/musl-heap-dump.png) DEFCON CTF 2021 qualsのmoooslを復習するときに作った機能です. glibcではなくmusl libcを使った問題もチラホラでてきていたので,作ってみました. malloc/freeの実装が,glibcとは全く異なるアロケータであり,非常に苦労しましたが,https://h-noson.hatenablog.jp/entry/2021/05/03/161933 でかなり詳細に解説されていたので,なんとか作り上げることができました. この機能も,シンボルがない状態でもちゃんとダンプできるよう,ヒューリスティックに`__malloc_context`(glibcでいうarenaみたいなもの)を求める機能を実装してあります. muslはバージョンが色々あって,古いmuslではまたガラッとコードが違うのですが,とりあえず最新版のv1.2.2にのみ対応しています. # vmmapコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/vmmap-qemu-user.png) pedaの時代からあり,gefにも実装されているvmmapコマンドですが,普通に使う分には問題ないものの,qemu-userのgdbスタブを利用している時はうまく表示できないという問題がありました. pwndbgでは,マップされているであろうアドレスを力技で特定するといった方法を実装していたので,参考にして自分でも実装し,qemu-userと連携しているときにも表示できるようにしました. 尚これを作る過程で,ELFのプログラムヘッダをパースしたり,スタック上にあるELF auxiliary vectorをパースしたりする必要があったので,それらも実装しました(`elf-info`コマンドと`auxv`コマンドを拡張する形で作成). またqemu-user自体のメモリマップがわかると嬉しいことも多いので,それを表示する機能も追加しています(`--outer`オプション). この他,Intel SDEやpinなどもgdbスタブを持っているので,併せてこれらと連携しているときにも同様にvmmapを表示できるようにしています. 2023/10/25追記: qemu 8.1からは,`info proc mappings`で表示できるようになりました.ただし私の環境では,x86_64のみうまく動作しませんでした.x86やARMなど他のアーキテクチャは行けます. `vmmap`コマンド側も,この変更に合わせて今後修正予定です. # contextコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/context-syscall-args.png) gdbスクリプトでデバッグするときに必ず目にする,現在の状況を表示する画面です.細かいところでめちゃめちゃ手を加えています. - 本家gefの,バイトコードのリアルタイム表示機能 - 私がforkしたあとに本家で実装されたので,頑張って取り込みました. - 本家gefの,システムコールの引数表示機能 - 引数のデータベースがかなり間違っていたので,手作業で全部直しました. - x64,x86ネイティブ,x64上で動くx86,AArch64,ARMネイティブ,AArch64上で動くARM,は正確にはそれぞれ引数の型が異なるので,それら全てをパターン分けして実装しました. - メモリアクセス時にその中身を表示する機能 - `[]`がある場合に,その中身を表示する機能を追加したものです. - x86/x64では,`fs:[]`のようなセグメントレジスタ経由のメモリアクセスが存在しますが,アセンブリ上ではそのアドレスがわかりません.リアルタイムにセグメントレジスタが参照しているアドレスを取得して,そのメモリアドレスの中身を表示するように対応しています. - 長いシンボルを省略表示する機能 - C++のコードは,シンボルが長すぎて画面が見づらくなることがあります.特にchromiumのデバッグをするときに鬱陶しいと思っていたので,この機能を追加しました. - メモリをダンプする`dereference`コマンドの改善 - スタックを表示する機能は内部的に`dereference`コマンドを利用しています.メモリの参照を自動的に辿ってくれるので非常に良い機能ですが,いくつか問題もあります. - このコマンドは`/proc`ファイルシステムの情報を利用する関係で,qemu-system配下ではまともに動作しない問題がありました.これを修正(というか大幅に書き直し)しています. - また特定のメモリがシリアルデバイスにマップされているケースでは,スタック上のアドレスをgdbから読み取りアクセスするだけでOSごと落ちてしまうケースがありました.そのようなアドレスをブラックリスト化し,次回以降アクセスしない機能を追加しています. など,本家gefのコードを流用しつつも,細かな機能追加,修正を多数施してあります. # checksecコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/checksec.png) pwndbgやpwntoolsにもこの機能はありますが,本来は[checksec.sh](https://github.com/slimm609/checksec.sh)が実装していた機能です. 本家checksec.shではいつの間にか色々と検出機能が強化されていたので,追随して表示する内容を増やしました. またCETやASLR周りなど,独自に追加したものもいくつかあります. 2023/05/16追記: カーネル版checksecであるkchecksecというコマンドも作りました. ![](https://raw.githubusercontent.com/bata24/gef/dev/images/kchecksec.png) # kmagicコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/kmagic.png) [angelboyさんのpeda拡張](https://github.com/scwuaptx/peda)にあった機能のカーネル版です. 本来のmagicコマンドは,ユーザランドのexploitで良く使うアドレスを一覧表示する機能でした.これはそれほど難しくないので,簡単に実装することができました. 思い入れのあるのはここから先でして,カーネルデバッグ時にも似たような機能を提供できないか,というリクエストを後輩から頂きました. ということで色々開発したのがこの機能です. 有用な関数だけでなく,有用なグローバル変数も対象にしたかったのですが,カーネルにシンボルがない状況でもこれらを求められるようにしないとあまり嬉しくはありません. すでに実装済みの`ksymaddr-remote`を活用すれば良いようにも思えますが,実はグローバル変数のアドレスを求めることは結構難しいのです. というのも,カーネルのビルドコンフィグである`CONFIG_KALLSYMS_ALL`オプションの設定によっては,kallsymsに含まれなくなるシンボルがかなりあるからです. つまり`ksymaddr-remote`を使ってkallsymsの情報をパースしても,求められないシンボルがあるということです.実際,ほとんどのグローバル変数のアドレスは求められません. そういったグローバル変数のアドレスを求めるために,それらグローバル変数を利用している関数をまず`ksymaddr-remote`で求めて,その関数のアセンブリコードをパースして目的のメモリアドレスを求めるなど,かなりヒューリスティックな方法を駆使して実装しました. コンパイラの吐くアセンブリに強く依存しているので,上手く検出できないケースもありますが,なるべく汎用的に求められるよう頑張っています. `commit_creds`,`prepare_kernel_cred`,`modprobe_path`といった有名なシンボルの他,`native_write_cr0`,`set_memory_rw`,`n_tty_ops`,`text_poke`など,余り知られていないけれどカーネルexploitに有用な関数・グローバル変数も,なるべく取り込んでいます. # fpu/mmx/sse/avxコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/fpu-mmx-sse-avx.png) x86/x64には浮動小数点演算やSIMD演算を行うための特殊なレジスタがあります. CTFの制約付きシェルコード問では,これら(特にFPUレジスタ)を活用するテクニックがあります.例えば1命令ごとにレジスタがオールクリアされる状況でも値を保持したい場合には,FPUレジスタに値を保存しておくと消されない,などが過去の問題で出題されました. ただやってみるとわかりますが,gdbでこういったFPUレジスタやSIMD系レジスタを表示すると,非常に見づらいという問題があります. まずFPUレジスタの話ですが,汎用レジスタ(`$rax`/`$eax`など)は64ビット/32ビットなのに対し,FPUレジスタは80ビットという点に注意しなければなりません.`fst/fstp`命令などでFPUレジスタから汎用レジスタへ値をコピーするとき,浮動小数点数としての値の同一性を優先するため,そのバイト表現は大きく変化してしまうのです.これはシェルコード問題では致命的です.知っていれば気づけますが,知らなければどこで値が狂ったかわからない上に,普通は`fst/fstp`命令を実行しないと得られる値がわかりません.これらを解決するために,FPUレジスタをダンプし,80ビットでのバイト表現,64ビットでのバイト表現,32ビットでのバイト表現を計算して,見やすく表示する機能を作ったわけです. またFPUレジスタには8つのレジスタがあり,その表現としては例えば`st(0)`という書き方になるのですが,これは8つのレジスタをスタックのように扱ったときの0番目のレジスタを意味する仮想的な表現方法となっています.つまり実際の物理的なレジスタ(mmレジスタと共有)との紐付きは状況によって変わります.とはいえ実際にはスタックトップとなるレジスタ位置が替わるだけなのと,どのレジスタがスタックトップなのかは`$fstat`レジスタが情報を持っているので,これをパースすれば良いです.このようにして,これらの紐付きをわかりやすく表示しています(これは`info float`コマンドでも可能なのですが,どうやらバグっている様なので自作しました). SIMD系レジスタはここまで複雑ではありませんが,単純に`info register $xmm0`とかすると,1バイト,2バイト,4バイト,8バイト,16バイト区切りなど複数の表現で表示されます.個人的にはめちゃくちゃ読み辛く,こんな風に表示されて嬉しい人おるんか?という感想です.これらを見やすく一覧表示できた方が嬉しいので,機能を追加しました. 実はgdbのバージョンによって出力形式が微妙に違ったりするので,その辺を吸収するのに苦労しています. 尚,これをさらに発展させて,`mmxset`コマンドや`xmmset`コマンドといった,SIMD系レジスタへ値を簡単に設定するコマンドも機能追加しています. # dwarf-exception-handlerコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/dwarf-exception-handler.png) [CodeGate 2014のmembership](https://pwning.net/pwn/2014/02/27/codegate-2014-membership-800pt-pwnable-write-up/)や,[hack.lu 2014のbreakout](https://github.com/jhector/breakout)という問題があります. 例外発生時のunwindにおいて,DWARFによる復帰情報を偽装してexploitに使う,というとても難しい問題なのですが,DWARFをまともに解釈するツールは余り知られていません. readelf,eu-readelfで表示することはできますが,各バイトの意味やオフセットは詳細に表示することができません.また[katana](https://katana.nongnu.org/)は良くできたツールですが,開発は停止している上に,導入が手間です.IDAやGhidraを使ってもよいのですが、結局は勉強も兼ねてDWARFのunwind情報をパースして表示する機能を作りたくなり,実装したのがこの機能です. ファイル上の各種情報のオフセット,バイトコード,解釈した値,備考などを表示するようにしたので,DWARF問もこれでバッチリです. 尚この機能を実装するときにDWARFやunwindの実装を調べ直したのですが,`DW.ref.__gxx_personality_v0`という面白いポインタも見つけました.`g++`で以下のようにtry-catchを含むコードをビルドしたとしましょう. ```c++= #include <vector> int main() { std::vector<int> vec{1,2,3}; try { vec.at(10); } catch ( ... ) { } } ``` すると暗黙的に`DW.ref.__gxx_personality_v0`というポインタが.data領域に配置されており,例外が起きてunwindする時にこのポインタが呼び出されます.つまり書き換えておくと,例外発生時にRIPを奪うことができます. Full RELRO有効下でもRWなので,GOT Overwriteと似たような感じでRIPを奪うのに使うことができます.ググってもpwnの文脈ではほとんどでてこないので,新規性のあるテクニックなのかなと思っています. # asm-listコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/asm-list.png) x86/x64のシェルコード問を解く時,バイトコードとアセンブリ命令の一覧があると嬉しいなと何度も思ったことがあります. いつも http://ref.x86asm.net/index.html を参考にしながら考えていたわけですが,スマートに全部表示することはできないのか?と思ったのがきっかけです. 実はこれ,実際にやってみると非常に難しい問題でした. そもそもx86の命令は,新しいCPUがリリースされる度に現在でも追加され続けています.また公式にバイトコードと命令のリストがあるわけでもありません.手に入る公式な情報はIntelのマニュアルだけです. 命令体系も非常に複雑で,その都度パースするわけにも行きませんし,keystone-engine,capstone,unicornなど有名どころのpythonで使えるライブラリでも対応していない命令が結構あり,そちらをデータベースとすることも困難でした. 最終的に,新し目の命令はバイトコードも長く,シェルコードで使うことはなさそうとのことから,追随しなくてもいいかと切り捨てました.その上で,[asmdb](https://github.com/asmjit/asmdb)が欲しい情報をいい感じに保持していたので,こちらの`x86data.js`を利用させて頂きました. ちなみにARM版やAArch64版なども作ろうと思ったのですが,色々検討した結果無理だという判断になり,断念しました. # dtor-dumpコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/dtor-dump.png) SECCON Beginners CTF 2022のmonkey-heapを解く直前に,たまたま作っていた機能です. ELFやglibcには色々なところにデストラクタが存在し,それらの情報を一括で表示したいと前から思っていました.そこで作ったのがこの機能です. glibc内でデストラクタをポインタとして保持する箇所はいくつかあるのですが,分かる範囲で色々と洗い出したつもりです.尚,RWな領域にあるポインタは大抵PTR_MANGLED,つまり乱数値とのXORやローテートが施されているので,メモリをパッと見ただけでは何が書かれているかわかりません.また復号も結構手間です.このコマンドは,それらを自動的に復号して表示するようにしています. この機能を作っていたおかげで,monkey-heapは簡単に解くことができました. # gotコマンド ![](https://raw.githubusercontent.com/bata24/gef/dev/images/got.png) PLTやGOTを一覧表示するコマンドです. 本家gefではPLTのダンプ機能がなかったので,その機能を独自に追加しています. またIntel CETに対応したELFでは.plt.secというセクションが増えており,そちらにも対応しています.後輩からの機能追加リクエストにより`-v`オプションを指定すればreloc_argの値も表示するようにしました(多分合ってると思う). この他,メインのバイナリだけではなく,ライブラリのGOTを表示する機能なども追加しています.これ実は結構大変でして,ライブラリのGOTにはIRELATIVEなエントリ(環境によって解決される関数が異なる,`*ABS*+0xXXX`みたいな名前を持つエントリ)があり,これも表示したかったのですが,その扱い方が難しいのです. x64ではRELA構造体を利用しており,r_addendメンバにある情報を用いれば,複数存在するIRELATIVEなエントリを一意に識別できます.つまりIRELATIVEなエントリであっても,GOTとPLTの紐付けは簡単に達成できます.こちらは問題ありません. しかしx86ではREL構造体を利用しており,r_addendメンバがないため,複数存在するIRELATIVEなエントリを一意に識別する手段がないのです.最終的には,格納順序に規則性があるように見えたので,それを利用してそれっぽく表示することにしましたが,この方法が常に正しいのかは今現在でも不明です. 2023/10/25追記: `got`コマンドは,PLTのアドレスを求めるために,内部的にbinutilsパッケージのobjdumpを使っています. しかしglibc-2.37以降のglibcを,ubuntu 22.04のデフォルトであるbinutils-2.38でパースしようとするとうまく行きません.objdumpのシンボル検出アルゴリズムが変わったためのようです. ubuntu 22.04のglibcはglibc-2.35であるため,通常は問題になりませんが,もしpatchelfなどでubuntu 22.04上で新しいglibcを無理やり使おうとすると,PLTが検出されません.素直にubuntu 23.10を使いましょう. - glibc-2.35 + binutils-2.38: OK - glibc-2.37~ + binutils-2.40: OK - glibc-2.37~ + binutils-2.38: NG # Qemu-user連携 2023/07/25追記: CTFでは,変態アーキをデバッグする問題もそこそこ出題されます.そもそもgdbが未対応なアーキテクチャはダメですが,gdbさえ対応していれば,理論上はGEFで対応できるはずです. 安定して動作させられ,かつデバッグしやすいという観点から,qemu-user + gdb + toolchainの3つが揃っているアーキテクチャを色々探しました. x86/x64/ARM/ARM64だけでなく,そのほかのアーキテクチャも色々取り込んだところ,20を超えるアーキテクチャに対応することができました. Qemu-user配下で動かすという制約はありますが,現時点で対応しているアーキテクチャは以下のとおりです。 1. x86-32 2. x86-64 3. arm (arm/thumb2) 4. aarch64 5. mips 6. mipsn32 7. mips64 8. powerpc 9. powerpc64 10. sparc 11. sparc32plus 12. sparc64 13. riscv32 14. riscv64 15. s390x 16. sh4 17. m68k 18. alpha 19. hppa32 (parisc) 20. or1k (openrisc 1000) 21. nios2 22. microblaze 23. xtensa (lx60) 24. cris 25. loongarch64 26. arc v2 32 (hs38) 27. arc v3 32 (hs58) 28. arc v3 64 (hs68) 29. csky 詳細は以下にまとめています. https://github.com/bata24/gef/blob/dev/docs/QEMU-USER-SUPPORTED-ARCH.md 2023/12/1追記 これら変態アーキテクチャのバイナリでも,ヒープ周りのコマンドが使えるように対応しました.しかもシンボルがなくても(おそらく)動作するようになっています.Glibcを使っていないとダメ(uClibcではダメ)という制約はありますが,中々上手くできたのではないかと思っています. # 終わりに bata24/gefで実装した機能を色々と紹介してきました.とりあえず,面白そうな機能はこんなところです.CTFで役に立つ(かも知れない),かなり独自性のある機能が多かったのではないかと思います.もちろん,ここには書ききれていない機能も色々実装しています. まあ未だバグがよく見つかるので,pwndbgとかpedaから直ぐに乗り換える必要はないと思いますが,今回紹介したような機能を使ってみたいという時には,是非使ってみて頂ければ,こちらも開発した甲斐があるというものです. バグ報告,フィードバック,機能リクエストなどもお待ちしています. ---- 9日目の記事は,[kurenaif(@fwarashi)](https://twitter.com/fwarashi)さんの「[普段の動画作り,Crypto作問風景](https://kurenaif.hatenablog.com/entry/2022/12/09/001604)」です. YoutubeにCTF系の動画を投稿していらっしゃる,数少ない方のひとりです(私も良く見ています).記事の公開を楽しみに待ちましょう.