gef
gdb
ctf
この記事は,CTF Advent Calendar 2022 の8日目の記事です.
7日目はだこつ(@y011d4)さんの「面白かった Crypto問 2022」でした.
今年はCrypto勢の投稿が多いですね.最近のCrypto問は全くわからずついて行けてないので,Crypto勢の皆さまは凄いなと思います(小並感).
https://github.com/bata24/gef のことです.
もう少し説明すると,「gdb拡張スクリプトとして有名所の1つであるhugsy/gefを,勝手にforkして色々機能を追加したもの」です.
作成に至った背景としては,CTF Advent Calendar 2020で「gefを改造した話」という記事にしているので,そちらをご覧ください.
このスクリプトを世にリリースしてから2年が経ちました.
2年も経つと,各機能を実装しようとした背景とか苦労話などもそこそこ溜まってきます.それらを忘れないうちにどこかに書き留めておこうと思ったからです.
また
みたいな話題を提供したかったのです.
こういった話題は,このスクリプトを使ってない方でも楽しめるでしょうし,機能開発のきっかけが特定のCTFの問題だったりするので,CTF Advent Calendarで公開することにしました.
全てのコマンドについて触れると膨大になってしまうので,思い入れのあるコマンドを中心に解説していこうかと思います.
では,早速書いていきます.
qemu-system連携時に利用できる,ページテーブルをダンプする機能です.
きっかけはHITCON CTF 2018 の Super Hexagonを復習していた時です(日本語解説).Write-upを読み進める過程でaarch64-pagewalk.pyというものに出会いました.これについては衝撃を受けました.以下その理由です.
まずそれまで知っていた私の知識は以下の通りでした.
info mem
コマンドがあり,pagewalkっぽいことができます.
qemuのinfo mem
は使えないな,都度メモリを自分で追いかけるしか無いかなと思っていた矢先,スクリプトによる力技で解決するという方針に出会い,感動しました.スクリプトでこんな事ができるとは思っていなかったのです.
色々調べていると,pagewalkを行うために必要な情報は,システムレジスタとメモリの読み取りだけで賄えることがわかりました.つまりアーキテクチャさえ分かるのなら,ゲストOSがなんであるかには依存せず,gdbの機能だけで実装可能なのです.
これは汎用性があるぞと思い,CPUマニュアルとにらめっこしながら,1ヶ月くらい掛けてできたのがpagewalk
コマンドです(その後何度かリファクタリングはしています).出力はなるべく短くすっきりと,それでいて必要な情報は得られるようにしています.
また,以下のような機能をつけました.
最初はAArch64,続いてx64,さらにx86,ARMまで対応しました.それ以外のアーキは詳細なマニュアルを見つける事ができず,断念しました.まあCTFではこの4つのアーキさえ対応していれれば十分でしょう.
ちなみにpagewalk
のソフトウェア実装はかなり大変で,コード量はとても多くなってしまいました.コメントが多いこともありますが,4つのアーキで合計4000行近くをスクラッチで書いています.
尚,同じような機能を持つgdb-pt-dumpというのも後から知ったのですが,これはx86とARMに対応していません.x86とARMのpagewalkを実装し今でも使えるのは,bata24/gef以外にないと思っています.
またx86_64限定の話っぽいのですが,ある1つの物理アドレスに対して65536個(?)の異なる仮想アドレスがマップされていることがあります.表示が長くなる原因の一つはこれなのですが,幸いアドレスは連続でないながらも似てはいるので,*
でまとめるなどの工夫をしています.
※どうやらKASAN
用のシャドウメモリらしいです.環境によってはページテーブルを数百万エントリも利用することがあります.愚直にパースするとOOMが発生するため,現在はそれっぽいエントリのパースをデフォルトでスキップするようにしています.
2024/07/03追記:
pagewalk
コマンドの結果に,付加情報をつけて表示するpagewalk-with-hints
コマンドも作りました.実行に時間が掛かるため使い所は難しいのですが,現在のプロセスのコンテキストにおける,仮想メモリの全体感を把握したいときなどは非常に便利です.
qemu-system連携時に利用できる,シンボルのないカーネルでもシンボルのアドレスを表示する機能です.
シンボルのないLinuxカーネルをデバッグするという状況は,CTFではよくあることです.対処法としてはkASLRを無効化し,rootでログインできるように問題イメージを書き換えてから,/proc/kallsyms
の情報をダンプして控えておいて,その情報をもとにデバッグするというのが私がいつも行う方法でした.
毎回面倒だなーと思っていたわけですが,そもそもシンボルのないカーネルをデバッグするのはCTFプレイヤーくらいです.ググってもそういう時のテクニックなんて都合の良い情報が見つかるはずもなく,解決策もなかなか思いつかなかったのでした.
そんな時,binjaのとある方に昔教えてもらったことを思い出しました.それは「シンボルがstripされたカーネルでも/proc/kallsyms
で表示するための情報はカーネル内に保持している」ということです.つまり,これをパースすればリアルタイムにシンボルのアドレスを特定できるのではないか,とひらめいたのがきっかけです.
pagewalk
コマンドを実装したことで,qemu-systemで動くLinuxは,そのメモリマップがわかるようになっています.だいたいパターンも見えてきて,ヒューリスティックではありますが,Linuxカーネル自体がメモリのどこにあるかも特定できるようになりました.
後はkallsyms
用の情報を見つけることができれば,そのデータをパースすることで,全てのアドレスをリアルタイムに得ることができるはずです.
カーネルのソースを読んで,色々検討した結果,以下の配列のアドレスや値を求めていけば,kallsyms
の内容をパースするのに必要な情報を揃えられそうなことがわかりました.
kallsyms_relative_base
kallsyms_num_syms
kallsyms_names
CONFIG_KALLSYMS_ABSOLUTE_PERCPU
が有効か無効かkallsyms_offsets
kallsyms_markers
kallsyms_token_table
kallsyms_token_index
実際にやってみたところ,長い試行錯誤を経てなんとか達成することができました.
特に苦労したのは,これらを特定するためにメモリをスキャンする過程で,メモリ上に値が格納される微妙なパターンの違いが色々あることです.
kallsyms
の情報として,絶対アドレスを格納するケースkallsyms
の情報として,ベース値からの相対アドレスを格納するケースkallsyms_relative_base
などの格納位置が想定と異なるケースこれらのパターンは,過去のCTFで出題された,手元にあった30種類くらいのqemuイメージで動作確認する過程で見つけたものです.なぜこんなにパターンがあるのか理由はわかりませんでしたが,とりあえず見つけたパターンは片っ端から対応していきました.
2023/3/23追記:
さてその後もたまに見つかるバグを潰したりしていたわけですが,どうにも判定ロジックが複雑になりすぎメンテが大変になりました.そこでロジックの見直しをしたところ,後述のvmlinux-to-elfに同梱のkallsyms_finder.py
がスマートなロジックで導出していたので,こちらのロジックへ切り替えました.
2024/07/03追記:
カーネル6.2で新たにテーブルが増えました(kallsyms_seqs_of_names
).またカーネル6.4でkallsyms_offsets
の配置される位置が変わりました.カーネル6.9でもまた変更が入っています.gefではこれらにも対応済みです.どのように配置が変わったのかは,gefのソースコードを見てください.コメント等で解説しています.
ksymaddr-remote
コマンドができたことで,求めたシンボルをgdb上にインポートしたいと思うようになりました.ksymaddr-remote
コマンドでシンボルとそのアドレスは求められても,実際には毎回アドレスの生の値を直接使う必要があり,大変なことには変わらないと思ったのです.
さて手元には,シンボルとそのアドレスのペアが大量にあります.これをgdbにシンボル情報としてインポートしたいのですが,そんな都合の良い機能はgdbにはありませんでした(あっても良さそうなのに).
しかし当たり前ですが,gdbには,シンボル付きのELFであればその中にあるシンボルを読み取ってインポートする機能があります(add-symbol-file
コマンド).そこで考えたのが「空のELFを作り,objcopy
でシンボルとアドレスをそのELFに埋め込んで,そのELFをgdbにロードすればいいのでは?」ということです.
ということで実装してみたらできました.
正確には,シンボルを追加する機能自体はadd-symbol-temporary
コマンドとして独立に実装し,ksymaddr-remote-apply
コマンドからはそのadd-symbol-temporary
コマンドを呼ぶようにしています.
このテクニックは,IDAのデコンパイル結果をgdbに反映する機能を提供するdecomp2dbgでも使われており,ソースにアイデア元として私の名前が書かれています.嬉しいですね.
ksymaddr-remote-apply
コマンドを実装して満足していたところ,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/gef
に保存しておき使い回すようにはしました.デバッグ対象カーネルを再起動させた後にシンボルを再インポートするケースでは,kASLRでアドレスが変わり得ますが,ベースアドレスからの各関数のオフセット自体は変わりません.つまりベースアドレスを修正して読み込むだけで良く,この機能はgdbのadd-symbol-file
コマンドにあります.従って時間のかかるvmlinux-to-elf
を動かす必要がなくなるので,かなり時間短縮できます.
とはいえFGKASLRが有効な環境など,毎回アドレスがオフセットごとガラッと変わる環境ではksymaddr-remote
のほうがストレスなく使えて良いので,こちらの機能も残しています.
どの問題だったかおぼえていないのですが,LinuxのスラブアロケータのUAFを使って解くカーネル問題で,スラブアロケータのフリーリストのパーサがほしいなと思ったことがありました.saltの存在は知っていたのですが,開発は停止しており,最近のカーネルには適用できなくなっていました.
そこで,saltのコードを参考にしながら新規に開発したのがこの機能です.
最近のSLUBでは,解放済みチャンクが保持するリンクリストのポインタのオフセットがチャンク先頭にないケース(上図内のoffset
の値)や,そのポインタを暗号化しているケース(上図内のrandom (xor key)
の値)があります.saltが未対応なのはこの辺りの処理なので,これらを考慮しないといけません.
すでにksymaddr-remote
によって必要なシンボルが特定できていたため,とっかかり自体は簡単でした.しかし構造体のメンバがカーネルのバージョンによって結構変わることもあって,メモリ内の構造をもとに,ヒューリスティックにその差分を吸収する必要があり,そこが結構難しかったです.
現在のカーネルにも対応しており,シンボルが無くても利用可能なSLUBのフリーリストダンプ機能は,他に見たことがないので,これもなかなか良い機能だと思っています.
2023/05/16追記:
SLUBだけでなく,SLAB,SLOB,SLUB-TINYのダンプ機能も追加しました.SLOBやSLABはそろそろカーネルから削除されますけど,一応昔の問題向けということで残しています.
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の起点を探すのが格段に楽になりました.
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
コマンドのSecure Memory版の意味です.wsm
はWrite to Secure Memoryです.
xsm
コマンドの実装によって,ノーマルワールドにいながらセキュアワールドのメモリの中身を読み取ることができるようになりました.
これはつまり「ノーマルワールドにいながらセキュアワールドのページテーブルをpagewalk
できる」ようになったことを意味します.
もともとpagewalk
自体は実装できていたので,pagewalk
にセキュアワールド向けのメモリ読み取り処理を追加することで,セキュアワールド向けのページテーブルもpagewalk
ができるようになりました.
さてpagewalk
が達成できれば,仮想アドレスと物理アドレスの紐づけができたことになります.つまりノーマルワールドにいながら,セキュアワールドの仮想アドレスから物理アドレスへの変換,またその逆変換も達成できます.
これを達成できると,更にいろんな機能の実装に役立ちそうだということで,実装することにしました.
仮想アドレスと物理アドレスを相互変換するコマンドなので,実際にはセキュアワールドに関係ないアドレスに対しても使えるコマンドなのですが,きっかけはこういった一連の流れによるものでした.
v2p
/p2v
コマンドを実装したことによって,ノーマルワールドにいながら,セキュアワールドの仮想アドレスと物理アドレスを任意に変換できるようになりました.
そこで次に思いついたのが,ノーマルワールドにいながらセキュアワールドのアドレスにブレークポイントを仕掛ける機能です.
OP-TEEはセキュアワールドで動作するOSなわけですが,当然kASLRがあり,起動する度に仮想アドレスが毎回変わります.しかし仮想アドレスは物理アドレスから特定でき(p2v
),セキュアワールドが利用する物理アドレス自体も既に特定できています(monitor info mtree -f
).
それらの情報を元に,OP-TEEの仮想アドレスにブレークポイントを仕掛けるコードを実装したのがこのコマンドです.
なおbsm
はb
コマンドのSecure Memory版の意味です.
先の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-
というプリフィックスをつけています.
OP-TEEのユーザランドのコードが使うライブラリにもmalloc/freeがあるのですが,glibcではないので,当然アロケータのアルゴリズムも異なります.
bgetと呼ばれるアロケータなのですが,これをダンプする機能が欲しかったので,実装しました.
DEFCON CTF 2022 qualsのteedium walletを復習するために,xsm
/wsm
から始まって,pagewalk
の修正,v2p
/p2v
,bsm
,optee-break-ta
,そしてoptee-bget-dump
と様々な機能追加をしたわけですが,なかなかいい感じにできたので満足しています.
0CTF 2020のchromium fullchainを復習するときに作った機能です.
chromiumではglibcのmalloc
やfree
はあまり使わず,独自メモリアロケータであるpartition allocを主に利用しています.chromium問(not V8問)で,特に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のアドレスを求められるようにしてあります.
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にのみ対応しています.
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
コマンド側も,この変更に合わせて今後修正予定です.
gdbスクリプトでデバッグするときに必ず目にする,現在の状況を表示する画面です.細かいところでめちゃめちゃ手を加えています.
[]
がある場合に,その中身を表示する機能を追加したものです.fs:[]
のようなセグメントレジスタ経由のメモリアクセスが存在しますが,アセンブリ上ではそのアドレスがわかりません.リアルタイムにセグメントレジスタが参照しているアドレスを取得して,そのメモリアドレスの中身を表示するように対応しています.dereference
コマンドの改善
dereference
コマンドを利用しています.メモリの参照を自動的に辿ってくれるので非常に良い機能ですが,いくつか問題もあります./proc
ファイルシステムの情報を利用する関係で,qemu-system配下ではまともに動作しない問題がありました.これを修正(というか大幅に書き直し)しています.など,本家gefのコードを流用しつつも,細かな機能追加,修正を多数施してあります.
pwndbgやpwntoolsにもこの機能はありますが,本来はchecksec.shが実装していた機能です.
本家checksec.shではいつの間にか色々と検出機能が強化されていたので,追随して表示する内容を増やしました.
またCETやASLR周りなど,独自に追加したものもいくつかあります.
2023/05/16追記:
カーネル版checksec
であるkchecksec
というコマンドも作りました.
angelboyさんの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に有用な関数・グローバル変数も,なるべく取り込んでいます.
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系レジスタへ値を簡単に設定するコマンドも機能追加しています.
CodeGate 2014のmembershipや,hack.lu 2014のbreakoutという問題があります.
例外発生時のunwindにおいて,DWARFによる復帰情報を偽装してexploitに使う,というとても難しい問題なのですが,DWARFをまともに解釈するツールは余り知られていません.
readelf,eu-readelfで表示することはできますが,各バイトの意味やオフセットは詳細に表示することができません.またkatanaは良くできたツールですが,開発は停止している上に,導入が手間です.IDAやGhidraを使ってもよいのですが,結局は勉強も兼ねてDWARFのunwind情報をパースして表示する機能を作りたくなり,実装したのがこの機能です.
ファイル上の各種情報のオフセット,バイトコード,解釈した値,備考などを表示するようにしたので,DWARF問もこれでバッチリです.
尚この機能を実装するときにDWARFやunwindの実装を調べ直したのですが,DW.ref.__gxx_personality_v0
という面白いポインタも見つけました.g++
で以下のようにtry-catchを含むコードをビルドしたとしましょう.
#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の文脈ではほとんどでてこないので,新規性のあるテクニックなのかなと思っています.
x86/x64のシェルコード問を解く時,バイトコードとアセンブリ命令の一覧があると嬉しいなと何度も思ったことがあります.
いつも http://ref.x86asm.net/index.html を参考にしながら考えていたわけですが,スマートに全部表示することはできないのか?と思ったのがきっかけです.
実はこれ,実際にやってみると非常に難しい問題でした.
そもそもx86の命令は,新しいCPUがリリースされる度に現在でも追加され続けています.また公式にバイトコードと命令のリストがあるわけでもありません.手に入る公式な情報はIntelのマニュアルだけです.
命令体系も非常に複雑で,その都度パースするわけにも行きませんし,keystone-engine,capstone,unicornなど有名どころのpythonで使えるライブラリでも対応していない命令が結構あり,そちらをデータベースとすることも困難でした.
最終的に,新し目の命令はバイトコードも長く,シェルコードで使うことはなさそうとのことから,追随しなくてもいいかと切り捨てました.その上で,asmdbが欲しい情報をいい感じに保持していたので,こちらのx86data.js
を利用させて頂きました.
ちなみにARM版やAArch64版なども作ろうと思ったのですが,色々検討した結果無理だという判断になり,断念しました.
SECCON Beginners CTF 2022のmonkey-heapを解く直前に,たまたま作っていた機能です.
ELFやglibcには色々なところにデストラクタが存在し,それらの情報を一括で表示したいと前から思っていました.そこで作ったのがこの機能です.
glibc内でデストラクタをポインタとして保持する箇所はいくつかあるのですが,分かる範囲で色々と洗い出したつもりです.尚,RWな領域にあるポインタは大抵PTR_MANGLED,つまり乱数値とのXORやローテートが施されているので,メモリをパッと見ただけでは何が書かれているかわかりません.また復号も結構手間です.このコマンドは,それらを自動的に復号して表示するようにしています.
この機能を作っていたおかげで,monkey-heapは簡単に解くことができました.
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を使いましょう.
2023/07/25追記:
CTFでは,変態アーキをデバッグする問題もそこそこ出題されます.そもそもgdbが未対応なアーキテクチャはダメですが,gdbさえ対応していれば,理論上はGEFで対応できるはずです.
安定して動作させられ,かつデバッグしやすいという観点から,qemu-user + gdb + toolchainの3つが揃っているアーキテクチャを色々探しました.
x86/x64/ARM/ARM64だけでなく,そのほかのアーキテクチャも色々取り込んだところ,20を超えるアーキテクチャに対応することができました.
Qemu-user配下で動かすという制約はありますが,現時点で対応しているアーキテクチャは以下のとおりです.
詳細は以下にまとめています.
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)さんの「普段の動画作り,Crypto作問風景」です.
YoutubeにCTF系の動画を投稿していらっしゃる,数少ない方のひとりです(私も良く見ています).記事の公開を楽しみに待ちましょう.