Try   HackMD

bata24/gefの機能紹介とか 2024

tags: gef gdb ctf

はじめに

この記事は,CTF Advent Calendar 2024 の6日目の記事です.
5日目はArk(@arkark_)さんの「about:blank テクニック."disconnection"の解説?」でした.
※まだ投稿されていないみたいなので、投稿され次第URLを差し替えます.

本記事は,2022年の記事「bata24/gefの機能紹介とか」の続きみたいなものです.
但し,bata24/gefの特長というか目玉機能は,その2022年の記事でほとんど紹介してしまっています.
そこで本記事では「使用頻度は高くないだろうけれど,そこそこ使えると思う機能の紹介」や「それに関連した話」などをします.
※カーネル関連の話は殆どありません,ご注意ください.

タイトルのbata24/gefって何?

gdb拡張スクリプトGEFのfork版で,https://github.com/bata24/gef のことです.
主にCTFer向けに機能拡張しています.

詳しく知りたい方は,過去のCTF Advent Calendarにも記事を投稿しているので,参照してみてください.


2019年くらいから細々と自分用に作り始めたツールも,今や5年が経ち,機能も非常に多くなりました.どれくらいかと言うと,以下はGEFのロード画面なのですが,素のgdbにないコマンドが342コマンドも追加されています.エイリアスを含めると342+81=423コマンドです.
※本家gefから存在する機能もあるため,私が独自に作ったコマンドは多分半分程度です.
※サブコマンドも数に含みます.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

ありがたいことに,本ツールの有用性を見出して使ってくれているユーザが,国内・海外問わずそこそこいるみたいです.しかしこんなにコマンドがあると,機能の一部しか使いこなせていないユーザもいると思うので,そういった方にとって新たな発見になれば良いなと思っています.

それでは書いていきます.

contextコマンド

最も目にする頻度が高いであろう,例の画面を提供するコマンドです.

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 →

再表示

この画面は,実態はcontextコマンドとして実装されています.

画面が流れてしまったとき,context(もしくはエイリアスのctx)と打てば再表示させることが出来ます.

SIMDレジスタ表示

レジスタ表示にも小ネタがあります.
以下のケースでは,レジスタ表示箇所に,関連する$xmmNレジスタを自動的に表示します.

  • SSEレジスタ($xmmNみたいなやつ)に関連する命令を実行しようとしている
  • その次の命令を実行しようとしている

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

わざわざxmmコマンドを打たなくても確認できるので,何かと便利です.
AVXレジスタ,MMXレジスタ,FPUレジスタも同様です.

もちろん,こういった特殊なレジスタを「全て」ダンプしたければ,mmxコマンド,sse(/xmm)コマンド,avxコマンド,fpuコマンドを使って下さい.

バックトレース

バックトレースを表示する箇所では,set backtrace past-main onしています.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

これはmainを呼び出すよりも前の,_startまで遡ってバックトレースを表示する,gdbのオプションです.
デフォルトでは以下のようにmainまでしか表示されません.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

ctx off / ctx on

contextコマンドを実行したときの表示を有効化・無効化できます.
例えば画面表示がウザいとき,一時的に消すことができます.

dereferenceコマンド

これも多くの方が使うコマンドでしょう.
gdb-pedaと同じ感覚で使えるよう,telescopeでも同じコマンドが呼び出せます.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

普段使いだとあまり気にしないかもしれませんが,実はオプションが結構用意されています.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

良さげなオプションを紹介していきましょう.

-t/--tagオプション

結果にタグ,つまりメモを付与することができます.
わかっている内容などを整理するのに使えるかもしれません.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

-T/--tag-offsetオプションで,タグのオフセットを一括でずらす事もできます.

-Z/--non-zeroフィルタオプション

ゼロがずっと続く場合は,表示をスキップしたくなることがあるかもしれません.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

-Zオプションでゼロ以外の値だけを表示すれば,目的のデータがスッキリと表示できます.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

他にも以下のオプションがあります.

  • -a/--is-addr: アドレスとして有効な値のみを表示
  • -A/--is-not-addr: アドレスとして無効な値のみを表示
  • -z/--is-zero: ゼロの箇所だけを表示 (大量のゴミデータのうち,区切り・末尾の位置を知りたいなどで使えるかも)

