PLTとGOTってなんだっけ

May 6, 2020 (Updated on: May 7, 2020)
by Keichi Takahashi

PLTとGOTが何だったか今までに何回も検索したので,忘れないよう自分なりにまとめておくことにしました.

TL;DR PLTとGOTは共有ライブラリの関数のリロケーションを実行時まで遅延するための仕組み

  • PLT (Procedure Linkage Table): アプリケーションから直接呼ばれる.GOTから対応する共有ライブラリ関数のアドレスを取得し,間接ジャンプする.
  • GOT (Global Offsets Table): 共有ライブラリ関数のアドレス一覧.実際に関数が呼ばれてからアドレスが設定される.

概要

共有ライブラリ関数の呼び出しは,プログラム->PLT->共有ライブラリという2段ジャンプ方式になっています.これは,共有ライブラリはロードされるアドレスが実行時まで不明なためです.プログラムがある共有ライブラリ関数を呼び出すと,その際に動的リンカが共有ライブラリから関数のアドレスを探し出し,GOTに設定します.PLTはGOTに設定されているアドレスを参照し,共有ライブラリにジャンプします.

もう少し細かい流れは下記の通りです:

初回の呼び出し

  1. アプリケーションがPLT内のエントリを呼ぶ
  2. PLTはGOTの対応するエントリが示すアドレスへジャンプ.初期状態では,PLTにあるリロケーション処理のアドレスが設定されている.
  3. PLTはリロケーションのための準備を行い,動的リンカにジャンプ
  4. 動的リンカはライブラリ関数のアドレスを解決し,GOTのエントリに上書き
  5. ライブラリ関数へジャンプ

2回目以降の呼び出し

  1. アプリケーションがPLT内のエントリを呼ぶ
  2. PLTはGOTが示す共有ライブラリの関数へジャンプ

PLTとGOTの動作を確かめてみる

実際に手を動かして,PLTとGOTが動く仕組みを確かめてみました.

下準備

まず,gcc -no-pie -o hello hello.cで次のソースコードをコンパイルします.-no-pieフラグでPIE (と後述するRELRO) を切っています.

#include <stdio.h>

int main()
{
    puts("hello, world");
    puts("hello, again");
}

生成された実行可能ファイルをlddで調べると,libcと動的リンカ (ld) に依存していることがわかります.

$ ldd hello
	linux-vdso.so.1 (0x00007fff5efe1000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb18c594000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fb18c795000)

readelf -a helloすると,関係ありそうなセクションが見えます.

...
  [13] .plt              PROGBITS         0000000000401020  00001020
       0000000000000020  0000000000000010  AX       0     0     16
  [14] .plt.sec          PROGBITS         0000000000401040  00001040
       0000000000000010  0000000000000010  AX       0     0     16
...
  [23] .got              PROGBITS         0000000000403ff0  00002ff0
       0000000000000010  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000404000  00003000
       0000000000000020  0000000000000008  WA       0     0     8
...

初回の呼び出し

gdbでhelloを起動し,mainをディスアセンブルすると,下記のようになります.puts@pltというアドレスを2回呼んでいることがわかります.

(gdb) disas
Dump of assembler code for function main:
=> 0x0000000000401136 <+0>:	endbr64
   0x000000000040113a <+4>:	push   %rbp
   0x000000000040113b <+5>:	mov    %rsp,%rbp
   0x000000000040113e <+8>:	lea    0xebf(%rip),%rdi        # 0x402004
   0x0000000000401145 <+15>:	callq  0x401040 
   0x000000000040114a <+20>:	lea    0xec1(%rip),%rdi        # 0x402012
   0x0000000000401151 <+27>:	callq  0x401040 
   0x0000000000401156 <+32>:	mov    $0x0,%eax
   0x000000000040115b <+37>:	pop    %rbp
   0x000000000040115c <+38>:	retq
End of assembler dump.

puts@pltは名前の通りPLTのエントリです.puts@pltをディスアセンブルすると,puts@got.pltが指すアドレスへジャンプしています.

(gdb) disas 'puts@plt'
Dump of assembler code for function puts@plt:
   0x0000000000401040 <+0>:	endbr64
   0x0000000000401044 <+4>:	bnd jmpq *0x2fcd(%rip)        # 0x404018 
   0x000000000040104b <+11>:	nopl   0x0(%rax,%rax,1)
End of assembler dump.

puts@got.pltは,同じく名前の通りGOTのエントリです.この時点では,0x401030という.plt内のアドレスになっています.

(gdb) x/a 0x404018
0x404018 :	0x401030

ジャンプ先の.plt内の処理では,GOTの先頭アドレスとputsのGOT内でのインデックスをスタックにプッシュし,動的リンカ (ld) へジャンプしています.ldはglibcからputsのアドレスを解決した後,puts@got.pltに書き込みます.その後,glibcのputsにジャンプします.

2回目以降の呼び出し

2回目のputsの呼び出しでputs@got.pltの中身を調べると,下記の通り,puts本体のアドレスが設定されていることがわかります.

(gdb) x/a 0x404018
0x404018 :	0x7ffff7e555a0 <__GI__IO_puts>

メモ

  • 何らかの方法で攻撃者がGOTへ値を書き込めてしまうと,任意コード実行が成立してしてしまいます.そのため,プログラム起動時にGOTのエントリを全て埋めた後,GOTをread-onlyに設定する,RELRO (Relocation Read-Only) という機能があるそうです.
  • ltraceはPLTにブレークポイントを書き込むことによって関数呼び出しをトレースしているそうです.

参考URL