はじめに:なぜ「GOT/PLTフック」をGDBで極める必要があるのか
テックリードの視点から言えば、現代のセキュアコーディングやリバースエンジニアリングにおいて、ブラックボックス化したバイナリの挙動を完全に支配下置くスキルは、もはや「特技」ではなく「必須要件」です。特に、動的リンク(Dynamic Linking)されたバイナリを解析する際、ソースコードがない状態で特定のライブラリ関数(例えば `strcpy` や `system`、あるいは暗号化関連のAPIなど)がどのように呼び出されているかを監視・改ざんしたい衝動に駆られることはないでしょうか。
ネットを検索すれば「GDBの基本的なブレークポイントの貼り方」のような初心者向けの記事は山のように見つかります。しかし、実務の現場で求められるのは、そうした表面的な知識ではありません。ライブラリのロードアドレスがASLR(Address Space Layout Randomization)によって毎回ランダム化される過酷な環境下で、いかにして狙った関数の実行をトラップし、引数を書き換え、あるいは独自の処理にすり替えるかという「動的バイナリのアーキテクチャをハックする技術」です。
今回は、ELF(Executable and Linkable Format)の心臓部である GOT(Global Offset Table) と PLT(Procedure Linkage Table) のメカニズムをGDBの低レイヤ操作によって直接書き換え、APIコールを完全監視・制御する実戦的テクニックを解き明かします。
—
1. 内部アーキテクチャの理解:なぜGOT/PLTを書き換えるのか?
動的リンクバイナリが外部共有ライブラリ(libcなど)の関数を呼び出すとき、直接そのメモリアドレスをジャンプ先として固定することはできません。ASLRや遅延バインディング(Lazy Binding)が存在するためです。ここで登場するのが PLT と GOT です。
1. PLT(Procedure Linkage Table): コードセグメント(`.plt`)に存在する小さなスタブコードの集合。外部関数呼び出しはこのスタブを必ず経由します。
2. GOT(Global Offset Table): データセグメント(`.got.plt`)に存在し、実際の関数のメモリアドレスが格納されるテーブル。
初回の関数呼び出し時、PLTはダイナミックリンカ(`ld.so`)を呼び出して実際の関数アドレスを解決し、GOTを書き換えます。2回目以降の呼び出しでは、PLTはGOTに格納された実アドレスへ直接ジャンプします。
【アーキテクチャの深層】
GDBを用いて通常のブレークポイント(`break`コマンド)を仕掛けると、CPUの `INT 3` 命令(0xcc)がコード領域に書き込まれます。しかし、これでは検知をバイパスされたり、コードの完全性をチェックする改ざん検知ロジック(アンチデバッグ)に引っかかるリスクがあります。
そこで、コード領域ではなく、データ領域であるGOTのエントリ自体を書き換えることで、プログラムの制御フローを完全に透明な形でハイジャック(フック)することが可能になります。これが「GOTフック」の本質です。
—
2. 開発スピードを極限まで高める:GDBの秘匿された設定と拡張
日々のデバッグ作業で、プレーンなGDBをそのまま使っているようではプロのアーキテクトとは言えません。ここでは、GDBの戦闘力を劇的に引き上げる `.gdbinit` のベストプラクティス設定と、導入必須の神プラグインを紹介します。
絶対に入れるべき神プラグイン:GEF (GDB Enhanced Features)
PEDAやPwndbgなどがありますが、現代の脆弱性調査や低レイヤ解析において最も拡張性が高く、出力の見やすさとスクリーンの美しさを両立しているのが GEF です。レジスタ、スタック、メモリ、逆アセンブル結果がワンビューで同期表示され、視覚的な認知負荷がゼロになります。
実務仕様の `.gdbinit` 設定ファイル(ベストプラクティス)
チーム全体でデバッグ効率を標準化し、かつ高度なメモリ操作コマンドを即座に呼び出せるようにするための設定例です。
==============================================================================
GDB Enterprise-Grade Configuration (.gdbinit)
==============================================================================
セキュリティ警告の抑制と非対話モードの安全なデフォルト化
set pagination off
set confirm off
逆アセンブルのフレーバーをIntel形式に固定(直感的な可読性の向上)
set disassembly-flavor intel
履歴の保存件数を無制限にし、過去の強力なコマンドを永続化
set history save on
set history size 10000
set history filename ~/.gdb_history
デバッグ中のプロセスがfork/cloneした際の子プロセスを自動追跡
set detach-on-fork off
set follow-fork-mode child
カラースキームの最適化(GEFやPinthesizerとの親和性向上)
set print pretty on
set print array-indexes on
独自便利コマンドの定義:指定したシンボルのGOTエントリをダンプする
define print-got
# 引数として渡されたシンボル(例: puts)のGOT上の解決アドレスを表示
p/x (void)(&.got.plt + $arg0)
end
document print-got
指定した共有ライブラリ関数のGOTエントリの現在値を16進数で表示します。
使用例: print-got puts
end
起動時に自動でログ出力を有効化し、解析証跡を残す
set logging file /tmp/gdb_audit_trail.log
set logging enabled on
echo [+] GDB Initialized with Security Architecture Profile.\n
—
3. 実践:GDBを用いたGOT/PLTフックの手順とコード
ここからが本題です。ターゲットとなるバイナリが実行する `puts` または `printf` などのライブラリ関数呼び出しを傍受し、別のカスタム関数にすり替える手順をハンズオン形式で解説します。
ステップ1: ターゲットプログラムと解析対象の特定
今回は簡単なC言語のバイナリ(ASLR有効、PIE有効のモダンなビルド)を想定します。
include
include
void target_function(const char msg) {
// 監視対象とするライブラリ関数
puts(msg);
}
int main() {
while(1) {
target_function(“Hello, Security World!”);
sleep(2);
}
return 0;
}
ステップ2: GDBによるアタッチとGOTアドレスの特定
GDBでバイナリを起動し、PLT経由で呼び出される `puts` のGOTエントリの位置を特定します。
$ gdb -q ./target_binary
GDBプロンプトから、`puts` のPLTエントリと、それが参照するGOTのアドレスを特定します。
(gdb) b main
(gdb) run
Breakpoint 1, main () at target.c:10
10 target_function(“Hello, Security World!”);
putsのPLTスラブを確認
(gdb) disass puts@plt
Dump of assembler code for function puts@plt:
0x0000555555401030 <+0>: endbr64
0x0000555555401034 <+4>: bnd jmp 0x2ffd(%rip) # 0x555555404038
0x0000555555401040 <+16>: jmp 0x555555401020
End of assembler dump.
ここで、`0x555555404038` が `puts` のGOTエントリであることが分かりました。このメモリ領域には、現在 `libc` 内の実際の `puts` 関数の実アドレスが格納されています。
ステップ3: メモリ書き換えによるGOTフックの実行
GDBの `set` コマンドを使用することで、プロセス空間内の任意のメモリ領域(書き込み権限 `.got.plt` セグメント)を書き換えることができます。
ここでは、本来の `puts` が呼ばれた際に、独自のハンドラ関数(あるいは任意のインジェクションコード、今回は分かりやすくダミーのアドレスにリダイレクトする例)に差し替えます。
まず、自作のフック関数をメモリ上に用意するか、あるいはロード済みのア別の無害な関数(例: `getchar`)のアドレスに書き換えて挙動の変化を確認します。
putsのGOTエントリ(0x555555404038)に、getcharのアドレスを書き込む (gdb) p getchar メモリの書き換え(GOTの書き換え) この瞬間から、プログラムが `puts(“Hello, Security World!”)` を呼び出すと、PLTはGOTを参照し、結果として `getchar` が実行されるようになります。標準出力への文字列出力は完全にバイパスされ、入力待ち状態にハックされます。 — このGOTフック技術は、単なる悪用(ハイジャック)だけでなく、セキュアな開発・テスト工程において極めて強力な武器になります。 例えば、安全ではないとされる危険な関数(`strcpy`, `sprintf`, `system` など)が、サードパーティ製ライブラリの内部から密かに呼び出されていないかをテスト環境で全自動検知したい場合、GDBをラッパーとして起動し、該当するGOTエントリに「トラップ用ハンドラ(ブレークポイントを設定するだけのカスタム関数や、バックトレースを吐き出して強制終了する処理)」を強制的に埋め込むスクリプトを走らせます。 手動で毎回GOTを書き換えるのは非効率です。以下に、GDBのPythonインターフェースを用いて、バイナリ起動時に自動で特定のAPIコールをインターセプトし、その際の引数をログに記録する堅牢なPythonスクリプトの構成例を示します。 ============================================================================== class GotHookBreakpoint(gdb.Breakpoint): def stop(self): # メモリから文字列を安全に読み取る(最大128バイト) print(f”\n[SECURITY AUDIT] Intercepted API Call to target function!”) except Exception as e: # 処理を継続させたい場合は False、ここで停止させたい場合は True を返す スクリプトロード時に自動実行される初期化処理 def invoke(self, arg, from_tty): コマンドの登録 このスクリプトを `.gdbinit` から読み込ませることで、CI/CDパイプラインやローカルでのセキュリティ回帰テスト時に、ブラックボックスなバイナリが外部へ送信する機密データや文字列を完全にモニタリングすることが可能になります。 — 個人のローカル環境だけでこのような高度なデバッグテクニックやスクリプトを維持していても、チーム全体の生産性向上には繋がりません。組織として低レイヤの品質管理・セキュリティ調査能力を底上げするためには、以下のガバナンスルールをリポジトリに組み込むべきです。 1. プロジェクト固有の `.gdbinit` のリポジトリ管理 { この設定をチームの標準とすることで、新しくアサインされたエンジニアであっても、リポジトリをクローンしてF5キーを押すだけで、最先端のセキュアなAPI監視環境と高度なデバッグセッションを即座に手に入れることができます。 — GDBによるGOT/PLTフックは、単なるリバースエンジニアリングのテクニックに留まりません。バイナリの内部構造(ELF、動的リンク、メモリレイアウト)の深い理解に裏打ちされたこの手法は、予期せぬ脆弱性の発見、レガシーライブラリの安全なモダナイゼーション、そして悪意あるAPIコールの動的な検出において、他の追随を許さない圧倒的なアドバンテージをもたらします。 ツールにただ使われるのではなく、ツールの内部動作(今回の場合はGOT/PLTというデータとコードの架け橋)を完全に掌握し、開発・テスト・セキュリティのパイプラインに昇華させること。それこそが、真にプロダクトの品質とチームの開発スピードを極限まで高めるテックリードの仕事です。ぜひ、今日の開発環境からこの知見を実践してください。
(gdb) p puts
$1 = {
$2 = {
(gdb) set {void}(0x555555404038) = 0x7ffff7e226004. セキュリティテスト・不正APIコール検知への応用
1. 不正APIコールのブラックリスト監視
2. Python API (GDB Python Scripting) による自動化監視のベストプラクティス
GDB Python Automation Script: GotHookMonitor.py
役割: 起動時に指定した危殆化しやすい関数のGOTを監視・フックし、引数をダンプする
==============================================================================
import gdb
“””
GOTフックと連動し、対象関数が呼び出された瞬間にレジスタや引数を解析するブレークポイント
“””
def __init__(self, spec):
super(GotHookBreakpoint, self).__init__(spec, gdb.BP_BREAKPOINT)
# x86_64 Calling Convention における第1引数 (RDIレジスタ) の文字列を取得
try:
inferior = gdb.selected_inferior()
arch = inferior.architecture()
# RDIレジスタの値(ポインタ)を取得
rdi_val = int(gdb.parse_and_eval(“$rdi”))
cstring = inferior.read_memory(rdi_val, 128).tobytes()
# ヌル文字以降を切り捨てる
actual_str = cstring.split(b’\x00′)[0].decode(‘utf-8′, errors=’ignore’)
print(f” [+] Captured Argument (RDI): {actual_str}”)
print(f”[-] Failed to parse arguments during hook: {e}”)
return False
class InitializeGotHook(gdb.Command):
def __init__(self):
super(InitializeGotHook, self).__init__(“init-got-monitor”, gdb.COMMAND_USER)
print(“[] Installing GOT/PLT Hooks for Security Auditing…”)
# puts関数の実アドレスにブレークポイントを設置して監視
GotHookBreakpoint(“puts”)
print(“[] Hooks successfully armed.”)
InitializeGotHook()5. チーム開発における設定共有化ルールとガバナンス
ターゲットプロジェクトのルートディレクトリに `.gdbinit`(または専用の `debug.gdb`)を配置し、IDE(VS Codeなど)のデバッグ設定(`.vscode/launch.json`)の `gdbpath` や `setupCommands` から自動的に読み込まれるようにチームで標準化します。
2. VS Code `launch.json` との統合設定例
GUIベースの開発であっても、内部でGDBの強力なパワーをフルに引き出すための設定例を共有します。
“version”: “0.2.0”,
“configurations”: [
{
“name”: “Secure Debug with GOT Hook”,
“type”: “cppdbg”,
“request”: “launch”,
“program”: “${workspaceFolder}/build/target_binary”,
“args”: [],
“stopAtEntry”: false,
“cwd”: “${workspaceFolder}”,
“environment”: [],
“externalConsole”: false,
“MIMode”: “gdb”,
“setupCommands”: [
{
“description”: “Enable pretty-printing for gdb”,
“text”: “enable pretty-printing”,
“ignoreFailures”: true
},
{
“description”: “Load custom security monitoring script”,
“text”: “source ${workspaceFolder}/tools/GotHookMonitor.py”,
“ignoreFailures”: false
},
{
“description”: “Initialize security hooks”,
“text”: “init-got-monitor”,
“ignoreFailures”: false
}
],
“logging”: {
“engineLogging”: true
}
}
]
}おわりに