# bata24/gefの機能紹介とか 2024
###### tags: `gef` `gdb` `ctf`
# はじめに
この記事は,[CTF Advent Calendar 2024](https://adventar.org/calendars/10469) の6日目の記事です.
5日目は[Ark(@arkark_)](https://x.com/arkark_)さんの「[about:blank テクニック."disconnection"の解説?](https://)」でした.
※まだ投稿されていないみたいなので、投稿され次第URLを差し替えます.
本記事は,2022年の記事「[bata24/gefの機能紹介とか](https://hackmd.io/@bata24/SycIO4qPi)」の続きみたいなものです.
但し,[bata24/gef](https://github.com/bata24/gef)の特長というか目玉機能は,その2022年の記事でほとんど紹介してしまっています.
そこで本記事では「使用頻度は高くないだろうけれど,そこそこ使えると思う機能の紹介」や「それに関連した話」などをします.
※カーネル関連の話は殆どありません,ご注意ください.
# タイトルのbata24/gefって何?
gdb拡張スクリプト[GEF](https://github.com/hugsy/gef)のfork版で,https://github.com/bata24/gef のことです.
主にCTFer向けに機能拡張しています.
詳しく知りたい方は,過去のCTF Advent Calendarにも記事を投稿しているので,参照してみてください.
- CTF Advent Calendar 2020: [gefを改造した話](https://hackmd.io/@bata24/rJVtBJsrP)
- CTF Advent Calendar 2022: [bata24/gefの機能紹介とか](https://hackmd.io/@bata24/SycIO4qPi)
----
2019年くらいから細々と自分用に作り始めたツールも,今や5年が経ち,機能も非常に多くなりました.どれくらいかと言うと,以下はGEFのロード画面なのですが,素のgdbにないコマンドが341コマンドも追加されています.エイリアスを含めると341+90=431コマンドです.
※本家gefから存在する機能もあるため,私が独自に作ったコマンドは多分半分程度です.
※サブコマンドも数に含みます.
![image](https://hackmd.io/_uploads/rkDV2jLVJx.png)
ありがたいことに,本ツールの有用性を見出して使ってくれているユーザが,国内・海外問わずそこそこいるみたいです.しかしこんなにコマンドがあると,機能の一部しか使いこなせていないユーザもいると思うので,そういった方にとって新たな発見になれば良いなと思っています.
それでは書いていきます.
# `context`コマンド
最も目にする頻度が高いであろう,例の画面を提供するコマンドです.
![](https://raw.githubusercontent.com/bata24/gef/dev/images/context.png)
## 再表示
この画面は,実態は`context`コマンドとして実装されています.
画面が流れてしまったとき,`context`(もしくはエイリアスの`ctx`)と打てば再表示させることが出来ます.
## SIMDレジスタ表示
レジスタ表示にも小ネタがあります.
以下のケースでは,レジスタ表示箇所に,関連する`$xmmN`レジスタを自動的に表示します.
- SSEレジスタ(`$xmmN`みたいなやつ)に関連する命令を実行しようとしている
- その次の命令を実行しようとしている
![image](https://hackmd.io/_uploads/SyLVJh5Xyl.png)
わざわざ`xmm`コマンドを打たなくても確認できるので,何かと便利です.
AVXレジスタ,MMXレジスタ,FPUレジスタも同様です.
もちろん,こういった特殊なレジスタを「全て」ダンプしたければ,`mmx`コマンド,`sse`(/`xmm`)コマンド,`avx`コマンド,`fpu`コマンドを使って下さい.
## バックトレース
バックトレースを表示する箇所では,`set backtrace past-main on`しています.
![image](https://hackmd.io/_uploads/B1yjk3qm1g.png)
これは`main`を呼び出すよりも前の,`_start`まで遡ってバックトレースを表示する,gdbのオプションです.
デフォルトでは以下のように`main`までしか表示されません.
![image](https://hackmd.io/_uploads/S1VLZSj7kg.png)
## `ctx off` / `ctx on`
`context`コマンドを実行したときの表示を有効化・無効化できます.
例えば画面表示がウザいとき,一時的に消すことができます.
# `dereference`コマンド
これも多くの方が使うコマンドでしょう.
gdb-pedaと同じ感覚で使えるよう,`telescope`でも同じコマンドが呼び出せます.
![image](https://hackmd.io/_uploads/SkoYmBiQ1l.png)
普段使いだとあまり気にしないかもしれませんが,実はオプションが結構用意されています.
![image](https://hackmd.io/_uploads/BkdvZAeN1e.png)
良さげなオプションを紹介していきましょう.
## `-t`/`--tag`オプション
結果にタグ,つまりメモを付与することができます.
わかっている内容などを整理するのに使えるかもしれません.
![image](https://hackmd.io/_uploads/H1inFHsX1x.png)
`-T`/`--tag-offset`オプションで,タグのオフセットを一括でずらす事もできます.
## `-Z`/`--non-zero`フィルタオプション
ゼロがずっと続く場合は,表示をスキップしたくなることがあるかもしれません.
![image](https://hackmd.io/_uploads/H17YqSj7kx.png)
`-Z`オプションでゼロ以外の値だけを表示すれば,目的のデータがスッキリと表示できます.
![image](https://hackmd.io/_uploads/rk135HimJx.png)
他にも以下のオプションがあります.
- `-a`/`--is-addr`: アドレスとして有効な値のみを表示
- `-A`/`--is-not-addr`: アドレスとして無効な値のみを表示
- `-z`/`--is-zero`: ゼロの箇所だけを表示 (大量のゴミデータのうち,区切り・末尾の位置を知りたいなどで使えるかも)
## `-l`/`--list-head`オプション
カーネルの構造のうち,`LIST_HEAD`によるリンクリストは重要です.
それっぽい構造を自動的に検出して補足表示することができます.
![image](https://hackmd.io/_uploads/Sk-woSoQyg.png)
## `-s`/`--slab-contains`オプション
同じように,スラブのチャンクかどうかを補足表示する事もできます.
![image](https://hackmd.io/_uploads/HkarnBiXyl.png)
# `main-break`コマンド
- `starti`コマンドで開始した場合(`run`コマンドや`start`コマンドではありません)
- qemu-userのgdbスタブに接続した場合
などは,以下のように`<_start>`で停止します.
これは`ld.so`のエントリポイントで,ユーザランドのバイナリを実行する際の真に最初の位置です.
![image](https://hackmd.io/_uploads/Hy_tWe0mJg.png)
このように`main`が呼び出されるよりも前に停止している状態(※)で使えるのがこのコマンドです.
`main-break`コマンドを実行すると,`main`まで実行してくれます.
![image](https://hackmd.io/_uploads/ByosbgCQ1g.png)
`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](https://hackmd.io/_uploads/r17dEZ2Xyg.png)
尚`;`のパースはヒューリスティックにやってるので,実行したい各コマンドそれ自身が`;`を含む場合は変なところで区切ってしまって誤動作する可能性もあります.
しかしわざわざ`;`を使うコマンドは殆どないはずです.通常使用の範囲であれば問題ないでしょう.
# `angr`コマンド
バイナリの実行中の状態から,`angr`が呼び出せたら便利だと思いませんか?
![image](https://hackmd.io/_uploads/BJuiXlCXyx.png)
`angr`はとても便利なのですが,いつもテンプレートを忘れてしまって,書き方を調べるところからやるので,大変なんですよね.
自動でスクリプトを作って,セットアップまでしてくれるなら便利かなと思ったので作りました.
このコマンドはざっくり以下のような動きです.
- 実行中バイナリのメモリマップを全てダンプ
- 以下を行うスクリプトを書き出す
- 現在のレジスタと同じ値を`angr`のエミュレーション初期状態としてロード
- メモリダンプを`angr`のエミュレーション用メモリにロード
- PLTをangrの高速エミュレーション関数に差し替え
- これがないと不正確な結果になる
- 以下をコマンド引数から読み取ってセットアップ
- ![image](https://hackmd.io/_uploads/H19q7x0mye.png)
- `find`(到達したいアドレス.複数指定可能)
- `avoid`(到達したくないアドレス.複数指定可能)
- `sym`(シンボリック変数にしたいアドレスとサイズ.複数指定可能)
- `type`(`sym`の文字種指定.各`sym`毎に指定可能)
- セットアップした内容で探索開始
- 書き出したスクリプトを実行
結果を得るのに失敗しても,スクリプトはディスク上(`/tmp/gef`配下)に保存されているので必要なら手動で直せばよいでしょう.
x86, x64, arm, arm64で動作を確認しています.
# `tls`コマンド
TLSは,スレッドローカルな変数を格納する領域です.
このコマンド自体は,`tls`周辺のメモリを見るときによく使うと思います.
![image](https://hackmd.io/_uploads/HyLkLl0QJl.png)
## 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](https://hackmd.io/_uploads/r1evUg0myx.png)
# `stdio-dump`コマンド
`stdin`とか`stdout`の中身をなるべく見やすくダンプするコマンドです.
![image](https://hackmd.io/_uploads/SkItwe0mke.png)
引数にアドレスを渡せば,そのアドレスを強制的に`FILE`構造体として解釈してくれます.
FSOP(File-Structure Oriented Programming)のお供にどうぞ.
# `link-map`コマンド
LinkMapをダンプするコマンドです.
ライブラリが互いにリンクリストで参照しあう構造なので,あるライブラリ(もしくはバイナリ自身)のLinkMapから,別のライブラリのLinkMapを参照することができ,それを辿っています.
![image](https://hackmd.io/_uploads/S14QOlAXyx.png)
実は`-v`オプションをつけると,もっと詳しくダンプできます.
※全ての要素をダンプするわけではありません.構造体前半の有用そうな部分だけです.
![image](https://hackmd.io/_uploads/ryRUOxRmye.png)
# `dynamic`コマンド
`_DYNAMIC`領域をダンプするコマンドです.
![image](https://hackmd.io/_uploads/ByRrKx0Xkl.png)
`_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](https://hackmd.io/_uploads/SykGpxCXJe.png)
# `types`コマンド
シンボルの一覧が取れるなら,型の一覧も欲しくなります.ということで作りました.
ソースコードが手元にあったとしても,複雑な`#define`マクロを追いかけるのは大変です.
既にビルドされたバイナリがあれば,その型情報を追いかけるほうが楽でしょう.
![image](https://hackmd.io/_uploads/BkSwk-RQJg.png)
型の大まかな分類や,型名で表示をフィルタすることもできます.
# `dt`コマンド
## 引数が一つの場合
`ptype /ox`と同じなのですが,大きすぎる構造体や,多数の構造体を重ね合わせている共用体(`union`)を含む構造体を表示しようとすると,結果が長くなりすぎる問題があります.
特にカーネル内部の構造体はすごくて,10画面くらいになってしまうこともあります.
これは,構造体のメンバが構造体だった場合に,再帰的にそれらも表示するからです.
トップレベルのメンバだけが知りたい,全体感をざっと把握したい,みたいなときに使えると思います.
![image](https://hackmd.io/_uploads/HkkgAg0myl.png)
他にも,以下の機能を持っています.
- go言語でビルドされたバイナリのシンボル表示時の問題を解決
- カラーリングを残したままページャを起動
- 大きすぎる構造体を表示する際,バッファが足りないというエラーが起きないよう,自動的に設定を調整
## 引数が2つの場合
`dt`コマンドの第一引数(型)を,第二引数(アドレス)に適用して解釈した結果を表示します.
![image](https://hackmd.io/_uploads/ry8518yNkl.png)
`p ((mstate) $rsp)[0]`みたいにすればデフォルトのコマンドでも実現できるんですが,すぐ書き方を忘れてしまうので,合わせて`dt`コマンドに実装しました.
型のメンバを確認したあと,その型として,指定したアドレスを解釈させられると便利だろうと思っています.
# `hexdump-flexible`コマンド
メンバのサイズも指定できる`hexdump`が欲しかったので,作りました.
![image](https://hackmd.io/_uploads/ByGeZbR7kx.png)
`__attribute__((__packed__))`属性のついた構造体など,「アライメントされていない構造体」の配列をダンプするケースで便利かと思います.
# `hash`コマンド
よく使われるハッシュ関数と少しレアなハッシュ関数を一括で計算するコマンドです.
![image](https://hackmd.io/_uploads/HkAbFX4V1l.png)
- 指定した値のハッシュを計算するモード
- メモリ中の値のハッシュを計算するモード
の2つがあります.
些細なことですけど,メモリ中のデータのハッシュを求めるには以下のどれかが必要だと思います.
- データをダンプして`sha1sum`などを叩く
- データをダンプしてpythonコードなどで計算
- `pi hashlib.sha1(read_memory(0x555555558da0, 10)).hexdigest()`のようなコードをgdb上で実行
ハッシュの種類によってはかなり大変なので,これも便利なコマンドです.
# `crc`コマンド
CRC32やその亜種を一括で計算するコマンドです.
これも`hash`コマンドと同じく2つのモードがあります.
![image](https://hackmd.io/_uploads/SyK1X-RXyx.png)
CRCはハッシュと違って亜種が多すぎます.
見落としを防ぐためにも,様々なCRCを一括で計算してくれるのは良い機能だと思っています.
# `base-n-decode`/`base-n-encode`コマンド
base64やその亜種を一括でデコード/エンコードするコマンドです.
これも`hash`コマンドや`crc`コマンドと同じく2つのモードがあります.
![image](https://hackmd.io/_uploads/r1-qG-Cmkl.png)
baseNも亜種が多いです.
見落としを防ぐためにも,様々なbaseNを一括で計算してくれるのは良い機能だと思っています.
# `morse-decode`/`morse-encode`コマンド
一応モールス信号のデコーダ/エンコーダも作ってあります.
これも`hash`コマンドや`crc`コマンドなどと同じく2つのモードがあります.
![image](https://hackmd.io/_uploads/rko2X-07Jx.png)
# `binwalk-memory`コマンド
メモリ中を`binwalk`したいと思ったことはありませんか?
ダンプして別端末で`binwalk`,とすれば実現できますが非常に手間がかかります.
私は手軽に`binwalk`したいと思ったので,作りました.
メモリセクション毎に`binwalk`を実行します.ただしあくまで結果を閲覧するだけです.
検出したファイルの抽出はしないため,抽出が必要な場合は,必要に応じて手動でメモリをダンプし,別端末で`binwalk`をやり直してください.
![image](https://hackmd.io/_uploads/SJMzNbRmkx.png)
# `filetype-memory`コマンド
メモリ中のデータに`file`コマンドを実行したいと思ったことはありませんか?
私はあるので,作りました.
少し前にGoogleが公開した`magika`でも判定するようにしています.
![image](https://hackmd.io/_uploads/BJVJrZC7yl.png)
# `sixel-memory`コマンド
`convert`コマンドで画像を`sixel`形式にすると,端末に画像をそのまま表示できるようなので,作りました.
やっていることは,メモリをダンプして`sixel`化して表示しているだけです.
BMP,PNG,JPGのいずれかの形式の場合,画像のヘッダ等からダンプすべきファイルサイズを自動判定するロジックを作り込んでいたりします.
![image](https://hackmd.io/_uploads/rJ1fS-CQyx.png)
# `peek-pageframe`コマンド
tramasysさんがPRしてくれた機能ですが,よくできていたのでそのまま取り込みました.
ある仮想アドレスのPFN(Page Frame Number; 物理アドレスとしてのページインデックス)を求めるときに使えます.
![image](https://hackmd.io/_uploads/B1BnHbAX1g.png)
指定したPFNに対応するフラグを表示する`peek-pageflags`コマンドもあります.
![image](https://hackmd.io/_uploads/SyqcLbCXke.png)
昔と違って今は`/proc/$PID/pagemap`にアクセス制限がかかっています.
読み取るには`root`権限が必要となっているため,権限昇格などで利用することはできなくなりましたが,ダンプする機能自体は良いものだと思います.
# `gdtinfo`コマンド
名前の通りGDTをダンプするコマンドですが,LDTにも対応しています.
![image](https://hackmd.io/_uploads/r19WvbAQJe.png)
LDTのエントリの構造に関しては情報が少なく,実装するのにとても苦労しました.
なお,qemu-systemでカーネルをデバッグしている時にのみ,実際の値をダンプします.
ユーザランドのプログラムを実行しているときは,値の例を表示するのみです.
# `idtinfo`コマンド
x86の割り込みテーブルをダンプするコマンドもあります.
![image](https://hackmd.io/_uploads/Skeqw-0X1e.png)
qemu-systemのqemuモニタで得られる,IDTレジスタの値を基にメモリをダンプしています.
これも,ユーザランドのプログラムを実行しているときは,値の例を表示するのみです.
カーネルのベースアドレス(`kbase`)を求める`get_kernel_base()`関数では,x86/x64の場合にこの`idtinfo`の結果を使い,「0除算時のハンドラ(`#DE`)」のアドレスから,カーネルのベースアドレスを高速に求めています.
# `visual-heap`コマンド
ヒープ内のチャンクを,色を付けてダンプするコマンドです.
![image](https://hackmd.io/_uploads/HybycWRQkg.png)
もともとGEFには`heap XXX`コマンド群もありますが,ヒープexploitに慣れている方なら,`visual-heap`コマンドの方がわかりやすいのではないかと思います.
`-d`オプション(使用中のチャンクをダークカラーにする)や`-s`オプション(tcacheやfastbinで使われるprotected fdをデコードした状態で表示する)も使ってみてください.
# `heap`コマンドの`-a`オプション
ヒープには,main arena以外のthread arenaが存在する場合があります.
![image](https://hackmd.io/_uploads/BkNexMCXJg.png)
`heap chunks`コマンドや,`heap bins`コマンドでは,対象とするアリーナを`-a`オプションで指定できます.この時,`-a`にはアドレスだけでなく,序数も使うことができます.
以下の画像は`0x7fffe8000030`にあるthread arenaを序数`1`でも同じ様に指定できることを示しています.
![image](https://hackmd.io/_uploads/SkuugfCQJl.png)
わざわざアドレスを指定するのが面倒なときに,ちょっと楽ができます.
# `heap try-free`コマンド
あるチャンク(アドレス)を解放したときに,エラーが起きるかどうかを判定します.
![image](https://hackmd.io/_uploads/H1Akfz0mJx.png)
内部的には,以下のように実装されています.
- `call free(address)`するコードを,メモリ上にパッチ
- `unicorn-emulate`コマンドを使って,(gefと独立したスクリプトで)エミュレーション
- 途中でエラーが起きるかどうかを判定
- パッチをもとに戻す
実装の都合で,システムコール呼び出しは全てエラーと判定されてしまうため,`free()`の内部でシステムコールを呼ぶようなケースでは誤判定するのですが,おそらく稀なので,あまり問題にはならないと思います.
# `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](https://hackmd.io/_uploads/HJcPMGA7kg.png)
探索対象は以下のとおりです.
![image](https://hackmd.io/_uploads/Bkprl907yx.png)
高位アドレスのメモリ全域を走査するため,実行に時間がかかってしまう(数分程度)のが難点ですが,前回結果の再表示は`-c`オプションですぐ見ることができます.
使い所はほとんどありませんが,一度は実行してもらいたいイチオシの機能です.
# `v8`コマンド
v8のオブジェクトの情報をダンプしたいときに使えるコマンドです.
![image](https://hackmd.io/_uploads/rJqbQzCQke.png)
これは単に`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](https://hackmd.io/_uploads/S1tyNGR7yl.png)
`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](https://hackmd.io/_uploads/r1fxo2AQyl.png)
# pipe (gdbのビルトインコマンド)
gdbやgefのコマンド結果を,シェルコマンドに渡して加工したいと思ったことはありませんか?
これは,コマンドの先頭に`pipe`または`|`をつければ実現できます.
![image](https://hackmd.io/_uploads/S1KuNqAmyx.png)
なお,GEFの多くのコマンドはページャを起動してしまうため,パイプ先に結果が渡りません.必要に応じてページャは無効化(`-n`)しておきましょう.
# qemu-user連携
gdb+qemu-userでは,`Ctrl+C`が効かないという現象が起きます.
例えばこの様にバイナリをqemu-user配下で起動して,
![image](https://hackmd.io/_uploads/HJkrL207Jx.png)
GEFなし(`-nx`)で接続(`-ex 'target remote localhost:1234'`)してみましょう.
![image](https://hackmd.io/_uploads/BkmcLhC7kl.png)
`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](https://hackmd.io/_uploads/Hkkdo2C7kx.png)
GEF内で定義されている関数も使えるので,非常に便利です.どんなコマンドがあるのかは[FAQ](https://github.com/bata24/gef/blob/dev/docs/FAQ.md#about-python-interface)を見てください.
※もっと知りたい場合は,`gef pyobj-list`で使えそうな関数・クラスを見つけて,必要に応じてGEFのソースコードを読んでください.
注目してほしいのは,出力がちゃんとhex化されているところです.つまり10進数の`93824992251296`ではなく,16進数の`0x555555558da0`が表示されている点です.
また配列や辞書を表示すると,1行に表示しきれない場合は要素ごとに改行を入れるなどの処理も入っています.
これは`sys.displayhook`を独自の関数で上書きすることで実現しています.
`monkeyhex`と`pprint.pprint`の処理を併せ持つようにしたかったので,自前で定義して利用しています.
デフォルトの挙動に戻したい場合は,`pi hexoff()`で戻せます.再度有効にするには`pi hexon()`です.
# `page`コマンド
`page2virt`, `virt2page`, `page2phys`, `phys2page`のベースとなっているコマンドです.
このコマンドがやっていることは`page`と`phys`や`virt`の変換ですので,使うことは難しくはありません.
しかしこれは非常に実装が大変なコマンドでした.以下はこれをどのように実装しているかを記載しています.
----
まず`page`から`pfn`に変換することを考えてみます.
以下のヘルプの関係図を見てください.
![image](https://hackmd.io/_uploads/r1X1Fyy41e.png)
上半分の図を見てほしいのですが,`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カーネル内の計算に沿って実装しているため,意図的に少し遠回りなことをしていますが,本質的には同じことです.
全く逆の手順を踏めば,`virt`や`phys`から`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`の結果を使って,有効な`page`と`virt`のペアから逆算することで求めています.
※補足
SLUBは,各スラブキャッシュに割り当てた領域を`page`構造体を使って保持しています.その`page`構造体に対応した`virt`を保持しているわけではありません.従って,本来はSLUBに関する構造体をいくら探しても,`page`と`virt`のペアが手に入るわけではありません.
しかしフリーリストに繋がったチャンクは`virt`で表現されています.充分な数のチャンクがフリーリストに繋がっているならば,各スラブキャッシュに割り当てた領域(大抵複数ページです)の各ページに散らばっていることが期待できます.
例えばあるスラブキャッシュに3ページ(`0x3000`バイト)割り当てているとしましょう.フリーリストに繋がった各チャンクの末尾12ビットを無視すれば,それぞれが存在するページがわかります.それが連続した3ページ分あれば,まさしくそのスラブキャッシュに割り当てられた3ページ分の領域であると特定できるわけです.このようにして検出された一番若いページのアドレスが,`page`に対応した`virt`のアドレスという事になります.
`slub-dump`は内部的にこれを行い,`virt`の情報を画面上に表示しています.
※実際にはパターン2として,「そのスラブキャッシュ内で最も若いチャンクは,ページサイズでアライメントされている」という事実を使って,取り得るページの組が1つしかありえない場合を検出するロジックも組み込んでいます.
![image](https://hackmd.io/_uploads/By_iG0eNJg.png)
----
さて,ここまで書いたことは`x86_64`の話です.`x86`, `ARM32`, `ARM64`では細部が異なっているため,微修正が必要となります.特に`x86`では`CONFIG_NUMA`が`y`か`n`かによって,内部構造が大きく異なります(ヘルプの下半分の図が`CONFIG_NUMA=y`のときです).
`x86_64`以外のアーキにおける実装の説明はここでは省略しますが,気になる方はGEFのソースコードを読んでみてください.
# `ktask`コマンド
非常に巨大なコマンドです.カーネル内のタスク一覧をダンプします.
![image](https://hackmd.io/_uploads/BJPY-fkEJl.png)
ヘルプを見てもらうとわかりますが,オプションをつけると以下の情報もダンプできます.
- ユーザプロセスのメモリマップ
- カーネルスタックへの保存済みレジスタ
- uid系
- スレッド
- オープンしているファイルディスクリプタ
- シグナルハンドラ
- seccompフィルタ
- 名前空間
![image](https://hackmd.io/_uploads/HyWLjvmNkg.png)
pwnに必要な情報はほぼ揃っていると思うので,色々遊んでみてください.
# `buddy-dump`コマンド
スラブアロケータよりも下位に位置する,ページアロケータ(バディシステム)をダンプするコマンドです.
最近流行りのクロスキャッシュ攻撃などで,使うことがあるかもしれません.
![image](https://hackmd.io/_uploads/r15AmMJEJe.png)
PCP(PerCPU-Page)に関してもダンプ対象に含まれています.
# `vmalloc-dump`コマンド
スラブアロケータとページアロケータのダンプコマンドがあるならば,`vmalloc`アロケータをダンプするコマンドも作らなければなりません.ということで作りました.
![image](https://hackmd.io/_uploads/HkKZNGyNke.png)
# お願い
機能拡張の勢いに陰りが見えてきました(=ネタ切れ).
こんな機能があればなーというアイデアがあれば,ぜひぜひissueを立ててください.日本語でもOKです.
# 終わりに
2022年の記事に続き,bata24/gefで実装した機能を色々と紹介してきました.
本ツールを使っている方で,しばらく更新してないなーと思った方は,更新してみてください.
※コンフィグの形式が変わっていたり,必要なパッケージが増えていたりするので,単純なアップグレードだと新しい機能が動かない可能性があります.一度アンインストールして,インストーラを再実行するなどしてみてください.
※少し前から`venv`版のインストーラも用意しています.ぜひ使ってみてください(使い方は[FAQ](https://github.com/bata24/gef/blob/dev/docs/FAQ.md)に書いてあります).
バグ報告,フィードバック,機能リクエストなどもお待ちしています.
----
7日目の記事は,[hama(@hama7230)](https://x.com/hama7230)さんの「[Automotive CTFのこと](https://amenable-muscari-82a.notion.site/Automotive-CTF-15554c2c788a80609e26cd9fb1686a46)」です.
私も同じチームで出場させて頂いたCTFです.新鮮な部分も多く楽しかったです.
記事の公開を楽しみに待ちましょう.