最近は今まで避けてきた pwn に手を付け始めています。 pwn を勉強すると早い段階で libc アドレスをリークする問題に出くわすわけですが、そのときには PLT や GOT の挙動を理解しておかないとよくわからないことになります。 今回、 gdb を使ってこれらの振る舞いを見てみた結果を簡単にまとめます。
実験コード
実験には以下のコードを使います。
#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 <puts@got.plt>
0x0000000000401036 <+6>: push 0x0
0x000000000040103b <+11>: jmp 0x401020
*0x404018
に飛ぶようですが、最初の puts を呼ぶ段階では、
gdb-peda$ x/x 0x404018
0x404018 <puts@got.plt>: 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 を書き換えていそうです。 puts@got.plt を眺めてみると、先程と値が変わっていました。
gdb-peda$ x/x 0x404018
0x404018 <puts@got.plt>: 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 は共有ライブラリ内のアドレスを指しているので、そこに飛ぶ
- 呼び出し元に戻る
となります。