-l/--list-headオプション

カーネルの構造のうち,LIST_HEADによるリンクリストは重要です.
それっぽい構造を自動的に検出して補足表示することができます.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

-s/--slab-containsオプション

同じように,スラブのチャンクかどうかを補足表示する事もできます.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

main-breakコマンド

  • startiコマンドで開始した場合(runコマンドやstartコマンドではありません)
  • qemu-userのgdbスタブに接続した場合

などは,以下のように<_start>で停止します.
これはld.soのエントリポイントで,ユーザランドのバイナリを実行する際の真に最初の位置です.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

このようにmainが呼び出されるよりも前に停止している状態(※)で使えるのがこのコマンドです.

main-breakコマンドを実行すると,mainまで実行してくれます.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

b mainしてrunでいいのでは?と思うかもしれませんが,それが通用するのはバイナリにシンボルがある場合だけです.このコマンドのいいところは,バイナリそれ自体にはシンボルがなくても,glibcのシンボルがあれば良い点です.

本来,シンボルがないバイナリではmainのアドレスは解析しないとわかりません.
IDAやGhidraで解析して例えばオフセットが0x4da0だと判明したら,b *(0x555555554000+0x4da0)みたいにしてブレークポイントを仕掛けないといけません.

main-breakコマンドは,バイナリにシンボルがない場合,__libc_start_mainにブレークポイントを仕掛けて第一引数(=main)を取得し,そのアドレスに再度ブレークポイントを仕掛けてそこまで実行します.このため,解析なしにmainまで自動的に実行してくれることになり,結構便利です.

(※): より正確に書くと,バイナリにシンボルがない場合は,__libc_start_mainが呼ばれるより前でのみ正常動作します.

multi-lineコマンド

ワンライナーでコマンドを書きたいときに便利です.
-ex ...を何個も書く必要がなくなり,実行したいコマンドを;で区切るだけで良くなります.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

;のパースはヒューリスティックにやってるので,実行したい各コマンドそれ自身が;を含む場合は変なところで区切ってしまって誤動作する可能性もあります.
しかしわざわざ;を使うコマンドは殆どないはずです.通常使用の範囲であれば問題ないでしょう.

angrコマンド

バイナリの実行中の状態から,angrが呼び出せたら便利だと思いませんか?

image

angrはとても便利なのですが,いつもテンプレートを忘れてしまって,書き方を調べるところからやるので,大変なんですよね.
自動でスクリプトを作って,セットアップまでしてくれるなら便利かなと思ったので作りました.

このコマンドはざっくり以下のような動きです.

  • 実行中バイナリのメモリマップを全てダンプ
  • 以下を行うスクリプトを書き出す
    • 現在のレジスタと同じ値をangrのエミュレーション初期状態としてロード
    • メモリダンプをangrのエミュレーション用メモリにロード
    • PLTをangrの高速エミュレーション関数に差し替え
      • これがないと不正確な結果になる
    • 以下をコマンド引数から読み取ってセットアップ
      • image
      • find(到達したいアドレス.複数指定可能)
      • avoid(到達したくないアドレス.複数指定可能)
      • sym(シンボリック変数にしたいアドレスとサイズ.複数指定可能)
      • type(symの文字種指定.各sym毎に指定可能)
    • セットアップした内容で探索開始
  • 書き出したスクリプトを実行

結果を得るのに失敗しても,スクリプトはディスク上(/tmp/gef配下)に保存されているので必要なら手動で直せばよいでしょう.
x86, x64, arm, arm64で動作を確認しています.

tlsコマンド

TLSは,スレッドローカルな変数を格納する領域です.

このコマンド自体は,tls周辺のメモリを見るときによく使うと思います.

image

TLSのサポートについて

TLSは,glibcでは多くのアーキテクチャでサポートされています.しかし他のライブラリではサポートされていないこともあります.例えばuClibcではサポートされていません(多分).

このように,非glibc環境ではtlsコマンドが失敗することに注意して下さい.

またglibc環境であっても,バイナリの起動直後(main到達前の話です)にはTLSが初期化されていないタイミングがあります.この初期化が完了するまでは,tlsコマンドが失敗することに注意してください.

