gef
gdb
ctf
この記事は,CTF Advent Calendar 2024 の6日目の記事です.
5日目はArk(@arkark_)さんの「about:blank テクニック."disconnection"の解説?」でした.
※まだ投稿されていないみたいなので、投稿され次第URLを差し替えます.
本記事は,2022年の記事「bata24/gefの機能紹介とか」の続きみたいなものです.
但し,bata24/gefの特長というか目玉機能は,その2022年の記事でほとんど紹介してしまっています.
そこで本記事では「使用頻度は高くないだろうけれど,そこそこ使えると思う機能の紹介」や「それに関連した話」などをします.
※カーネル関連の話は殆どありません,ご注意ください.
gdb拡張スクリプトGEFのfork版で,https://github.com/bata24/gef のことです.
主にCTFer向けに機能拡張しています.
詳しく知りたい方は,過去のCTF Advent Calendarにも記事を投稿しているので,参照してみてください.
2019年くらいから細々と自分用に作り始めたツールも,今や5年が経ち,機能も非常に多くなりました.どれくらいかと言うと,以下はGEFのロード画面なのですが,素のgdbにないコマンドが342コマンドも追加されています.エイリアスを含めると342+81=423コマンドです.
※本家gefから存在する機能もあるため,私が独自に作ったコマンドは多分半分程度です.
※サブコマンドも数に含みます.
ありがたいことに,本ツールの有用性を見出して使ってくれているユーザが,国内・海外問わずそこそこいるみたいです.しかしこんなにコマンドがあると,機能の一部しか使いこなせていないユーザもいると思うので,そういった方にとって新たな発見になれば良いなと思っています.
それでは書いていきます.
context
コマンド最も目にする頻度が高いであろう,例の画面を提供するコマンドです.
この画面は,実態はcontext
コマンドとして実装されています.
画面が流れてしまったとき,context
(もしくはエイリアスのctx
)と打てば再表示させることが出来ます.
レジスタ表示にも小ネタがあります.
以下のケースでは,レジスタ表示箇所に,関連する$xmmN
レジスタを自動的に表示します.
$xmmN
みたいなやつ)に関連する命令を実行しようとしているわざわざxmm
コマンドを打たなくても確認できるので,何かと便利です.
AVXレジスタ,MMXレジスタ,FPUレジスタも同様です.
もちろん,こういった特殊なレジスタを「全て」ダンプしたければ,mmx
コマンド,sse
(/xmm
)コマンド,avx
コマンド,fpu
コマンドを使って下さい.
バックトレースを表示する箇所では,set backtrace past-main on
しています.
これはmain
を呼び出すよりも前の,_start
まで遡ってバックトレースを表示する,gdbのオプションです.
デフォルトでは以下のようにmain
までしか表示されません.
ctx off
/ ctx on
context
コマンドを実行したときの表示を有効化・無効化できます.
例えば画面表示がウザいとき,一時的に消すことができます.
dereference
コマンドこれも多くの方が使うコマンドでしょう.
gdb-pedaと同じ感覚で使えるよう,telescope
でも同じコマンドが呼び出せます.
普段使いだとあまり気にしないかもしれませんが,実はオプションが結構用意されています.
良さげなオプションを紹介していきましょう.
-t
/--tag
オプション結果にタグ,つまりメモを付与することができます.
わかっている内容などを整理するのに使えるかもしれません.
-T
/--tag-offset
オプションで,タグのオフセットを一括でずらす事もできます.
-Z
/--non-zero
フィルタオプションゼロがずっと続く場合は,表示をスキップしたくなることがあるかもしれません.
-Z
オプションでゼロ以外の値だけを表示すれば,目的のデータがスッキリと表示できます.
他にも以下のオプションがあります.
-a
/--is-addr
: アドレスとして有効な値のみを表示-A
/--is-not-addr
: アドレスとして無効な値のみを表示-z
/--is-zero
: ゼロの箇所だけを表示 (大量のゴミデータのうち,区切り・末尾の位置を知りたいなどで使えるかも)-l
/--list-head
オプションカーネルの構造のうち,LIST_HEAD
によるリンクリストは重要です.
それっぽい構造を自動的に検出して補足表示することができます.
-s
/--slab-contains
オプション同じように,スラブのチャンクかどうかを補足表示する事もできます.
main-break
コマンドstarti
コマンドで開始した場合(run
コマンドやstart
コマンドではありません)などは,以下のように<_start>
で停止します.
これはld.so
のエントリポイントで,ユーザランドのバイナリを実行する際の真に最初の位置です.
このようにmain
が呼び出されるよりも前に停止している状態(※)で使えるのがこのコマンドです.
main-break
コマンドを実行すると,main
まで実行してくれます.
b main
してrun
でいいのでは?と思うかもしれませんが,それが通用するのはバイナリにシンボルがある場合だけです.このコマンドのいいところは,バイナリそれ自体にはシンボルがなくても,glibcのシンボルがあれば良い点です.
本来,シンボルがないバイナリではmain
のアドレスは解析しないとわかりません.
IDAやGhidraで解析して例えばオフセットが0x4da0
だと判明したら,b *(0x555555554000+0x4da0)
みたいにしてブレークポイントを仕掛けないといけません.
main-break
コマンドは,バイナリにシンボルがない場合,__libc_start_main
にブレークポイントを仕掛けて第一引数(=main
)を取得し,そのアドレスに再度ブレークポイントを仕掛けてそこまで実行します.このため,解析なしにmain
まで自動的に実行してくれることになり,結構便利です.
(※): より正確に書くと,バイナリにシンボルがない場合は,__libc_start_main
が呼ばれるより前でのみ正常動作します.
multi-line
コマンドワンライナーでコマンドを書きたいときに便利です.
-ex ...
を何個も書く必要がなくなり,実行したいコマンドを;
で区切るだけで良くなります.
尚;
のパースはヒューリスティックにやってるので,実行したい各コマンドそれ自身が;
を含む場合は変なところで区切ってしまって誤動作する可能性もあります.
しかしわざわざ;
を使うコマンドは殆どないはずです.通常使用の範囲であれば問題ないでしょう.
angr
コマンドバイナリの実行中の状態から,angr
が呼び出せたら便利だと思いませんか?
angr
はとても便利なのですが,いつもテンプレートを忘れてしまって,書き方を調べるところからやるので,大変なんですよね.
自動でスクリプトを作って,セットアップまでしてくれるなら便利かなと思ったので作りました.
このコマンドはざっくり以下のような動きです.
angr
のエミュレーション初期状態としてロードangr
のエミュレーション用メモリにロードfind
(到達したいアドレス.複数指定可能)avoid
(到達したくないアドレス.複数指定可能)sym
(シンボリック変数にしたいアドレスとサイズ.複数指定可能)type
(sym
の文字種指定.各sym
毎に指定可能)結果を得るのに失敗しても,スクリプトはディスク上(/tmp/gef
配下)に保存されているので必要なら手動で直せばよいでしょう.
x86, x64, arm, arm64で動作を確認しています.
tls
コマンドTLSは,スレッドローカルな変数を格納する領域です.
このコマンド自体は,tls
周辺のメモリを見るときによく使うと思います.
TLSは,glibcでは多くのアーキテクチャでサポートされています.しかし他のライブラリではサポートされていないこともあります.例えばuClibcではサポートされていません(多分).
このように,非glibc環境ではtls
コマンドが失敗することに注意して下さい.
またglibc環境であっても,バイナリの起動直後(main
到達前の話です)にはTLSが初期化されていないタイミングがあります.この初期化が完了するまでは,tls
コマンドが失敗することに注意してください.
glibcの場合に限定した話となりますが,tls
のアドレス保持方法はアーキテクチャごとに異なります.
GEFではこれを考慮して,以下のようにtls
のアドレスを取得しています.
$tp
レジスタmrc
命令でCP15
コプロセッサから読み取り$TPIDR_EL0
レジスタもしくは$tpidr
レジスタ$gs
レジスタの値をptrace
システムコール経由で取得fs
レジスタの値をptrace
システムコール経由で取得$r2
レジスタから0x7000
を引いた値$r13
レジスタから0x7000
を引いた値$g7
レジスタrdhwr
命令で$29
を読み取り0x7000
を引いた値($acc0 << 32) | $acc1
$gbr
レジスタ面倒くさくなってきたので列挙はこの辺でやめますが,アーキテクチャごとに色々異なる実装をしているのがわかると思います.面白いですね.
-v
オプションTLSはスレッドローカルな変数を格納する領域でした.
当然ですが,使おうと思えばどれだけでも使うことが出来ます.
従ってバイナリがスレッドローカルな変数をどれだけ利用しているかによって,デフォルトの上下16行では表示しきれない変数があったりします.
tls
周辺をもっと広げて見たい!という場合に,-v
オプションが使えます.
-v
, -vv
, -vvv
, …と重ねるたびに16行ずつ表示が増えます.
stdio-dump
コマンドstdin
とかstdout
の中身をなるべく見やすくダンプするコマンドです.
引数にアドレスを渡せば,そのアドレスを強制的にFILE
構造体として解釈してくれます.
FSOP(File-Structure Oriented Programming)のお供にどうぞ.
link-map
コマンドLinkMapをダンプするコマンドです.
ライブラリが互いにリンクリストで参照しあう構造なので,あるライブラリ(もしくはバイナリ自身)のLinkMapから,別のライブラリのLinkMapを参照することができ,それを辿っています.
実は-v
オプションをつけると,もっと詳しくダンプできます.
※全ての要素をダンプするわけではありません.構造体前半の有用そうな部分だけです.
dynamic
コマンド_DYNAMIC
領域をダンプするコマンドです.
_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
で得られるんですが,コマンドが長すぎてよく忘れてしまいます.
なので,ショートカットの目的で作りました.
一応,メモリマップに対応した色を付けていたり,タイプ別でのフィルタ機能があります.
types
コマンドシンボルの一覧が取れるなら,型の一覧も欲しくなります.ということで作りました.
ソースコードが手元にあったとしても,複雑な#define
マクロを追いかけるのは大変です.
既にビルドされたバイナリがあれば,その型情報を追いかけるほうが楽でしょう.
型の大まかな分類や,型名で表示をフィルタすることもできます.
dt
コマンドptype /ox
と同じなのですが,大きすぎる構造体や,多数の構造体を重ね合わせている共用体(union
)を含む構造体を表示しようとすると,結果が長くなりすぎる問題があります.
特にカーネル内部の構造体はすごくて,10画面くらいになってしまうこともあります.
これは,構造体のメンバが構造体だった場合に,再帰的にそれらも表示するからです.
トップレベルのメンバだけが知りたい,全体感をざっと把握したい,みたいなときに使えると思います.
他にも,以下の機能を持っています.
dt
コマンドの第一引数(型)を,第二引数(アドレス)に適用して解釈した結果を表示します.
p ((mstate) $rsp)[0]
みたいにすればデフォルトのコマンドでも実現できるんですが,すぐ書き方を忘れてしまうので,合わせてdt
コマンドに実装しました.
型のメンバを確認したあと,その型として,指定したアドレスを解釈させられると便利だろうと思っています.
hexdump-flexible
コマンドメンバのサイズも指定できるhexdump
が欲しかったので,作りました.
__attribute__((__packed__))
属性のついた構造体など,「アライメントされていない構造体」の配列をダンプするケースで便利かと思います.
hash
コマンドよく使われるハッシュ関数と少しレアなハッシュ関数を一括で計算するコマンドです.
の2つがあります.
些細なことですけど,メモリ中のデータのハッシュを求めるには以下のどれかが必要だと思います.
sha1sum
などを叩くpi hashlib.sha1(read_memory(0x555555558da0, 10)).hexdigest()
のようなコードをgdb上で実行ハッシュの種類によってはかなり大変なので,これも便利なコマンドです.
crc
コマンドCRC32やその亜種を一括で計算するコマンドです.
これもhash
コマンドと同じく2つのモードがあります.
CRCはハッシュと違って亜種が多すぎます.
見落としを防ぐためにも,様々なCRCを一括で計算してくれるのは良い機能だと思っています.
base-n-decode
/base-n-encode
コマンドbase64やその亜種を一括でデコード/エンコードするコマンドです.
これもhash
コマンドやcrc
コマンドと同じく2つのモードがあります.
baseNも亜種が多いです.
見落としを防ぐためにも,様々なbaseNを一括で計算してくれるのは良い機能だと思っています.
morse-decode
/morse-encode
コマンド一応モールス信号のデコーダ/エンコーダも作ってあります.
これもhash
コマンドやcrc
コマンドなどと同じく2つのモードがあります.
binwalk-memory
コマンドメモリ中をbinwalk
したいと思ったことはありませんか?
ダンプして別端末でbinwalk
,とすれば実現できますが非常に手間がかかります.
私は手軽にbinwalk
したいと思ったので,作りました.
メモリセクション毎にbinwalk
を実行します.ただしあくまで結果を閲覧するだけです.
検出したファイルの抽出はしないため,抽出が必要な場合は,必要に応じて手動でメモリをダンプし,別端末でbinwalk
をやり直してください.
※実はgefのインストーラでは,このためだけにbinwalk
をインストールするのですが,binwalk
は非常に依存関係が多いツールです.gefのインストーラでapt
を呼び出したとき,長々とインストールが行われるのはこいつ(binwalk
)のせいです.
filetype-memory
コマンドメモリ中のデータにfile
コマンドを実行したいと思ったことはありませんか?
私はあるので,作りました.
少し前にGoogleが公開したmagika
でも判定するようにしています.
sixel-memory
コマンドconvert
コマンドで画像をsixel
形式にすると,端末に画像をそのまま表示できるようなので,作りました.
やっていることは,メモリをダンプしてsixel
化して表示しているだけです.
BMP,PNG,JPGのいずれかの形式の場合,画像のヘッダ等からダンプすべきファイルサイズを自動判定するロジックを作り込んでいたりします.
※画像内に何らかのバーコードがある場合,それを読み取るオプションも実装済みです.
peek-pageframe
コマンドtramasysさんがPRしてくれた機能ですが,よくできていたのでそのまま取り込みました.
ある仮想アドレスのPFN(Page Frame Number; 物理アドレスとしてのページインデックス)を求めるときに使えます.
指定したPFNに対応するフラグを表示するpeek-pageflags
コマンドもあります.
昔と違って今は/proc/$PID/pagemap
にアクセス制限がかかっています.
読み取るにはroot
権限が必要となっているため,権限昇格などで利用することはできなくなりましたが,ダンプする機能自体は良いものだと思います.
gdtinfo
コマンド名前の通りGDTをダンプするコマンドですが,LDTにも対応しています.
LDTのエントリの構造に関しては情報が少なく,実装するのにとても苦労しました.
なお,qemu-systemでカーネルをデバッグしている時にのみ,実際の値をダンプします.
ユーザランドのプログラムを実行しているときは,値の例を表示するのみです.
idtinfo
コマンドx86の割り込みテーブルをダンプするコマンドもあります.
qemu-systemのqemuモニタで得られる,IDTレジスタの値を基にメモリをダンプしています.
これも,ユーザランドのプログラムを実行しているときは,値の例を表示するのみです.
カーネルのベースアドレス(kbase
)を求めるget_kernel_base()
関数では,x86/x64の場合にこのidtinfo
の結果を使い,「0除算時のハンドラ(#DE
)」のアドレスから,カーネルのベースアドレスを高速に求めています.
visual-heap
コマンドヒープ内のチャンクを,色を付けてダンプするコマンドです.
もともとGEFにはheap XXX
コマンド群もありますが,ヒープexploitに慣れている方なら,visual-heap
コマンドの方がわかりやすいのではないかと思います.
-d
オプション(使用中のチャンクをダークカラーにする)や-s
オプション(tcacheやfastbinで使われるprotected fdをデコードした状態で表示する)も使ってみてください.
heap
コマンドの-a
オプションヒープには,main arena以外のthread arenaが存在する場合があります.
heap chunks
コマンドや,heap bins
コマンドでは,対象とするアリーナを-a
オプションで指定できます.この時,-a
にはアドレスだけでなく,序数も使うことができます.
以下の画像は0x7fffe8000030
にあるthread arenaを序数1
でも同じ様に指定できることを示しています.
わざわざアドレスを指定するのが面倒なときに,ちょっと楽ができます.
heap try-free
コマンドあるチャンク(アドレス)を解放したときに,エラーが起きるかどうかを判定します.
内部的には,以下のように実装されています.
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を勉強し始めた時に気になっていた情報が一括で確認できて,非常に便利です.
探索対象は以下のとおりです.
高位アドレスのメモリ全域を走査するため,実行に時間がかかってしまう(数分程度)のが難点ではあります.
ただしデフォルトで前回結果の再表示を行いますので,同一セッションなら2度目以降はすぐ表示することができます.
使い所はほとんどありませんが,一度は実行してもらいたいイチオシの機能です.
v8
コマンドv8のオブジェクトの情報をダンプしたいときに使えるコマンドです.
これは単にcall (void) _v8_internal_Print_Object((void*)(アドレス))
を呼び出すことで実現しています.
またあまり知られていませんが,v8
のデバッグで有用なコマンドを公式のgdbinit
が提供しています.
-l
オプションを付けると,このgdbinit
をDLし,全てのコマンドをロードします.
https://chromium.googlesource.com/v8/v8/+/refs/heads/main/tools/gdbinit
distance
コマンドあるアドレスの,ベースからのオフセットを求めたいときに使えます.
xinfo
コマンドでも同じような結果は得られますが,distance
コマンドの方がより簡潔に表示されます.
saveo
コマンド/diffo
コマンドgdb(もしくはgef)のコマンド出力を記録しておき,それらを比較するコマンドです.
出力結果の差分を調べるのが大変だったので作りました.
して保存してから
で保存番号を確認し
とすることで,結果をdiffできます.
尚saveo
コマンドは,<好きなコマンド>
を実行する際,GEF全体のページャ設定を一時的に無効化して実行しています.従って<好きなコマンド>
に-n
オプションを付ける必要はありません.
xs
コマンドgdbって何で8進数がデフォルトなんでしょうか.
理由はわかりませんが,x/s
コマンドで\302
とか出力されてもピンと来ません.
\xc2
と表示してくれた方が嬉しいので,そのためのコマンドです.
gdbやgefのコマンド結果を,シェルコマンドに渡して加工したいと思ったことはありませんか?
これは,コマンドの先頭にpipe
または|
をつければ実現できます.
なお,GEFの多くのコマンドはページャを起動してしまうため,パイプ先に結果が渡りません.必要に応じてページャは無効化(-n
)しておきましょう.
gdb+qemu-userでは,Ctrl+C
が効かないという現象が起きます.
例えばこの様にバイナリをqemu-user配下で起動して,
GEFなし(-nx
)で接続(-ex 'target remote localhost:1234'
)してみましょう.
c
で実行を再開した後,Ctrl+C
を発行しても止まらないのが伝わるでしょうか.
これは非常に面倒なので,GEFではCtrl+C
を受け付けるようにしています.
ただし残念ながらスマートに実現することが出来ず,ちょっとトリッキーなことをしています.
fork
Ctrl+C
を監視gdb.execute("continue")
で実行再開Ctrl+C
を検出した時はqemu-user
のpidにSIGTRAP
を通知して終了Ctrl+C
を検出することなく親プロセス側が停止(ブレークポイント,例外発生など)した場合は,子プロセスにSIGKILL
を通知何故わざわざfork
しているかと言うと,以下の問題を全て解決する必要があったからです.
gdb.execute("continue")
すると,continue
コマンドが実行中の間はそのシグナルハンドラが無視されるgdb.execute("continue")
が許可されないこれら全てを解決する唯一思いついた方法が,子プロセスを作ってシグナルを監視することでした.シグナルハンドラを機能させつつgdb.exeute("continue")
するには,必ず別スレッドか別プロセスでそれぞれを行わなければならず(条件1),またそれら両方がメインスレッドで実行されなければならない(条件2,3)ので,必然的にfork
が必須となる,というロジックです.
作ってみたら,今のところうまく動いているようです.
尚,つい最近までは解決すべき条件3を解決しておらず,スレッドで実装していました.つまり非x86アーキでは,glibcのロード前にc
コマンドを使うとgdb
がクラッシュしていました.修正前のgefを使っている方はご注意ください.
python-interactive
におけるdisplayhook
pi <pythonのコード>
とすると,GEF上でpythonが実行できます.
GEF内で定義されている関数も使えるので,非常に便利です.どんなコマンドがあるのかはFAQを見てください.
※もっと知りたい場合は,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
に変換することを考えてみます.
以下のヘルプの関係図を見てください.
上半分の図を見てほしいのですが,page
構造体というのは,メモリの何処かに配列(struct page[]
)で存在しています.そしてそのpage
構造体の位置する配列インデックスがpfn
です.
このことから,page
からpfn
の変換に必要なのは,以下の2つであることがわかるでしょう.
VMEMMAP_START
もしくはmem_map
)page
構造体一つあたりのサイズつまり
という式が成り立っています.
あとはこのpfn
を,virt
なりphys
に変換すればよいでしょう.
12ビッㇳ左シフトすればphys
になり,それに物理メモリのダイレクトマップのベースアドレス(PAGE_OFFSET
)を足せばvirt
になります.
※GEFではLinuxカーネル内の計算に沿って実装しているため,意図的に少し遠回りなことをしていますが,本質的には同じことです.
全く逆の手順を踏めば,virt
やphys
からpage
への変換もできます.
こう書くと簡単に聞こえますが,幾つか問題があります.以下の値を求める必要があるからです.
VMEMMAP
(struct page[]
)PAGE_OFFSET
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
バイト)割り当てているとしましょう.まずこのスラブキャッシュのメタデータには,連続した3ページ分割り当てているということと(num_pages
),その連続した3ページに対応した最初のpage
のアドレスが入手できます.
続いて,頑張ってフリーリストを解析します.フリーリストに繋がった各チャンクの末尾12ビットを無視すれば,それぞれが存在するページがわかります.それが連続した3ページ分あれば,まさしくそのスラブキャッシュに割り当てられた3ページ分の領域であると特定できるわけです.このようにして検出された一番若いページのアドレスが,page
に対応したvirt
のアドレスという事になります.
slub-dump
は内部的にこれを行い,virt
の情報を画面上に表示しています.
※実際にはパターン2として,「そのスラブキャッシュ内で最も若いチャンクは,ページサイズでアライメントされている」という事実を使って,取り得るページの組が1つしかありえない場合を検出するロジックも組み込んでいます.
さて,ここまで書いたことはx86_64
の話です.x86
, ARM32
, ARM64
では細部が異なっているため,微修正が必要となります.特にx86
ではCONFIG_NUMA
がy
かn
かによって,内部構造が大きく異なります(ヘルプの下半分の図がCONFIG_NUMA=y
のときです).
x86_64
以外のアーキにおける実装の説明はここでは省略しますが,気になる方はGEFのソースコードを読んでみてください.
ktask
コマンド非常に巨大なコマンドです.カーネル内のタスク一覧をダンプします.
ヘルプを見てもらうとわかりますが,オプションをつけると以下の情報もダンプできます.
GEFの実装では,シンボルや型情報無しで(力技で),これらを保持する構造体(task_struct
)のメンバのうち,必要なメンバのオフセットを判別しています.
そこから情報をたどるときに必要な各構造体も同様に対応しています.rbtree
やxarray
やMaple Tree
のパースなどは面倒くさかったですが,なんとかなりました.
pwnに必要な情報はほぼ揃っていると思うので,色々遊んでみてください.
buddy-dump
コマンドスラブアロケータよりも下位に位置する,ページアロケータ(バディシステム)をダンプするコマンドです.
最近流行りのクロスキャッシュ攻撃などで,使うことがあるかもしれません.
PCP(PerCPU-Page)に関してもダンプ対象に含まれています.
vmalloc-dump
コマンドスラブアロケータとページアロケータのダンプコマンドがあるならば,vmalloc
アロケータをダンプするコマンドも作らなければなりません.ということで作りました.
機能拡張の勢いに陰りが見えてきました(=ネタ切れ).
こんな機能があればなーというアイデアがあれば,ぜひぜひissueを立ててください.日本語でもOKです.
2022年の記事に続き,bata24/gefで実装した機能を色々と紹介してきました.
本ツールを使っている方で,しばらく更新してないなーと思った方は,更新してみてください.
※コンフィグの形式が変わっていたり,必要なパッケージが増えていたりするので,単純なアップグレードだと新しい機能が動かない可能性があります.一度アンインストールして,インストーラを再実行するなどしてみてください.
※少し前からvenv
版のインストーラも用意しています.ぜひ使ってみてください(使い方はFAQに書いてあります).
バグ報告,フィードバック,機能リクエストなどもお待ちしています.
7日目の記事は,hama(@hama7230)さんの「Automotive CTFのこと」です.
私も同じチームで出場させて頂いたCTFです.新鮮な部分も多く楽しかったです.
記事の公開を楽しみに待ちましょう.