gdb で GOT の動作を追う

Sun Nov 15 2020

最近は今まで避けてきた pwn に手を付け始めています。 pwn を勉強すると早い段階で libc アドレスをリークする問題に出くわすわけですが、そのときには PLT や GOT の挙動を理解しておかないとよくわからないことになります。 今回、 gdb を使ってこれらの振る舞いを見てみた結果を簡単にまとめます。

実験コード

実験には以下のコードを使います。

test_plt_got.c
#include <stdio.h>

int main() {
  puts("hoge");
  puts("fuga");
}

2回 puts を呼ぶだけです。この2回呼ぶ間に GOT の puts がどうなるかを見てみます。 gcc test_plt_got.c -o test_plt_got -no-pie でコンパイルします。

gdb での動作確認

main 関数はこのようになっています。

gdb-peda$ disas main
Dump of assembler code for function main:
   0x0000000000401126 <+0>:	push   rbp
   0x0000000000401127 <+1>:	mov    rbp,rsp
   0x000000000040112a <+4>:	lea    rdi,[rip+0xed3]        # 0x402004
   0x0000000000401131 <+11>:	call   0x401030 <puts@plt>
   0x0000000000401136 <+16>:	lea    rdi,[rip+0xecc]        # 0x402009
   0x000000000040113d <+23>:	call   0x401030 <puts@plt>
   0x0000000000401142 <+28>:	mov    eax,0x0
   0x0000000000401147 <+33>:	pop    rbp
   0x0000000000401148 <+34>:	ret

0x401030 に breakpoint を貼ってみます。

gdb-peda$ disas
Dump of assembler code for function puts@plt:
=> 0x0000000000401030 <+0>:	jmp    QWORD PTR [rip+0x2fe2]        # 0x404018 <[email protected]>
   0x0000000000401036 <+6>:	push   0x0
   0x000000000040103b <+11>:	jmp    0x401020

*0x404018 に飛ぶようですが、最初の puts を呼ぶ段階では、

gdb-peda$ x/x 0x404018
0x404018 <[email protected]>:	0x0000000000401036

となっていて、 puts@plt+6 を指しています。 0x0 を push した後、 0x401020 に飛びます。

=> 0x401020:	push   QWORD PTR [rip+0x2fe2]        # 0x404008
   0x401026:	jmp    QWORD PTR [rip+0x2fe4]        # 0x404010
   0x40102c:	nop    DWORD PTR [rax+0x0]

*0x404008 を push します。値は以下のようになっているようです。

gdb-peda$ x/x 0x404008
0x404008:	0x00007ffff7ffe1a0

puts 呼び出し時に、スタックには2つ値が push されました。

0000| 0x7fffffffe338 --> 0x7ffff7ffe1a0 --> 0x0
0008| 0x7fffffffe340 --> 0x0

その後、 *0x404010 に飛びます。中身は下の通り。

gdb-peda$ x/x 0x404010
0x404010:	0x00007ffff7fe7d30

_dl_runtime_resolve_xsavec に飛ばされます。

   0x7ffff7fe7d26 <_dl_runtime_resolve_xsave+198>:	add    rsp,0x18
   0x7ffff7fe7d2a <_dl_runtime_resolve_xsave+202>:	bnd jmp r11
   0x7ffff7fe7d2e:	xchg   ax,ax
=> 0x7ffff7fe7d30 <_dl_runtime_resolve_xsavec>:	endbr64
   0x7ffff7fe7d34 <_dl_runtime_resolve_xsavec+4>:	push   rbx
   0x7ffff7fe7d35 <_dl_runtime_resolve_xsavec+5>:	mov    rbx,rsp
   0x7ffff7fe7d38 <_dl_runtime_resolve_xsavec+8>:	and    rsp,0xffffffffffffffc0
   0x7ffff7fe7d3c <_dl_runtime_resolve_xsavec+12>:	sub    rsp,QWORD PTR [rip+0x1487d]        # 0x7ffff7ffc5c0 <_rtld_local_ro+352>

この関数内部の挙動は追えなかったのですが、スタックした値を元に、 GOT を書き換えていそうです。 [email protected] を眺めてみると、先程と値が変わっていました。

gdb-peda$ x/x 0x404018
0x404018 <[email protected]>:	0x00007ffff7e44380

その後、共有ライブラリ中の puts に飛び、 puts("hoge") が実行されます。

gdb-peda$ disas puts
Dump of assembler code for function puts:
=> 0x00007ffff7e44380 <+0>:	endbr64
   0x00007ffff7e44384 <+4>:	push   r14
   0x00007ffff7e44386 <+6>:	push   r13
   0x00007ffff7e44388 <+8>:	push   r12

2回目の puts("fuga") ではどうなるかというと、 0x401030 (puts@plt) に飛んだ後 *0x404018 に飛ぶわけですが、上で見た通り 0x404018 の中身が puts に置き換わっているため、直接飛ぶことができます。

まとめ

以上の流れをまとめますと、

  • plt に飛ぶ
  • got は plt を指しているので plt に戻る
  • スタックに必要な値を push し、 _dl_runtime_resolve_xsavec
  • got の指すアドレスを共有ライブラリ内部のアドレスに更新する
  • 呼び出したかった関数を実行
  • 呼び出し元に戻る

となります。次に同じ関数を呼ぶときは、

  • plt に飛ぶ
  • got は共有ライブラリ内のアドレスを指しているので、そこに飛ぶ
  • 呼び出し元に戻る

となります。