アーキテクチャごとのTLSアドレス取得

glibcの場合に限定した話となりますが,tlsのアドレス保持方法はアーキテクチャごとに異なります.

GEFではこれを考慮して,以下のようにtlsのアドレスを取得しています.

  • RISCV64/32: $tpレジスタ
  • ARM: mrc命令でCP15コプロセッサから読み取り
  • ARM64: $TPIDR_EL0レジスタもしくは$tpidrレジスタ
  • x86: $gsレジスタの値をptraceシステムコール経由で取得
  • x86_64: fsレジスタの値をptraceシステムコール経由で取得
  • PPC: $r2レジスタから0x7000を引いた値
  • PPC64: $r13レジスタから0x7000を引いた値
  • SPARC32/64: $g7レジスタ
  • MIPS32/64: rdhwr命令で$29を読み取り0x7000を引いた値
  • S390X: ($acc0 << 32) | $acc1
  • SH4: $gbrレジスタ

面倒くさくなってきたので列挙はこの辺でやめますが,アーキテクチャごとに色々異なる実装をしているのがわかると思います.面白いですね.

-vオプション

TLSはスレッドローカルな変数を格納する領域でした.
当然ですが,使おうと思えばどれだけでも使うことが出来ます.
従ってバイナリがスレッドローカルな変数をどれだけ利用しているかによって,デフォルトの上下16行では表示しきれない変数があったりします.

tls周辺をもっと広げて見たい!という場合に,-vオプションが使えます.
-v, -vv, -vvv, と重ねるたびに16行ずつ表示が増えます.

image

stdio-dumpコマンド

stdinとかstdoutの中身をなるべく見やすくダンプするコマンドです.

image

引数にアドレスを渡せば,そのアドレスを強制的にFILE構造体として解釈してくれます.
FSOP(File-Structure Oriented Programming)のお供にどうぞ.

link-mapコマンド

LinkMapをダンプするコマンドです.

ライブラリが互いにリンクリストで参照しあう構造なので,あるライブラリ(もしくはバイナリ自身)のLinkMapから,別のライブラリのLinkMapを参照することができ,それを辿っています.

image

実は-vオプションをつけると,もっと詳しくダンプできます.
※全ての要素をダンプするわけではありません.構造体前半の有用そうな部分だけです.

image

dynamicコマンド

_DYNAMIC領域をダンプするコマンドです.

image

_DYNAMIC領域にある情報は,タグとセットならばメモリ配置上の順番を変えても問題ありません.
ELFバイナリのゴルフ系問題(ゴルフはショートコーディングのこと.つまり,制約を満たす中でどれだけ小さなELFを作れるかという問題のこと)などで,正しくロードできているかなどを確認する目的で使えるかもしれません.

elf-infoコマンド

gdbserver経由でリモートにあるバイナリをデバッグしているとき,-rオプションが使えます.

手元にバイナリがなくても,裏でgdbserverの機能を使ってバイナリをダウンロードし,elf-infoコマンドを実行してくれます.

同じ機能(-r/--remoteオプション)は以下のコマンドにも実装されています.

  • checksecコマンド
  • dwarf-exception-handlerコマンド
  • dtor-dumpコマンド
  • gotコマンド
  • got-allコマンド

symbolsコマンド

シンボル一覧がほしいと思ったことはありませんか?
maintenance print msymbolsで得られるんですが,コマンドが長すぎてよく忘れてしまいます.

なので,ショートカットの目的で作りました.
一応,メモリマップに対応した色を付けていたり,タイプ別でのフィルタ機能があります.

image

typesコマンド

シンボルの一覧が取れるなら,型の一覧も欲しくなります.ということで作りました.

ソースコードが手元にあったとしても,複雑な#defineマクロを追いかけるのは大変です.
既にビルドされたバイナリがあれば,その型情報を追いかけるほうが楽でしょう.

image

型の大まかな分類や,型名で表示をフィルタすることもできます.

dtコマンド

引数が一つの場合

ptype /oxと同じなのですが,大きすぎる構造体や,多数の構造体を重ね合わせている共用体(union)を含む構造体を表示しようとすると,結果が長くなりすぎる問題があります.

