🐡

glibcのheapをgdbで調べる方法

2022/12/14に公開

この記事はCTF tools/tips Advent Calendar 2022の11日目の記事です。
遅れてしまって申し訳ない...

対象

  • メモリリークが起きているのは分かるが、ヒープの中がどうなっているのかよくわからない
  • pwnの問題でヒープをいじるための情報を表示したい

方針

pwndbg等のgdb拡張を入れた後、set debug-file-directoryでbuild-id等を指定し、main_arenaのシンボルを認識させる。
Ubuntuの人はlibc6-dbgを入れるだけでいい。

gdbを拡張する

もともとgdbにはheapを可視化するツールは提供されていないため、外部の拡張機能が必要になります。heap関連のコマンドを追加してくれるプラグインとして、gefgdb-pedaなどが有名ですが、ここではpwndbgを使います。
未検証ですが、この記事の内容はどれを選んでも使えると思います。

さて、pwndbgを入れるとheapコマンドが使えるようになりますが、何もしていない状態では以下のような表示になります。

pwndbg> heap
heap: This command only works with libc debug symbols.
They can probably be installed via the package manager of your choice.
See also: https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html

E.g. on Ubuntu/Debian you might need to do the following steps (for 64-bit and 32-bit binaries):
sudo apt-get install libc6-dbg
sudo dpkg --add-architecture i386
sudo apt-get install libc-dbg:i386

gdbの拡張機能は、基本的にヒープの管理構造体である main_arena というlibc内の変数を参照しますが、通常のlibc.so.6にはそのシンボルが公開されていないのが原因です。

解決方法として、エラー通りDebian系の人はlibc6-dbg を入れることで解決できますが、それ以外の人は手動でシンボルを取ってきて指定する必要があります。

シンボル解決として build-id を使う

詳細は割愛しますが、gdbにはデバッグファイルの形式として、debug-link方式とbuild-id方式があります。
両方とも main_arena 等シンボルのアドレスを記録したファイルを用意しているという部分は共通しています。形式や使い方が異なるだけで、どちらかが使えれば目的を達成できます。

build-id方式を使う場合、まずglibcのbuild-idデータを用意する必要があります。拙著ですがpwn用にlibcを調べる/用意するにlibcの入手のアレコレを纏めました。build-idも自動的に取得できます。

libc6-dbgを展開すると、内部に.debugがあります(上の記事を参考にした場合、libs/2.xx-hogehoge/.debug)。
この中にある libc.so.6 にシンボル情報が入っています。実際、nmコマンドを使うことでシンボル情報を得ることができます。

$ nm libc-2.31.so
00000000001b5680 b CSWTCH.1
00000000001ec798 b DW.ref.__gcc_personality_v0
(省略)
00000000001b1bc0 b zeroes
00000000001eeeb0 b zone_names

$ nm libc-2.31.so | grep main_arena
00000000001eeb70 b dumped_main_arena_end
00000000001eeb78 b dumped_main_arena_start
00000000001ebb80 b main_arena

この情報を使うためには、gdbコマンドで次の様にすればいいです(.gdbinitとかに書いておくと良いと思います)。

set debug-file-directory /path/to/libc/.debug

また、デバッグファイルのlibcと実際に使っているlibcを揃える必要があります。
この場合、/path/to/libc/libc.so.6を共有ライブラリとして使いつつ、/path/to/libc/.debug/libc.so.6を使うといった形です。
これも拙著ですが、共有ライブラリを変える方法はpatchelfを使って動的ライブラリ(libcとか)を変えるに纏めました。

debug-linkと言うより、分離されたデバッグ情報を使う場合ですが、古い方法なので使うことは無いかもしれません。一応軽く解説しておきます。

その場合、libcにデバッグリンクを張るか、もしくは(管見の限りでは)gdbの公開されていないAPIを使うことで解決できます。
前者はソースコードを使ってバイナリにデバッグシンボルを付けるの応用で、libcにデバッグリンクを張ります。
ただし、libc.so.6にはデフォルトでdebuglinkが張られており、それを削除してからリンクを張る必要があります。デバッグリンクは.gnu_debuglinkセクションに格納されています。

$ strip -R .gnu_debuglink ./libc.so.6
$ objcopy --add-gnu-debuglink=./.debug/libc.so.6 ./libc.so.6

非公開APIを使う場合、gdb内のPythonを使って、libcが割り当てられている領域にデバッグファイルをくっつける事で実現できます。
例えば、

pwndbg> python
import gdb

for libc in filter(lambda o: 'libc' in o.filename, gdb.objfiles()):
  libc.add_separate_debug_file("/path/to/libc/.debug/libc.so.6")
  return
end

とすればいいです。このテクニック自体、読み込まれている動的ライブラリにランタイムでデバッグシンボルを与えるというものなので、色々応用することができます。

終わりに

誤字・脱字・間違い等は、ご指摘いただければ修正します。
コメントでもTwitter(@iwancof_ptr)でも

Discussion