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 <[email protected]>
       0x0000000000401136 <+16>:	lea    rdi,[rip+0xecc]        # 0x402009
       0x000000000040113d <+23>:	call   0x401030 <[email protected]>
       0x0000000000401142 <+28>:	mov    eax,0x0
       0x0000000000401147 <+33>:	pop    rbp
       0x0000000000401148 <+34>:	ret
    

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

    gdb-peda$ disas
    Dump of assembler code for function [email protected]:
    => 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
    

    となっていて、 [email protected]+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 ([email protected]) に飛んだ後 *0x404018 に飛ぶわけですが、上で見た通り 0x404018 の中身が puts に置き換わっているため、直接飛ぶことができます。

    まとめ

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

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

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

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

    となります。