特にカーネル内部の構造体はすごくて,10画面くらいになってしまうこともあります.
これは,構造体のメンバが構造体だった場合に,再帰的にそれらも表示するからです.

トップレベルのメンバだけが知りたい,全体感をざっと把握したい,みたいなときに使えると思います.

image

他にも,以下の機能を持っています.

  • go言語でビルドされたバイナリのシンボル表示時の問題を解決
  • カラーリングを残したままページャを起動
  • 大きすぎる構造体を表示する際,バッファが足りないというエラーが起きないよう,自動的に設定を調整

引数が2つの場合

dtコマンドの第一引数(型)を,第二引数(アドレス)に適用して解釈した結果を表示します.

image

p ((mstate) $rsp)[0]みたいにすればデフォルトのコマンドでも実現できるんですが,すぐ書き方を忘れてしまうので,合わせてdtコマンドに実装しました.

型のメンバを確認したあと,その型として,指定したアドレスを解釈させられると便利だろうと思っています.

hexdump-flexibleコマンド

メンバのサイズも指定できるhexdumpが欲しかったので,作りました.

image

__attribute__((__packed__))属性のついた構造体など,「アライメントされていない構造体」の配列をダンプするケースで便利かと思います.

hashコマンド

よく使われるハッシュ関数と少しレアなハッシュ関数を一括で計算するコマンドです.

image

  • 指定した値のハッシュを計算するモード
  • メモリ中の値のハッシュを計算するモード

の2つがあります.

些細なことですけど,メモリ中のデータのハッシュを求めるには以下のどれかが必要だと思います.

  • データをダンプしてsha1sumなどを叩く
  • データをダンプしてpythonコードなどで計算
  • pi hashlib.sha1(read_memory(0x555555558da0, 10)).hexdigest()のようなコードをgdb上で実行

ハッシュの種類によってはかなり大変なので,これも便利なコマンドです.

crcコマンド

CRC32やその亜種を一括で計算するコマンドです.
これもhashコマンドと同じく2つのモードがあります.

image

CRCはハッシュと違って亜種が多すぎます.
見落としを防ぐためにも,様々なCRCを一括で計算してくれるのは良い機能だと思っています.

base-n-decode/base-n-encodeコマンド

base64やその亜種を一括でデコード/エンコードするコマンドです.
これもhashコマンドやcrcコマンドと同じく2つのモードがあります.

image

baseNも亜種が多いです.
見落としを防ぐためにも,様々なbaseNを一括で計算してくれるのは良い機能だと思っています.

morse-decode/morse-encodeコマンド

一応モールス信号のデコーダ/エンコーダも作ってあります.
これもhashコマンドやcrcコマンドなどと同じく2つのモードがあります.

image

binwalk-memoryコマンド

メモリ中をbinwalkしたいと思ったことはありませんか?
ダンプして別端末でbinwalk,とすれば実現できますが非常に手間がかかります.
私は手軽にbinwalkしたいと思ったので,作りました.

メモリセクション毎にbinwalkを実行します.ただしあくまで結果を閲覧するだけです.
検出したファイルの抽出はしないため,抽出が必要な場合は,必要に応じて手動でメモリをダンプし,別端末でbinwalkをやり直してください.

image

※実はgefのインストーラでは,このためだけにbinwalkをインストールするのですが,binwalkは非常に依存関係が多いツールです.gefのインストーラでaptを呼び出したとき,長々とインストールが行われるのはこいつ(binwalk)のせいです.

filetype-memoryコマンド

メモリ中のデータにfileコマンドを実行したいと思ったことはありませんか?
私はあるので,作りました.

少し前にGoogleが公開したmagikaでも判定するようにしています.

image

sixel-memoryコマンド

convertコマンドで画像をsixel形式にすると,端末に画像をそのまま表示できるようなので,作りました.

やっていることは,メモリをダンプしてsixel化して表示しているだけです.
BMP,PNG,JPGのいずれかの形式の場合,画像のヘッダ等からダンプすべきファイルサイズを自動判定するロジックを作り込んでいたりします.

image

※画像内に何らかのバーコードがある場合,それを読み取るオプションも実装済みです.

