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 は共有ライブラリ内のアドレスを指しているので、そこに飛ぶ
- 呼び出し元に戻る
となります。