こんにちは!日々のデバッグやセキュリティ調査、本当にお疲れ様です。
開発者として経験を積んでいくと、「動いているプログラムの内部で、ライブラリの関数がどう呼ばれているのかを覗き見たい」「特定の危険なAPIコールの瞬間をキャッチして、挙動を捻じ曲げたい」という、ちょっとダークで知的な衝動に駆られる瞬間が訪れませんか?
今回は、Linuxの低レイヤデバッグの王様である GDB(GNU Debugger) を使って、動的リンクの心臓部である GOT(Global Offset Table)とPLT(Procedure Linkage Table) を意図的に書き換え、ライブラリ関数の呼び出しを完全に監視・傍受・差し替えるテクニックを伝授します。
「難しそう……」なんて身構える必要はありません。仕組みの本質さえ掴んでしまえば、GDBはあなたにとって最強の「プログラム操作メス」になります。さあ、一緒に低レイヤの扉を開けていきましょう!
—
1. なぜGOT/PLTフックなのか?(ツールの役割と背景)
私たちが普段C言語などで書いたプログラムは、`printf` や `malloc` といった標準ライブラリの関数を呼ぶとき、実は自分自身のコードの中だけで完結していません。プログラムが実行される瞬間、OSの動的リンカ(`ld.so`)が裏側でライブラリをメモリにロードし、関数への正確な番地(アドレス)を解決しています。
ここで登場するのが、PLT と GOT です。
- PLT (Procedure Linkage Table): 関数を呼び出すための「窓口(ジャンプ台)」。
- GOT (Global Offset Table): 実際のライブラリ関数がメモリ上のどこにあるかを指し示す「電話帳(アドレスのリスト)」。
通常、プログラムが `puts()` を呼ぶと、PLTを経由してGOTに書かれたアドレスを参照し、実際のライブラリコードへジャンプします。
セキュリティ調査や解析における爆発的なメリット
もし、この GOTに書かれているアドレスを、自分が用意した別の関数(フック関数)のアドレスに書き換える(書き換える=フックする) ことができたらどうなるでしょう?
プログラム側は何も知らずにいつものように `puts()` を呼んでいるつもりが、あなたの意図したカスタム関数に強制送還され、引数の内容をロギングしたり、値を書き換えて元の関数に流したりすることが自由自在になります。マルウェア解析、不正APIコールの検知、脆弱性テストにおいて、これほど強力で美しい手法はありません。
—
2. 開発環境のセットアップと今回の実験台
百聞は一見に如かず。実際に手を動かして、この魔法を体験しましょう。
今回の実験に必要なツール群(GCC, GDB, makeなど)をインストールします。UbuntuやDebian系の環境を想定して進めますね。
必要なパッケージのインストール
以下のコマンドを端末(ターミナル)で実行し、デバッグに必要なミニマムかつ最強の環境を整えてください。
パッケージリストを最新化し、コンパイラとGDBをインストールします
sudo apt-get update
sudo apt-get install -y build-essential gdb
実験用プログラムの作成
まずは、標準的な `puts` 関数を使って文字列を表示するだけの、極めてシンプルなC言語のプログラム(`target.c`)を作成します。
include
include
int main() {
// 無限ループにすることで、GDBからいつでもアタッチして介入できるようにします
while(1) {
puts(“Hello, Low-Level World!”);
sleep(2); // 2秒ごとにメッセージを出力
}
return 0;
}
これをコンパイルします。今回はGOTの書き換え実験をスムーズに行うため、学習目的としてあえて PIE(Position Independent Executable)を無効化 し、アドレスを固定して分かりやすくします。
-no-pie オプションを付与してコンパイル(アドレスを固定化)
gcc -no-pie target.c -o target
実行して動作確認(Ctrl+Cで止められます)
./target
—
3. GDBを用いた「HelloWorld」的な動作確認とGOTの覗き見
まずは、GDBの基本操作として「どうやってプログラムのメモリを覗くのか」を確認しましょう。
別のターミナルを開き、先ほど作成したバイナリをGDBで読み込みます。
gdb ./target
GDBが起動したら、プログラムを一時停止させてGOTの領域を探してみましょう。
(gdb) # プログラムを起動しつつ、main関数で一時停止させます
(gdb) break main
(gdb) run
プログラムが `main` で止まったら、`puts` 関数のGOTエントリを確認します。GDB上で以下のコマンドを叩いてみてください。
(gdb) # puts関数のGOT上のアドレス(実際に関数アドレスが格納されている場所)を特定
(gdb) info symbol puts
出力結果から、`puts` のシンボル情報を得たら、次は逆アセンブルを見てみましょう。
(gdb) # main関数のアセンブリコードを表示
(gdb) disassemble main
ここに現れる `puts@plt` が、まさに先ほど解説した「ジャンプ台」です。
—
4. 【本丸】GDBでGOTを書き換え、関数呼び出しをフックする
いよいよ本題です。GDBの強力なメモリ書き込みコマンド `set` を使って、GOTに格納されている `puts` の本来のアドレスを、別の命令や自作の関数(あるいは安全なダミー関数)へと強制的に書き換えてみましょう。
手順のロジック
1. `puts` のGOTエントリのアドレスを特定する。
2. そのアドレスに格納されている値を、別のアドレス(今回は例として、プログラム内の無害な関数や、何もしない `_exit` など)に書き換える。
3. プログラムを再開させ、挙動の変化を確認する。
GDB上で以下のステップを実行します。
(gdb) # 1. putsのGOTエントリの正確なアドレスを調べます(例として記号で直接書き換えます) (gdb) # 2. putsのGOT(0x601018)に格納されているポインタを、 (gdb) # メモリ書き換え実行:0x601018番地の内容を、exit関数のアドレス(例: 0x400450)に書き換える この状態でGDBからプログラムの実行を再開(`continue`)させてみてください。 (gdb) continue コンソールを観察すると、先ほどまで 2 秒おきに `Hello, Low-Level World!` と出力していたはずのプログラムが、文字を出力しなくなった(あるいは即座に終了した) ことが確認できます。 これは、プログラムが `puts()` を呼び出した瞬間、GOTが書き換えられていたために `exit` 関数へと華麗にジャンプし、そのまま処理が曲げられたからです! — お疲れ様でした! 低レイヤデバッガの真髄である「GOT/PLTフック」の基礎、いかがでしたでしょうか。 ソースコードを変更することなく、実行中のバイナリの「外部依存関係(APIコール)」を自由自在にコントロールできるようになったこと。これは、レガシーな商用ソフトウエアの挙動解析や、セキュリティインシデント時の動的解析において、唯一無二の武器になります。 実務においては、GDBを手動で操作するだけでなく、Pythonスクリプト(GDB Python API)を組み合わせて自動でフックし、引数(`RDI`レジスタやスタックの値)をリアルタイムに書き換えてファジングを行うといった高度な手法へ発展させていきます。 「これをマスターすれば、毎日のコーディングが劇的に楽に……」というよりは、「コンピュータの挙動で分からないものはもう何一つない」という圧倒的な自信と、深淵を覗き見る知的な快感が手に入ります。 ぜひご自身のローカル環境で試して、低レイヤの世界を遊び尽くしてくださいね。それでは、次のハッキング・デバッグライフでお会いしましょう!
(gdb) print &puts
$1 = (int ()(const char )) 0x601018
(gdb) # ここでは安全のためにプログラム内の exit 関数のアドレスに書き換えてみます
(gdb) print puts
$2 = {int (const char )} 0x400430
(gdb) # ※環境によってアドレスは異なるため、ご自身の環境の exit アドレスを `p exit` で調べて代入してください
(gdb) set {void0x601018} = 0x400450挙動の確認
5. 先輩エンジニアからの実務アドバイスとまとめ