peek-pageframeコマンド

tramasysさんがPRしてくれた機能ですが,よくできていたのでそのまま取り込みました.
ある仮想アドレスのPFN(Page Frame Number; 物理アドレスとしてのページインデックス)を求めるときに使えます.

image

指定したPFNに対応するフラグを表示するpeek-pageflagsコマンドもあります.

image

昔と違って今は/proc/$PID/pagemapにアクセス制限がかかっています.
読み取るにはroot権限が必要となっているため,権限昇格などで利用することはできなくなりましたが,ダンプする機能自体は良いものだと思います.

gdtinfoコマンド

名前の通りGDTをダンプするコマンドですが,LDTにも対応しています.

image

LDTのエントリの構造に関しては情報が少なく,実装するのにとても苦労しました.

なお,qemu-systemでカーネルをデバッグしている時にのみ,実際の値をダンプします.
ユーザランドのプログラムを実行しているときは,値の例を表示するのみです.

idtinfoコマンド

x86の割り込みテーブルをダンプするコマンドもあります.

image

qemu-systemのqemuモニタで得られる,IDTレジスタの値を基にメモリをダンプしています.
これも,ユーザランドのプログラムを実行しているときは,値の例を表示するのみです.

カーネルのベースアドレス(kbase)を求めるget_kernel_base()関数では,x86/x64の場合にこのidtinfoの結果を使い,「0除算時のハンドラ(#DE)」のアドレスから,カーネルのベースアドレスを高速に求めています.

visual-heapコマンド

ヒープ内のチャンクを,色を付けてダンプするコマンドです.

image

もともとGEFにはheap XXXコマンド群もありますが,ヒープexploitに慣れている方なら,visual-heapコマンドの方がわかりやすいのではないかと思います.

-dオプション(使用中のチャンクをダークカラーにする)や-sオプション(tcacheやfastbinで使われるprotected fdをデコードした状態で表示する)も使ってみてください.

heapコマンドの-aオプション

ヒープには,main arena以外のthread arenaが存在する場合があります.

image

heap chunksコマンドや,heap binsコマンドでは,対象とするアリーナを-aオプションで指定できます.この時,-aにはアドレスだけでなく,序数も使うことができます.

以下の画像は0x7fffe8000030にあるthread arenaを序数1でも同じ様に指定できることを示しています.

image

わざわざアドレスを指定するのが面倒なときに,ちょっと楽ができます.

heap try-freeコマンド

あるチャンク(アドレス)を解放したときに,エラーが起きるかどうかを判定します.

image

内部的には,以下のように実装されています.

  • call free(address)するコードを,メモリ上にパッチ
  • unicorn-emulateコマンドを使って,(gefと独立したスクリプトで)エミュレーション
  • 途中でエラーが起きるかどうかを判定
  • パッチをもとに戻す

実装の都合で,システムコール呼び出しは全てエラーと判定されてしまうため,free()の内部でシステムコールを呼ぶようなケースでは誤判定するのですが,おそらく稀なので,あまり問題にはならないと思います.

heap try-mallocコマンドも作りました.

onegadgetコマンド

現在利用中のlibc.soに,one_gadgetを適用して結果を表示します.

-sオプションで,その瞬間のメモリ状態に合致する候補のみを表示するよう,フィルタできます.

rpコマンド

いい感じに引数を調整して,外部のrp++を呼び出すコマンドです.

-aオプションで,rp++コマンドを呼び出すときに--allow-branchesオプションを追加できます.
複雑なROPガジェットも表示するようになり,表示される候補が増えます.

誰かの書いたWrite-upを読んで--allow-branchesオプションの存在を知ったので,実装しておきました.

pagewalk riscvコマンド

カーネル関連のコマンドはx86, x64, ARM32, ARM64しか対応していませんが,pagewalkだけはRISC-V (32/64)も対応しています.

ほとんどx64と同じなので,実装コストが低く,サクッと作れました.

pagewalk-with-hintsコマンド

一度はカーネルのメモリマップ全体を見ておいたほうが良い気がしたので,作りました.
カーネルexploitを勉強し始めた時に気になっていた情報が一括で確認できて,非常に便利です.

image

探索対象は以下のとおりです.

image

高位アドレスのメモリ全域を走査するため,実行に時間がかかってしまう(数分程度)のが難点ではあります.
ただしデフォルトで前回結果の再表示を行いますので,同一セッションなら2度目以降はすぐ表示することができます.

使い所はほとんどありませんが,一度は実行してもらいたいイチオシの機能です.

v8コマンド

v8のオブジェクトの情報をダンプしたいときに使えるコマンドです.

image

これは単にcall (void) _v8_internal_Print_Object((void*)(アドレス))を呼び出すことで実現しています.

またあまり知られていませんが,v8のデバッグで有用なコマンドを公式のgdbinitが提供しています.
-lオプションを付けると,このgdbinitをDLし,全てのコマンドをロードします.
https://chromium.googlesource.com/v8/v8/+/refs/heads/main/tools/gdbinit

distanceコマンド

あるアドレスの,ベースからのオフセットを求めたいときに使えます.

image

xinfoコマンドでも同じような結果は得られますが,distanceコマンドの方がより簡潔に表示されます.

saveoコマンド/diffoコマンド

gdb(もしくはgef)のコマンド出力を記録しておき,それらを比較するコマンドです.
出力結果の差分を調べるのが大変だったので作りました.

saveo <好きなコマンド>

して保存してから

diffo list

で保存番号を確認し

diffo git-diff <比較対象1の保存番号> <比較対象2の保存番号>

とすることで,結果をdiffできます.

saveoコマンドは,<好きなコマンド>を実行する際,GEF全体のページャ設定を一時的に無効化して実行しています.従って<好きなコマンド>-nオプションを付ける必要はありません.

xsコマンド

gdbって何で8進数がデフォルトなんでしょうか.

理由はわかりませんが,x/sコマンドで\302とか出力されてもピンと来ません.
\xc2と表示してくれた方が嬉しいので,そのためのコマンドです.

image

pipe (gdbのビルトインコマンド)

gdbやgefのコマンド結果を,シェルコマンドに渡して加工したいと思ったことはありませんか?
これは,コマンドの先頭にpipeまたは|をつければ実現できます.

image

なお,GEFの多くのコマンドはページャを起動してしまうため,パイプ先に結果が渡りません.必要に応じてページャは無効化(-n)しておきましょう.

qemu-user連携

gdb+qemu-userでは,Ctrl+Cが効かないという現象が起きます.

例えばこの様にバイナリをqemu-user配下で起動して,

image

GEFなし(-nx)で接続(-ex 'target remote localhost:1234')してみましょう.

image

cで実行を再開した後,Ctrl+Cを発行しても止まらないのが伝わるでしょうか.
これは非常に面倒なので,GEFではCtrl+Cを受け付けるようにしています.

ただし残念ながらスマートに実現することが出来ず,ちょっとトリッキーなことをしています.

  • gdbのプロセスをfork
  • 子プロセス側で,python上にシグナルハンドラを定義してCtrl+Cを監視
  • 親プロセス側でgdb.execute("continue")で実行再開
  • 子プロセス側でCtrl+Cを検出した時はqemu-userのpidにSIGTRAPを通知して終了
  • 子プロセス側でCtrl+Cを検出することなく親プロセス側が停止(ブレークポイント,例外発生など)した場合は,子プロセスにSIGKILLを通知

何故わざわざforkしているかと言うと,以下の問題を全て解決する必要があったからです.

  1. pythonでシグナルハンドラを定義後に,同一スレッドでgdb.execute("continue")すると,continueコマンドが実行中の間はそのシグナルハンドラが無視される
  2. pythonのシグナルハンドラはメインスレッドでしか定義できない
  3. 非x86アーキでは,glibcのロード前に,非メインスレッドでのgdb.execute("continue")が許可されない

これら全てを解決する唯一思いついた方法が,子プロセスを作ってシグナルを監視することでした.シグナルハンドラを機能させつつgdb.exeute("continue")するには,必ず別スレッドか別プロセスでそれぞれを行わなければならず(条件1),またそれら両方がメインスレッドで実行されなければならない(条件2,3)ので,必然的にforkが必須となる,というロジックです.

作ってみたら,今のところうまく動いているようです.

尚,つい最近までは解決すべき条件3を解決しておらず,スレッドで実装していました.つまり非x86アーキでは,glibcのロード前にcコマンドを使うとgdbがクラッシュしていました.修正前のgefを使っている方はご注意ください.

python-interactiveにおけるdisplayhook

pi <pythonのコード>とすると,GEF上でpythonが実行できます.

image

GEF内で定義されている関数も使えるので,非常に便利です.どんなコマンドがあるのかはFAQを見てください.
※もっと知りたい場合は,gef pyobj-listで使えそうな関数・クラスを見つけて,必要に応じてGEFのソースコードを読んでください.

注目してほしいのは,出力がちゃんとhex化されているところです.つまり10進数の93824992251296ではなく,16進数の0x555555558da0が表示されている点です.
また配列や辞書を表示すると,1行に表示しきれない場合は要素ごとに改行を入れるなどの処理も入っています.

これはsys.displayhookを独自の関数で上書きすることで実現しています.
monkeyhexpprint.pprintの処理を併せ持つようにしたかったので,自前で定義して利用しています.

デフォルトの挙動に戻したい場合は,pi hexoff()で戻せます.再度有効にするにはpi hexon()です.

pageコマンド

page2virt, virt2page, page2phys, phys2pageのベースとなっているコマンドです.

このコマンドがやっていることはpagephysvirtの変換ですので,使うことは難しくはありません.しかしこれは非常に実装が大変なコマンドでした.以下はこれをどのように実装しているかを記載しています.


まずpageからpfnに変換することを考えてみます.
以下のヘルプの関係図を見てください.

image

上半分の図を見てほしいのですが,page構造体というのは,メモリの何処かに配列(struct page[])で存在しています.そしてそのpage構造体の位置する配列インデックスがpfnです.

このことから,pageからpfnの変換に必要なのは,以下の2つであることがわかるでしょう.

  • 配列の先頭アドレス(VMEMMAP_STARTもしくはmem_map)
  • page構造体一つあたりのサイズ

つまり

pfn = (page - 配列の先頭アドレス) // (page構造体一つあたりのサイズ)

という式が成り立っています.

あとはこのpfnを,virtなりphysに変換すればよいでしょう.
12ビッㇳ左シフトすればphysになり,それに物理メモリのダイレクトマップのベースアドレス(PAGE_OFFSET)を足せばvirtになります.
※GEFではLinuxカーネル内の計算に沿って実装しているため,意図的に少し遠回りなことをしていますが,本質的には同じことです.

全く逆の手順を踏めば,virtphysからpageへの変換もできます.


こう書くと簡単に聞こえますが,幾つか問題があります.以下の値を求める必要があるからです.

  1. VMEMMAP(struct page[])
  2. PAGE_OFFSET
  3. page構造体一つあたりのサイズ

それぞれ以下のように解決しました.

VMEMMAPはカーネル4.8未満では固定値でしたが,現在は変動値です.vmemmap_baseというシンボルがある場合はその中身を読み取れば良いですが,ない場合は他の方法で求めなければなりません.この変数を参照する都合の良い関数は無かったので(あればアセンブリをパースして求められたのですが),最終的にslub-dumpの実行結果から最も若いpageアドレスを探し,マスクをかけることで無理やり求める方法に落ち着きました.

PAGE_OFFSETもカーネル4.8未満では固定値でしたが,現在は変動値です.page_offset_baseというシンボルがあればその中身を読み取ればよいですが,ない場合は他の方法で求めなければなりません.この変数を参照する都合の良い関数も見つかりませんでした.最終的にカーネルがマップされるような高位アドレスのうち最も若いアドレスがPAGE_OFFSETっぽいと経験的にわかってきたので,とりあえずそれを採用しています.

page構造体1つ辺りのサイズは,多くの場合64バイトなのですが,ビルドコンフィグによってそれ以上のサイズになることがあります.正攻法ではロジカルに求める方法がないので,slub-dumpの結果を使って,有効なpagevirtのペア(※)から逆算することで求めています.


※補足

SLUBは,各スラブキャッシュに割り当てた領域を「page構造体を使って」保持しています.page構造体に対応した「virtを保持しているわけではない」のです.従って,本来はSLUBに関する構造体をいくら探しても,pagevirtのペアが手に入るわけではありません.

しかしフリーリストに繋がったチャンクはvirtで表現されています.充分な数のチャンクがフリーリストに繋がっているならば,各スラブキャッシュに割り当てた領域(大抵複数ページです)の各ページに散らばっていることが期待できます.

例えばあるスラブキャッシュに3ページ(0x3000バイト)割り当てているとしましょう.まずこのスラブキャッシュのメタデータには,連続した3ページ分割り当てているということと(num_pages),その連続した3ページに対応した最初のpageのアドレスが入手できます.

続いて,頑張ってフリーリストを解析します.フリーリストに繋がった各チャンクの末尾12ビットを無視すれば,それぞれが存在するページがわかります.それが連続した3ページ分あれば,まさしくそのスラブキャッシュに割り当てられた3ページ分の領域であると特定できるわけです.このようにして検出された一番若いページのアドレスが,pageに対応したvirtのアドレスという事になります.

slub-dumpは内部的にこれを行い,virtの情報を画面上に表示しています.

※実際にはパターン2として,「そのスラブキャッシュ内で最も若いチャンクは,ページサイズでアライメントされている」という事実を使って,取り得るページの組が1つしかありえない場合を検出するロジックも組み込んでいます.

image


さて,ここまで書いたことはx86_64の話です.x86, ARM32, ARM64では細部が異なっているため,微修正が必要となります.特にx86ではCONFIG_NUMAynかによって,内部構造が大きく異なります(ヘルプの下半分の図がCONFIG_NUMA=yのときです).

x86_64以外のアーキにおける実装の説明はここでは省略しますが,気になる方はGEFのソースコードを読んでみてください.

ktaskコマンド

非常に巨大なコマンドです.カーネル内のタスク一覧をダンプします.

image

ヘルプを見てもらうとわかりますが,オプションをつけると以下の情報もダンプできます.

  • ユーザプロセスのメモリマップ
  • カーネルスタックへの保存済みレジスタ
  • uid系
  • スレッド
  • オープンしているファイルディスクリプタ
  • シグナルハンドラ
  • seccompフィルタ
  • 名前空間

image

GEFの実装では,シンボルや型情報無しで(力技で),これらを保持する構造体(task_struct)のメンバのうち,必要なメンバのオフセットを判別しています.

そこから情報をたどるときに必要な各構造体も同様に対応しています.rbtreexarrayMaple Treeのパースなどは面倒くさかったですが,なんとかなりました.

pwnに必要な情報はほぼ揃っていると思うので,色々遊んでみてください.

buddy-dumpコマンド

スラブアロケータよりも下位に位置する,ページアロケータ(バディシステム)をダンプするコマンドです.
最近流行りのクロスキャッシュ攻撃などで,使うことがあるかもしれません.

image

PCP(PerCPU-Page)に関してもダンプ対象に含まれています.

vmalloc-dumpコマンド

スラブアロケータとページアロケータのダンプコマンドがあるならば,vmallocアロケータをダンプするコマンドも作らなければなりません.ということで作りました.

image

お願い

機能拡張の勢いに陰りが見えてきました(=ネタ切れ).
こんな機能があればなーというアイデアがあれば,ぜひぜひissueを立ててください.日本語でもOKです.

終わりに

2022年の記事に続き,bata24/gefで実装した機能を色々と紹介してきました.

本ツールを使っている方で,しばらく更新してないなーと思った方は,更新してみてください.
※コンフィグの形式が変わっていたり,必要なパッケージが増えていたりするので,単純なアップグレードだと新しい機能が動かない可能性があります.一度アンインストールして,インストーラを再実行するなどしてみてください.
※少し前からvenv版のインストーラも用意しています.ぜひ使ってみてください(使い方はFAQに書いてあります).

バグ報告,フィードバック,機能リクエストなどもお待ちしています.


7日目の記事は,hama(@hama7230)さんの「Automotive CTFのこと」です.

私も同じチームで出場させて頂いたCTFです.新鮮な部分も多く楽しかったです.
記事の公開を楽しみに待ちましょう.